synth-ai 0.2.13.dev2__py3-none-any.whl → 0.2.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (293) hide show
  1. examples/README.md +1 -0
  2. examples/multi_step/SFT_README.md +147 -0
  3. examples/multi_step/configs/README_verilog_rl.md +77 -0
  4. examples/multi_step/configs/VERILOG_REWARDS.md +90 -0
  5. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +183 -0
  6. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
  7. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
  8. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +12 -11
  9. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  10. examples/multi_step/configs/crafter_synth_backend.md +40 -0
  11. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
  12. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
  13. examples/multi_step/configs/verilog_rl_lora.toml +190 -0
  14. examples/multi_step/convert_traces_to_sft.py +84 -0
  15. examples/multi_step/judges/crafter_backend_judge.py +220 -0
  16. examples/multi_step/judges/verilog_backend_judge.py +234 -0
  17. examples/multi_step/readme.md +48 -0
  18. examples/multi_step/run_sft_qwen30b.sh +45 -0
  19. examples/multi_step/verilog_rl_lora.md +218 -0
  20. examples/qwen_coder/configs/coder_lora_30b.toml +3 -2
  21. examples/qwen_coder/configs/coder_lora_4b.toml +2 -1
  22. examples/qwen_coder/configs/coder_lora_small.toml +2 -1
  23. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  24. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  25. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  26. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  27. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  28. examples/qwen_vl/QUICKSTART.md +327 -0
  29. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  30. examples/qwen_vl/README.md +154 -0
  31. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  32. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  33. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  34. examples/qwen_vl/SETUP_COMPLETE.md +275 -0
  35. examples/qwen_vl/VISION_TESTS_COMPLETE.md +490 -0
  36. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  37. examples/qwen_vl/__init__.py +2 -0
  38. examples/qwen_vl/collect_data_via_cli.md +423 -0
  39. examples/qwen_vl/collect_vision_traces.py +368 -0
  40. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +127 -0
  41. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +60 -0
  42. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +43 -0
  43. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  44. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +45 -0
  45. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +44 -0
  46. examples/qwen_vl/configs/filter_qwen2vl_sft.toml +50 -0
  47. examples/qwen_vl/configs/filter_vision_sft.toml +53 -0
  48. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  49. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  50. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  51. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  52. examples/qwen_vl/run_vision_comparison.sh +62 -0
  53. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  54. examples/qwen_vl/test_image_validation.py +201 -0
  55. examples/qwen_vl/test_sft_vision_data.py +110 -0
  56. examples/rl/README.md +1 -1
  57. examples/rl/configs/eval_base_qwen.toml +17 -0
  58. examples/rl/configs/eval_rl_qwen.toml +13 -0
  59. examples/rl/configs/rl_from_base_qwen.toml +37 -0
  60. examples/rl/configs/rl_from_base_qwen17.toml +76 -0
  61. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  62. examples/rl/run_eval.py +436 -0
  63. examples/rl/run_rl_and_save.py +111 -0
  64. examples/rl/task_app/README.md +22 -0
  65. examples/rl/task_app/math_single_step.py +990 -0
  66. examples/rl/task_app/math_task_app.py +111 -0
  67. examples/sft/README.md +5 -5
  68. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -2
  69. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -3
  70. examples/sft/evaluate.py +4 -4
  71. examples/sft/export_dataset.py +7 -4
  72. examples/sft/generate_traces.py +2 -0
  73. examples/swe/task_app/README.md +1 -1
  74. examples/swe/task_app/grpo_swe_mini.py +1 -1
  75. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  76. examples/swe/task_app/hosted/envs/mini_swe/environment.py +13 -13
  77. examples/swe/task_app/hosted/policy_routes.py +0 -2
  78. examples/swe/task_app/hosted/rollout.py +2 -8
  79. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
  80. examples/task_apps/crafter/CREATE_SFT_DATASET.md +273 -0
  81. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
  82. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +174 -0
  83. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +268 -0
  84. examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
  85. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
  86. examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
  87. examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
  88. examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
  89. examples/task_apps/crafter/task_app/__init__.py +3 -0
  90. examples/task_apps/crafter/task_app/grpo_crafter.py +309 -14
  91. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +10 -0
  92. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +75 -4
  93. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +17 -2
  94. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +55 -3
  95. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +114 -32
  96. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +127 -27
  97. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +156 -0
  98. examples/task_apps/enron/__init__.py +1 -0
  99. examples/task_apps/enron/filter_sft.toml +5 -0
  100. examples/task_apps/enron/tests/__init__.py +2 -0
  101. examples/task_apps/enron/tests/integration/__init__.py +2 -0
  102. examples/task_apps/enron/tests/integration/test_enron_eval.py +2 -0
  103. examples/task_apps/enron/tests/unit/__init__.py +2 -0
  104. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
  105. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
  106. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +415 -0
  107. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +29 -0
  108. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +2 -0
  109. examples/task_apps/pokemon_red/task_app.py +199 -6
  110. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +2 -0
  111. examples/task_apps/sokoban/filter_sft.toml +5 -0
  112. examples/task_apps/sokoban/tests/__init__.py +2 -0
  113. examples/task_apps/sokoban/tests/integration/__init__.py +2 -0
  114. examples/task_apps/sokoban/tests/unit/__init__.py +2 -0
  115. examples/task_apps/verilog/eval_groq_qwen32b.toml +8 -4
  116. examples/task_apps/verilog/filter_sft.toml +5 -0
  117. examples/task_apps/verilog/task_app/grpo_verilog.py +258 -23
  118. examples/task_apps/verilog/tests/__init__.py +2 -0
  119. examples/task_apps/verilog/tests/integration/__init__.py +2 -0
  120. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +2 -0
  121. examples/task_apps/verilog/tests/unit/__init__.py +2 -0
  122. examples/vlm/README.md +3 -3
  123. examples/vlm/configs/crafter_vlm_gpt4o.toml +2 -0
  124. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  125. examples/vlm/filter_image_rows.py +1 -1
  126. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  127. examples/warming_up_to_rl/_utils.py +92 -0
  128. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  129. examples/warming_up_to_rl/configs/crafter_fft.toml +2 -0
  130. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +2 -0
  131. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  132. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  133. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  134. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  135. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  136. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  137. examples/warming_up_to_rl/groq_test.py +2 -0
  138. examples/warming_up_to_rl/readme.md +63 -132
  139. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  140. examples/warming_up_to_rl/run_local_rollout.py +2 -0
  141. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -0
  142. examples/warming_up_to_rl/run_local_rollout_parallel.py +2 -0
  143. examples/warming_up_to_rl/run_local_rollout_traced.py +2 -0
  144. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  145. examples/warming_up_to_rl/run_rollout_remote.py +2 -0
  146. examples/warming_up_to_rl/task_app/README.md +42 -0
  147. examples/warming_up_to_rl/task_app/grpo_crafter.py +696 -0
  148. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  149. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  150. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  152. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  153. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  154. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  155. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  156. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  157. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +478 -0
  158. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  159. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  160. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  161. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  162. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  163. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  164. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  165. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1081 -0
  166. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  167. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  168. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  169. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  170. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  171. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  172. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  173. synth_ai/__init__.py +44 -30
  174. synth_ai/_utils/__init__.py +47 -0
  175. synth_ai/_utils/base_url.py +10 -0
  176. synth_ai/_utils/http.py +10 -0
  177. synth_ai/_utils/prompts.py +10 -0
  178. synth_ai/_utils/task_app_state.py +12 -0
  179. synth_ai/_utils/user_config.py +10 -0
  180. synth_ai/api/models/supported.py +145 -7
  181. synth_ai/api/train/__init__.py +13 -1
  182. synth_ai/api/train/cli.py +30 -7
  183. synth_ai/api/train/config_finder.py +18 -11
  184. synth_ai/api/train/env_resolver.py +13 -10
  185. synth_ai/cli/__init__.py +66 -49
  186. synth_ai/cli/_modal_wrapper.py +9 -6
  187. synth_ai/cli/_typer_patch.py +0 -2
  188. synth_ai/cli/_validate_task_app.py +22 -4
  189. synth_ai/cli/legacy_root_backup.py +3 -1
  190. synth_ai/cli/lib/__init__.py +10 -0
  191. synth_ai/cli/lib/task_app_discovery.py +7 -0
  192. synth_ai/cli/lib/task_app_env.py +518 -0
  193. synth_ai/cli/recent.py +1 -0
  194. synth_ai/cli/setup.py +266 -0
  195. synth_ai/cli/task_app_deploy.py +16 -0
  196. synth_ai/cli/task_app_list.py +25 -0
  197. synth_ai/cli/task_app_modal_serve.py +16 -0
  198. synth_ai/cli/task_app_serve.py +18 -0
  199. synth_ai/cli/task_apps.py +392 -141
  200. synth_ai/cli/train.py +18 -0
  201. synth_ai/cli/tui.py +62 -0
  202. synth_ai/demos/__init__.py +10 -0
  203. synth_ai/demos/core/__init__.py +28 -1
  204. synth_ai/demos/crafter/__init__.py +1 -0
  205. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  206. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  207. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  208. synth_ai/demos/demo_registry.py +176 -0
  209. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  210. synth_ai/demos/math/__init__.py +1 -0
  211. synth_ai/demos/math/_common.py +16 -0
  212. synth_ai/demos/math/app.py +38 -0
  213. synth_ai/demos/math/config.toml +76 -0
  214. synth_ai/demos/math/deploy_modal.py +54 -0
  215. synth_ai/demos/math/modal_task_app.py +702 -0
  216. synth_ai/demos/math/task_app_entry.py +51 -0
  217. synth_ai/environments/environment/core.py +7 -1
  218. synth_ai/environments/examples/bandit/engine.py +0 -1
  219. synth_ai/environments/examples/bandit/environment.py +0 -1
  220. synth_ai/environments/examples/crafter_classic/environment.py +1 -1
  221. synth_ai/environments/examples/verilog/engine.py +76 -10
  222. synth_ai/environments/examples/wordle/environment.py +0 -1
  223. synth_ai/evals/base.py +16 -5
  224. synth_ai/evals/client.py +1 -1
  225. synth_ai/inference/client.py +1 -1
  226. synth_ai/learning/client.py +1 -1
  227. synth_ai/learning/health.py +1 -1
  228. synth_ai/learning/jobs.py +1 -1
  229. synth_ai/learning/rl/client.py +1 -1
  230. synth_ai/learning/rl/env_keys.py +1 -1
  231. synth_ai/learning/rl/secrets.py +1 -1
  232. synth_ai/learning/sft/client.py +1 -1
  233. synth_ai/learning/sft/data.py +407 -4
  234. synth_ai/learning/validators.py +4 -1
  235. synth_ai/task/__init__.py +11 -1
  236. synth_ai/task/apps/__init__.py +5 -2
  237. synth_ai/task/config.py +259 -0
  238. synth_ai/task/contracts.py +15 -2
  239. synth_ai/task/rubrics/__init__.py +4 -2
  240. synth_ai/task/rubrics/loaders.py +27 -4
  241. synth_ai/task/rubrics/scoring.py +3 -0
  242. synth_ai/task/rubrics.py +219 -0
  243. synth_ai/task/trace_correlation_helpers.py +328 -0
  244. synth_ai/task/tracing_utils.py +14 -3
  245. synth_ai/task/validators.py +145 -2
  246. synth_ai/tracing_v3/config.py +15 -13
  247. synth_ai/tracing_v3/constants.py +21 -0
  248. synth_ai/tracing_v3/db_config.py +3 -1
  249. synth_ai/tracing_v3/decorators.py +10 -7
  250. synth_ai/tracing_v3/session_tracer.py +10 -0
  251. synth_ai/tracing_v3/turso/daemon.py +2 -2
  252. synth_ai/tracing_v3/turso/native_manager.py +108 -77
  253. synth_ai/tracing_v3/utils.py +1 -1
  254. synth_ai/tui/__init__.py +5 -0
  255. synth_ai/tui/__main__.py +13 -0
  256. synth_ai/tui/cli/__init__.py +1 -0
  257. synth_ai/tui/cli/query_experiments.py +164 -0
  258. synth_ai/tui/cli/query_experiments_v3.py +164 -0
  259. synth_ai/tui/dashboard.py +911 -0
  260. synth_ai/utils/__init__.py +101 -0
  261. synth_ai/utils/base_url.py +94 -0
  262. synth_ai/utils/cli.py +131 -0
  263. synth_ai/utils/env.py +287 -0
  264. synth_ai/utils/http.py +169 -0
  265. synth_ai/utils/modal.py +308 -0
  266. synth_ai/utils/process.py +212 -0
  267. synth_ai/utils/prompts.py +39 -0
  268. synth_ai/utils/sqld.py +122 -0
  269. synth_ai/utils/task_app_discovery.py +882 -0
  270. synth_ai/utils/task_app_env.py +186 -0
  271. synth_ai/utils/task_app_state.py +318 -0
  272. synth_ai/utils/user_config.py +137 -0
  273. synth_ai/v0/config/__init__.py +1 -5
  274. synth_ai/v0/config/base_url.py +1 -7
  275. synth_ai/v0/tracing/config.py +1 -1
  276. synth_ai/v0/tracing/decorators.py +1 -1
  277. synth_ai/v0/tracing/upload.py +1 -1
  278. synth_ai/v0/tracing_v1/config.py +1 -1
  279. synth_ai/v0/tracing_v1/decorators.py +1 -1
  280. synth_ai/v0/tracing_v1/upload.py +1 -1
  281. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/METADATA +85 -31
  282. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/RECORD +286 -135
  283. synth_ai/cli/man.py +0 -106
  284. synth_ai/compound/cais.py +0 -0
  285. synth_ai/core/experiment.py +0 -13
  286. synth_ai/core/system.py +0 -15
  287. synth_ai/demo_registry.py +0 -295
  288. synth_ai/handshake.py +0 -109
  289. synth_ai/http.py +0 -26
  290. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/WHEEL +0 -0
  291. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/entry_points.txt +0 -0
  292. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/licenses/LICENSE +0 -0
  293. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,101 @@
