synth-ai 0.2.8.dev4__py3-none-any.whl → 0.2.23.dev3__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.
Files changed (889) hide show
  1. examples/README.md +1 -0
  2. examples/__init__.py +16 -0
  3. examples/analyze_semantic_words.sh +17 -0
  4. examples/baseline/banking77_baseline.py +243 -0
  5. examples/baseline/banking77_pipeline_baseline.py +294 -0
  6. examples/baseline/crafter_baseline.py +407 -0
  7. examples/baseline/pokemon_red_baseline.py +326 -0
  8. examples/baseline/simple_baseline.py +56 -0
  9. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  10. examples/blog_posts/gepa/README.md +355 -0
  11. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  12. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +80 -0
  13. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +50 -0
  14. examples/blog_posts/gepa/configs/banking77_pipeline_gepa_local.toml +101 -0
  15. examples/blog_posts/gepa/configs/banking77_pipeline_gepa_test.toml +96 -0
  16. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +57 -0
  17. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +35 -0
  18. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +51 -0
  19. examples/blog_posts/gepa/configs/hover_gepa_local.toml +57 -0
  20. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +35 -0
  21. examples/blog_posts/gepa/configs/hover_mipro_local.toml +51 -0
  22. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +57 -0
  23. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +35 -0
  24. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +51 -0
  25. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +58 -0
  26. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +52 -0
  27. examples/blog_posts/gepa/deploy_banking77_task_app.sh +54 -0
  28. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  29. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  30. examples/blog_posts/gepa/run_gepa_banking77.sh +112 -0
  31. examples/blog_posts/gepa/run_gepa_banking77_pipeline.sh +163 -0
  32. examples/blog_posts/gepa/task_apps.py +105 -0
  33. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  34. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  35. examples/blog_posts/mipro/README.md +415 -0
  36. examples/blog_posts/mipro/configs/banking77_mipro_local.toml +91 -0
  37. examples/blog_posts/mipro/configs/banking77_mipro_test.toml +87 -0
  38. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gemini_flash_lite_local.toml +98 -0
  39. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gpt41mini_local.toml +96 -0
  40. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_local.toml +94 -0
  41. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_test.toml +170 -0
  42. examples/blog_posts/mipro/deploy_banking77_pipeline_task_app.sh +59 -0
  43. examples/blog_posts/mipro/deploy_banking77_task_app.sh +41 -0
  44. examples/blog_posts/mipro/multi_step.md +79 -0
  45. examples/blog_posts/mipro/run_mipro_banking77.sh +191 -0
  46. examples/blog_posts/mipro/run_mipro_banking77_pipeline.sh +171 -0
  47. examples/blog_posts/mipro/run_mipro_banking77_pipeline_gemini_flash_lite.sh +177 -0
  48. examples/blog_posts/mipro/run_mipro_banking77_pipeline_gpt41mini.sh +173 -0
  49. examples/blog_posts/mipro/verify_banking77_setup.sh +117 -0
  50. examples/blog_posts/pokemon_vl/README.md +98 -0
  51. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  52. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
  53. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  54. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  55. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
  56. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  57. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  58. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  59. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  60. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  61. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  62. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  63. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  64. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  65. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  66. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  67. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  68. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  69. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  70. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  71. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  72. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  73. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  74. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  75. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
  76. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  77. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  78. examples/crafter_debug_render.py +186 -0
  79. examples/dev/qwen3_32b_qlora_4xh100.toml +45 -0
  80. examples/gepa/banking77_pipeline_gepa.toml +96 -0
  81. examples/gepa/multi_stage_gepa_example.toml +84 -0
  82. examples/gepa/run_gepa_banking77_pipeline.sh +157 -0
  83. examples/multi_step/SFT_README.md +147 -0
  84. examples/multi_step/configs/README_verilog_rl.md +77 -0
  85. examples/multi_step/configs/VERILOG_REWARDS.md +103 -0
  86. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +196 -0
  87. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
  88. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
  89. examples/multi_step/configs/crafter_rl_outcome.toml +75 -0
  90. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +145 -0
  91. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +84 -0
  92. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +79 -0
  93. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  94. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  95. examples/multi_step/configs/crafter_synth_backend.md +40 -0
  96. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
  97. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
  98. examples/multi_step/configs/verilog_rl_lora.toml +147 -0
  99. examples/multi_step/convert_traces_to_sft.py +84 -0
  100. examples/multi_step/crafter_rl_lora.md +70 -0
  101. examples/multi_step/judges/crafter_backend_judge.py +220 -0
  102. examples/multi_step/judges/verilog_backend_judge.py +234 -0
  103. examples/multi_step/readme.md +48 -0
  104. examples/multi_step/run_sft_qwen30b.sh +45 -0
  105. examples/multi_step/sse_metrics_streaming_notes.md +357 -0
  106. examples/multi_step/task_app_config_notes.md +494 -0
  107. examples/multi_step/verilog_rl_lora.md +218 -0
  108. examples/qwen_coder/README.md +102 -0
  109. examples/qwen_coder/_shared.py +113 -0
  110. examples/qwen_coder/configs/coder_lora_30b.toml +60 -0
  111. examples/qwen_coder/configs/coder_lora_4b.toml +61 -0
  112. examples/qwen_coder/configs/coder_lora_small.toml +57 -0
  113. examples/qwen_coder/generate_dataset.py +98 -0
  114. examples/qwen_coder/infer_ft_smoke.py +65 -0
  115. examples/qwen_coder/infer_prod_proxy.py +73 -0
  116. examples/qwen_coder/infer_via_synth.py +87 -0
  117. examples/qwen_coder/scripts/infer_coder.sh +19 -0
  118. examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
  119. examples/qwen_coder/sft_full_17b.py +103 -0
  120. examples/qwen_coder/sft_lora_30b.py +110 -0
  121. examples/qwen_coder/subset_jsonl.py +39 -0
  122. examples/qwen_coder/todos.md +38 -0
  123. examples/qwen_coder/validate_jsonl.py +60 -0
  124. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  125. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  126. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  127. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  128. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  129. examples/qwen_vl/QUICKSTART.md +327 -0
  130. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  131. examples/qwen_vl/README.md +152 -0
  132. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  133. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  134. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  135. examples/qwen_vl/SETUP_COMPLETE.md +274 -0
  136. examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
  137. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  138. examples/qwen_vl/__init__.py +2 -0
  139. examples/qwen_vl/collect_data_via_cli.md +415 -0
  140. examples/qwen_vl/collect_vision_traces.py +368 -0
  141. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
  142. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
  143. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
  144. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  145. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
  146. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  147. examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
  148. examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
  149. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  150. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  151. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  152. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  153. examples/qwen_vl/run_vision_comparison.sh +61 -0
  154. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  155. examples/qwen_vl/test_image_validation.py +201 -0
  156. examples/qwen_vl/test_sft_vision_data.py +110 -0
  157. examples/rl/README.md +169 -0
  158. examples/rl/configs/eval_base_qwen.toml +17 -0
  159. examples/rl/configs/eval_rl_qwen.toml +13 -0
  160. examples/rl/configs/rl_from_base_qwen.toml +62 -0
  161. examples/rl/configs/rl_from_base_qwen17.toml +80 -0
  162. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  163. examples/rl/download_dataset.py +80 -0
  164. examples/rl/run_eval.py +436 -0
  165. examples/rl/run_rl_and_save.py +111 -0
  166. examples/rl/task_app/README.md +21 -0
  167. examples/rl/task_app/math_single_step.py +990 -0
  168. examples/rl/task_app/math_task_app.py +111 -0
  169. examples/run_crafter_demo.sh +10 -0
  170. examples/sdk_prompt_learning_example.py +55 -0
  171. examples/sft/README.md +139 -0
  172. examples/sft/configs/crafter_fft_qwen0p6b.toml +49 -0
  173. examples/sft/configs/crafter_lora_qwen0p6b.toml +49 -0
  174. examples/sft/evaluate.py +117 -0
  175. examples/sft/export_dataset.py +120 -0
  176. examples/sft/generate_traces.py +164 -0
  177. examples/swe/__init__.py +12 -0
  178. examples/swe/task_app/README.md +135 -0
  179. examples/swe/task_app/__init__.py +2 -0
  180. examples/swe/task_app/grpo_swe_mini.py +604 -0
  181. examples/swe/task_app/grpo_swe_mini_task_app.py +124 -0
  182. examples/swe/task_app/hosted/README.md +173 -0
  183. examples/swe/task_app/hosted/__init__.py +5 -0
  184. examples/swe/task_app/hosted/branching.py +143 -0
  185. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  186. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  187. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  188. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  189. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  190. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  191. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  192. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  193. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  194. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  195. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1191 -0
  196. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  197. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  198. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  199. examples/swe/task_app/hosted/hosted_app.py +204 -0
  200. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  201. examples/swe/task_app/hosted/inference/openai_client.py +584 -0
  202. examples/swe/task_app/hosted/main.py +100 -0
  203. examples/swe/task_app/hosted/policy_routes.py +1094 -0
  204. examples/swe/task_app/hosted/registry.py +195 -0
  205. examples/swe/task_app/hosted/rollout.py +1905 -0
  206. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  207. examples/swe/task_app/hosted/storage/volume.py +211 -0
  208. examples/swe/task_app/hosted/test_agents.py +161 -0
  209. examples/swe/task_app/hosted/test_service.py +136 -0
  210. examples/swe/task_app/hosted/utils.py +62 -0
  211. examples/swe/task_app/morph_backend.py +178 -0
  212. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
  213. examples/task_apps/TESTING.md +275 -0
  214. examples/task_apps/banking77/__init__.py +6 -0
  215. examples/task_apps/banking77/banking77_task_app.py +912 -0
  216. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  217. examples/task_apps/banking77_pipeline/__init__.py +6 -0
  218. examples/task_apps/banking77_pipeline/banking77_pipeline_task_app.py +489 -0
  219. examples/task_apps/banking77_pipeline/deploy_wrapper.py +50 -0
  220. examples/task_apps/crafter/CREATE_SFT_DATASET.md +286 -0
  221. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
  222. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +187 -0
  223. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +281 -0
  224. examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
  225. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
  226. examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
  227. examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
  228. examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
  229. examples/task_apps/crafter/task_app/README.md +42 -0
  230. examples/task_apps/crafter/task_app/__init__.py +5 -0
  231. examples/task_apps/crafter/task_app/grpo_crafter.py +1055 -0
  232. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +146 -0
  233. examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +173 -0
  234. examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +5 -0
  235. examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +143 -0
  236. examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  237. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  238. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  239. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  240. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +532 -0
  241. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +583 -0
  242. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +122 -0
  243. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  244. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  245. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +253 -0
  246. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  247. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +999 -0
  248. examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +100 -0
  249. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +1252 -0
  250. examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +195 -0
  251. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +2233 -0
  252. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  253. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +211 -0
  254. examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +161 -0
  255. examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +136 -0
  256. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +411 -0
  257. examples/task_apps/dev/pokemon_emerald/__init__.py +2 -0
  258. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +811 -0
  259. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +120 -0
  260. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +160 -0
  261. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +155 -0
  262. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +69 -0
  263. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +96 -0
  264. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +1502 -0
  265. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +4 -0
  266. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +68 -0
  267. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +216 -0
  268. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +35 -0
  269. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +631 -0
  270. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +1544 -0
  271. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +1428 -0
  272. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +4848 -0
  273. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +41 -0
  274. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +298 -0
  275. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +95 -0
  276. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +204 -0
  277. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +2152 -0
  278. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +429 -0
  279. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +155 -0
  280. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +78 -0
  281. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +122 -0
  282. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +76 -0
  283. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +413 -0
  284. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +204 -0
  285. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +133 -0
  286. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +229 -0
  287. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +300 -0
  288. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +205 -0
  289. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +200 -0
  290. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +284 -0
  291. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +468 -0
  292. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +575 -0
  293. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +311 -0
  294. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +259 -0
  295. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +372 -0
  296. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +296 -0
  297. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +275 -0
  298. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +22 -0
  299. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +44 -0
  300. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +514 -0
  301. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +415 -0
  302. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +1763 -0
  303. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +33 -0
  304. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +106 -0
  305. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +334 -0
  306. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +1020 -0
  307. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +188 -0
  308. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +1481 -0
  309. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +862 -0
  310. examples/task_apps/dev/pokemon_emerald/modal_app.py +114 -0
  311. examples/task_apps/dev/pokemon_emerald/task_app/README.md +81 -0
  312. examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +6 -0
  313. examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +685 -0
  314. examples/task_apps/enron/__init__.py +2 -0
  315. examples/task_apps/enron/eval_groq_qwen32.toml +16 -0
  316. examples/task_apps/enron/filter_sft.toml +5 -0
  317. examples/task_apps/enron/task_app/README.md +14 -0
  318. examples/task_apps/enron/task_app/__init__.py +1 -0
  319. examples/task_apps/enron/task_app/grpo_enron.py +906 -0
  320. examples/task_apps/enron/task_app/grpo_enron_task_app.py +146 -0
  321. examples/task_apps/enron/tests/__init__.py +4 -0
  322. examples/task_apps/enron/tests/conftest.py +115 -0
  323. examples/task_apps/enron/tests/integration/__init__.py +4 -0
  324. examples/task_apps/enron/tests/integration/test_enron_eval.py +179 -0
  325. examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
  326. examples/task_apps/enron/tests/unit/__init__.py +4 -0
  327. examples/task_apps/enron/tests/unit/test_enron_environment.py +126 -0
  328. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  329. examples/task_apps/gepa_benchmarks/common.py +260 -0
  330. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  331. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  332. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  333. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  334. examples/task_apps/math/README.md +21 -0
  335. examples/task_apps/math/math_single_step.py +1000 -0
  336. examples/task_apps/math/math_task_app.py +115 -0
  337. examples/task_apps/pokemon_battle/__init__.py +2 -0
  338. examples/task_apps/pokemon_battle/modal_app.py +104 -0
  339. examples/task_apps/pokemon_battle/task_app/README.md +68 -0
  340. examples/task_apps/pokemon_battle/task_app/__init__.py +6 -0
  341. examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +932 -0
  342. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
  343. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
  344. examples/task_apps/pokemon_red/README.md +356 -0
  345. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +428 -0
  346. examples/task_apps/pokemon_red/__init__.py +3 -0
  347. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +30 -0
  348. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +224 -0
  349. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +75 -0
  350. examples/task_apps/pokemon_red/task_app.py +1048 -0
  351. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +193 -0
  352. examples/task_apps/sokoban/README.md +306 -0
  353. examples/task_apps/sokoban/__init__.py +3 -0
  354. examples/task_apps/sokoban/eval_groq_qwen32.toml +16 -0
  355. examples/task_apps/sokoban/eval_openai_gpt5.toml +16 -0
  356. examples/task_apps/sokoban/filter_sft.toml +5 -0
  357. examples/task_apps/sokoban/task_app.py +1058 -0
  358. examples/task_apps/sokoban/tests/__init__.py +4 -0
  359. examples/task_apps/sokoban/tests/conftest.py +113 -0
  360. examples/task_apps/sokoban/tests/integration/__init__.py +4 -0
  361. examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +57 -0
  362. examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +198 -0
  363. examples/task_apps/sokoban/tests/unit/__init__.py +4 -0
  364. examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +114 -0
  365. examples/task_apps/verilog/__init__.py +1 -0
  366. examples/task_apps/verilog/eval_groq_qwen32b.toml +22 -0
  367. examples/task_apps/verilog/filter_sft.toml +5 -0
  368. examples/task_apps/verilog/task_app/README.md +12 -0
  369. examples/task_apps/verilog/task_app/__init__.py +1 -0
  370. examples/task_apps/verilog/task_app/grpo_verilog.py +1166 -0
  371. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
  372. examples/task_apps/verilog/tests/__init__.py +4 -0
  373. examples/task_apps/verilog/tests/conftest.py +115 -0
  374. examples/task_apps/verilog/tests/integration/__init__.py +4 -0
  375. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +181 -0
  376. examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
  377. examples/task_apps/verilog/tests/unit/__init__.py +4 -0
  378. examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +118 -0
  379. examples/tunnel_gepa_banking77/README.md +106 -0
  380. examples/tunnel_gepa_banking77/banking77_gepa_tunnel.toml +95 -0
  381. examples/tunnel_gepa_banking77/keep_tunnel_running.py +60 -0
  382. examples/tunnel_gepa_banking77/run_gepa_with_tunnel.sh +226 -0
  383. examples/vlm/PROPOSAL.md +53 -0
  384. examples/vlm/README.md +68 -0
  385. examples/vlm/configs/crafter_vlm_gpt4o.toml +49 -0
  386. examples/vlm/crafter_image_only_agent.py +207 -0
  387. examples/vlm/crafter_openai_vlm_agent.py +275 -0
  388. examples/vlm/filter_image_rows.py +63 -0
  389. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  390. examples/warming_up_to_rl/_utils.py +92 -0
  391. examples/warming_up_to_rl/analyze_trace_db.py +422 -0
  392. examples/warming_up_to_rl/configs/crafter_fft.toml +53 -0
  393. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +54 -0
  394. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +22 -0
  395. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +15 -0
  396. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +24 -0
  397. examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +35 -0
  398. examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +26 -0
  399. examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +36 -0
  400. examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +32 -0
  401. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +85 -0
  402. examples/warming_up_to_rl/configs/rl_from_ft.toml +58 -0
  403. examples/warming_up_to_rl/export_trace_sft.py +837 -0
  404. examples/warming_up_to_rl/groq_test.py +97 -0
  405. examples/warming_up_to_rl/manage_secrets.py +131 -0
  406. examples/warming_up_to_rl/old/event_rewards.md +234 -0
  407. examples/warming_up_to_rl/old/notes.md +73 -0
  408. examples/warming_up_to_rl/readme.md +110 -0
  409. examples/warming_up_to_rl/run_eval.py +736 -0
  410. examples/warming_up_to_rl/run_fft_and_save.py +380 -0
  411. examples/warming_up_to_rl/run_local_rollout.py +239 -0
  412. examples/warming_up_to_rl/run_local_rollout_modal.py +248 -0
  413. examples/warming_up_to_rl/run_local_rollout_parallel.py +405 -0
  414. examples/warming_up_to_rl/run_local_rollout_traced.py +477 -0
  415. examples/warming_up_to_rl/run_rl_and_save.py +124 -0
  416. examples/warming_up_to_rl/run_rollout_remote.py +156 -0
  417. examples/warming_up_to_rl/task_app/README.md +42 -0
  418. examples/warming_up_to_rl/task_app/grpo_crafter.py +876 -0
  419. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  420. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  421. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  422. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  423. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  424. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  425. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  426. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  427. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  428. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
  429. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  430. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  431. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  432. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +253 -0
  433. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  434. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +729 -0
  435. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  436. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1114 -0
  437. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  438. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1891 -0
  439. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  440. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  441. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  442. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  443. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +129 -0
  444. examples/workflows/math_rl/configs/eval_base_qwen.toml +15 -0
  445. examples/workflows/math_rl/configs/eval_rl_qwen.toml +11 -0
  446. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +62 -0
  447. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +80 -0
  448. examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +35 -0
  449. examples/workflows/math_rl/download_dataset.py +80 -0
  450. examples/workflows/math_rl/run_eval.py +436 -0
  451. examples/workflows/math_rl/run_rl_and_save.py +111 -0
  452. synth_ai/__init__.py +47 -23
  453. synth_ai/_utils/__init__.py +47 -0
  454. synth_ai/_utils/base_url.py +10 -0
  455. synth_ai/_utils/http.py +10 -0
  456. synth_ai/_utils/prompts.py +10 -0
  457. synth_ai/_utils/task_app_state.py +12 -0
  458. synth_ai/_utils/user_config.py +10 -0
  459. synth_ai/api/models/supported.py +514 -0
  460. synth_ai/api/train/__init__.py +63 -0
  461. synth_ai/api/train/builders.py +473 -0
  462. synth_ai/api/train/cli.py +1185 -0
  463. synth_ai/api/train/config_finder.py +246 -0
  464. synth_ai/api/train/configs/__init__.py +65 -0
  465. synth_ai/api/train/configs/prompt_learning.py +496 -0
  466. synth_ai/api/train/configs/rl.py +188 -0
  467. synth_ai/api/train/configs/sft.py +99 -0
  468. synth_ai/api/train/configs/shared.py +81 -0
  469. synth_ai/api/train/env_resolver.py +352 -0
  470. synth_ai/api/train/pollers.py +91 -0
  471. synth_ai/api/train/prompt_learning.py +425 -0
  472. synth_ai/api/train/sft.py +390 -0
  473. synth_ai/api/train/supported_algos.py +147 -0
  474. synth_ai/api/train/task_app.py +195 -0
  475. synth_ai/api/train/utils.py +244 -0
  476. synth_ai/api/train/validators.py +1117 -0
  477. synth_ai/api/tunnel.py +49 -0
  478. synth_ai/auth/credentials.py +94 -0
  479. synth_ai/baseline/__init__.py +25 -0
  480. synth_ai/baseline/config.py +209 -0
  481. synth_ai/baseline/discovery.py +214 -0
  482. synth_ai/baseline/execution.py +146 -0
  483. synth_ai/cfgs.py +227 -0
  484. synth_ai/cli/__init__.py +90 -45
  485. synth_ai/cli/_modal_wrapper.py +31 -0
  486. synth_ai/cli/_storage.py +20 -0
  487. synth_ai/cli/_typer_patch.py +47 -0
  488. synth_ai/cli/_validate_task_app.py +29 -0
  489. synth_ai/cli/balance.py +16 -4
  490. synth_ai/cli/calc.py +36 -21
  491. synth_ai/cli/claude.py +70 -0
  492. synth_ai/cli/codex.py +267 -0
  493. synth_ai/cli/commands/__init__.py +18 -0
  494. synth_ai/cli/commands/baseline/__init__.py +12 -0
  495. synth_ai/cli/commands/baseline/core.py +637 -0
  496. synth_ai/cli/commands/baseline/list.py +93 -0
  497. synth_ai/cli/commands/demo/__init__.py +6 -0
  498. synth_ai/cli/commands/demo/core.py +163 -0
  499. synth_ai/cli/commands/eval/__init__.py +19 -0
  500. synth_ai/cli/commands/eval/core.py +1112 -0
  501. synth_ai/cli/commands/eval/errors.py +81 -0
  502. synth_ai/cli/commands/eval/validation.py +133 -0
  503. synth_ai/cli/commands/filter/__init__.py +12 -0
  504. synth_ai/cli/commands/filter/core.py +424 -0
  505. synth_ai/cli/commands/filter/errors.py +55 -0
  506. synth_ai/cli/commands/filter/validation.py +77 -0
  507. synth_ai/cli/commands/help/__init__.py +185 -0
  508. synth_ai/cli/commands/help/core.py +72 -0
  509. synth_ai/cli/commands/smoke/__init__.py +7 -0
  510. synth_ai/cli/commands/smoke/core.py +1437 -0
  511. synth_ai/cli/commands/status/__init__.py +66 -0
  512. synth_ai/cli/commands/status/client.py +192 -0
  513. synth_ai/cli/commands/status/config.py +92 -0
  514. synth_ai/cli/commands/status/errors.py +20 -0
  515. synth_ai/cli/commands/status/formatters.py +164 -0
  516. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  517. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  518. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  519. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  520. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  521. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  522. synth_ai/cli/commands/status/subcommands/session.py +183 -0
  523. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  524. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  525. synth_ai/cli/commands/status/utils.py +114 -0
  526. synth_ai/cli/commands/train/__init__.py +53 -0
  527. synth_ai/cli/commands/train/core.py +21 -0
  528. synth_ai/cli/commands/train/errors.py +117 -0
  529. synth_ai/cli/commands/train/judge_schemas.py +200 -0
  530. synth_ai/cli/commands/train/judge_validation.py +305 -0
  531. synth_ai/cli/commands/train/validation.py +386 -0
  532. synth_ai/cli/demo.py +32 -140
  533. synth_ai/cli/deploy.py +233 -0
  534. synth_ai/cli/eval/__init__.py +36 -0
  535. synth_ai/cli/eval/core.py +5 -0
  536. synth_ai/cli/eval/errors.py +31 -0
  537. synth_ai/cli/eval/validation.py +5 -0
  538. synth_ai/cli/filter/__init__.py +28 -0
  539. synth_ai/cli/filter/core.py +5 -0
  540. synth_ai/cli/filter/errors.py +23 -0
  541. synth_ai/cli/filter/validation.py +5 -0
  542. synth_ai/cli/legacy_root_backup.py +28 -22
  543. synth_ai/cli/lib/__init__.py +10 -0
  544. synth_ai/cli/lib/task_app_discovery.py +7 -0
  545. synth_ai/cli/lib/task_app_env.py +518 -0
  546. synth_ai/cli/mcp.py +34 -0
  547. synth_ai/cli/modal_serve/__init__.py +12 -0
  548. synth_ai/cli/modal_serve/core.py +14 -0
  549. synth_ai/cli/modal_serve/errors.py +8 -0
  550. synth_ai/cli/modal_serve/validation.py +11 -0
  551. synth_ai/cli/opencode.py +256 -0
  552. synth_ai/cli/recent.py +13 -7
  553. synth_ai/cli/rl_demo.py +166 -114
  554. synth_ai/cli/root.py +143 -112
  555. synth_ai/cli/serve/__init__.py +12 -0
  556. synth_ai/cli/serve/core.py +14 -0
  557. synth_ai/cli/serve/errors.py +8 -0
  558. synth_ai/cli/serve/validation.py +11 -0
  559. synth_ai/cli/setup.py +49 -0
  560. synth_ai/cli/status.py +7 -125
  561. synth_ai/cli/task_app_deploy.py +7 -0
  562. synth_ai/cli/task_app_list.py +25 -0
  563. synth_ai/cli/task_app_modal_serve.py +11 -0
  564. synth_ai/cli/task_app_serve.py +11 -0
  565. synth_ai/cli/task_apps.py +3134 -0
  566. synth_ai/cli/traces.py +9 -5
  567. synth_ai/cli/train/__init__.py +12 -0
  568. synth_ai/cli/train/core.py +21 -0
  569. synth_ai/cli/train/errors.py +8 -0
  570. synth_ai/cli/train/validation.py +24 -0
  571. synth_ai/cli/train.py +5 -0
  572. synth_ai/cli/turso.py +73 -0
  573. synth_ai/cli/watch.py +13 -18
  574. synth_ai/demos/__init__.py +10 -0
  575. synth_ai/demos/core/__init__.py +28 -1
  576. synth_ai/demos/core/cli.py +745 -416
  577. synth_ai/demos/crafter/__init__.py +1 -0
  578. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  579. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  580. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  581. synth_ai/demos/demo_registry.py +176 -0
  582. synth_ai/demos/demo_task_apps/__init__.py +7 -1
  583. synth_ai/demos/demo_task_apps/core.py +75 -37
  584. synth_ai/demos/demo_task_apps/crafter/__init__.py +1 -0
  585. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +53 -0
  586. synth_ai/demos/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +73 -0
  587. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +184 -0
  588. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  589. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  590. synth_ai/demos/demo_task_apps/math/config.toml +55 -110
  591. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
  592. synth_ai/demos/demo_task_apps/math/modal_task_app.py +491 -166
  593. synth_ai/demos/demo_task_apps/math/task_app_entry.py +37 -0
  594. synth_ai/demos/math/__init__.py +1 -0
  595. synth_ai/demos/math/_common.py +16 -0
  596. synth_ai/demos/math/app.py +38 -0
  597. synth_ai/demos/math/config.toml +76 -0
  598. synth_ai/demos/math/deploy_modal.py +54 -0
  599. synth_ai/demos/math/modal_task_app.py +703 -0
  600. synth_ai/demos/math/task_app_entry.py +51 -0
  601. synth_ai/environments/environment/core.py +7 -1
  602. synth_ai/environments/examples/bandit/engine.py +12 -5
  603. synth_ai/environments/examples/bandit/environment.py +0 -1
  604. synth_ai/environments/examples/bandit/taskset.py +4 -4
  605. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  606. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  607. synth_ai/environments/examples/crafter_classic/environment.py +93 -2
  608. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  609. synth_ai/environments/examples/enron/engine.py +7 -2
  610. synth_ai/environments/examples/enron/environment.py +68 -0
  611. synth_ai/environments/examples/red/engine.py +60 -12
  612. synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
  613. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  614. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
  615. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
  616. synth_ai/environments/examples/red/environment.py +86 -0
  617. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  618. synth_ai/environments/examples/sokoban/taskset.py +116 -0
  619. synth_ai/environments/examples/verilog/engine.py +104 -12
  620. synth_ai/environments/examples/wordle/environment.py +0 -1
  621. synth_ai/environments/reproducibility/tree.py +5 -6
  622. synth_ai/environments/service/app.py +11 -12
  623. synth_ai/environments/service/core_routes.py +10 -9
  624. synth_ai/environments/stateful/engine.py +1 -1
  625. synth_ai/environments/tasks/core.py +1 -0
  626. synth_ai/environments/tasks/filters.py +5 -6
  627. synth_ai/environments/tasks/utils.py +4 -5
  628. synth_ai/evals/__init__.py +15 -0
  629. synth_ai/evals/base.py +14 -5
  630. synth_ai/evals/client.py +82 -0
  631. synth_ai/evals/types.py +42 -0
  632. synth_ai/http.py +8 -22
  633. synth_ai/http_client.py +45 -12
  634. synth_ai/inference/__init__.py +0 -2
  635. synth_ai/inference/client.py +21 -7
  636. synth_ai/jobs/client.py +129 -80
  637. synth_ai/judge_schemas.py +127 -0
  638. synth_ai/learning/__init__.py +51 -6
  639. synth_ai/learning/algorithms.py +14 -0
  640. synth_ai/learning/client.py +122 -30
  641. synth_ai/learning/config.py +2 -40
  642. synth_ai/learning/constants.py +0 -2
  643. synth_ai/learning/ft_client.py +4 -56
  644. synth_ai/learning/health.py +14 -8
  645. synth_ai/learning/jobs.py +43 -47
  646. synth_ai/learning/prompt_learning_client.py +276 -0
  647. synth_ai/learning/prompt_learning_types.py +185 -0
  648. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  649. synth_ai/learning/rl/client.py +269 -0
  650. synth_ai/learning/rl/config.py +31 -0
  651. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  652. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  653. synth_ai/learning/rl/secrets.py +13 -0
  654. synth_ai/learning/rl_client.py +2 -253
  655. synth_ai/learning/sft/__init__.py +29 -0
  656. synth_ai/learning/sft/client.py +68 -0
  657. synth_ai/learning/sft/config.py +270 -0
  658. synth_ai/learning/sft/data.py +698 -0
  659. synth_ai/learning/sse.py +25 -26
  660. synth_ai/learning/validators.py +29 -25
  661. synth_ai/mcp/__init__.py +5 -0
  662. synth_ai/mcp/__main__.py +8 -0
  663. synth_ai/mcp/main.py +254 -0
  664. synth_ai/mcp/setup.py +100 -0
  665. synth_ai/modal.py +257 -0
  666. synth_ai/pricing/__init__.py +3 -0
  667. synth_ai/pricing/model_pricing.py +64 -0
  668. synth_ai/session/__init__.py +75 -0
  669. synth_ai/session/client.py +383 -0
  670. synth_ai/session/constants.py +63 -0
  671. synth_ai/session/exceptions.py +105 -0
  672. synth_ai/session/manager.py +139 -0
  673. synth_ai/session/models.py +89 -0
  674. synth_ai/session/query.py +110 -0
  675. synth_ai/spec/__init__.py +46 -0
  676. synth_ai/spec/dataclasses.py +149 -0
  677. synth_ai/spec/loader.py +144 -0
  678. synth_ai/spec/serializer.py +199 -0
  679. synth_ai/spec/validation.py +250 -0
  680. synth_ai/streaming/__init__.py +29 -0
  681. synth_ai/streaming/config.py +94 -0
  682. synth_ai/streaming/handlers.py +589 -0
  683. synth_ai/streaming/streamer.py +320 -0
  684. synth_ai/streaming/types.py +95 -0
  685. synth_ai/task/__init__.py +116 -3
  686. synth_ai/task/apps/__init__.py +132 -0
  687. synth_ai/task/auth.py +165 -0
  688. synth_ai/task/client.py +167 -0
  689. synth_ai/task/config.py +261 -0
  690. synth_ai/task/contracts.py +173 -57
  691. synth_ai/task/datasets.py +108 -0
  692. synth_ai/task/errors.py +50 -0
  693. synth_ai/task/health.py +17 -11
  694. synth_ai/task/inference_api.py +101 -0
  695. synth_ai/task/json.py +111 -0
  696. synth_ai/task/proxy.py +251 -0
  697. synth_ai/task/rubrics/__init__.py +55 -0
  698. synth_ai/task/rubrics/loaders.py +156 -0
  699. synth_ai/task/rubrics/models.py +57 -0
  700. synth_ai/task/rubrics/scoring.py +116 -0
  701. synth_ai/task/rubrics/strict.py +149 -0
  702. synth_ai/task/rubrics.py +219 -0
  703. synth_ai/task/server.py +432 -0
  704. synth_ai/task/trace_correlation_helpers.py +328 -0
  705. synth_ai/task/tracing_utils.py +95 -0
  706. synth_ai/task/validators.py +449 -6
  707. synth_ai/task/vendors.py +59 -0
  708. synth_ai/tracing_v3/__init__.py +4 -0
  709. synth_ai/tracing_v3/abstractions.py +21 -4
  710. synth_ai/tracing_v3/config.py +167 -22
  711. synth_ai/tracing_v3/constants.py +21 -0
  712. synth_ai/tracing_v3/db_config.py +42 -29
  713. synth_ai/tracing_v3/decorators.py +80 -45
  714. synth_ai/tracing_v3/examples/basic_usage.py +15 -9
  715. synth_ai/tracing_v3/hooks.py +6 -4
  716. synth_ai/tracing_v3/llm_call_record_helpers.py +161 -61
  717. synth_ai/tracing_v3/migration_helper.py +1 -2
  718. synth_ai/tracing_v3/replica_sync.py +12 -7
  719. synth_ai/tracing_v3/serialization.py +130 -0
  720. synth_ai/tracing_v3/session_tracer.py +86 -21
  721. synth_ai/tracing_v3/storage/base.py +98 -12
  722. synth_ai/tracing_v3/storage/config.py +63 -16
  723. synth_ai/tracing_v3/storage/factory.py +11 -9
  724. synth_ai/tracing_v3/storage/utils.py +15 -11
  725. synth_ai/tracing_v3/trace_utils.py +317 -0
  726. synth_ai/tracing_v3/turso/__init__.py +8 -21
  727. synth_ai/tracing_v3/turso/daemon.py +123 -15
  728. synth_ai/tracing_v3/turso/models.py +5 -2
  729. synth_ai/tracing_v3/turso/native_manager.py +1293 -0
  730. synth_ai/tracing_v3/utils.py +5 -4
  731. synth_ai/tunnel.py +143 -0
  732. synth_ai/tunnel_deploy.py +278 -0
  733. synth_ai/types.py +8 -0
  734. synth_ai/urls.py +11 -0
  735. synth_ai/utils/__init__.py +166 -0
  736. synth_ai/utils/agents.py +74 -0
  737. synth_ai/utils/apps.py +152 -0
  738. synth_ai/utils/base_url.py +94 -0
  739. synth_ai/utils/bin.py +39 -0
  740. synth_ai/utils/claude.py +36 -0
  741. synth_ai/utils/cli.py +284 -0
  742. synth_ai/utils/config.py +81 -0
  743. synth_ai/utils/env.py +346 -0
  744. synth_ai/utils/errors.py +85 -0
  745. synth_ai/utils/http.py +172 -0
  746. synth_ai/utils/json.py +72 -0
  747. synth_ai/utils/log_filter.py +99 -0
  748. synth_ai/utils/logging.py +198 -0
  749. synth_ai/utils/modal.py +299 -0
  750. synth_ai/utils/paths.py +95 -0
  751. synth_ai/utils/process.py +233 -0
  752. synth_ai/utils/prompts.py +39 -0
  753. synth_ai/utils/sqld.py +122 -0
  754. synth_ai/utils/ssl.py +25 -0
  755. synth_ai/utils/task_app_discovery.py +882 -0
  756. synth_ai/utils/task_app_env.py +186 -0
  757. synth_ai/utils/task_app_state.py +318 -0
  758. synth_ai/utils/tunnel/__init__.py +12 -0
  759. synth_ai/utils/tunnel/config.py +55 -0
  760. synth_ai/utils/user_config.py +137 -0
  761. synth_ai/uvicorn.py +77 -0
  762. synth_ai-0.2.23.dev3.dist-info/METADATA +357 -0
  763. synth_ai-0.2.23.dev3.dist-info/RECORD +983 -0
  764. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/entry_points.txt +0 -1
  765. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/top_level.txt +1 -0
  766. synth_ai/cli/man.py +0 -106
  767. synth_ai/core/experiment.py +0 -15
  768. synth_ai/core/system.py +0 -15
  769. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  770. synth_ai/experimental/synth_oss.py +0 -446
  771. synth_ai/handshake.py +0 -63
  772. synth_ai/install_sqld.sh +0 -40
  773. synth_ai/learning/offline/dpo.py +0 -0
  774. synth_ai/learning/offline/providers.py +0 -7
  775. synth_ai/learning/offline/sft.py +0 -0
  776. synth_ai/learning/offline/shared.py +0 -0
  777. synth_ai/learning/online/grpo.py +0 -0
  778. synth_ai/learning/online/irft.py +0 -0
  779. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  780. synth_ai/learning/prompts/gepa.py +0 -0
  781. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  782. synth_ai/learning/prompts/mipro.py +0 -289
  783. synth_ai/learning/prompts/random_search.py +0 -246
  784. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  785. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  786. synth_ai/lm/__init__.py +0 -51
  787. synth_ai/lm/caching/constants.py +0 -6
  788. synth_ai/lm/caching/dbs.py +0 -0
  789. synth_ai/lm/caching/ephemeral.py +0 -102
  790. synth_ai/lm/caching/handler.py +0 -137
  791. synth_ai/lm/caching/initialize.py +0 -11
  792. synth_ai/lm/caching/persistent.py +0 -114
  793. synth_ai/lm/config.py +0 -110
  794. synth_ai/lm/constants.py +0 -32
  795. synth_ai/lm/core/__init__.py +0 -8
  796. synth_ai/lm/core/all.py +0 -73
  797. synth_ai/lm/core/exceptions.py +0 -7
  798. synth_ai/lm/core/main.py +0 -319
  799. synth_ai/lm/core/main_v3.py +0 -594
  800. synth_ai/lm/core/synth_models.py +0 -48
  801. synth_ai/lm/core/vendor_clients.py +0 -188
  802. synth_ai/lm/cost/monitor.py +0 -1
  803. synth_ai/lm/cost/statefulness.py +0 -1
  804. synth_ai/lm/injection.py +0 -80
  805. synth_ai/lm/overrides.py +0 -206
  806. synth_ai/lm/provider_support/__init__.py +0 -8
  807. synth_ai/lm/provider_support/anthropic.py +0 -972
  808. synth_ai/lm/provider_support/openai.py +0 -1139
  809. synth_ai/lm/provider_support/suppress_logging.py +0 -31
  810. synth_ai/lm/structured_outputs/handler.py +0 -440
  811. synth_ai/lm/structured_outputs/inject.py +0 -297
  812. synth_ai/lm/structured_outputs/rehabilitate.py +0 -185
  813. synth_ai/lm/tools/__init__.py +0 -3
  814. synth_ai/lm/tools/base.py +0 -172
  815. synth_ai/lm/unified_interface.py +0 -202
  816. synth_ai/lm/vendors/base.py +0 -81
  817. synth_ai/lm/vendors/core/anthropic_api.py +0 -387
  818. synth_ai/lm/vendors/core/gemini_api.py +0 -292
  819. synth_ai/lm/vendors/core/mistral_api.py +0 -322
  820. synth_ai/lm/vendors/core/openai_api.py +0 -225
  821. synth_ai/lm/vendors/core/synth_dev_api.py +0 -0
  822. synth_ai/lm/vendors/local/ollama.py +0 -0
  823. synth_ai/lm/vendors/openai_standard.py +0 -780
  824. synth_ai/lm/vendors/openai_standard_responses.py +0 -256
  825. synth_ai/lm/vendors/retries.py +0 -22
  826. synth_ai/lm/vendors/supported/custom_endpoint.py +0 -417
  827. synth_ai/lm/vendors/supported/deepseek.py +0 -69
  828. synth_ai/lm/vendors/supported/grok.py +0 -75
  829. synth_ai/lm/vendors/supported/groq.py +0 -16
  830. synth_ai/lm/vendors/supported/ollama.py +0 -15
  831. synth_ai/lm/vendors/supported/openrouter.py +0 -74
  832. synth_ai/lm/vendors/supported/together.py +0 -11
  833. synth_ai/lm/vendors/synth_client.py +0 -808
  834. synth_ai/lm/warmup.py +0 -186
  835. synth_ai/rl/secrets.py +0 -19
  836. synth_ai/scripts/verify_rewards.py +0 -100
  837. synth_ai/tracing/__init__.py +0 -30
  838. synth_ai/tracing_v1/__init__.py +0 -33
  839. synth_ai/tracing_v3/turso/manager.py +0 -760
  840. synth_ai/v0/tracing/abstractions.py +0 -224
  841. synth_ai/v0/tracing/base_client.py +0 -91
  842. synth_ai/v0/tracing/client_manager.py +0 -131
  843. synth_ai/v0/tracing/config.py +0 -142
  844. synth_ai/v0/tracing/context.py +0 -146
  845. synth_ai/v0/tracing/decorators.py +0 -682
  846. synth_ai/v0/tracing/events/__init__.py +0 -0
  847. synth_ai/v0/tracing/events/manage.py +0 -147
  848. synth_ai/v0/tracing/events/scope.py +0 -86
  849. synth_ai/v0/tracing/events/store.py +0 -228
  850. synth_ai/v0/tracing/immediate_client.py +0 -151
  851. synth_ai/v0/tracing/local.py +0 -18
  852. synth_ai/v0/tracing/log_client_base.py +0 -73
  853. synth_ai/v0/tracing/retry_queue.py +0 -186
  854. synth_ai/v0/tracing/trackers.py +0 -515
  855. synth_ai/v0/tracing/upload.py +0 -512
  856. synth_ai/v0/tracing/utils.py +0 -9
  857. synth_ai/v0/tracing_v1/__init__.py +0 -16
  858. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  859. synth_ai/v0/tracing_v1/base_client.py +0 -91
  860. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  861. synth_ai/v0/tracing_v1/config.py +0 -142
  862. synth_ai/v0/tracing_v1/context.py +0 -146
  863. synth_ai/v0/tracing_v1/decorators.py +0 -703
  864. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  865. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  866. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  867. synth_ai/v0/tracing_v1/events/store.py +0 -228
  868. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  869. synth_ai/v0/tracing_v1/local.py +0 -18
  870. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  871. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  872. synth_ai/v0/tracing_v1/trackers.py +0 -515
  873. synth_ai/v0/tracing_v1/upload.py +0 -527
  874. synth_ai/v0/tracing_v1/utils.py +0 -9
  875. synth_ai/zyk/__init__.py +0 -30
  876. synth_ai-0.2.8.dev4.dist-info/METADATA +0 -129
  877. synth_ai-0.2.8.dev4.dist-info/RECORD +0 -420
  878. {synth_ai/lm/caching → examples/task_apps}/__init__.py +0 -0
  879. {synth_ai/lm/cost → examples/task_apps/crafter}/__init__.py +0 -0
  880. {synth_ai/lm/structured_outputs → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server}/__init__.py +0 -0
  881. {synth_ai/lm/vendors → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests}/__init__.py +0 -0
  882. {synth_ai/lm/vendors/core → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils}/__init__.py +0 -0
  883. {synth_ai/lm/vendors/local → examples/task_apps/math}/__init__.py +0 -0
  884. {synth_ai/lm/vendors/supported → examples/workflows}/__init__.py +0 -0
  885. {synth_ai/v0/tracing → examples/workflows/math_rl}/__init__.py +0 -0
  886. /synth_ai/{compound/cais.py → cli/__main__.py} +0 -0
  887. /synth_ai/{learning/filtering.py → py.typed} +0 -0
  888. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/WHEEL +0 -0
  889. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,38 @@
