synth-ai 0.2.14__py3-none-any.whl → 0.2.17__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 (354) hide show
  1. examples/README.md +1 -0
  2. examples/analyze_semantic_words.sh +2 -2
  3. examples/blog_posts/pokemon_vl/README.md +98 -0
  4. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -0
  5. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  6. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  7. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +42 -0
  8. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  9. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  10. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  11. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  12. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  13. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  14. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +41 -0
  15. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  16. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  17. examples/multi_step/SFT_README.md +147 -0
  18. examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
  19. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +73 -115
  20. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
  21. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
  22. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  23. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  24. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  25. examples/multi_step/convert_traces_to_sft.py +84 -0
  26. examples/multi_step/run_sft_qwen30b.sh +45 -0
  27. examples/qwen_coder/configs/coder_lora_30b.toml +1 -2
  28. examples/qwen_coder/configs/coder_lora_4b.toml +5 -1
  29. examples/qwen_coder/configs/coder_lora_small.toml +1 -2
  30. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  31. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  32. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  33. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  34. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  35. examples/qwen_vl/QUICKSTART.md +327 -0
  36. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  37. examples/qwen_vl/README.md +152 -0
  38. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  39. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  40. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  41. examples/qwen_vl/SETUP_COMPLETE.md +274 -0
  42. examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
  43. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  44. examples/qwen_vl/__init__.py +2 -0
  45. examples/qwen_vl/collect_data_via_cli.md +415 -0
  46. examples/qwen_vl/collect_vision_traces.py +368 -0
  47. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
  48. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
  49. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
  50. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  51. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
  52. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  53. examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
  54. examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
  55. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  56. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  57. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  58. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  59. examples/qwen_vl/run_vision_comparison.sh +61 -0
  60. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  61. examples/qwen_vl/test_image_validation.py +201 -0
  62. examples/qwen_vl/test_sft_vision_data.py +110 -0
  63. examples/rl/README.md +6 -6
  64. examples/rl/configs/eval_base_qwen.toml +17 -0
  65. examples/rl/configs/eval_rl_qwen.toml +13 -0
  66. examples/rl/configs/rl_from_base_qwen.toml +62 -0
  67. examples/rl/configs/rl_from_base_qwen17.toml +79 -0
  68. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  69. examples/rl/run_eval.py +436 -0
  70. examples/rl/run_rl_and_save.py +111 -0
  71. examples/rl/task_app/README.md +21 -0
  72. examples/rl/task_app/math_single_step.py +990 -0
  73. examples/rl/task_app/math_task_app.py +111 -0
  74. examples/run_crafter_demo.sh +2 -2
  75. examples/sft/README.md +6 -6
  76. examples/sft/configs/crafter_fft_qwen0p6b.toml +7 -2
  77. examples/sft/configs/crafter_lora_qwen0p6b.toml +7 -3
  78. examples/sft/evaluate.py +2 -4
  79. examples/sft/export_dataset.py +7 -4
  80. examples/swe/task_app/README.md +33 -3
  81. examples/swe/task_app/grpo_swe_mini.py +4 -1
  82. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  83. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  84. examples/swe/task_app/hosted/envs/mini_swe/environment.py +50 -23
  85. examples/swe/task_app/hosted/inference/openai_client.py +4 -4
  86. examples/swe/task_app/hosted/policy_routes.py +0 -2
  87. examples/swe/task_app/hosted/rollout.py +0 -8
  88. examples/swe/task_app/morph_backend.py +178 -0
  89. examples/task_apps/crafter/task_app/README.md +1 -1
  90. examples/task_apps/crafter/task_app/grpo_crafter.py +70 -10
  91. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  92. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +63 -27
  93. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  94. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +48 -50
  95. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +75 -36
  96. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +31 -15
  97. examples/task_apps/enron/__init__.py +1 -0
  98. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  99. examples/task_apps/math/README.md +1 -2
  100. examples/task_apps/pokemon_red/README.md +3 -4
  101. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  102. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  103. examples/task_apps/pokemon_red/task_app.py +36 -5
  104. examples/task_apps/sokoban/README.md +2 -3
  105. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  106. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  107. examples/vlm/README.md +3 -3
  108. examples/vlm/configs/crafter_vlm_gpt4o.toml +5 -0
  109. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  110. examples/vlm/filter_image_rows.py +1 -1
  111. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  112. examples/warming_up_to_rl/_utils.py +92 -0
  113. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  114. examples/warming_up_to_rl/configs/crafter_fft.toml +5 -0
  115. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  116. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  117. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  118. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  119. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  120. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  121. examples/warming_up_to_rl/readme.md +63 -132
  122. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  123. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  124. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  125. examples/warming_up_to_rl/task_app/README.md +42 -0
  126. examples/warming_up_to_rl/task_app/grpo_crafter.py +827 -0
  127. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  128. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  129. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  130. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  131. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  132. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  133. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  134. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  135. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  136. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
  137. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  138. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  139. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1084 -0
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  147. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  148. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  149. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  150. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  152. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  153. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
  154. synth_ai/__init__.py +44 -30
  155. synth_ai/_utils/__init__.py +47 -0
  156. synth_ai/_utils/base_url.py +10 -0
  157. synth_ai/_utils/http.py +10 -0
  158. synth_ai/_utils/prompts.py +10 -0
  159. synth_ai/_utils/task_app_state.py +12 -0
  160. synth_ai/_utils/user_config.py +10 -0
  161. synth_ai/api/models/supported.py +144 -7
  162. synth_ai/api/train/__init__.py +13 -1
  163. synth_ai/api/train/builders.py +9 -3
  164. synth_ai/api/train/cli.py +155 -17
  165. synth_ai/api/train/config_finder.py +18 -11
  166. synth_ai/api/train/configs/__init__.py +8 -1
  167. synth_ai/api/train/configs/rl.py +32 -7
  168. synth_ai/api/train/configs/sft.py +6 -2
  169. synth_ai/api/train/configs/shared.py +59 -2
  170. synth_ai/api/train/env_resolver.py +13 -10
  171. synth_ai/auth/credentials.py +119 -0
  172. synth_ai/cli/__init__.py +61 -69
  173. synth_ai/cli/_modal_wrapper.py +7 -5
  174. synth_ai/cli/_typer_patch.py +0 -2
  175. synth_ai/cli/_validate_task_app.py +22 -4
  176. synth_ai/cli/commands/__init__.py +17 -0
  177. synth_ai/cli/commands/demo/__init__.py +6 -0
  178. synth_ai/cli/commands/demo/core.py +163 -0
  179. synth_ai/cli/commands/deploy/__init__.py +23 -0
  180. synth_ai/cli/commands/deploy/core.py +614 -0
  181. synth_ai/cli/commands/deploy/errors.py +72 -0
  182. synth_ai/cli/commands/deploy/validation.py +11 -0
  183. synth_ai/cli/commands/eval/__init__.py +19 -0
  184. synth_ai/cli/commands/eval/core.py +1109 -0
  185. synth_ai/cli/commands/eval/errors.py +81 -0
  186. synth_ai/cli/commands/eval/validation.py +133 -0
  187. synth_ai/cli/commands/filter/__init__.py +12 -0
  188. synth_ai/cli/commands/filter/core.py +388 -0
  189. synth_ai/cli/commands/filter/errors.py +55 -0
  190. synth_ai/cli/commands/filter/validation.py +77 -0
  191. synth_ai/cli/commands/help/__init__.py +177 -0
  192. synth_ai/cli/commands/help/core.py +73 -0
  193. synth_ai/cli/commands/status/__init__.py +64 -0
  194. synth_ai/cli/commands/status/client.py +192 -0
  195. synth_ai/cli/commands/status/config.py +92 -0
  196. synth_ai/cli/commands/status/errors.py +20 -0
  197. synth_ai/cli/commands/status/formatters.py +164 -0
  198. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  199. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  200. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  201. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  202. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  203. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  204. synth_ai/cli/commands/status/utils.py +114 -0
  205. synth_ai/cli/commands/train/__init__.py +53 -0
  206. synth_ai/cli/commands/train/core.py +21 -0
  207. synth_ai/cli/commands/train/errors.py +117 -0
  208. synth_ai/cli/commands/train/judge_schemas.py +199 -0
  209. synth_ai/cli/commands/train/judge_validation.py +304 -0
  210. synth_ai/cli/commands/train/validation.py +443 -0
  211. synth_ai/cli/demo.py +2 -162
  212. synth_ai/cli/deploy/__init__.py +28 -0
  213. synth_ai/cli/deploy/core.py +5 -0
  214. synth_ai/cli/deploy/errors.py +23 -0
  215. synth_ai/cli/deploy/validation.py +5 -0
  216. synth_ai/cli/eval/__init__.py +36 -0
  217. synth_ai/cli/eval/core.py +5 -0
  218. synth_ai/cli/eval/errors.py +31 -0
  219. synth_ai/cli/eval/validation.py +5 -0
  220. synth_ai/cli/filter/__init__.py +28 -0
  221. synth_ai/cli/filter/core.py +5 -0
  222. synth_ai/cli/filter/errors.py +23 -0
  223. synth_ai/cli/filter/validation.py +5 -0
  224. synth_ai/cli/legacy_root_backup.py +3 -1
  225. synth_ai/cli/lib/__init__.py +10 -0
  226. synth_ai/cli/lib/task_app_discovery.py +7 -0
  227. synth_ai/cli/lib/task_app_env.py +518 -0
  228. synth_ai/cli/modal_serve/__init__.py +12 -0
  229. synth_ai/cli/modal_serve/core.py +14 -0
  230. synth_ai/cli/modal_serve/errors.py +8 -0
  231. synth_ai/cli/modal_serve/validation.py +11 -0
  232. synth_ai/cli/recent.py +2 -1
  233. synth_ai/cli/serve/__init__.py +12 -0
  234. synth_ai/cli/serve/core.py +14 -0
  235. synth_ai/cli/serve/errors.py +8 -0
  236. synth_ai/cli/serve/validation.py +11 -0
  237. synth_ai/cli/setup.py +21 -0
  238. synth_ai/cli/status.py +7 -126
  239. synth_ai/cli/task_app_deploy.py +7 -0
  240. synth_ai/cli/task_app_list.py +25 -0
  241. synth_ai/cli/task_app_modal_serve.py +11 -0
  242. synth_ai/cli/task_app_serve.py +11 -0
  243. synth_ai/cli/task_apps.py +110 -1499
  244. synth_ai/cli/traces.py +1 -1
  245. synth_ai/cli/train/__init__.py +12 -0
  246. synth_ai/cli/train/core.py +21 -0
  247. synth_ai/cli/train/errors.py +8 -0
  248. synth_ai/cli/train/validation.py +24 -0
  249. synth_ai/cli/train.py +5 -0
  250. synth_ai/cli/turso.py +1 -1
  251. synth_ai/cli/watch.py +1 -1
  252. synth_ai/demos/__init__.py +10 -0
  253. synth_ai/demos/core/__init__.py +28 -1
  254. synth_ai/demos/crafter/__init__.py +1 -0
  255. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  256. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  257. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  258. synth_ai/demos/demo_registry.py +176 -0
  259. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  260. synth_ai/demos/math/__init__.py +1 -0
  261. synth_ai/demos/math/_common.py +16 -0
  262. synth_ai/demos/math/app.py +38 -0
  263. synth_ai/demos/math/config.toml +76 -0
  264. synth_ai/demos/math/deploy_modal.py +54 -0
  265. synth_ai/demos/math/modal_task_app.py +702 -0
  266. synth_ai/demos/math/task_app_entry.py +51 -0
  267. synth_ai/environments/environment/core.py +7 -1
  268. synth_ai/environments/examples/bandit/engine.py +0 -1
  269. synth_ai/environments/examples/bandit/environment.py +0 -1
  270. synth_ai/environments/examples/red/engine.py +33 -12
  271. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  272. synth_ai/environments/examples/red/environment.py +26 -0
  273. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  274. synth_ai/environments/examples/wordle/environment.py +0 -1
  275. synth_ai/evals/base.py +16 -5
  276. synth_ai/evals/client.py +1 -1
  277. synth_ai/http.py +8 -22
  278. synth_ai/inference/client.py +1 -1
  279. synth_ai/judge_schemas.py +4 -5
  280. synth_ai/learning/client.py +1 -1
  281. synth_ai/learning/health.py +1 -1
  282. synth_ai/learning/jobs.py +1 -1
  283. synth_ai/learning/rl/client.py +4 -2
  284. synth_ai/learning/rl/env_keys.py +1 -1
  285. synth_ai/learning/rl/secrets.py +1 -1
  286. synth_ai/learning/sft/client.py +1 -1
  287. synth_ai/learning/sft/data.py +407 -4
  288. synth_ai/learning/validators.py +4 -1
  289. synth_ai/streaming/__init__.py +29 -0
  290. synth_ai/streaming/config.py +94 -0
  291. synth_ai/streaming/handlers.py +469 -0
  292. synth_ai/streaming/streamer.py +301 -0
  293. synth_ai/streaming/types.py +95 -0
  294. synth_ai/task/apps/__init__.py +4 -2
  295. synth_ai/task/config.py +6 -4
  296. synth_ai/task/rubrics/__init__.py +1 -2
  297. synth_ai/task/rubrics/loaders.py +14 -10
  298. synth_ai/task/rubrics.py +219 -0
  299. synth_ai/task/trace_correlation_helpers.py +24 -11
  300. synth_ai/task/tracing_utils.py +14 -3
  301. synth_ai/task/validators.py +0 -1
  302. synth_ai/tracing_v3/abstractions.py +3 -3
  303. synth_ai/tracing_v3/config.py +15 -13
  304. synth_ai/tracing_v3/constants.py +21 -0
  305. synth_ai/tracing_v3/db_config.py +3 -1
  306. synth_ai/tracing_v3/decorators.py +10 -7
  307. synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
  308. synth_ai/tracing_v3/migration_helper.py +1 -2
  309. synth_ai/tracing_v3/session_tracer.py +7 -7
  310. synth_ai/tracing_v3/storage/base.py +29 -29
  311. synth_ai/tracing_v3/storage/config.py +3 -3
  312. synth_ai/tracing_v3/turso/daemon.py +8 -9
  313. synth_ai/tracing_v3/turso/native_manager.py +80 -72
  314. synth_ai/tracing_v3/utils.py +2 -2
  315. synth_ai/utils/__init__.py +101 -0
  316. synth_ai/utils/base_url.py +94 -0
  317. synth_ai/utils/cli.py +131 -0
  318. synth_ai/utils/env.py +294 -0
  319. synth_ai/utils/http.py +172 -0
  320. synth_ai/utils/modal.py +308 -0
  321. synth_ai/utils/process.py +212 -0
  322. synth_ai/utils/prompts.py +39 -0
  323. synth_ai/utils/sqld.py +122 -0
  324. synth_ai/utils/task_app_discovery.py +882 -0
  325. synth_ai/utils/task_app_env.py +186 -0
  326. synth_ai/utils/task_app_state.py +318 -0
  327. synth_ai/utils/user_config.py +137 -0
  328. synth_ai/v0/config/__init__.py +1 -5
  329. synth_ai/v0/config/base_url.py +1 -7
  330. synth_ai/v0/tracing/config.py +1 -1
  331. synth_ai/v0/tracing/decorators.py +1 -1
  332. synth_ai/v0/tracing/upload.py +1 -1
  333. synth_ai/v0/tracing_v1/config.py +1 -1
  334. synth_ai/v0/tracing_v1/decorators.py +1 -1
  335. synth_ai/v0/tracing_v1/upload.py +1 -1
  336. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/METADATA +91 -32
  337. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/RECORD +341 -154
  338. synth_ai/cli/man.py +0 -106
  339. synth_ai/cli/tui.py +0 -57
  340. synth_ai/compound/cais.py +0 -0
  341. synth_ai/core/experiment.py +0 -13
  342. synth_ai/core/system.py +0 -15
  343. synth_ai/demo_registry.py +0 -295
  344. synth_ai/handshake.py +0 -109
  345. synth_ai/tui/__init__.py +0 -5
  346. synth_ai/tui/__main__.py +0 -13
  347. synth_ai/tui/cli/__init__.py +0 -1
  348. synth_ai/tui/cli/query_experiments.py +0 -164
  349. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  350. synth_ai/tui/dashboard.py +0 -906
  351. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
  352. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
  353. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
  354. {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from datetime import datetime
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from ..abstractions import SessionTrace
8
8
 
@@ -28,7 +28,7 @@ class TraceStorage(ABC):
28
28
  pass
29
29
 
30
30
  @abstractmethod
31
- async def get_session_trace(self, session_id: str) -> Optional[dict[str, Any]]:
31
+ async def get_session_trace(self, session_id: str) -> dict[str, Any] | None:
32
32
  """Retrieve a session trace by ID.
33
33
 
34
34
  Args:
@@ -40,7 +40,7 @@ class TraceStorage(ABC):
40
40
  pass
41
41
 
42
42
  @abstractmethod
43
- async def query_traces(self, query: str, params: Optional[dict[str, Any]] = None) -> Any:
43
+ async def query_traces(self, query: str, params: dict[str, Any] | None = None) -> Any:
44
44
  """Execute a query and return results.
45
45
 
46
46
  Args:
@@ -55,9 +55,9 @@ class TraceStorage(ABC):
55
55
  @abstractmethod
56
56
  async def get_model_usage(
57
57
  self,
58
- start_date: Optional[datetime] = None,
59
- end_date: Optional[datetime] = None,
60
- model_name: Optional[str] = None,
58
+ start_date: datetime | None = None,
59
+ end_date: datetime | None = None,
60
+ model_name: str | None = None,
61
61
  ) -> Any:
62
62
  """Get model usage statistics.
63
63
 
@@ -95,8 +95,8 @@ class TraceStorage(ABC):
95
95
  self,
96
96
  session_id: str,
97
97
  *,
98
- created_at: Optional[datetime] = None,
99
- metadata: Optional[dict[str, Any]] = None,
98
+ created_at: datetime | None = None,
99
+ metadata: dict[str, Any] | None = None,
100
100
  ) -> None:
101
101
  """Ensure a session row exists for the given session id."""
102
102
  pass
@@ -108,10 +108,10 @@ class TraceStorage(ABC):
108
108
  *,
109
109
  step_id: str,
110
110
  step_index: int,
111
- turn_number: Optional[int] = None,
112
- started_at: Optional[datetime] = None,
113
- completed_at: Optional[datetime] = None,
114
- metadata: Optional[dict[str, Any]] = None,
111
+ turn_number: int | None = None,
112
+ started_at: datetime | None = None,
113
+ completed_at: datetime | None = None,
114
+ metadata: dict[str, Any] | None = None,
115
115
  ) -> int:
116
116
  """Ensure a timestep row exists and return its database id."""
117
117
  pass
@@ -121,9 +121,9 @@ class TraceStorage(ABC):
121
121
  self,
122
122
  session_id: str,
123
123
  *,
124
- timestep_db_id: Optional[int],
124
+ timestep_db_id: int | None,
125
125
  event: Any,
126
- metadata_override: Optional[dict[str, Any]] = None,
126
+ metadata_override: dict[str, Any] | None = None,
127
127
  ) -> int:
128
128
  """Insert an event and return its database id."""
129
129
  pass
@@ -133,12 +133,12 @@ class TraceStorage(ABC):
133
133
  self,
134
134
  session_id: str,
135
135
  *,
136
- timestep_db_id: Optional[int],
136
+ timestep_db_id: int | None,
137
137
  message_type: str,
138
138
  content: Any,
139
- event_time: Optional[float] = None,
140
- message_time: Optional[int] = None,
141
- metadata: Optional[dict[str, Any]] = None,
139
+ event_time: float | None = None,
140
+ message_time: int | None = None,
141
+ metadata: dict[str, Any] | None = None,
142
142
  ) -> int:
143
143
  """Insert a message row linked to a session/timestep."""
144
144
  pass
@@ -151,7 +151,7 @@ class TraceStorage(ABC):
151
151
  total_reward: int,
152
152
  achievements_count: int,
153
153
  total_steps: int,
154
- reward_metadata: Optional[dict] = None,
154
+ reward_metadata: dict | None = None,
155
155
  ) -> int:
156
156
  """Record an outcome reward for a session."""
157
157
  pass
@@ -162,13 +162,13 @@ class TraceStorage(ABC):
162
162
  session_id: str,
163
163
  *,
164
164
  event_id: int,
165
- message_id: Optional[int] = None,
166
- turn_number: Optional[int] = None,
165
+ message_id: int | None = None,
166
+ turn_number: int | None = None,
167
167
  reward_value: float = 0.0,
168
- reward_type: Optional[str] = None,
169
- key: Optional[str] = None,
170
- annotation: Optional[dict[str, Any]] = None,
171
- source: Optional[str] = None,
168
+ reward_type: str | None = None,
169
+ key: str | None = None,
170
+ annotation: dict[str, Any] | None = None,
171
+ source: str | None = None,
172
172
  ) -> int:
173
173
  """Record a reward tied to a specific event."""
174
174
  pass
@@ -178,8 +178,8 @@ class TraceStorage(ABC):
178
178
  self,
179
179
  experiment_id: str,
180
180
  name: str,
181
- description: Optional[str] = None,
182
- configuration: Optional[dict[str, Any]] = None,
181
+ description: str | None = None,
182
+ configuration: dict[str, Any] | None = None,
183
183
  ) -> str:
184
184
  """Create a new experiment."""
185
185
  raise NotImplementedError("Experiment management not supported by this backend")
@@ -189,14 +189,14 @@ class TraceStorage(ABC):
189
189
  raise NotImplementedError("Experiment management not supported by this backend")
190
190
 
191
191
  async def get_sessions_by_experiment(
192
- self, experiment_id: str, limit: Optional[int] = None
192
+ self, experiment_id: str, limit: int | None = None
193
193
  ) -> list[dict[str, Any]]:
194
194
  """Get all sessions for an experiment."""
195
195
  raise NotImplementedError("Experiment management not supported by this backend")
196
196
 
197
197
  # Batch operations
198
198
  async def batch_insert_sessions(
199
- self, traces: list[SessionTrace], batch_size: Optional[int] = 1000
199
+ self, traces: list[SessionTrace], batch_size: int | None = 1000
200
200
  ) -> list[str]:
201
201
  """Batch insert multiple session traces.
202
202
 
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
- from typing import Any, Optional
6
+ from typing import Any
7
7
 
8
8
 
9
9
  class StorageBackend(str, Enum):
@@ -14,7 +14,7 @@ class StorageBackend(str, Enum):
14
14
  POSTGRES = "postgres" # Future support
15
15
 
16
16
 
17
- def _is_enabled(value: Optional[str]) -> bool:
17
+ def _is_enabled(value: str | None) -> bool:
18
18
  if value is None:
19
19
  return False
20
20
  return value.lower() in {"1", "true", "yes", "on"}
@@ -25,7 +25,7 @@ class StorageConfig:
25
25
  """Configuration for storage backend."""
26
26
 
27
27
  backend: StorageBackend = StorageBackend.TURSO_NATIVE
28
- connection_string: Optional[str] = None
28
+ connection_string: str | None = None
29
29
 
30
30
  # Turso-specific settings
31
31
  turso_url: str = os.getenv("TURSO_DATABASE_URL", "sqlite+libsql://http://127.0.0.1:8080")
@@ -7,7 +7,6 @@ import time
7
7
 
8
8
  import requests
9
9
  from requests import RequestException
10
- from typing import Any, Optional
11
10
 
12
11
  from ..config import CONFIG
13
12
 
@@ -17,9 +16,9 @@ class SqldDaemon:
17
16
 
18
17
  def __init__(
19
18
  self,
20
- db_path: Optional[str] = None,
21
- http_port: Optional[int] = None,
22
- binary_path: Optional[str] = None,
19
+ db_path: str | None = None,
20
+ http_port: int | None = None,
21
+ binary_path: str | None = None,
23
22
  ):
24
23
  """Initialize sqld daemon manager.
25
24
 
@@ -31,7 +30,7 @@ class SqldDaemon:
31
30
  self.db_path = db_path or CONFIG.sqld_db_path
32
31
  self.http_port = http_port or CONFIG.sqld_http_port
33
32
  self.binary_path = binary_path or self._find_binary()
34
- self.process: Optional[Any] = None
33
+ self.process: subprocess.Popen[str] | None = None
35
34
 
36
35
  def _find_binary(self) -> str:
37
36
  """Find sqld binary in PATH."""
@@ -85,7 +84,7 @@ class SqldDaemon:
85
84
  pass
86
85
 
87
86
  # Check if process crashed
88
- if self.process.poll() is not None:
87
+ if self.process and self.process.poll() is not None:
89
88
  stdout, stderr = self.process.communicate()
90
89
  raise RuntimeError(
91
90
  f"sqld daemon failed to start:\nstdout: {stdout}\nstderr: {stderr}"
@@ -124,10 +123,10 @@ class SqldDaemon:
124
123
 
125
124
 
126
125
  # Convenience functions
127
- _daemon: Optional[SqldDaemon] = None
126
+ _daemon: SqldDaemon | None = None
128
127
 
129
128
 
130
- def start_sqld(db_path: Optional[str] = None, port: Optional[int] = None) -> SqldDaemon:
129
+ def start_sqld(db_path: str | None = None, port: int | None = None) -> SqldDaemon:
131
130
  """Start a global sqld daemon instance."""
132
131
  global _daemon
133
132
  if _daemon and _daemon.is_running():
@@ -146,6 +145,6 @@ def stop_sqld():
146
145
  _daemon = None
147
146
 
148
147
 
149
- def get_daemon() -> Optional[SqldDaemon]:
148
+ def get_daemon() -> SqldDaemon | None:
150
149
  """Get the global daemon instance."""
151
150
  return _daemon
@@ -13,7 +13,7 @@ import logging
13
13
  import re
14
14
  from collections.abc import Callable
15
15
  from dataclasses import asdict, dataclass
16
- from datetime import datetime, timezone
16
+ from datetime import UTC, datetime
17
17
  from typing import TYPE_CHECKING, Any, cast
18
18
 
19
19
  import libsql
@@ -378,8 +378,10 @@ class NativeLibsqlTraceManager(TraceStorage):
378
378
  session_exists = await self._session_exists(trace.session_id)
379
379
  _logger.info(f"[TRACE_DEBUG] Session exists: {session_exists}")
380
380
 
381
+ step_id_map: dict[str, int] = {}
382
+
381
383
  if session_exists:
382
- _logger.warning(f"[TRACE_DEBUG] Session {trace.session_id} already exists, need to save messages anyway!")
384
+ _logger.warning(f"[TRACE_DEBUG] Session {trace.session_id} already exists, skipping events/timesteps, only updating messages!")
383
385
  # Don't return early - we need to save messages!
384
386
  # Just update metadata
385
387
  async with self._op_lock:
@@ -390,10 +392,9 @@ class NativeLibsqlTraceManager(TraceStorage):
390
392
  (_json_dumps(trace.metadata or {}), trace.session_id),
391
393
  )
392
394
  conn.commit()
393
- # Continue to save messages instead of returning
394
-
395
- if not session_exists:
396
- created_at = trace.created_at or datetime.now(timezone.utc)
395
+ # Skip events and timesteps to ensure idempotency
396
+ else:
397
+ created_at = trace.created_at or datetime.now(UTC)
397
398
 
398
399
  async with self._op_lock:
399
400
  conn = self._conn
@@ -417,73 +418,76 @@ class NativeLibsqlTraceManager(TraceStorage):
417
418
  ),
418
419
  )
419
420
  conn.commit()
420
- _logger.info(f"[TRACE_DEBUG] Session row inserted")
421
-
422
- step_id_map: dict[str, int] = {}
421
+ _logger.info("[TRACE_DEBUG] Session row inserted")
423
422
 
424
- for step in trace.session_time_steps:
425
- step_db_id = await self.ensure_timestep(
426
- trace.session_id,
427
- step_id=step.step_id,
428
- step_index=step.step_index,
429
- turn_number=step.turn_number,
430
- started_at=step.timestamp,
431
- completed_at=step.completed_at,
432
- metadata=step.step_metadata or {},
433
- )
434
- step_id_map[step.step_id] = step_db_id
435
-
436
- for event in trace.event_history:
437
- step_ref = None
438
- metadata = event.metadata or {}
439
- if isinstance(metadata, dict):
440
- step_ref = metadata.get("step_id")
441
- timestep_db_id = step_id_map.get(step_ref) if step_ref else None
442
- await self.insert_event_row(
443
- trace.session_id,
444
- timestep_db_id=timestep_db_id,
445
- event=event,
446
- metadata_override=event.metadata or {},
447
- )
423
+ # Only insert timesteps and events if this is a new session
424
+ for step in trace.session_time_steps:
425
+ step_db_id = await self.ensure_timestep(
426
+ trace.session_id,
427
+ step_id=step.step_id,
428
+ step_index=step.step_index,
429
+ turn_number=step.turn_number,
430
+ started_at=step.timestamp,
431
+ completed_at=step.completed_at,
432
+ metadata=step.step_metadata or {},
433
+ )
434
+ step_id_map[step.step_id] = step_db_id
435
+
436
+ for event in trace.event_history:
437
+ step_ref = None
438
+ metadata = event.metadata or {}
439
+ if isinstance(metadata, dict):
440
+ step_ref = metadata.get("step_id")
441
+ timestep_db_id = step_id_map.get(step_ref) if step_ref else None
442
+ await self.insert_event_row(
443
+ trace.session_id,
444
+ timestep_db_id=timestep_db_id,
445
+ event=event,
446
+ metadata_override=event.metadata or {},
447
+ )
448
448
 
449
449
  import logging as _logging
450
450
  _logger = _logging.getLogger(__name__)
451
- _logger.info(f"[TRACE_DEBUG] insert_session_trace: saving {len(trace.markov_blanket_message_history)} messages")
451
+ _logger.info(f"[TRACE_DEBUG] insert_session_trace: saving {len(trace.markov_blanket_message_history)} messages (session_exists={session_exists})")
452
452
 
453
- for idx, msg in enumerate(trace.markov_blanket_message_history):
454
- metadata = dict(getattr(msg, "metadata", {}) or {})
455
- step_ref = metadata.get("step_id")
456
- content_value = msg.content
457
- if isinstance(msg.content, SessionMessageContent):
458
- if msg.content.json_payload:
459
- metadata.setdefault("json_payload", msg.content.json_payload)
460
- content_value = msg.content.json_payload
461
- else:
462
- content_value = msg.content.as_text()
463
- if msg.content.text:
464
- metadata.setdefault("text", msg.content.text)
465
- elif not isinstance(content_value, str):
453
+ # Only insert messages if this is a new session (for idempotency)
454
+ if not session_exists:
455
+ for idx, msg in enumerate(trace.markov_blanket_message_history):
456
+ metadata = dict(getattr(msg, "metadata", {}) or {})
457
+ step_ref = metadata.get("step_id")
458
+ content_value = msg.content
459
+ if isinstance(msg.content, SessionMessageContent):
460
+ if msg.content.json_payload:
461
+ metadata.setdefault("json_payload", msg.content.json_payload)
462
+ content_value = msg.content.json_payload
463
+ else:
464
+ content_value = msg.content.as_text()
465
+ if msg.content.text:
466
+ metadata.setdefault("text", msg.content.text)
467
+ elif not isinstance(content_value, str):
468
+ try:
469
+ content_value = json.dumps(content_value, ensure_ascii=False)
470
+ except (TypeError, ValueError):
471
+ content_value = str(content_value)
472
+
473
+ _logger.info(f"[TRACE_DEBUG] Message {idx+1}: type={msg.message_type}, content_len={len(str(content_value))}")
474
+
466
475
  try:
467
- content_value = json.dumps(content_value, ensure_ascii=False)
468
- except (TypeError, ValueError):
469
- content_value = str(content_value)
470
-
471
- _logger.info(f"[TRACE_DEBUG] Message {idx+1}: type={msg.message_type}, content_len={len(str(content_value))}")
472
-
473
- try:
474
- await self.insert_message_row(
475
- trace.session_id,
476
- timestep_db_id=step_id_map.get(step_ref) if step_ref else None,
477
- message_type=msg.message_type,
478
- content=content_value,
479
- event_time=msg.time_record.event_time,
480
- message_time=msg.time_record.message_time,
481
- metadata=metadata,
482
- )
483
- _logger.info(f"[TRACE_DEBUG] Message {idx+1}: saved successfully")
484
- except Exception as exc:
485
- _logger.error(f"[TRACE_DEBUG] Message {idx+1}: FAILED TO SAVE: {exc}", exc_info=True)
486
- raise
476
+ await self.insert_message_row(
477
+ trace.session_id,
478
+ timestep_db_id=step_id_map.get(step_ref) if step_ref else None,
479
+ message_type=msg.message_type,
480
+ content=content_value,
481
+ event_time=msg.time_record.event_time,
482
+ message_time=msg.time_record.message_time,
483
+ metadata=metadata,
484
+ )
485
+ _logger.info(f"[TRACE_DEBUG] Message {idx+1}: saved successfully")
486
+ except Exception as exc:
487
+ _logger.error(f"[TRACE_DEBUG] Message {idx+1}: FAILED TO SAVE: {exc}", exc_info=True)
488
+ raise
489
+ else:
490
+ _logger.info("[TRACE_DEBUG] Skipping message insertion for existing session (idempotency)")
487
491
 
488
492
  async with self._op_lock:
489
493
  conn = self._conn
@@ -806,7 +810,7 @@ class NativeLibsqlTraceManager(TraceStorage):
806
810
  ) -> None:
807
811
  await self.initialize()
808
812
 
809
- created_at_val = (created_at or datetime.now(timezone.utc)).isoformat()
813
+ created_at_val = (created_at or datetime.now(UTC)).isoformat()
810
814
  metadata_json = _json_dumps(metadata or {})
811
815
 
812
816
  async with self._op_lock:
@@ -838,7 +842,7 @@ class NativeLibsqlTraceManager(TraceStorage):
838
842
  ) -> int:
839
843
  await self.initialize()
840
844
 
841
- started_at_val = (started_at or datetime.now(timezone.utc)).isoformat()
845
+ started_at_val = (started_at or datetime.now(UTC)).isoformat()
842
846
  completed_at_val = completed_at.isoformat() if completed_at else None
843
847
  metadata_json = _json_dumps(metadata or {})
844
848
 
@@ -927,7 +931,11 @@ class NativeLibsqlTraceManager(TraceStorage):
927
931
  if isinstance(event, LMCAISEvent):
928
932
  call_records = None
929
933
  if getattr(event, "call_records", None):
930
- call_records = [asdict(record) for record in event.call_records]
934
+ # Handle both dataclass instances and dicts (from deserialization)
935
+ call_records = [
936
+ asdict(record) if not isinstance(record, dict) else record
937
+ for record in event.call_records
938
+ ]
931
939
  payload.update(
932
940
  {
933
941
  "event_type": "cais",
@@ -1150,7 +1158,7 @@ class NativeLibsqlTraceManager(TraceStorage):
1150
1158
  total_reward,
1151
1159
  achievements_count,
1152
1160
  total_steps,
1153
- datetime.now(timezone.utc).isoformat(),
1161
+ datetime.now(UTC).isoformat(),
1154
1162
  _json_dumps(reward_metadata),
1155
1163
  ),
1156
1164
  )
@@ -1202,7 +1210,7 @@ class NativeLibsqlTraceManager(TraceStorage):
1202
1210
  key,
1203
1211
  _json_dumps(annotation),
1204
1212
  source,
1205
- datetime.now(timezone.utc).isoformat(),
1213
+ datetime.now(UTC).isoformat(),
1206
1214
  ),
1207
1215
  )
1208
1216
  conn.commit()
@@ -5,13 +5,13 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  import uuid
8
- from datetime import datetime, timezone
8
+ from datetime import UTC, datetime
9
9
  from typing import Any
10
10
 
11
11
 
12
12
  def iso_now() -> str:
13
13
  """Get current timezone.utc time as ISO format string."""
14
- return datetime.now(timezone.utc).isoformat()
14
+ return datetime.now(UTC).isoformat()
15
15
 
16
16
 
17
17
  def json_dumps(obj: Any) -> str:
@@ -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
+ ]