1
+ from . import task_app_state
2
+ from .base_url import PROD_BASE_URL_DEFAULT, get_backend_from_env, get_learning_v2_base_url
3
+ from .cli import PromptedChoiceOption, PromptedChoiceType, print_next_step
4
+ from .env import mask_str, resolve_env_var, write_env_var_to_dotenv, write_env_var_to_json
5
+ from .http import AsyncHttpClient, HTTPError, http_request
6
+ from .modal import (
7
+ ensure_modal_installed,
8
+ ensure_task_app_ready,
9
+ find_asgi_apps,
10
+ is_local_demo_url,
11
+ is_modal_public_url,
12
+ normalize_endpoint_url,
13
+ )
14
+ from .process import ensure_local_port_available, popen_capture, popen_stream, popen_stream_capture
15
+ from .sqld import SQLD_VERSION, find_sqld_binary, install_sqld
16
+ from .task_app_discovery import AppChoice, discover_eval_config_paths, select_app_choice
17
+ from .task_app_env import ensure_env_credentials, ensure_port_free, preflight_env_key
18
+ from .task_app_state import (
19
+ DEFAULT_TASK_APP_SECRET_NAME,
20
+ current_task_app_id,
21
+ load_demo_dir,
22
+ load_template_id,
23
+ now_iso,
24
+ persist_api_key,
25
+ persist_demo_dir,
26
+ persist_env_api_key,
27
+ persist_task_url,
28
+ persist_template_id,
29
+ read_task_app_config,
30
+ record_task_app,
31
+ resolve_task_app_entry,
32
+ task_app_config_path,
33
+ task_app_id_from_path,
34
+ update_task_app_entry,
35
+ write_task_app_config,
36
+ )
37
+ from .user_config import (
38
+ USER_CONFIG_PATH,
39
+ load_user_config,
40
+ load_user_env,
41
+ save_user_config,
42
+ update_user_config,
43
+ )
44
+
45
+ __all__ = [
46
+ "AppChoice",
47
+ "AsyncHttpClient",
48
+ "DEFAULT_TASK_APP_SECRET_NAME",
49
+ "HTTPError",
50
+ "PROD_BASE_URL_DEFAULT",
51
+ "PromptedChoiceOption",
52
+ "PromptedChoiceType",
53
+ "SQLD_VERSION",
54
+ "USER_CONFIG_PATH",
55
+ "current_task_app_id",
56
+ "discover_eval_config_paths",
57
+ "ensure_env_credentials",
58
+ "ensure_local_port_available",
59
+ "ensure_modal_installed",
60
+ "ensure_port_free",
61
+ "ensure_task_app_ready",
62
+ "find_asgi_apps",
63
+ "find_sqld_binary",
64
+ "get_backend_from_env",
65
+ "get_learning_v2_base_url",
66
+ "http_request",
67
+ "install_sqld",
68
+ "is_local_demo_url",
69
+ "is_modal_public_url",
70
+ "load_demo_dir",
71
+ "load_template_id",
72
+ "load_user_config",
73
+ "load_user_env",
74
+ "mask_str",
75
+ "normalize_endpoint_url",
76
+ "now_iso",
77
+ "persist_api_key",
78
+ "persist_demo_dir",
79
+ "persist_env_api_key",
80
+ "persist_task_url",
81
+ "persist_template_id",
82
+ "popen_capture",
83
+ "popen_stream",
84
+ "popen_stream_capture",
85
+ "preflight_env_key",
86
+ "print_next_step",
87
+ "read_task_app_config",
88
+ "record_task_app",
89
+ "resolve_env_var",
90
+ "resolve_task_app_entry",
91
+ "save_user_config",
92
+ "select_app_choice",
93
+ "task_app_config_path",
94
+ "task_app_id_from_path",
95
+ "task_app_state",
96
+ "update_task_app_entry",
97
+ "update_user_config",
98
+ "write_env_var_to_dotenv",
99
+ "write_env_var_to_json",
100
+ "write_task_app_config",
101
+ ]
@@ -0,0 +1,94 @@
1
+ """
2
+ Base URL resolution for learning-v2 and related backend APIs.
3
+
4
+ Default to production, allow overrides via environment variables:
5
+ - LEARNING_V2_BASE_URL (highest precedence)
6
+ - SYNTH_BASE_URL (legacy)
7
+ - SYNTH_LOCAL_BASE_URL
8
+ - SYNTH_DEV_BASE_URL
9
+ - SYNTH_PROD_BASE_URL (fallback if none provided)
10
+
11
+ Normalization: ensure the returned URL ends with "/api".
12
+ """
13
+
14
+ import os
15
+ from typing import Literal
16
+
17
+ PROD_BASE_URL_DEFAULT = "https://agent-learning.onrender.com"
18
+
19
+
20
+ def _normalize_base(url: str) -> str:
21
+ url = url.strip()
22
+ if url.endswith("/v1"):
23
+ url = url[:-3]
24
+ url = url.rstrip("/")
25
+ if not url.endswith("/api"):
26
+ url = f"{url}/api"
27
+ return url
28
+
29
+
30
+ def get_learning_v2_base_url(mode: Literal["dev", "prod"] = "prod") -> str:
31
+ if mode == "prod":
32
+ prod = os.getenv("SYNTH_PROD_BASE_URL") or PROD_BASE_URL_DEFAULT
33
+ return _normalize_base(prod)
34
+ env_url = os.getenv("LEARNING_V2_BASE_URL")
35
+ if env_url:
36
+ return _normalize_base(env_url)
37
+
38
+ legacy = os.getenv("SYNTH_BASE_URL")
39
+ if legacy:
40
+ return _normalize_base(legacy)
41
+
42
+ local = os.getenv("SYNTH_LOCAL_BASE_URL")
43
+ if local:
44
+ return _normalize_base(local)
45
+
46
+ dev = os.getenv("SYNTH_DEV_BASE_URL")
47
+ if dev:
48
+ return _normalize_base(dev)
49
+
50
+ raise ValueError("No base URL configured. Set one of: LEARNING_V2_BASE_URL, SYNTH_BASE_URL, SYNTH_LOCAL_BASE_URL, SYNTH_DEV_BASE_URL")
51
+
52
+
53
+ def _resolve_override_mode() -> str:
54
+ ov = (os.getenv("SYNTH_BACKEND_URL_OVERRIDE", "") or "").strip().lower()
55
+ if ov in {"local", "dev", "prod"}:
56
+ return ov
57
+ return "prod"
58
+
59
+
60
+ def get_backend_from_env() -> tuple[str, str]:
61
+ direct_override = (os.getenv("BACKEND_OVERRIDE") or "").strip()
62
+ if direct_override:
63
+ base = direct_override.rstrip("/")
64
+ if base.endswith("/api"):
65
+ base = base[: -len("/api")]
66
+ api_key = os.getenv("SYNTH_API_KEY", "").strip()
67
+ return base, api_key
68
+
69
+ mode = _resolve_override_mode()
70
+ if mode == "local":
71
+ base = os.getenv("LOCAL_BACKEND_URL", "http://localhost:8000")
72
+ key = os.getenv("TESTING_LOCAL_SYNTH_API_KEY", "")
73
+ return base.rstrip("/"), key
74
+ if mode == "dev":
75
+ base = os.getenv("DEV_BACKEND_URL", "") or "http://localhost:8000"
76
+ key = os.getenv("DEV_SYNTH_API_KEY", "")
77
+ return base.rstrip("/"), key
78
+ base = os.getenv("PROD_BACKEND_URL", f"{PROD_BASE_URL_DEFAULT}")
79
+ base = base.rstrip("/")
80
+ if base.endswith("/api"):
81
+ base = base[: -len("/api")]
82
+ key = (
83
+ os.getenv("PROD_SYNTH_API_KEY", "")
84
+ or os.getenv("TESTING_PROD_SYNTH_API_KEY", "")
85
+ or os.getenv("SYNTH_API_KEY", "")
86
+ )
87
+ return base, key
88
+
89
+
90
+ __all__ = [
91
+ "PROD_BASE_URL_DEFAULT",
92
+ "get_backend_from_env",
93
+ "get_learning_v2_base_url",
94
+ ]
synth_ai/utils/cli.py ADDED
@@ -0,0 +1,131 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any, cast
3
+
4
+ import click
5
+
6
+
7
+ class PromptedChoiceType(click.Choice):
8
+ """`click.Choice` variant that reprompts with an interactive menu on failure.
9
+
10
+ Example
11
+ -------
12
+ ```python
13
+ import click
14
+
15
+ from synth_ai.utils.cli import PromptedChoiceType, PromptedChoiceOption
16
+
17
+
18
+ @click.command()
19
+ @click.option(
20
+ "--mode",
21
+ cls=PromptedChoiceOption,
22
+ type=PromptedChoiceType(["sft", "rl"]),
23
+ required=True,
24
+ )
25
+ def train(mode: str) -> None:
26
+ click.echo(f"Selected mode: {mode}")
27
+ ```
28
+ """
29
+
30
+ def convert(
31
+ self,
32
+ value: Any,
33
+ param: click.Parameter | None,
34
+ ctx: click.Context | None,
35
+ ) -> str:
36
+ """Validate *value*; prompt for a replacement when it is missing or invalid."""
37
+ if param is None:
38
+ raise RuntimeError("Invalid parameter")
39
+ if ctx is None:
40
+ raise RuntimeError("Invalid context")
41
+ if value in (None, ""):
42
+ return self._prompt_user(param, ctx)
43
+ try:
44
+ return super().convert(value, param, ctx)
45
+ except click.BadParameter:
46
+ cmd_name = self._get_cmd_name(ctx)
47
+ if getattr(param, "opts", None):
48
+ click.echo(f'\n[{cmd_name}] Invalid value "{value}" for {self._get_arg_name(param)}')
49
+ else:
50
+ click.echo(f'\n[{cmd_name}] Invalid value "{value}"')
51
+ return self._prompt_user(param, ctx)
52
+
53
+ def _prompt_user(
54
+ self,
55
+ param: click.Parameter,
56
+ ctx: click.Context | None,
57
+ ) -> str:
58
+ """Render a numbered picker and return the user’s selection."""
59
+ arg_name = self._get_arg_name(param)
60
+ click.echo(f"\n[{self._get_cmd_name(ctx)}] Please choose a value for {arg_name}")
61
+ for index, choice in enumerate(self.choices, 1):
62
+ click.echo(f" [{index}] {choice}")
63
+ while True:
64
+ selection = click.prompt("> ", type=int)
65
+ if 1 <= selection <= len(self.choices):
66
+ return cast(str, self.choices[selection - 1])
67
+ click.echo(f"Invalid selection for {arg_name}, please try again")
68
+
69
+ def _get_cmd_name(self, ctx: click.Context | None) -> str:
70
+ """Safely extract the current command name for diagnostic output."""
71
+ cmd = getattr(ctx, "command", None) if ctx is not None else None
72
+ if cmd is None:
73
+ return "?"
74
+ name = getattr(cmd, "name", None)
75
+ return name or "?"
76
+
77
+ def _get_arg_name(self, param: click.Parameter) -> str:
78
+ """Return the most human-friendly identifier for the parameter."""
79
+ opts = getattr(param, "opts", None)
80
+ if opts:
81
+ return opts[-1]
82
+ name = getattr(param, "name", None)
83
+ if name:
84
+ return name
85
+ human_name = getattr(param, "human_readable_name", None)
86
+ if human_name:
87
+ return human_name
88
+ return "?"
89
+
90
+
91
+ class PromptedChoiceOption(click.Option):
92
+ """`click.Option` subclass that triggers the interactive picker when missing.
93
+
94
+ Example
95
+ -------
96
+ ```python
97
+ import click
98
+
99
+ from synth_ai.utils.cli import PromptedChoiceType, PromptedChoiceOption
100
+
101
+
102
+ @click.command()
103
+ @click.option(
104
+ "--mode",
105
+ cls=PromptedChoiceOption,
106
+ type=PromptedChoiceType(["sft", "rl"]),
107
+ required=True,
108
+ )
109
+ def train(mode: str) -> None:
110
+ click.echo(f"Selected mode: {mode}")
111
+ ```
112
+ """
113
+
114
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
115
+ kwargs.setdefault("prompt", True)
116
+ kwargs.setdefault("prompt_required", True)
117
+ super().__init__(*args, **kwargs)
118
+
119
+ def prompt_for_value(self, ctx: click.Context) -> Any:
120
+ """Invoke the choice picker when the option was omitted."""
121
+ option_type = getattr(self, "type", None)
122
+ if isinstance(option_type, PromptedChoiceType):
123
+ return option_type._prompt_user(self, ctx)
124
+ return super().prompt_for_value(ctx)
125
+
126
+
127
+ def print_next_step(message: str, lines: Sequence[str]) -> None:
128
+ print(f"\n➡️ Next, {message}:")
129
+ for line in lines:
130
+ print(f" {line}")
131
+ print("")
synth_ai/utils/env.py ADDED
@@ -0,0 +1,287 @@
1
+ import json
2
+ import os
3
+ import string
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ _ENV_SAFE_CHARS = set(string.ascii_letters + string.digits + "_-./:@+=")
9
+
10
+
11
+ def _format_env_value(value: str) -> str:
12
+ if value == "":
13
+ return '""'
14
+ if all(char in _ENV_SAFE_CHARS for char in value):
15
+ return value
16
+ return json.dumps(value)
17
+
18
+
19
+ def _strip_inline_comment(value: str) -> str:
20
+ in_single = False
21
+ in_double = False
22
+ escaped = False
23
+ for idx, char in enumerate(value):
24
+ if escaped:
25
+ escaped = False
26
+ continue
27
+ if char == "\\":
28
+ escaped = True
29
+ continue
30
+ if char == "'" and not in_double:
31
+ in_single = not in_single
32
+ continue
33
+ if char == '"' and not in_single:
34
+ in_double = not in_double
35
+ continue
36
+ if char == '#' and not in_single and not in_double:
37
+ return value[:idx].rstrip()
38
+ return value.rstrip()
39
+
40
+
41
+ def _parse_env_assignment(line: str) -> tuple[str, str] | None:
42
+ stripped = line.strip()
43
+ if not stripped or stripped.startswith('#'):
44
+ return None
45
+ if stripped.lower().startswith("export "):
46
+ stripped = stripped[7:].lstrip()
47
+ if '=' not in stripped:
48
+ return None
49
+ key_part, value_part = stripped.split('=', 1)
50
+ key = key_part.strip()
51
+ if not key:
52
+ return None
53
+ value_candidate = _strip_inline_comment(value_part.strip())
54
+ if not value_candidate:
55
+ return key, ""
56
+ if (
57
+ len(value_candidate) >= 2
58
+ and value_candidate[0] in {'"', "'"}
59
+ and value_candidate[-1] == value_candidate[0]
60
+ ):
61
+ value = value_candidate[1:-1]
62
+ else:
63
+ value = value_candidate
64
+ return key, value
65
+
66
+
67
+ def _prompt_manual_env_value(key: str) -> str:
68
+ while True:
69
+ value = click.prompt(
70
+ f"Enter value for {key}",
71
+ hide_input=False,
72
+ default="",
73
+ show_default=False,
74
+ type=str,
75
+ ).strip()
76
+ if value:
77
+ return value
78
+ if click.confirm("Save empty value?", default=False):
79
+ return ""
80
+ click.echo("Empty value discarded; enter a value or confirm empty to continue")
81
+
82
+
83
+ def mask_str(input: str, position: int = 3) -> str:
84
+ return input[:position] + "..." + input[-position:] if len(input) > position * 2 else "***"
85
+
86
+
87
+ def get_env_file_paths(base_dir: str | Path = '.') -> list[Path]:
88
+ base = Path(base_dir).resolve()
89
+ return [path for path in base.rglob(".env*") if path.is_file()]
90
+
91
+
92
+ def get_synth_config_file_paths() -> list[Path]:
93
+ dir = Path.home() / ".synth-ai"
94
+ if not dir.exists():
95
+ return []
96
+ return [path for path in dir.glob("*.json") if path.is_file()]
97
+
98
+
99
+ def filter_env_files_by_key(key: str, paths: list[Path]) -> list[tuple[Path, str]]:
100
+ matches: list[tuple[Path, str]] = []
101
+ for path in paths:
102
+ try:
103
+ with path.open('r', encoding="utf-8") as file:
104
+ for line in file:
105
+ parsed = _parse_env_assignment(line)
106
+ if parsed is None:
107
+ continue
108
+ parsed_key, value = parsed
109
+ if parsed_key == key:
110
+ matches.append((path, value))
111
+ break
112
+ except (OSError, UnicodeDecodeError):
113
+ continue
114
+ return matches
115
+
116
+
117
+ def filter_json_files_by_key(key: str, paths: list[Path]) -> list[tuple[Path, str]]:
118
+ matches: list[tuple[Path, str]] = []
119
+ for path in paths:
120
+ try:
121
+ with path.open('r', encoding="utf-8") as file:
122
+ data = json.load(file)
123
+ if key in data and isinstance(data[key], str):
124
+ matches.append((path, data[key]))
125
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
126
+ continue
127
+ return matches
128
+
129
+
130
+ def resolve_env_var(key: str) -> None:
131
+ env_value = os.getenv(key)
132
+ if env_value is not None:
133
+ click.echo(f"Using {key}={mask_str(env_value)} from process environment")
134
+ return
135
+
136
+ value: str = ""
137
+
138
+ env_file_paths = filter_env_files_by_key(key, get_env_file_paths())
139
+ synth_file_paths = filter_json_files_by_key(key, get_synth_config_file_paths())
140
+
141
+ options: list[tuple[str, str]] = []
142
+ for path, value in env_file_paths:
143
+ resolved_path = path.resolve()
144
+ try:
145
+ rel_path = str(resolved_path.relative_to(Path.cwd()))
146
+ except ValueError:
147
+ rel_path = str(resolved_path)
148
+ label = f"({rel_path}) {mask_str(value)}"
149
+ options.append((label, value))
150
+ for path, value in synth_file_paths:
151
+ label = f"({path}) {mask_str(value)}"
152
+ options.append((label, value))
153
+
154
+ if options:
155
+ click.echo(f"\nFound the following options for {key}")
156
+ for i, (label, _) in enumerate(options, start=1):
157
+ click.echo(f" [{i}] {label}")
158
+ click.echo(" [m] Enter value manually")
159
+ click.echo()
160
+
161
+ while True:
162
+ try:
163
+ choice = click.prompt(
164
+ "Select option",
165
+ default=1,
166
+ type=str,
167
+ show_choices=False,
168
+ ).strip()
169
+ except click.Abort:
170
+ return
171
+
172
+ if choice.lower() == 'm':
173
+ value = _prompt_manual_env_value(key)
174
+ break
175
+
176
+ try:
177
+ index = int(choice)
178
+ except ValueError:
179
+ click.echo('Invalid selection. Enter a number or "m".')
180
+ continue
181
+
182
+ if 1 <= index <= len(options):
183
+ _, value = options[index - 1]
184
+ break
185
+
186
+ click.echo(f"Invalid selection. Enter a number between 1 and {len(options)} or 'm'.")
187
+
188
+ else:
189
+ click.echo(f"No value found for {key}")
190
+ value = _prompt_manual_env_value(key)
191
+
192
+ os.environ[key] = value
193
+ click.echo(f"Loaded {key}={mask_str(value)} into process environment")
194
+ return
195
+
196
+
197
+ def write_env_var_to_dotenv(
198
+ key: str,
199
+ value: str,
200
+ output_file_path: str | Path,
201
+ ) -> None:
202
+ path = Path(output_file_path).expanduser()
203
+ path.parent.mkdir(parents=True, exist_ok=True)
204
+
205
+ encoded_value = _format_env_value(value)
206
+
207
+ lines: list[str] = []
208
+ key_written = False
209
+
210
+ if path.is_file():
211
+ try:
212
+ with path.open('r', encoding="utf-8") as handle:
213
+ lines = handle.readlines()
214
+ except OSError as exc:
215
+ raise click.ClickException(f"Failed to read {path}: {exc}") from exc
216
+
217
+ for index, line in enumerate(lines):
218
+ parsed = _parse_env_assignment(line)
219
+ if parsed is None or parsed[0] != key:
220
+ continue
221
+
222
+ leading_len = len(line) - len(line.lstrip(' \t'))
223
+ leading = line[:leading_len]
224
+ stripped = line.lstrip()
225
+ has_export = stripped.lower().startswith('export ')
226
+ newline = '\n' if line.endswith('\n') else ''
227
+ prefix = 'export ' if has_export else ''
228
+ lines[index] = f"{leading}{prefix}{key}={encoded_value}{newline}"
229
+ key_written = True
230
+ break
231
+
232
+ if not key_written:
233
+ if lines and not lines[-1].endswith('\n'):
234
+ lines[-1] = f"{lines[-1]}\n"
235
+ lines.append(f"{key}={encoded_value}\n")
236
+
237
+ try:
238
+ with path.open('w', encoding="utf-8") as handle:
239
+ handle.writelines(lines)
240
+ except OSError as exc:
241
+ raise click.ClickException(f"Failed to write {path}: {exc}") from exc
242
+
243
+ click.echo(f"Wrote {key}={mask_str(value)} to {path}")
244
+
245
+
246
+ def write_env_var_to_json(
247
+ key: str,
248
+ value: str,
249
+ output_file_path: str | Path,
250
+ ) -> None:
251
+ path = Path(output_file_path).expanduser()
252
+ if path.exists() and not path.is_file():
253
+ raise click.ClickException(f"{path} exists and is not a file")
254
+
255
+ data: dict[str, str] = {}
256
+
257
+ if path.is_file():
258
+ try:
259
+ with path.open('r', encoding="utf-8") as handle:
260
+ existing = json.load(handle)
261
+ except json.JSONDecodeError as exc:
262
+ raise click.ClickException(f"Invalid JSON in {path}: {exc}") from exc
263
+ except OSError as exc:
264
+ raise click.ClickException(f"Failed to read {path}: {exc}") from exc
265
+
266
+ if not isinstance(existing, dict):
267
+ raise click.ClickException(f"Expected JSON object in {path}")
268
+
269
+ for existing_key, existing_value in existing.items():
270
+ if existing_key == key:
271
+ continue
272
+ data[str(existing_key)] = (
273
+ existing_value if isinstance(existing_value, str) else str(existing_value)
274
+ )
275
+
276
+ data[key] = value
277
+
278
+ path.parent.mkdir(parents=True, exist_ok=True)
279
+
280
+ try:
281
+ with path.open('w', encoding="utf-8") as handle:
282
+ json.dump(data, handle, indent=2, sort_keys=True)
283
+ handle.write('\n')
284
+ except OSError as exc:
285
+ raise click.ClickException(f"Failed to write {path}: {exc}") from exc
286
+
287
+ click.echo(f"Wrote {key}={mask_str(value)} to {path}")