1
1
  from __future__ import annotations
2
2
 
3
- import argparse
3
+ import contextlib
4
4
  import json
5
5
  import os
6
- import sys
7
- import time
8
- from pathlib import Path
9
- from typing import Any, Dict, Callable
10
6
  import shutil
11
7
  import stat
8
+ import sys
12
9
  import textwrap
10
+ import time
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import Any
13
14
 
15
+ from synth_ai.demo_registry import (
16
+ DemoTemplate,
17
+ get_demo_template,
18
+ list_demo_templates,
19
+ )
14
20
  from synth_ai.demos.demo_task_apps import core as demo_core
15
- from synth_ai.handshake import run_handshake, HandshakeError
16
- from synth_ai.demos.demo_task_apps.core import DemoEnv
21
+ from synth_ai.demos.demo_task_apps.core import DEFAULT_TASK_APP_SECRET_NAME, DemoEnv
22
+ from synth_ai.handshake import HandshakeError, run_handshake
23
+ from synth_ai.utils.process import get_subprocess_env, should_filter_log_line
24
+
25
+
26
+ def _key_preview(value: str, label: str) -> str:
27
+ """Return a short descriptor for a secret without leaking the full value."""
28
+ try:
29
+ text = value or ""
30
+ length = len(text)
31
+ prefix = text[:6] if length >= 6 else text
32
+ suffix = text[-5:] if length >= 5 else text
33
+ return f"{label} len={length} prefix={prefix} last5={suffix}"
34
+ except Exception:
35
+ return f"{label} len=0"
17
36
 
18
37
 
19
38
  def _is_modal_public_url(u: str) -> bool:
@@ -26,35 +45,71 @@ def _is_modal_public_url(u: str) -> bool:
26
45
  return False
27
46
 
28
47
 
29
- def cmd_setup(_args: argparse.Namespace) -> int:
30
- # 1) Always perform SDK handshake and overwrite .env with returned keys
48
+ def setup() -> int:
49
+ # Change to demo directory if stored
50
+ demo_dir = demo_core.load_demo_dir()
51
+ if demo_dir and os.path.isdir(demo_dir):
52
+ os.chdir(demo_dir)
53
+ print(f"Using demo directory: {demo_dir}")
54
+
55
+ # 1) Try to fetch keys from frontend; fall back to manual input if fetch fails
56
+ synth_key = ""
57
+ rl_env_key = ""
58
+ org_name = "this organization"
59
+
31
60
  try:
32
61
  print("\n⏳ Connecting SDK to your browser session…")
33
62
  res = run_handshake()
34
- user = res.get("user") or {}
35
63
  org = res.get("org") or {}
36
64
  keys = res.get("keys") or {}
37
65
  synth_key = str(keys.get("synth") or "").strip()
38
66
  rl_env_key = str(keys.get("rl_env") or "").strip()
39
- if not synth_key or not rl_env_key:
40
- raise HandshakeError("handshake returned missing keys")
41
- # Overwrite .env with the latest values from the account/org
42
- demo_core.persist_dotenv_values({
67
+ org_name = org.get("name") or "this organization"
68
+ print(f" Connected to {org_name}!")
69
+ except (HandshakeError, Exception) as e:
70
+ print(f"⚠️ Failed to fetch keys from frontend: {e}")
71
+ print("Falling back to manual entry...")
72
+
73
+ # Prompt for manual input if any key is missing
74
+ if not synth_key:
75
+ try:
76
+ synth_key = input(
77
+ "Failed to fetch your Synth API key. Please enter your Synth API key here:\n> "
78
+ ).strip()
79
+ except (EOFError, KeyboardInterrupt):
80
+ print("\nSetup cancelled.")
81
+ return 1
82
+ if not synth_key:
83
+ print("Synth API key is required.")
84
+ return 1
85
+
86
+ if not rl_env_key:
87
+ try:
88
+ rl_env_key = input(
89
+ "Failed to fetch your RL Environment API key. Please enter your RL Environment API key here:\n> "
90
+ ).strip()
91
+ except (EOFError, KeyboardInterrupt):
92
+ print("\nSetup cancelled.")
93
+ return 1
94
+ if not rl_env_key:
95
+ print("RL Environment API key is required.")
96
+ return 1
97
+
98
+ # Persist both keys to .env
99
+ dotenv_path = demo_core.persist_dotenv_values(
100
+ {
43
101
  "SYNTH_API_KEY": synth_key,
44
102
  "ENVIRONMENT_API_KEY": rl_env_key,
45
- })
46
- org_name = (org.get("name") or "this organization")
47
- print(f"✅ Connected to {org_name}!")
48
- except HandshakeError as e:
49
- print(f"Handshake failed: {e}")
50
- return 1
51
- except Exception as e:
52
- print(f"Unexpected handshake error: {e}")
53
- return 1
103
+ }
104
+ )
105
+
106
+ # Store .env path for subsequent commands
107
+ demo_core.persist_env_file_path(dotenv_path)
54
108
 
55
109
  # 2) Reload env after handshake to pick up values from .env (suppress env prints)
56
- import io
57
110
  import contextlib
111
+ import io
112
+
58
113
  _buf = io.StringIO()
59
114
  with contextlib.redirect_stdout(_buf):
60
115
  env = demo_core.load_env()
@@ -71,22 +126,22 @@ def cmd_setup(_args: argparse.Namespace) -> int:
71
126
  return
72
127
  current = env.task_app_base_url
73
128
  needs_lookup = False
74
- if not current:
75
- needs_lookup = True
76
- elif not _is_modal_public_url(current):
129
+ if not current or not _is_modal_public_url(current):
77
130
  needs_lookup = True
78
131
  if not needs_lookup:
79
132
  return
80
- code, out = _popen_capture([
81
- "uv",
82
- "run",
83
- "python",
84
- "-m",
85
- "modal",
86
- "app",
87
- "url",
88
- env.task_app_name,
89
- ])
133
+ code, out = _popen_capture(
134
+ [
135
+ "uv",
136
+ "run",
137
+ "python",
138
+ "-m",
139
+ "modal",
140
+ "app",
141
+ "url",
142
+ env.task_app_name,
143
+ ]
144
+ )
90
145
  if code != 0 or not out:
91
146
  return
92
147
  new_url = ""
@@ -100,7 +155,6 @@ def cmd_setup(_args: argparse.Namespace) -> int:
100
155
  dotenv_values = {
101
156
  "TASK_APP_BASE_URL": new_url,
102
157
  "TASK_APP_NAME": env.task_app_name,
103
- "TASK_APP_SECRET_NAME": env.task_app_secret_name or f"{env.task_app_name}-secret",
104
158
  }
105
159
  demo_core.persist_dotenv_values(dotenv_values)
106
160
  os.environ["TASK_APP_BASE_URL"] = new_url
@@ -117,15 +171,16 @@ def cmd_setup(_args: argparse.Namespace) -> int:
117
171
 
118
172
  _maybe_fix_task_url()
119
173
 
120
- ok_backend = False
121
- ok_task = False
122
174
  if env.dev_backend_url:
123
- api = env.dev_backend_url.rstrip("/") + ("" if env.dev_backend_url.endswith("/api") else "/api")
124
- ok_backend = demo_core.assert_http_ok(api + "/health", method="GET")
175
+ api = env.dev_backend_url.rstrip("/") + (
176
+ "" if env.dev_backend_url.endswith("/api") else "/api"
177
+ )
178
+ demo_core.assert_http_ok(api + "/health", method="GET")
125
179
  # Intentionally suppress backend health print for concise output
126
180
  if env.task_app_base_url:
127
- ok_task = demo_core.assert_http_ok(env.task_app_base_url.rstrip("/") + "/health", method="GET") or \
128
- demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
181
+ demo_core.assert_http_ok(
182
+ env.task_app_base_url.rstrip("/") + "/health", method="GET"
183
+ ) or demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
129
184
  # Intentionally suppress task app health print
130
185
  else:
131
186
  print("\nSet your task app URL by running:\nuvx synth-ai rl_demo deploy\n")
@@ -133,13 +188,19 @@ def cmd_setup(_args: argparse.Namespace) -> int:
133
188
  # Omit uv version print to keep output concise
134
189
 
135
190
  # Keep exit code neutral; not all checks are critical for pairing
191
+ print(f"\nKeys saved to: {dotenv_path}")
136
192
  return 0
137
193
 
138
194
 
139
- def _popen_capture(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> tuple[int, str]:
195
+ def _popen_capture(
196
+ cmd: list[str], cwd: str | None = None, env: dict | None = None
197
+ ) -> tuple[int, str]:
140
198
  import subprocess
199
+
141
200
  try:
142
- proc = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
201
+ proc = subprocess.Popen(
202
+ cmd, cwd=cwd, env=get_subprocess_env(env), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
203
+ )
143
204
  out, _ = proc.communicate()
144
205
  return int(proc.returncode or 0), out or ""
145
206
  except Exception as e:
@@ -156,7 +217,7 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
156
217
  proc = subprocess.Popen(
157
218
  cmd,
158
219
  cwd=cwd,
159
- env=env,
220
+ env=get_subprocess_env(env),
160
221
  stdout=subprocess.PIPE,
161
222
  stderr=subprocess.STDOUT,
162
223
  text=True,
@@ -169,7 +230,8 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
169
230
  def _pump(stdout) -> None:
170
231
  try:
171
232
  for line in stdout:
172
- print(line.rstrip())
233
+ if not should_filter_log_line(line):
234
+ print(line.rstrip())
173
235
  except Exception:
174
236
  pass
175
237
 
@@ -183,7 +245,9 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
183
245
  return int(proc.returncode or 0)
184
246
 
185
247
 
186
- def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> tuple[int, str]:
248
+ def _popen_stream_capture(
249
+ cmd: list[str], cwd: str | None = None, env: dict | None = None
250
+ ) -> tuple[int, str]:
187
251
  """Stream subprocess output to stdout and also capture it into a buffer."""
188
252
  import subprocess
189
253
  import threading
@@ -193,7 +257,7 @@ def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | No
193
257
  proc = subprocess.Popen(
194
258
  cmd,
195
259
  cwd=cwd,
196
- env=env,
260
+ env=get_subprocess_env(env),
197
261
  stdout=subprocess.PIPE,
198
262
  stderr=subprocess.STDOUT,
199
263
  text=True,
@@ -207,8 +271,9 @@ def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | No
207
271
  try:
208
272
  for line in stdout:
209
273
  line = line.rstrip()
210
- print(line)
211
- buf_lines.append(line)
274
+ if not should_filter_log_line(line):
275
+ print(line)
276
+ buf_lines.append(line)
212
277
  except Exception:
213
278
  pass
214
279
 
@@ -222,55 +287,6 @@ def _popen_stream_capture(cmd: list[str], cwd: str | None = None, env: dict | No
222
287
  return int(proc.returncode or 0), "\n".join(buf_lines)
223
288
 
224
289
 
225
- def _mask_secret_args(args: list[str]) -> list[str]:
226
- masked: list[str] = []
227
- for a in args:
228
- if "=" in a and any(a.startswith(prefix) for prefix in ("ENVIRONMENT_API_KEY=", "OPENAI_API_KEY=", "SYNTH_API_KEY=")):
229
- try:
230
- key, value = a.split("=", 1)
231
- tail = value[-5:] if len(value) >= 5 else value
232
- masked.append(f"{key}=***{tail}")
233
- except Exception:
234
- masked.append("<masked>")
235
- else:
236
- masked.append(a)
237
- return masked
238
-
239
-
240
- def _ensure_modal_secret(
241
- secret_name: str,
242
- *,
243
- values: dict[str, str],
244
- label: str = "deploy",
245
- replace: bool = False,
246
- ) -> bool:
247
- prefix = f"[{label}]"
248
- if not secret_name.strip():
249
- raise RuntimeError("Secret name is required")
250
-
251
- if not values:
252
- raise RuntimeError("No values provided to create Modal secret")
253
-
254
- create_args = [f"{k}={v}" for k, v in values.items()]
255
- create_cmd = ["uv", "run", "modal", "secret", "create", secret_name, *create_args]
256
-
257
- if replace:
258
- print(f"{prefix} Removing Modal secret '{secret_name}' (if present)…")
259
- delete_cmd = ["bash", "-lc", f"printf 'y\\n' | uv run modal secret delete {secret_name}"]
260
- print(f"{prefix} Command:", " ".join(delete_cmd))
261
- delete_code = _popen_stream(delete_cmd)
262
- if delete_code != 0:
263
- print(f"{prefix} Warning: delete command exited with {delete_code}; continuing to create")
264
-
265
- print(f"\n{prefix} Creating Modal secret '{secret_name}'…")
266
- print(f"{prefix} Command:", " ".join(_mask_secret_args(create_cmd)))
267
- code = _popen_stream(create_cmd)
268
- if code != 0:
269
- raise RuntimeError("Failed to provision Modal secret (see logs above)")
270
-
271
- return True
272
-
273
-
274
290
  def _fmt_float(value: float) -> str:
275
291
  return f"{value:.10g}"
276
292
 
@@ -283,7 +299,19 @@ def _find_asgi_apps(root: Path) -> list[Path]:
283
299
  - "@modal.asgi_app()"
284
300
  """
285
301
  results: list[Path] = []
286
- skip_dirs = {".git", ".hg", ".svn", "node_modules", "dist", "build", "__pycache__", ".ruff_cache", ".mypy_cache", "venv", ".venv"}
302
+ skip_dirs = {
303
+ ".git",
304
+ ".hg",
305
+ ".svn",
306
+ "node_modules",
307
+ "dist",
308
+ "build",
309
+ "__pycache__",
310
+ ".ruff_cache",
311
+ ".mypy_cache",
312
+ "venv",
313
+ ".venv",
314
+ }
287
315
  for dirpath, dirnames, filenames in os.walk(root):
288
316
  dirnames[:] = [d for d in dirnames if d not in skip_dirs]
289
317
  for name in filenames:
@@ -297,16 +325,20 @@ def _find_asgi_apps(root: Path) -> list[Path]:
297
325
  results.append(path)
298
326
  except Exception:
299
327
  continue
328
+
300
329
  # Stable order: prioritize files under synth_demo/ first, then alphabetical
301
330
  def _priority(p: Path) -> tuple[int, str]:
302
331
  rel = str(p.resolve())
303
332
  in_demo = "/synth_demo/" in rel or rel.endswith("/synth_demo/task_app.py")
304
333
  return (0 if in_demo else 1, rel)
334
+
305
335
  results.sort(key=_priority)
306
336
  return results
307
337
 
308
338
 
309
- def _prompt_value(label: str, default: str | int | float, cast: Callable[[str], Any] | None = None) -> Any:
339
+ def _prompt_value(
340
+ label: str, default: str | int | float, cast: Callable[[str], Any] | None = None
341
+ ) -> Any:
310
342
  prompt = f"{label} [{default}]: "
311
343
  try:
312
344
  raw = input(prompt).strip()
@@ -325,7 +357,19 @@ def _prompt_value(label: str, default: str | int | float, cast: Callable[[str],
325
357
 
326
358
  def _find_vllm_tomls(root: Path) -> list[Path]:
327
359
  results: list[Path] = []
328
- skip_dirs = {".git", ".hg", ".svn", "node_modules", "dist", "build", "__pycache__", ".ruff_cache", ".mypy_cache", "venv", ".venv"}
360
+ skip_dirs = {
361
+ ".git",
362
+ ".hg",
363
+ ".svn",
364
+ "node_modules",
365
+ "dist",
366
+ "build",
367
+ "__pycache__",
368
+ ".ruff_cache",
369
+ ".mypy_cache",
370
+ "venv",
371
+ ".venv",
372
+ }
329
373
  for dirpath, dirnames, filenames in os.walk(root):
330
374
  dirnames[:] = [d for d in dirnames if d not in skip_dirs]
331
375
  for name in filenames:
@@ -345,7 +389,9 @@ def _create_new_config(env: DemoEnv) -> str:
345
389
  default_path = os.path.join(os.getcwd(), "demo_config.toml")
346
390
  while True:
347
391
  try:
348
- destination = input(f"Path to save new config [{default_path}]: ").strip() or default_path
392
+ destination = (
393
+ input(f"Path to save new config [{default_path}]: ").strip() or default_path
394
+ )
349
395
  except Exception:
350
396
  destination = default_path
351
397
  destination = os.path.abspath(destination)
@@ -354,7 +400,9 @@ def _create_new_config(env: DemoEnv) -> str:
354
400
  continue
355
401
  if os.path.exists(destination):
356
402
  try:
357
- overwrite = input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
403
+ overwrite = (
404
+ input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
405
+ )
358
406
  except Exception:
359
407
  overwrite = "n"
360
408
  if not overwrite.startswith("y"):
@@ -366,7 +414,9 @@ def _create_new_config(env: DemoEnv) -> str:
366
414
  model_name = _prompt_value("Model name", "Qwen/Qwen3-0.6B")
367
415
  compute_gpu_type = _prompt_value("Compute GPU type", "H100")
368
416
  compute_gpu_count = _prompt_value("Compute GPU count", 4, int)
369
- topology_gpu_type = _prompt_value("Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}")
417
+ topology_gpu_type = _prompt_value(
418
+ "Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}"
419
+ )
370
420
  gpus_for_vllm = _prompt_value("Topology gpus_for_vllm", 2, int)
371
421
  gpus_for_training = _prompt_value("Topology gpus_for_training", 1, int)
372
422
  tensor_parallel = _prompt_value("Topology tensor_parallel", 2, int)
@@ -384,8 +434,9 @@ def _create_new_config(env: DemoEnv) -> str:
384
434
  task_url_default = env.task_app_base_url or ""
385
435
  services_task_url = _prompt_value("services.task_url", task_url_default)
386
436
 
387
- template = textwrap.dedent(
388
- f"""\
437
+ template = (
438
+ textwrap.dedent(
439
+ f"""\
389
440
  # Crafter online RL training configuration (research local copy)
390
441
 
391
442
  [model]
@@ -527,7 +578,9 @@ def _create_new_config(env: DemoEnv) -> str:
527
578
  [services]
528
579
  task_url = \"{services_task_url}\"
529
580
  """
530
- ).strip() + "\n"
581
+ ).strip()
582
+ + "\n"
583
+ )
531
584
 
532
585
  with open(destination, "w", encoding="utf-8") as fh:
533
586
  fh.write(template)
@@ -546,7 +599,11 @@ def _select_or_create_config(explicit: str | None, env: DemoEnv) -> str:
546
599
  discovered = _find_vllm_tomls(search_root)
547
600
 
548
601
  extras: list[Path] = []
549
- packaged = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")))
602
+ packaged = Path(
603
+ os.path.abspath(
604
+ os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")
605
+ )
606
+ )
550
607
  extras.append(packaged)
551
608
  home_cfg = Path(os.path.expanduser("~/.synth-ai/demo_config.toml"))
552
609
  extras.append(home_cfg)
@@ -592,29 +649,36 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
592
649
 
593
650
  env_key = (env.env_api_key or "").strip()
594
651
  if not env_key:
595
- raise RuntimeError(f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first.")
652
+ raise RuntimeError(
653
+ f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first."
654
+ )
596
655
 
597
656
  task_url = env.task_app_base_url
598
657
  if not task_url or not _is_modal_public_url(task_url):
599
658
  resolved = ""
600
659
  if env.task_app_name:
601
660
  try:
602
- choice = input(
603
- f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: "
604
- ).strip().lower() or "y"
661
+ choice = (
662
+ input(f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: ")
663
+ .strip()
664
+ .lower()
665
+ or "y"
666
+ )
605
667
  except Exception:
606
668
  choice = "y"
607
669
  if choice.startswith("y"):
608
- code, out = _popen_capture([
609
- "uv",
610
- "run",
611
- "python",
612
- "-m",
613
- "modal",
614
- "app",
615
- "url",
616
- env.task_app_name,
617
- ])
670
+ code, out = _popen_capture(
671
+ [
672
+ "uv",
673
+ "run",
674
+ "python",
675
+ "-m",
676
+ "modal",
677
+ "app",
678
+ "url",
679
+ env.task_app_name,
680
+ ]
681
+ )
618
682
  if code == 0 and out:
619
683
  for tok in out.split():
620
684
  if _is_modal_public_url(tok):
@@ -623,7 +687,9 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
623
687
  if not resolved:
624
688
  print(f"[{label}] Task app URL not configured or not a valid Modal public URL.")
625
689
  print("Examples: https://<app-name>-fastapi-app.modal.run")
626
- entered = input("Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: ").strip()
690
+ entered = input(
691
+ "Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: "
692
+ ).strip()
627
693
  if not entered or not _is_modal_public_url(entered):
628
694
  raise RuntimeError(f"[{label}] Valid Task App URL is required.")
629
695
  task_url = entered.rstrip("/")
@@ -639,30 +705,26 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
639
705
  app_name = fallback
640
706
  demo_core.persist_task_url(task_url, name=app_name)
641
707
 
642
- secret_name = env.task_app_secret_name.strip() or f"{app_name}-secret"
643
708
  demo_core.persist_task_url(task_url, name=app_name)
644
- demo_core.persist_dotenv_values({
645
- "TASK_APP_BASE_URL": task_url,
646
- "TASK_APP_NAME": app_name,
647
- "TASK_APP_SECRET_NAME": secret_name,
648
- })
709
+ demo_core.persist_dotenv_values(
710
+ {
711
+ "TASK_APP_BASE_URL": task_url,
712
+ "TASK_APP_NAME": app_name,
713
+ "TASK_APP_SECRET_NAME": DEFAULT_TASK_APP_SECRET_NAME,
714
+ }
715
+ )
649
716
 
650
- openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip()
651
- secret_values: dict[str, str] = {"ENVIRONMENT_API_KEY": env_key}
652
- if openai_key:
653
- secret_values["OPENAI_API_KEY"] = openai_key
654
717
  if synth_key:
655
- secret_values["SYNTH_API_KEY"] = synth_key
718
+ os.environ["SYNTH_API_KEY"] = synth_key
656
719
 
657
- _ensure_modal_secret(secret_name, values=secret_values, label=label, replace=True)
720
+ openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip()
721
+ if openai_key:
722
+ os.environ["OPENAI_API_KEY"] = openai_key
658
723
 
659
- rollout_url = task_url.rstrip("/") + "/health/rollout"
660
724
  print(f"[{label}] Verifying rollout health:")
661
725
  try:
662
726
  ek = (env_key or "").strip()
663
- ek_len = len(ek)
664
- ek_tail = ek[-5:] if ek_len >= 5 else ek
665
- print(f"[{label}] Using ENVIRONMENT_API_KEY len={ek_len} last5={ek_tail}")
727
+ print(f"[{label}] {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
666
728
  except Exception:
667
729
  pass
668
730
  health_base = task_url.rstrip("/")
@@ -673,7 +735,6 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
673
735
  print(f"[{label}] GET", h)
674
736
  rc, body = _http("GET", h, headers={"X-API-Key": env_key})
675
737
  if rc == 200:
676
- rollout_url = h
677
738
  break
678
739
  print(f"[{label}] status: {rc}")
679
740
  try:
@@ -685,41 +746,64 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
685
746
  print(f"[{label}] body:", preview)
686
747
  if rc != 200:
687
748
  print(f"[{label}] Warning: rollout health check failed ({rc}). Response: {body}")
749
+ with contextlib.suppress(Exception):
750
+ print(f"[{label}] Sent header X-API-Key → {_key_preview(env_key, 'X-API-Key')}")
688
751
  else:
689
752
  print(f"[{label}] Task app rollout health check OK.")
690
753
 
691
754
  os.environ["TASK_APP_BASE_URL"] = task_url
692
755
  os.environ["ENVIRONMENT_API_KEY"] = env_key
756
+ os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
693
757
  updated_env = demo_core.load_env()
694
758
  updated_env.env_api_key = env_key
695
759
  updated_env.task_app_base_url = task_url
696
760
  updated_env.task_app_name = app_name
697
- updated_env.task_app_secret_name = secret_name
761
+ updated_env.task_app_secret_name = DEFAULT_TASK_APP_SECRET_NAME
698
762
  return updated_env
699
763
 
700
764
 
701
- def cmd_deploy(args: argparse.Namespace) -> int:
765
+ def deploy(
766
+ local: bool = False, app: str | None = None, name: str | None = None, script: str | None = None
767
+ ) -> int:
768
+ # Change to demo directory if stored
769
+ demo_dir = demo_core.load_demo_dir()
770
+ if demo_dir and os.path.isdir(demo_dir):
771
+ os.chdir(demo_dir)
772
+ print(f"Using demo directory: {demo_dir}")
773
+
702
774
  env = demo_core.load_env()
775
+ os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
703
776
  cwd_env_path = os.path.join(os.getcwd(), ".env")
704
777
  local_env = demo_core.load_dotenv_file(cwd_env_path)
705
778
  url = ""
706
779
  app_name = env.task_app_name or ""
707
780
  try:
708
- if args.local:
781
+ if local:
709
782
  print("Starting local Task App…")
710
783
  import subprocess
711
- subprocess.Popen([sys.executable, "-c", "from synth_ai.demos.demo_task_apps.math.app import run; run()"],
712
- stdout=sys.stdout, stderr=sys.stderr)
784
+
785
+ subprocess.Popen(
786
+ [
787
+ sys.executable,
788
+ "-c",
789
+ "from synth_ai.demos.demo_task_apps.math.app import run; run()",
790
+ ],
791
+ env=get_subprocess_env(),
792
+ stdout=sys.stdout,
793
+ stderr=sys.stderr,
794
+ )
713
795
  target = "http://127.0.0.1:8080"
714
796
  app_name = ""
715
797
  for _ in range(30):
716
- if demo_core.assert_http_ok(target + "/health", method="GET") or demo_core.assert_http_ok(target, method="GET"):
798
+ if demo_core.assert_http_ok(
799
+ target + "/health", method="GET"
800
+ ) or demo_core.assert_http_ok(target, method="GET"):
717
801
  url = target
718
802
  break
719
803
  time.sleep(1)
720
804
  else:
721
805
  # Auto-detect app path if not supplied; prompt interactively from discovered ASGI apps
722
- app_path = os.path.abspath(args.app) if args.app else None
806
+ app_path = os.path.abspath(app) if app else None
723
807
  if not app_path or not os.path.isfile(app_path):
724
808
  # First pass: look for known common filenames
725
809
  candidates = [
@@ -738,7 +822,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
738
822
  rel = os.path.relpath(str(pth), os.getcwd())
739
823
  print(f" [{idx}] {rel}")
740
824
  try:
741
- sel = input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
825
+ sel = (
826
+ input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
827
+ )
742
828
  except Exception:
743
829
  sel = "1"
744
830
  try:
@@ -747,12 +833,13 @@ def cmd_deploy(args: argparse.Namespace) -> int:
747
833
  choice = 1
748
834
  choice = max(1, min(choice, len(found)))
749
835
  app_path = str(found[choice - 1].resolve())
750
- if not app_path and args.script:
836
+ if not app_path and script:
751
837
  # Legacy script fallback if user supplied --script explicitly
752
838
  from synth_ai.demos.demo_task_apps.math.deploy_modal import deploy as modal_deploy
753
- url = modal_deploy(script_path=args.script, env_api_key=env.env_api_key)
754
- if args.name:
755
- app_name = args.name
839
+
840
+ url = modal_deploy(script_path=script, env_api_key=env.env_api_key)
841
+ if name:
842
+ app_name = name
756
843
  else:
757
844
  if not app_path:
758
845
  entered = input("Path to Modal app.py (e.g., ./task_app.py): ").strip()
@@ -763,7 +850,10 @@ def cmd_deploy(args: argparse.Namespace) -> int:
763
850
  raise FileNotFoundError(f"App file not found: {app_path}")
764
851
  # Surface the app path before asking for the name
765
852
  print(f"Using task app: {app_path}")
766
- suggested_name = args.name or f"synth-{os.path.splitext(os.path.basename(app_path))[0]}"
853
+ existing_name = (name or env.task_app_name or "").strip()
854
+ if not existing_name:
855
+ existing_name = f"synth-{os.path.splitext(os.path.basename(app_path))[0]}"
856
+ suggested_name = existing_name
767
857
  name_in = input(f"Modal app name [{suggested_name}]: ").strip() or suggested_name
768
858
  app_name = name_in
769
859
  print("\nAbout to deploy with:")
@@ -774,21 +864,23 @@ def cmd_deploy(args: argparse.Namespace) -> int:
774
864
  print("Aborted by user.")
775
865
  return 1
776
866
 
777
- secret_name = (env.task_app_secret_name or "").strip() or f"{name_in}-secret"
778
867
  existing_env_key = (env.env_api_key or "").strip()
779
868
  env_key: str | None = existing_env_key or None
780
869
  if existing_env_key:
781
870
  try:
782
- reuse_choice = input(
783
- "Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: "
784
- ).strip().lower() or "y"
871
+ reuse_choice = (
872
+ input("Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: ")
873
+ .strip()
874
+ .lower()
875
+ or "y"
876
+ )
785
877
  except Exception:
786
878
  reuse_choice = "y"
787
879
  if not reuse_choice.startswith("y"):
788
880
  env_key = None
789
881
 
790
882
  if env_key is None:
791
- from synth_ai.rl.secrets import mint_environment_api_key
883
+ from synth_ai.learning.rl.secrets import mint_environment_api_key
792
884
 
793
885
  env_key = mint_environment_api_key()
794
886
  demo_core.persist_env_api_key(env_key)
@@ -797,69 +889,90 @@ def cmd_deploy(args: argparse.Namespace) -> int:
797
889
  env.env_api_key = env_key
798
890
  local_env["ENVIRONMENT_API_KEY"] = env_key
799
891
  print("[deploy] Minted new ENVIRONMENT_API_KEY")
800
-
892
+ elif env_key:
893
+ os.environ["ENVIRONMENT_API_KEY"] = env_key
894
+
801
895
  # Optionally upload the new key to the backend using sealed box helper
802
- backend_base = env.dev_backend_url or ""
803
- synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
896
+ backend_base = (env.dev_backend_url or "").rstrip("/")
897
+ synth_key = (
898
+ env.synth_api_key
899
+ or os.environ.get("SYNTH_API_KEY")
900
+ or local_env.get("SYNTH_API_KEY")
901
+ or ""
902
+ ).strip()
804
903
  if backend_base and synth_key:
805
- backend_base = backend_base.rstrip("/")
806
- if not backend_base.endswith("/api"):
807
- backend_base = f"{backend_base}/api"
904
+ # Pass a base WITHOUT trailing /api to setup_environment_api_key,
905
+ # since it appends /api/v1/... internally.
906
+ non_api_base = (
907
+ backend_base[:-4] if backend_base.endswith("/api") else backend_base
908
+ )
909
+ try:
910
+ choice = (
911
+ input(f"Upload ENVIRONMENT_API_KEY to backend {non_api_base}? [Y/n]: ")
912
+ .strip()
913
+ .lower()
914
+ or "y"
915
+ )
916
+ except Exception:
917
+ choice = "y"
918
+ if choice.startswith("y"):
808
919
  try:
809
- choice = input(
810
- f"Upload ENVIRONMENT_API_KEY to backend {backend_base}? [Y/n]: "
811
- ).strip().lower() or "y"
812
- except Exception:
813
- choice = "y"
814
- if choice.startswith("y"):
815
- try:
816
- print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {backend_base} …")
817
- from synth_ai.rl.env_keys import setup_environment_api_key
818
-
819
- setup_environment_api_key(backend_base.rstrip("/"), synth_key, token=env_key)
820
- print("[deploy] Backend sealed-box upload complete.")
821
- except Exception as upload_err:
822
- print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
823
- print(
824
- "Hint: run `uvx python -c \"from synth_ai.rl.env_keys import setup_environment_api_key as s;"
825
- " s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
826
- )
827
-
828
- synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
920
+ print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {non_api_base} …")
921
+ from synth_ai.learning.rl.env_keys import setup_environment_api_key
922
+
923
+ setup_environment_api_key(non_api_base, synth_key, token=env_key)
924
+ print("[deploy] Backend sealed-box upload complete.")
925
+ except Exception as upload_err:
926
+ print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
927
+ print(
928
+ 'Hint: run `uvx python -c "from synth_ai.learning.rl.env_keys import setup_environment_api_key as s;'
929
+ " s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
930
+ )
931
+
932
+ synth_key = (
933
+ env.synth_api_key
934
+ or os.environ.get("SYNTH_API_KEY")
935
+ or local_env.get("SYNTH_API_KEY")
936
+ or ""
937
+ ).strip()
829
938
  if not synth_key:
830
- synth_key = input("Enter SYNTH_API_KEY for Modal secret (required): ").strip()
939
+ synth_key = input("Enter SYNTH_API_KEY for deployment (required): ").strip()
831
940
  if not synth_key:
832
- print("SYNTH_API_KEY is required to create the Modal secret.")
941
+ print("SYNTH_API_KEY is required for deployment.")
833
942
  return 1
834
943
  demo_core.persist_api_key(synth_key)
835
944
  demo_core.persist_dotenv_values({"SYNTH_API_KEY": synth_key})
836
945
  env.synth_api_key = synth_key
946
+ os.environ["SYNTH_API_KEY"] = synth_key
837
947
 
838
- openai_key = (os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or "").strip()
948
+ openai_key = (
949
+ os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or ""
950
+ ).strip()
839
951
  if not openai_key:
840
952
  openai_key = input(
841
953
  "Enter your OpenAI API key, found at https://platform.openai.com/api-keys\n> "
842
954
  ).strip()
843
955
  if not openai_key:
844
- print("OPENAI_API_KEY is required to create the Modal secret.")
956
+ print("OPENAI_API_KEY is required for deployment.")
845
957
  return 1
846
958
  demo_core.persist_dotenv_values({"OPENAI_API_KEY": openai_key})
847
959
  local_env["OPENAI_API_KEY"] = openai_key
960
+ os.environ["OPENAI_API_KEY"] = openai_key
848
961
 
849
- values = {"SYNTH_API_KEY": synth_key, "OPENAI_API_KEY": openai_key}
850
- if env_key:
851
- values["ENVIRONMENT_API_KEY"] = env_key
852
-
853
- try:
854
- created = _ensure_modal_secret(secret_name, values=values, label="deploy", replace=True)
855
- except RuntimeError as secret_err:
856
- print(f"Failed to prepare Modal secret '{secret_name}': {secret_err}")
857
- return 2
858
- if created:
859
- print(f"[deploy] Modal secret '{secret_name}' provisioned.")
860
-
861
- deploy_cmd = ["uv", "run", "python", "-m", "modal", "deploy", "--name", name_in, app_path]
862
- print("\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n")
962
+ deploy_cmd = [
963
+ "uv",
964
+ "run",
965
+ "python",
966
+ "-m",
967
+ "modal",
968
+ "deploy",
969
+ "--name",
970
+ name_in,
971
+ app_path,
972
+ ]
973
+ print(
974
+ "\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n"
975
+ )
863
976
  code, deploy_logs = _popen_stream_capture(deploy_cmd)
864
977
  if code != 0:
865
978
  raise RuntimeError(f"modal deploy failed (exit {code})")
@@ -867,6 +980,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
867
980
  if not url:
868
981
  try:
869
982
  import re as _re
983
+
870
984
  m_all = _re.findall(r"https?://[^\s]+\.modal\.run", deploy_logs or "")
871
985
  if m_all:
872
986
  url = m_all[-1].strip().rstrip("/")
@@ -881,7 +995,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
881
995
  break
882
996
  # Fallback: try reading recent Modal logs for the app to find a URL line
883
997
  if not url:
884
- code3, out3 = _popen_capture(["uv", "run", "python", "-m", "modal", "app", "list"])
998
+ code3, out3 = _popen_capture(
999
+ ["uv", "run", "python", "-m", "modal", "app", "list"]
1000
+ )
885
1001
  if code3 == 0 and out3:
886
1002
  for line in out3.splitlines():
887
1003
  if name_in in line:
@@ -894,7 +1010,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
894
1010
  # Prompt user if still no valid URL
895
1011
  if not url:
896
1012
  print("\nCould not auto-detect a public Modal URL for the app.")
897
- entered = input("Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: ").strip()
1013
+ entered = input(
1014
+ "Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: "
1015
+ ).strip()
898
1016
  if entered and _is_modal_public_url(entered):
899
1017
  url = entered.rstrip("/")
900
1018
  if not url:
@@ -906,7 +1024,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
906
1024
  dotenv_values = {"TASK_APP_BASE_URL": url}
907
1025
  if app_name:
908
1026
  dotenv_values["TASK_APP_NAME"] = app_name
909
- dotenv_values["TASK_APP_SECRET_NAME"] = f"{app_name}-secret"
1027
+ dotenv_values["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
910
1028
  dotenv_path = demo_core.persist_dotenv_values(dotenv_values)
911
1029
  print(f"TASK_APP_BASE_URL={url}")
912
1030
  if app_name:
@@ -915,16 +1033,16 @@ def cmd_deploy(args: argparse.Namespace) -> int:
915
1033
  print(f" export TASK_APP_BASE_URL={url}")
916
1034
  if app_name:
917
1035
  print(f" export TASK_APP_NAME={app_name}")
918
- print(f" export TASK_APP_SECRET_NAME={app_name}-secret")
919
1036
  print(f"Persisted to {dotenv_path}")
920
- print("Next: uvx synth-ai run")
1037
+ print("\nNext step:\n$ uvx synth-ai run")
921
1038
  return 0
922
1039
  except Exception as e:
923
1040
  print(f"Deploy error: {e}")
924
1041
  return 2
925
1042
 
926
-
927
- print("`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches.")
1043
+ print(
1044
+ "`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches."
1045
+ )
928
1046
  env = demo_core.load_env()
929
1047
  synth_key = (env.synth_api_key or "").strip()
930
1048
  if not synth_key:
@@ -956,133 +1074,314 @@ def cmd_deploy(args: argparse.Namespace) -> int:
956
1074
  return 0
957
1075
 
958
1076
 
959
- def cmd_init(args: argparse.Namespace) -> int:
960
- """Initialize a Modal-ready Math Task App in the current directory.
1077
+ def _ensure_modal_installed() -> None:
1078
+ """Install the modal package if it is not already available and check authentication."""
961
1079
 
962
- Copies `examples/rl/task_app.py` and `examples/rl/deploy_task_app.sh` into CWD.
963
- Creates a `.env` with placeholders if it does not exist.
964
- """
1080
+ # Check if modal is installed
1081
+ modal_installed = False
965
1082
  try:
966
- # Ensure `modal` is installed for deployment flows
967
- def _has_modal() -> bool:
968
- try:
969
- import importlib.util as _iu
970
- return _iu.find_spec("modal") is not None
971
- except Exception:
972
- return False
1083
+ import importlib.util as _iu
973
1084
 
974
- if not _has_modal():
975
- print("modal not found; installing…")
976
- # Prefer uv if available; otherwise fallback to pip
977
- try:
978
- if shutil.which("uv"):
979
- code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
980
- else:
981
- code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
982
- if code != 0:
983
- print(out)
984
- print("Failed to install modal; continuing may fail.")
985
- else:
986
- print("modal installed successfully.")
987
- except Exception as e:
988
- print(f"modal install error: {e}")
989
- # Re-check
990
- if not _has_modal():
991
- print("Warning: modal is still not importable after install attempt.")
992
- else:
993
- print("modal found")
994
-
995
- here = os.getcwd()
996
- demo_dir = os.path.join(here, "synth_demo")
997
- os.makedirs(demo_dir, exist_ok=True)
998
- # Paths inside synth_demo/
999
- dst_task_py = os.path.join(demo_dir, "task_app.py")
1000
- dst_deploy = os.path.join(demo_dir, "deploy_task_app.sh")
1001
- env_path = os.path.join(demo_dir, ".env")
1002
- dst_cfg = os.path.join(demo_dir, "demo_config.toml")
1003
-
1004
- # Copy packaged math modal task app into synth_demo/task_app.py
1005
- src_modal = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "modal_task_app.py"))
1006
- if not os.path.isfile(src_modal):
1007
- print("Init failed: packaged math modal task app not found.")
1008
- print(f"Looked for: {src_modal}")
1009
- return 1
1010
- if os.path.exists(dst_task_py) and not getattr(args, "force", False):
1011
- print(f"Refusing to overwrite existing file: {dst_task_py} (use --force)")
1012
- return 1
1013
- shutil.copy2(src_modal, dst_task_py)
1014
-
1015
- # Create deploy script in synth_demo/
1016
- deploy_text = r"""#!/usr/bin/env bash
1017
- set -euo pipefail
1018
-
1019
- HERE=$(cd "$(dirname "$0")" && pwd)
1020
- APP="$HERE/task_app.py"
1021
- if [ -f "$HERE/.env" ]; then
1022
- # shellcheck disable=SC2046
1023
- export $(grep -v '^#' "$HERE/.env" | xargs -I{} echo {})
1024
- fi
1025
- uv run modal deploy "$APP" | tee "$HERE/.last_deploy.log"
1026
- URL=$(grep -Eo 'https://[^ ]+\.modal\.run' "$HERE/.last_deploy.log" | tail -1 || true)
1027
- if [ -n "$URL" ]; then
1028
- if grep -q '^TASK_APP_BASE_URL=' "$HERE/.env" 2>/dev/null; then
1029
- sed -i.bak "s#^TASK_APP_BASE_URL=.*#TASK_APP_BASE_URL=$URL#" "$HERE/.env" || true
1030
- else
1031
- echo "TASK_APP_BASE_URL=$URL" >> "$HERE/.env"
1032
- fi
1033
- echo "Saved TASK_APP_BASE_URL to $HERE/.env"
1034
- fi
1035
- """
1036
- _write_text(dst_deploy, deploy_text)
1085
+ if _iu.find_spec("modal") is not None:
1086
+ modal_installed = True
1087
+ except Exception:
1088
+ pass
1089
+
1090
+ # Install modal if needed
1091
+ if not modal_installed:
1092
+ print("modal not found; installing…")
1093
+ try:
1094
+ if shutil.which("uv"):
1095
+ code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
1096
+ else:
1097
+ code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
1098
+ if code != 0:
1099
+ print(out)
1100
+ print("Failed to install modal; continuing may fail.")
1101
+ return
1102
+ else:
1103
+ print("✓ modal installed successfully")
1104
+ modal_installed = True
1105
+ except Exception as exc:
1106
+ print(f"modal install error: {exc}")
1107
+ return
1108
+
1109
+ # Verify modal is importable
1110
+ if modal_installed:
1037
1111
  try:
1038
- st = os.stat(dst_deploy)
1039
- os.chmod(dst_deploy, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
1112
+ import importlib.util as _iu
1113
+
1114
+ if _iu.find_spec("modal") is None:
1115
+ print("Warning: modal is still not importable after install attempt.")
1116
+ return
1040
1117
  except Exception:
1041
- pass
1118
+ print("Warning: unable to verify modal installation.")
1119
+ return
1120
+
1121
+ # Check modal authentication status
1122
+ auth_ok, auth_msg = demo_core.modal_auth_status()
1123
+ if auth_ok:
1124
+ print(f"✓ Modal authenticated: {auth_msg}")
1125
+ else:
1126
+ print("\n⚠️ Modal authentication required")
1127
+ print(f" Status: {auth_msg}")
1128
+ print("\n To authenticate Modal, run:")
1129
+ print(" modal setup")
1130
+ print("\n Or set environment variables:")
1131
+ print(" export MODAL_TOKEN_ID=your-token-id")
1132
+ print(" export MODAL_TOKEN_SECRET=your-token-secret")
1133
+ print("\n You can deploy later after authenticating.\n")
1134
+
1135
+
1136
+ def init(template: str | None = None, dest: str | None = None, force: bool = False) -> int:
1137
+ """Materialise a demo task app template into the current directory."""
1138
+
1139
+ templates = list(list_demo_templates())
1140
+ if not templates:
1141
+ print("No demo templates registered. Update synth_ai/demo_registry.py to add entries.")
1142
+ return 1
1143
+
1144
+ selected: DemoTemplate | None = None
1145
+ if template:
1146
+ selected = get_demo_template(template)
1147
+ if selected is None:
1148
+ available = ", ".join(t.template_id for t in templates)
1149
+ print(f"Unknown template '{template}'. Available: {available}")
1150
+ return 1
1151
+ else:
1152
+ if force:
1153
+ selected = templates[0]
1154
+ print(
1155
+ f"Using default template: {selected.name} ({selected.template_id}) "
1156
+ f"(pass --template to choose another)"
1157
+ )
1158
+ else:
1159
+ print("Select a demo template:" + "\n")
1160
+ for idx, tpl in enumerate(templates, start=1):
1161
+ print(f" [{idx}] {tpl.name} ({tpl.template_id})")
1162
+ print(f" {tpl.description}")
1163
+ try:
1164
+ choice_raw = input(f"Enter choice [1-{len(templates)}] (default 1): ").strip() or "1"
1165
+ except Exception:
1166
+ choice_raw = "1"
1167
+ if not choice_raw.isdigit():
1168
+ print("Selection must be a number.")
1169
+ return 1
1170
+ choice_idx = int(choice_raw)
1171
+ if not 1 <= choice_idx <= len(templates):
1172
+ print("Selection out of range.")
1173
+ return 1
1174
+ selected = templates[choice_idx - 1]
1175
+
1176
+ assert selected is not None
1177
+
1178
+ default_subdir = selected.default_subdir or selected.template_id
1179
+
1180
+ # Check if default destination is already occupied and switch to local_demos/ if needed
1181
+ if dest:
1182
+ default_dest = Path(dest).expanduser().resolve()
1183
+ else:
1184
+ primary_dest = Path.cwd() / default_subdir
1185
+ if primary_dest.exists() and any(primary_dest.iterdir()):
1186
+ # Switch to local_demos/ automatically if primary location is occupied
1187
+ default_dest = (Path.cwd() / "local_demos" / default_subdir).resolve()
1188
+ else:
1189
+ default_dest = primary_dest.resolve()
1042
1190
 
1043
- # Seed .env if not present
1044
- if not os.path.exists(env_path):
1045
- _write_text(env_path, "\n".join([
1046
- "# Required for task app auth to environment service",
1047
- "ENVIRONMENT_API_KEY=",
1048
- "",
1049
- "# Optional: for CLI job submission and proxying OpenAI models",
1050
- "SYNTH_API_KEY=",
1051
- "OPENAI_API_KEY=",
1052
- "",
1053
- "# Optional: set to 'prod' to use production names",
1054
- "ENVIRONMENT=",
1055
- ]) + "\n")
1056
-
1057
- # Seed demo_config.toml from packaged default if not present (or overwrite with --force)
1058
- packaged_cfg = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml"))
1191
+ if force:
1192
+ dest_input = ""
1193
+ else:
1059
1194
  try:
1060
- if os.path.isfile(packaged_cfg):
1061
- if not os.path.exists(dst_cfg) or getattr(args, "force", False):
1062
- shutil.copy2(packaged_cfg, dst_cfg)
1195
+ dest_input = input(f"Destination directory [{default_dest}]: ").strip()
1063
1196
  except Exception:
1064
- pass
1197
+ dest_input = ""
1198
+ destination = Path(dest_input).expanduser().resolve() if dest_input else default_dest
1199
+
1200
+ # Track whether we should skip individual file prompts (if we already cleared the directory)
1201
+ directory_cleared = False
1202
+
1203
+ if destination.exists():
1204
+ if destination.is_file():
1205
+ print(f"Destination {destination} is a file. Provide a directory path.")
1206
+ return 1
1207
+ if any(destination.iterdir()):
1208
+ if force:
1209
+ response = "y"
1210
+ else:
1211
+ try:
1212
+ response = (
1213
+ input(f"Destination {destination} is not empty. Overwrite? [y/N]: ")
1214
+ .strip()
1215
+ .lower()
1216
+ )
1217
+ except (EOFError, KeyboardInterrupt):
1218
+ print("\nCancelled.")
1219
+ return 1
1220
+ if response not in ("y", "yes"):
1221
+ print("Cancelled. Choose another directory or delete the existing one.")
1222
+ return 1
1223
+ # User agreed to overwrite - clear the entire directory including hidden files
1224
+ print(f"Clearing {destination}...")
1225
+ try:
1226
+ # Remove all contents including hidden files (.env, .git, etc.)
1227
+ shutil.rmtree(destination)
1228
+ except Exception as e:
1229
+ print(f"Error clearing directory: {e}")
1230
+ print("Please manually remove the directory and try again.")
1231
+ return 1
1232
+ # Recreate empty directory
1233
+ destination.mkdir(parents=True, exist_ok=True)
1234
+ # Verify it's actually empty
1235
+ if any(destination.iterdir()):
1236
+ print(f"Warning: Directory {destination} still contains files after clearing.")
1237
+ print("Some files may not have been removed. Please check manually.")
1238
+ return 1
1239
+ directory_cleared = True
1240
+ else:
1241
+ destination.mkdir(parents=True, exist_ok=True)
1242
+
1243
+ if selected.requires_modal:
1244
+ _ensure_modal_installed()
1245
+
1246
+ try:
1247
+ for spec in selected.iter_copy_specs():
1248
+ src_path = spec.absolute_source()
1249
+ if not src_path.exists():
1250
+ print(f"Template source missing: {src_path}")
1251
+ return 1
1252
+ dest_path = (destination / spec.destination).resolve()
1253
+
1254
+ # Handle directory copying
1255
+ if src_path.is_dir():
1256
+ if dest_path.exists() and not directory_cleared:
1257
+ if force:
1258
+ response = "y"
1259
+ else:
1260
+ try:
1261
+ response = (
1262
+ input(f"Directory {dest_path.name} exists. Overwrite? [y/N]: ")
1263
+ .strip()
1264
+ .lower()
1265
+ )
1266
+ except (EOFError, KeyboardInterrupt):
1267
+ print("\nCancelled.")
1268
+ return 1
1269
+ if response not in ("y", "yes"):
1270
+ print(f"Skipping {dest_path.name}")
1271
+ continue
1272
+ shutil.rmtree(dest_path)
1273
+ elif dest_path.exists() and directory_cleared:
1274
+ shutil.rmtree(dest_path)
1275
+ shutil.copytree(src_path, dest_path)
1276
+ else:
1277
+ # Handle file copying
1278
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
1279
+ if dest_path.exists() and not directory_cleared:
1280
+ if force:
1281
+ response = "y"
1282
+ else:
1283
+ try:
1284
+ response = (
1285
+ input(f"File {dest_path.name} exists. Overwrite? [y/N]: ")
1286
+ .strip()
1287
+ .lower()
1288
+ )
1289
+ except (EOFError, KeyboardInterrupt):
1290
+ print("\nCancelled.")
1291
+ return 1
1292
+ if response not in ("y", "yes"):
1293
+ print(f"Skipping {dest_path.name}")
1294
+ continue
1295
+ shutil.copy2(src_path, dest_path)
1296
+ if spec.make_executable:
1297
+ try:
1298
+ st = os.stat(dest_path)
1299
+ os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
1300
+ except Exception:
1301
+ pass
1065
1302
 
1066
- print("Initialized Math Task App in synth_demo/:")
1067
- print(f" - {dst_task_py}")
1068
- print(f" - {dst_deploy}")
1069
- print(f" - {env_path} (created if missing)")
1070
- if os.path.exists(dst_cfg):
1071
- print(f" - {dst_cfg} (seeded)")
1072
- print("")
1073
- print("Next steps:")
1074
- print(" 1) cd synth_demo && put your ENVIRONMENT_API_KEY in ./.env")
1075
- print(" 2) Deploy to Modal:")
1076
- print(" uvx bash ./deploy_task_app.sh")
1077
- print(" 3) From project root, run: uvx synth-ai run")
1303
+ if selected.env_lines:
1304
+ env_path = destination / ".env"
1305
+ should_write = True
1306
+ if env_path.exists() and not directory_cleared:
1307
+ if force:
1308
+ response = "y"
1309
+ else:
1310
+ try:
1311
+ response = input("File .env exists. Overwrite? [y/N]: ").strip().lower()
1312
+ except (EOFError, KeyboardInterrupt):
1313
+ print("\nCancelled.")
1314
+ return 1
1315
+ should_write = response in ("y", "yes")
1316
+ if should_write:
1317
+ _write_text(env_path, "\n".join(selected.env_lines) + "\n")
1318
+ elif not directory_cleared:
1319
+ print("Skipping .env")
1320
+
1321
+ config_src = selected.config_source_path()
1322
+ if config_src and config_src.exists():
1323
+ cfg_dst = (destination / selected.config_destination).resolve()
1324
+ should_copy = True
1325
+ if cfg_dst.exists() and not directory_cleared:
1326
+ if force:
1327
+ response = "y"
1328
+ else:
1329
+ try:
1330
+ response = (
1331
+ input(f"File {cfg_dst.name} exists. Overwrite? [y/N]: ").strip().lower()
1332
+ )
1333
+ except (EOFError, KeyboardInterrupt):
1334
+ print("\nCancelled.")
1335
+ return 1
1336
+ should_copy = response in ("y", "yes")
1337
+ if should_copy:
1338
+ cfg_dst.parent.mkdir(parents=True, exist_ok=True)
1339
+ shutil.copy2(config_src, cfg_dst)
1340
+ elif not directory_cleared:
1341
+ print(f"Skipping {cfg_dst.name}")
1342
+
1343
+ if selected.post_copy is not None:
1344
+ try:
1345
+ selected.post_copy(destination)
1346
+ except Exception as post_exc:
1347
+ print(f"Post-processing failed: {post_exc}")
1348
+ return 1
1349
+
1350
+ # Store demo directory for subsequent commands
1351
+ demo_core.persist_demo_dir(str(destination))
1352
+
1353
+ # Store .env path if it was created
1354
+ env_file = destination / ".env"
1355
+ if env_file.exists():
1356
+ demo_core.persist_env_file_path(str(env_file))
1357
+
1358
+ print(f"Demo template '{selected.name}' materialised at {destination}.")
1359
+ print("Files created:")
1360
+ for spec in selected.iter_copy_specs():
1361
+ print(f" - {spec.destination}")
1362
+ if selected.env_lines:
1363
+ print(" - .env")
1364
+ if selected.config_source_path():
1365
+ print(f" - {selected.config_destination}")
1366
+ print("\nDemo directory stored. Subsequent commands will use this directory automatically.")
1367
+ print("Review the files, edit .env, and run any provided deploy scripts when ready.")
1078
1368
  return 0
1079
- except Exception as e:
1080
- print(f"Init error: {e}")
1081
- return 2
1369
+ except KeyboardInterrupt:
1370
+ print("Aborted")
1371
+ return 1
1372
+ except Exception as exc:
1373
+ print(f"Init failed: {exc}")
1374
+ return 1
1082
1375
 
1083
1376
 
1084
- def _http(method: str, url: str, headers: Dict[str, str] | None = None, body: Dict[str, Any] | None = None) -> tuple[int, Dict[str, Any] | str]:
1085
- import urllib.request, urllib.error, json as _json, ssl
1377
+ def _http(
1378
+ method: str, url: str, headers: dict[str, str] | None = None, body: dict[str, Any] | None = None
1379
+ ) -> tuple[int, dict[str, Any] | str]:
1380
+ import json as _json
1381
+ import ssl
1382
+ import urllib.error
1383
+ import urllib.request
1384
+
1086
1385
  data = None
1087
1386
  if body is not None:
1088
1387
  data = _json.dumps(body).encode("utf-8")
@@ -1119,10 +1418,23 @@ def _write_text(path: str, content: str) -> None:
1119
1418
  # Note: `prepare` command has been removed; configuration now prepares TOML
1120
1419
 
1121
1420
 
1122
- def cmd_run(args: argparse.Namespace) -> int:
1421
+ def run(
1422
+ config: str | None = None,
1423
+ batch_size: int | None = None,
1424
+ group_size: int | None = None,
1425
+ model: str | None = None,
1426
+ timeout: int = 600,
1427
+ dry_run: bool = False,
1428
+ ) -> int:
1429
+ # Change to demo directory if stored
1430
+ demo_dir = demo_core.load_demo_dir()
1431
+ if demo_dir and os.path.isdir(demo_dir):
1432
+ os.chdir(demo_dir)
1433
+ print(f"Using demo directory: {demo_dir}")
1434
+
1123
1435
  env = demo_core.load_env()
1124
1436
  cwd_env_path = os.path.join(os.getcwd(), ".env")
1125
- local_env = demo_core.load_dotenv_file(cwd_env_path)
1437
+ demo_core.load_dotenv_file(cwd_env_path)
1126
1438
 
1127
1439
  synth_key = (env.synth_api_key or "").strip()
1128
1440
  if not synth_key:
@@ -1154,7 +1466,7 @@ def cmd_run(args: argparse.Namespace) -> int:
1154
1466
  import tomllib
1155
1467
 
1156
1468
  try:
1157
- cfg_path = _select_or_create_config(getattr(args, "config", None), env)
1469
+ cfg_path = _select_or_create_config(config, env)
1158
1470
  except FileNotFoundError as exc:
1159
1471
  print(exc)
1160
1472
  return 1
@@ -1162,7 +1474,11 @@ def cmd_run(args: argparse.Namespace) -> int:
1162
1474
  # Detect monorepo launcher and delegate if available (aligns with run_clustered.sh which works)
1163
1475
  launcher = "/Users/joshpurtell/Documents/GitHub/monorepo/tests/applications/math/rl/start_math_clustered.py"
1164
1476
  if os.path.isfile(launcher):
1165
- backend_base = env.dev_backend_url[:-4] if env.dev_backend_url.endswith("/api") else env.dev_backend_url
1477
+ backend_base = (
1478
+ env.dev_backend_url[:-4]
1479
+ if env.dev_backend_url.endswith("/api")
1480
+ else env.dev_backend_url
1481
+ )
1166
1482
  run_env = os.environ.copy()
1167
1483
  run_env["BACKEND_URL"] = backend_base
1168
1484
  run_env["SYNTH_API_KEY"] = env.synth_api_key
@@ -1172,12 +1488,12 @@ def cmd_run(args: argparse.Namespace) -> int:
1172
1488
  # Optional: TRAINER_START_URL passthrough if already set in environment
1173
1489
  run_env["TRAINER_START_URL"] = run_env.get("TRAINER_START_URL", "")
1174
1490
  # Forward convenience knobs
1175
- if args.batch_size is not None:
1176
- run_env["RL_BATCH_SIZE"] = str(int(args.batch_size))
1177
- if args.group_size is not None:
1178
- run_env["RL_GROUP_SIZE"] = str(int(args.group_size))
1179
- if args.model:
1180
- run_env["RL_MODEL"] = args.model
1491
+ if batch_size is not None:
1492
+ run_env["RL_BATCH_SIZE"] = str(int(batch_size))
1493
+ if group_size is not None:
1494
+ run_env["RL_GROUP_SIZE"] = str(int(group_size))
1495
+ if model:
1496
+ run_env["RL_MODEL"] = model
1181
1497
  cmd = ["uv", "run", "python", launcher]
1182
1498
  print(f"Launching monorepo clustered runner: {' '.join(cmd)}")
1183
1499
  code = _popen_stream(cmd, env=run_env)
@@ -1192,33 +1508,33 @@ def cmd_run(args: argparse.Namespace) -> int:
1192
1508
  ek = (env.env_api_key or "").strip()
1193
1509
  print("Hint: If backend responded 401, verify SYNTH_API_KEY for:", base_url)
1194
1510
  if sk:
1195
- print(f" SYNTH_API_KEY len={len(sk)} last5={sk[-5:]}")
1511
+ print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
1196
1512
  if ek:
1197
- print(f" ENVIRONMENT_API_KEY len={len(ek)} last5={ek[-5:]}")
1198
- print("Also ensure your Modal secret contains ENVIRONMENT_API_KEY and matches the task app.")
1513
+ print(f" {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
1514
+ print(
1515
+ "Ensure the ENVIRONMENT_API_KEY you deployed with matches the task app and remains exported."
1516
+ )
1199
1517
  return code
1200
1518
 
1201
1519
  # Fallback: legacy jobs API flow
1202
1520
  with open(cfg_path, "rb") as fh:
1203
1521
  inline_cfg = tomllib.load(fh)
1204
- with open(cfg_path, "r") as fh2:
1522
+ with open(cfg_path) as fh2:
1205
1523
  toml_text = fh2.read()
1206
- if args.batch_size is not None:
1207
- inline_cfg.setdefault("training", {})["batch_size"] = int(args.batch_size)
1208
- if args.group_size is not None:
1209
- inline_cfg.setdefault("training", {})["group_size"] = int(args.group_size)
1210
- model_name = args.model or (inline_cfg.get("model", {}) or {}).get("name", "Qwen/Qwen3-0.6B")
1524
+ if batch_size is not None:
1525
+ inline_cfg.setdefault("training", {})["batch_size"] = int(batch_size)
1526
+ if group_size is not None:
1527
+ inline_cfg.setdefault("training", {})["group_size"] = int(group_size)
1528
+ model_name = model or (inline_cfg.get("model", {}) or {}).get("name", "Qwen/Qwen3-0.6B")
1211
1529
  api = env.dev_backend_url.rstrip("/") + ("" if env.dev_backend_url.endswith("/api") else "/api")
1212
1530
  # Print backend and key preview before request for clearer diagnostics
1213
1531
  try:
1214
1532
  sk = (env.synth_api_key or "").strip()
1215
- sk_len = len(sk)
1216
- sk_tail = sk[-5:] if sk_len >= 5 else sk
1217
1533
  print(f"[run] Backend API: {api}")
1218
- print(f"[run] Using SYNTH_API_KEY len={sk_len} last5={sk_tail}")
1534
+ print(f"[run] {_key_preview(sk, 'SYNTH_API_KEY')}")
1219
1535
  except Exception:
1220
1536
  pass
1221
- data_fragment: Dict[str, Any] = {
1537
+ data_fragment: dict[str, Any] = {
1222
1538
  "model": model_name,
1223
1539
  "endpoint_base_url": env.task_app_base_url,
1224
1540
  "config": inline_cfg,
@@ -1236,23 +1552,28 @@ def cmd_run(args: argparse.Namespace) -> int:
1236
1552
  if inline_cfg["compute"].get("gpu_type"):
1237
1553
  compute["gpu_type"] = str(inline_cfg["compute"]["gpu_type"]).upper()
1238
1554
  if inline_cfg["compute"].get("gpu_count"):
1239
- compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
1555
+ compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
1240
1556
  if not compute:
1241
1557
  topo = inline_cfg.get("topology") or {}
1242
1558
  gshape = str(topo.get("gpu_type") or "")
1243
1559
  if ":" in gshape:
1244
1560
  t, c = gshape.split(":", 1)
1245
1561
  compute = {"gpu_type": t.upper(), "gpu_count": int(c)}
1246
- body: Dict[str, Any] = {
1562
+ body: dict[str, Any] = {
1247
1563
  "job_type": "rl",
1248
1564
  "data": data_fragment,
1249
1565
  }
1250
1566
  if compute:
1251
1567
  body["compute"] = compute
1252
- code, js = _http("POST", api + "/rl/jobs", headers={
1253
- "Content-Type": "application/json",
1254
- "Authorization": f"Bearer {env.synth_api_key}",
1255
- }, body=body)
1568
+ code, js = _http(
1569
+ "POST",
1570
+ api + "/rl/jobs",
1571
+ headers={
1572
+ "Content-Type": "application/json",
1573
+ "Authorization": f"Bearer {env.synth_api_key}",
1574
+ },
1575
+ body=body,
1576
+ )
1256
1577
  if code not in (200, 201) or not isinstance(js, dict):
1257
1578
  print("Job create failed:", code)
1258
1579
  print(f"Backend: {api}")
@@ -1264,15 +1585,77 @@ def cmd_run(args: argparse.Namespace) -> int:
1264
1585
  except Exception:
1265
1586
  print(str(js))
1266
1587
  print("Request body was:\n" + json.dumps(body, indent=2))
1588
+ try:
1589
+ auth_preview = _key_preview(env.synth_api_key or "", "SYNTH_API_KEY (auth)")
1590
+ print(f"[run] {auth_preview}")
1591
+ except Exception:
1592
+ pass
1593
+ try:
1594
+ data_block = body.get("data") if isinstance(body, dict) else None
1595
+ env_key_body = ""
1596
+ if isinstance(data_block, dict):
1597
+ env_key_body = str(data_block.get("environment_api_key") or "")
1598
+ if env_key_body:
1599
+ print(f"[run] {_key_preview(env_key_body, 'environment_api_key (body)')}")
1600
+ except Exception:
1601
+ pass
1602
+ try:
1603
+ current_env_key = env.env_api_key or ""
1604
+ if current_env_key:
1605
+ print(f"[run] {_key_preview(current_env_key, 'ENVIRONMENT_API_KEY (current)')}")
1606
+ except Exception:
1607
+ pass
1608
+ if isinstance(js, dict):
1609
+ detail = js.get("detail")
1610
+ if isinstance(detail, dict):
1611
+ try:
1612
+ sent_key = detail.get("sent_key")
1613
+ if isinstance(sent_key, str):
1614
+ print(
1615
+ f"[run] Backend detail.sent_key {_key_preview(sent_key, 'detail.sent_key')}"
1616
+ )
1617
+ except Exception:
1618
+ pass
1619
+ try:
1620
+ sent_keys = detail.get("sent_keys")
1621
+ if isinstance(sent_keys, list | tuple):
1622
+ previews = []
1623
+ for idx, val in enumerate(sent_keys):
1624
+ if isinstance(val, str):
1625
+ previews.append(_key_preview(val, f"detail.sent_keys[{idx}]"))
1626
+ if previews:
1627
+ joined = "; ".join(previews)
1628
+ print(f"[run] Backend detail.sent_keys previews: {joined}")
1629
+ except Exception:
1630
+ pass
1631
+ try:
1632
+ key_prefix = detail.get("sent_key_prefix")
1633
+ if isinstance(key_prefix, str):
1634
+ print(f"[run] Backend detail.sent_key_prefix={key_prefix}")
1635
+ except Exception:
1636
+ pass
1637
+ try:
1638
+ health_url = detail.get("health_url")
1639
+ if isinstance(health_url, str):
1640
+ print(f"[run] Backend detail.health_url={health_url}")
1641
+ except Exception:
1642
+ pass
1267
1643
  # Extra hints for auth failures
1268
1644
  try:
1269
1645
  sk = (env.synth_api_key or "").strip()
1270
- if int(code) == 401 or (isinstance(js, dict) and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())):
1646
+ if int(code) == 401 or (
1647
+ isinstance(js, dict)
1648
+ and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())
1649
+ ):
1271
1650
  base_url = env.dev_backend_url
1272
- print("Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url)
1651
+ print(
1652
+ "Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url
1653
+ )
1273
1654
  if sk:
1274
- print(f" SYNTH_API_KEY len={len(sk)} last5={sk[-5:]}")
1275
- print("Also ensure your Modal secret contains a valid ENVIRONMENT_API_KEY.")
1655
+ print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
1656
+ print(
1657
+ "Ensure the ENVIRONMENT_API_KEY and OPENAI_API_KEY used for deployment remain valid."
1658
+ )
1276
1659
  except Exception:
1277
1660
  pass
1278
1661
  return 2
@@ -1324,9 +1707,7 @@ def cmd_run(args: argparse.Namespace) -> int:
1324
1707
  "rl.performance.metrics",
1325
1708
  ):
1326
1709
  print(f"[{seq}] {typ}: {msg}")
1327
- mc, mj = _http(
1328
- "GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50"
1329
- )
1710
+ mc, mj = _http("GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50")
1330
1711
  if mc == 200 and isinstance(mj, dict):
1331
1712
  pts = mj.get("points") or []
1332
1713
  for p in pts:
@@ -1334,60 +1715,8 @@ def cmd_run(args: argparse.Namespace) -> int:
1334
1715
  if name == "eval.reward_mean":
1335
1716
  print(f"metric eval.reward_mean step={p.get('step')} value={p.get('value')}")
1336
1717
  break
1337
- if time.time() - start_t > (args.timeout or 600):
1718
+ if time.time() - start_t > (timeout or 600):
1338
1719
  print("Timeout waiting for terminal state.")
1339
1720
  break
1340
1721
  time.sleep(2)
1341
1722
  return 0
1342
-
1343
-
1344
- def main(argv: list[str] | None = None) -> int:
1345
- p = argparse.ArgumentParser(prog="synth-ai")
1346
- sub = p.add_subparsers(dest="cmd")
1347
-
1348
- def _add_parser(names: list[str], *, configure: Callable[[argparse.ArgumentParser], None]) -> None:
1349
- for name in names:
1350
- parser = sub.add_parser(name)
1351
- configure(parser)
1352
-
1353
- _add_parser(["rl_demo.setup", "demo.setup"], configure=lambda parser: parser.set_defaults(func=cmd_setup))
1354
-
1355
- def _init_opts(parser):
1356
- parser.add_argument("--force", action="store_true", help="Overwrite existing files in CWD")
1357
- parser.set_defaults(func=cmd_init)
1358
-
1359
- _add_parser(["rl_demo.init", "demo.init"], configure=_init_opts)
1360
-
1361
- # (prepare command removed)
1362
-
1363
- def _deploy_opts(parser):
1364
- parser.add_argument("--local", action="store_true", help="Run local FastAPI instead of Modal deploy")
1365
- parser.add_argument("--app", type=str, default=None, help="Path to Modal app.py for uv run modal deploy")
1366
- parser.add_argument("--name", type=str, default="synth-math-demo", help="Modal app name")
1367
- parser.add_argument("--script", type=str, default=None, help="Path to deploy_task_app.sh (optional legacy)")
1368
- parser.set_defaults(func=cmd_deploy)
1369
-
1370
- _add_parser(["rl_demo.deploy", "demo.deploy"], configure=_deploy_opts)
1371
-
1372
- _add_parser(["rl_demo.configure", "demo.configure"], configure=lambda parser: parser.set_defaults(func=cmd_run))
1373
-
1374
- def _run_opts(parser):
1375
- parser.add_argument("--config", type=str, default=None, help="Path to TOML config (skip prompt)")
1376
- parser.add_argument("--batch-size", type=int, default=None)
1377
- parser.add_argument("--group-size", type=int, default=None)
1378
- parser.add_argument("--model", type=str, default=None)
1379
- parser.add_argument("--timeout", type=int, default=600)
1380
- parser.add_argument("--dry-run", action="store_true", help="Print request body and exit")
1381
- parser.set_defaults(func=cmd_run)
1382
-
1383
- _add_parser(["run", "rl_demo.run", "demo.run"], configure=_run_opts)
1384
-
1385
- args = p.parse_args(argv)
1386
- if not hasattr(args, "func"):
1387
- p.print_help()
1388
- return 1
1389
- return int(args.func(args) or 0)
1390
-
1391
-
1392
- if __name__ == "__main__":
1393
- sys.exit(main())