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
@@ -0,0 +1,1185 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import importlib
6
+ import json
7
+ import os
8
+ from collections.abc import Callable, Mapping
9
+ from pathlib import Path
10
+ from typing import Any, NoReturn, cast
11
+
12
+ import click
13
+
14
+ try:
15
+ _config_module = cast(
16
+ Any, importlib.import_module("synth_ai.config.base_url")
17
+ )
18
+ get_backend_from_env = cast(Callable[[], str], _config_module.get_backend_from_env)
19
+ except Exception as exc: # pragma: no cover - critical dependency
20
+ raise RuntimeError("Unable to load backend configuration helpers") from exc
21
+
22
+ from synth_ai.streaming import (
23
+ CLIHandler,
24
+ JobStreamer,
25
+ LossCurveHandler,
26
+ StreamConfig,
27
+ StreamEndpoints,
28
+ StreamType,
29
+ )
30
+ from synth_ai.utils.env import load_env_file
31
+ from synth_ai.utils.errors import format_error_message, get_required_value
32
+
33
+ from .builders import build_prompt_learning_payload, build_rl_payload, build_sft_payload
34
+ from .config_finder import discover_configs, prompt_for_config
35
+ from .env_resolver import KeySpec, resolve_env
36
+ from .task_app import check_task_app_health
37
+ from .utils import (
38
+ TrainError,
39
+ ensure_api_base,
40
+ http_get,
41
+ http_post,
42
+ limit_jsonl_examples,
43
+ mask_value,
44
+ post_multipart,
45
+ preview_json,
46
+ sleep,
47
+ validate_sft_jsonl,
48
+ )
49
+
50
+ # Constants for prompt learning event types
51
+ _PROMPT_LEARNING_EVENT_BEST_PROMPT = "prompt.learning.best.prompt"
52
+ _PROMPT_LEARNING_EVENT_FINAL_RESULTS = "prompt.learning.final.results"
53
+ _PROMPT_LEARNING_EVENT_VALIDATION_SCORED = "prompt.learning.validation.scored"
54
+ _PROMPT_LEARNING_EVENT_GEPA_COMPLETE = "prompt.learning.gepa.complete"
55
+ _PROMPT_LEARNING_EVENT_MIPRO_COMPLETE = "prompt.learning.mipro.complete"
56
+
57
+ # Constants for formatting
58
+ _MAX_TEXT_REPLACEMENTS_DISPLAY = 3 # Max number of text replacements to show in output
59
+ _RESULTS_FILE_MAX_EVENTS = 10000 # Max events to fetch for results file generation
60
+
61
+
62
+ def _format_text_replacements(obj: dict[str, Any] | None, max_display: int = _MAX_TEXT_REPLACEMENTS_DISPLAY) -> list[str]:
63
+ """Extract and format text replacements from a candidate object.
64
+
65
+ Args:
66
+ obj: Candidate object dictionary containing text_replacements
67
+ max_display: Maximum number of replacements to display
68
+
69
+ Returns:
70
+ List of formatted lines showing role and replacement text
71
+ """
72
+ lines = []
73
+ if not obj or not isinstance(obj, dict):
74
+ return lines
75
+
76
+ text_replacements = obj.get("text_replacements", [])
77
+ if not text_replacements or not isinstance(text_replacements, list):
78
+ return lines
79
+
80
+ for replacement in text_replacements[:max_display]:
81
+ if isinstance(replacement, dict):
82
+ new_text = replacement.get("new_text", "")
83
+ role = replacement.get("apply_to_role", "system")
84
+ if new_text:
85
+ lines.append(f" [{role.upper()}]: {new_text}")
86
+ lines.append("")
87
+
88
+ return lines
89
+
90
+
91
+ def _default_backend() -> str:
92
+ """Resolve backend URL with proper production default."""
93
+ # Check explicit override first
94
+ explicit = os.getenv("BACKEND_BASE_URL", "").strip()
95
+ if explicit:
96
+ return explicit
97
+ # Use standard resolution logic
98
+ base, _ = get_backend_from_env()
99
+ return f"{base}/api" if not base.endswith("/api") else base
100
+
101
+
102
+ _DEFAULT_SFT_HIDDEN_EVENTS = {
103
+ "sft.created",
104
+ "sft.pricing.check.requested",
105
+ "sft.pricing.check.allowed",
106
+ "sft.stage",
107
+ "snapshot.fetch",
108
+ "hatchet.preflight",
109
+ "hatchet.submission.attempt",
110
+ "hatchet.submission.result",
111
+ "sft.running",
112
+ "sft.status",
113
+ "sft.worker.alive",
114
+ "sft.dispatch.selected",
115
+ "sft.config.prepared",
116
+ "sft.strategy.selected",
117
+ "sft.training.args",
118
+ }
119
+
120
+ _DEFAULT_RL_HIDDEN_SUBSTRINGS = {"modal", "hatchet"}
121
+
122
+ _DEFAULT_PROMPT_LEARNING_HIDDEN_EVENTS = {
123
+ "prompt.learning.policy.tokens",
124
+ }
125
+
126
+
127
+ def _build_stream_components(
128
+ stream_format: str,
129
+ *,
130
+ hidden_event_types: set[str] | None = None,
131
+ hidden_event_substrings: set[str] | None = None,
132
+ ) -> tuple[StreamConfig, list]:
133
+ """Return stream configuration and handlers for the requested format."""
134
+ if stream_format == "chart":
135
+ config = StreamConfig(
136
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
137
+ event_types={
138
+ "sft.progress",
139
+ "sft.training.started",
140
+ "sft.training.finish",
141
+ "sft.validation.summary",
142
+ "rl.train.step",
143
+ "rl.train.started",
144
+ "rl.train.completed",
145
+ "workflow.completed",
146
+ "workflow.failed",
147
+ },
148
+ metric_names={"train.loss"},
149
+ )
150
+ handlers = [LossCurveHandler()]
151
+ else:
152
+ config = StreamConfig.default()
153
+ handlers = [
154
+ CLIHandler(
155
+ hidden_event_types=hidden_event_types or set(),
156
+ hidden_event_substrings=hidden_event_substrings or set(),
157
+ )
158
+ ]
159
+ return config, handlers
160
+
161
+
162
+ @click.command("train")
163
+ @click.option(
164
+ "--config",
165
+ "config_paths",
166
+ multiple=True,
167
+ type=click.Path(),
168
+ help="Path to training TOML (repeatable)",
169
+ )
170
+ @click.option("--type", "train_type", type=click.Choice(["auto", "rl", "sft", "prompt_learning"]), default="auto")
171
+ @click.option(
172
+ "--env-file",
173
+ "env_files",
174
+ multiple=True,
175
+ type=click.Path(),
176
+ help=".env file(s) to preload (skips selection prompt)",
177
+ )
178
+ @click.option("--task-url", default=None, help="Override task app base URL (RL only)")
179
+ @click.option(
180
+ "--dataset",
181
+ "dataset_path",
182
+ type=click.Path(),
183
+ default=None,
184
+ help="Override dataset JSONL path (SFT)",
185
+ )
186
+ @click.option("--backend", default=_default_backend, help="Backend base URL")
187
+ @click.option("--model", default=None, help="Override model identifier")
188
+ @click.option(
189
+ "--allow-experimental",
190
+ "allow_experimental",
191
+ is_flag=True,
192
+ flag_value=True,
193
+ default=None,
194
+ help="Allow experimental models (overrides SDK_EXPERIMENTAL env)",
195
+ )
196
+ @click.option(
197
+ "--no-allow-experimental",
198
+ "allow_experimental",
199
+ is_flag=True,
200
+ flag_value=False,
201
+ help="Disallow experimental models (overrides SDK_EXPERIMENTAL env)",
202
+ )
203
+ @click.option("--idempotency", default=None, help="Idempotency-Key header for job creation")
204
+ @click.option("--dry-run", is_flag=True, hidden=True, help="Deprecated: no-op")
205
+ @click.option("--poll/--no-poll", default=True, help="Poll job status until terminal state")
206
+ @click.option(
207
+ "--poll-timeout", default=3600.0, type=float, help="Maximum seconds to poll before timing out"
208
+ )
209
+ @click.option("--poll-interval", default=5.0, type=float, help="Seconds between poll attempts")
210
+ @click.option(
211
+ "--stream-format",
212
+ type=click.Choice(["cli", "chart"]),
213
+ default="cli",
214
+ show_default=True,
215
+ help="Streaming output style (cli = line updates, chart = live loss panel)",
216
+ )
217
+ @click.option(
218
+ "--examples",
219
+ "examples_limit",
220
+ type=int,
221
+ default=None,
222
+ help="Limit SFT training to the first N examples",
223
+ )
224
+ def train_command(
225
+ config_paths: tuple[str, ...],
226
+ train_type: str,
227
+ env_files: tuple[str, ...],
228
+ task_url: str | None,
229
+ dataset_path: str | None,
230
+ backend: str,
231
+ model: str | None,
232
+ allow_experimental: bool | None,
233
+ idempotency: str | None,
234
+ dry_run: bool,
235
+ poll: bool,
236
+ poll_timeout: float,
237
+ poll_interval: float,
238
+ stream_format: str,
239
+ examples_limit: int | None,
240
+ ) -> None:
241
+ """Interactive launcher for RL / SFT / Prompt Learning jobs."""
242
+ load_env_file()
243
+
244
+ candidates = discover_configs(
245
+ list(config_paths), requested_type=train_type if train_type != "auto" else None
246
+ )
247
+ selection = prompt_for_config(
248
+ candidates,
249
+ requested_type=train_type if train_type != "auto" else None,
250
+ allow_autoselect=bool(config_paths),
251
+ )
252
+
253
+ effective_type = train_type if train_type != "auto" else selection.train_type
254
+ if effective_type not in {"rl", "sft", "prompt_learning"}:
255
+ raise click.UsageError(
256
+ format_error_message(
257
+ summary="Training type required",
258
+ context="Determining which trainer to invoke",
259
+ problem="Config metadata did not specify rl / sft / prompt_learning and no --type flag was provided",
260
+ impact="CLI cannot select the correct builder without a type",
261
+ solutions=[
262
+ ("Pass --type rl|sft|prompt_learning", "Explicitly tell the CLI which workflow to run"),
263
+ ("Add algorithm.type metadata to the config", "Include algorithm.type or prompt_learning markers in the TOML"),
264
+ ("Use separate config files per training mode", "Keeps intent unambiguous for automation"),
265
+ ],
266
+ )
267
+ )
268
+
269
+ cfg_path = selection.path
270
+ click.echo(f"Using config: {cfg_path} ({effective_type})")
271
+
272
+ required_keys: list[KeySpec] = []
273
+ if effective_type == "rl" or effective_type == "prompt_learning":
274
+ required_keys.append(KeySpec("SYNTH_API_KEY", "Synth API key for backend"))
275
+ required_keys.append(
276
+ KeySpec(
277
+ "ENVIRONMENT_API_KEY",
278
+ "Environment API key for task app",
279
+ allow_modal_secret=True,
280
+ modal_secret_pattern="env",
281
+ )
282
+ )
283
+ required_keys.append(
284
+ KeySpec(
285
+ "TASK_APP_URL",
286
+ "Task app base URL",
287
+ secret=False,
288
+ allow_modal_app=True,
289
+ optional=bool(task_url),
290
+ )
291
+ )
292
+ else: # sft
293
+ required_keys.append(KeySpec("SYNTH_API_KEY", "Synth API key for backend"))
294
+
295
+ env_path, env_values = resolve_env(
296
+ config_path=cfg_path,
297
+ explicit_env_paths=env_files,
298
+ required_keys=required_keys,
299
+ )
300
+
301
+ missing_keys = [
302
+ spec.name
303
+ for spec in required_keys
304
+ if not spec.optional and not (env_values.get(spec.name) or os.environ.get(spec.name))
305
+ ]
306
+ if missing_keys:
307
+ try:
308
+ _task_apps_module = cast(
309
+ Any, importlib.import_module("synth_ai.cli.task_apps")
310
+ )
311
+ _interactive_fill_env = cast(
312
+ Callable[[Path], Path | None], _task_apps_module._interactive_fill_env
313
+ )
314
+ except Exception as exc: # pragma: no cover - protective fallback
315
+ raise click.ClickException(f"Unable to prompt for env values: {exc}") from exc
316
+
317
+ target_dir = cfg_path.parent
318
+ generated = _interactive_fill_env(target_dir / ".env")
319
+ if generated is None:
320
+ raise click.ClickException("Required environment values missing; aborting.")
321
+ env_path, env_values = resolve_env(
322
+ config_path=cfg_path,
323
+ explicit_env_paths=(str(generated),),
324
+ required_keys=required_keys,
325
+ )
326
+ click.echo(f"Using env file: {env_path}")
327
+
328
+ synth_key = get_required_value(
329
+ "synth_api_key",
330
+ env_value=env_values.get("SYNTH_API_KEY") or os.environ.get("SYNTH_API_KEY"),
331
+ )
332
+ os.environ["SYNTH_API_KEY"] = synth_key
333
+
334
+ backend_base = ensure_api_base(backend)
335
+ click.echo(f"Backend base: {backend_base} (key {mask_value(synth_key)})")
336
+
337
+ if effective_type == "rl":
338
+ handle_rl(
339
+ cfg_path=cfg_path,
340
+ backend_base=backend_base,
341
+ synth_key=synth_key,
342
+ task_url_override=task_url,
343
+ model_override=model,
344
+ idempotency=idempotency,
345
+ allow_experimental=allow_experimental,
346
+ dry_run=dry_run,
347
+ poll=poll,
348
+ poll_timeout=poll_timeout,
349
+ poll_interval=poll_interval,
350
+ stream_format=stream_format,
351
+ )
352
+ elif effective_type == "prompt_learning":
353
+ handle_prompt_learning(
354
+ cfg_path=cfg_path,
355
+ backend_base=backend_base,
356
+ synth_key=synth_key,
357
+ task_url_override=task_url,
358
+ allow_experimental=allow_experimental,
359
+ dry_run=dry_run,
360
+ poll=poll,
361
+ poll_timeout=poll_timeout,
362
+ poll_interval=poll_interval,
363
+ stream_format=stream_format,
364
+ )
365
+ else:
366
+ dataset_override_path = Path(dataset_path).expanduser().resolve() if dataset_path else None
367
+ handle_sft(
368
+ cfg_path=cfg_path,
369
+ backend_base=backend_base,
370
+ synth_key=synth_key,
371
+ dataset_override=dataset_override_path,
372
+ allow_experimental=allow_experimental,
373
+ dry_run=dry_run,
374
+ poll=poll,
375
+ poll_timeout=poll_timeout,
376
+ poll_interval=poll_interval,
377
+ stream_format=stream_format,
378
+ examples_limit=examples_limit,
379
+ )
380
+
381
+
382
+ def _wait_for_training_file(
383
+ backend_base: str, api_key: str, file_id: str, *, timeout: float = 10.0
384
+ ) -> None:
385
+ """Wait for training file to be visible after upload.
386
+
387
+ Reduced from 120s to 10s because:
388
+ - POST response already confirms file is uploaded
389
+ - Backend now forces read-your-writes consistency
390
+ - By job creation time, replica lag has resolved
391
+ - Quick sanity check only, not critical path
392
+ """
393
+ url = f"{backend_base.rstrip('/')}/files/{file_id}"
394
+ headers = {"Authorization": f"Bearer {api_key}"}
395
+ elapsed = 0.0
396
+ interval = 2.0
397
+ first_check = True
398
+ while True:
399
+ resp = http_get(url, headers=headers, timeout=30.0)
400
+ if resp.status_code == 200:
401
+ try:
402
+ data = resp.json()
403
+ except json.JSONDecodeError:
404
+ data = {}
405
+ status = str(
406
+ data.get("status") or data.get("state") or data.get("storage_state") or "ready"
407
+ ).lower()
408
+ if first_check:
409
+ click.echo(f"File uploaded successfully (id={file_id}, status={status})")
410
+ first_check = False
411
+ if status in {"ready", "uploaded", "stored", "complete"}:
412
+ click.echo(f"✓ Training file ready (status={status})")
413
+ return
414
+ # Show progress for processing states
415
+ if status in {"processing", "pending", "validating"}:
416
+ click.echo(
417
+ f" Waiting for file processing... (status={status}, {elapsed:.0f}s elapsed)"
418
+ )
419
+ elif resp.status_code == 404:
420
+ # Keep polling; object may not be visible yet
421
+ if first_check:
422
+ click.echo(f"Waiting for file {file_id} to become visible...")
423
+ first_check = False
424
+ elif resp.status_code in {401, 403}:
425
+ # Auth errors won't resolve by polling - fail immediately
426
+ try:
427
+ error_body = resp.json()
428
+ except json.JSONDecodeError:
429
+ error_body = resp.text[:400]
430
+ click.echo("\n[ERROR] Authentication failed when checking training file:")
431
+ click.echo(f" URL: {url}")
432
+ click.echo(f" Status: {resp.status_code}")
433
+ click.echo(f" Response: {error_body}")
434
+ click.echo(f" API key: {mask_value(api_key)}")
435
+ raise click.ClickException(
436
+ f"Authentication error ({resp.status_code}). "
437
+ "Check that your SYNTH_API_KEY is valid and has permission to access this organization's files."
438
+ )
439
+ else:
440
+ # Other errors - show details but keep polling
441
+ try:
442
+ error_body = resp.json()
443
+ except json.JSONDecodeError:
444
+ error_body = resp.text[:400]
445
+ click.echo(f"[WARN] Unexpected response checking file {file_id}:")
446
+ click.echo(f" URL: {url}")
447
+ click.echo(f" Status: {resp.status_code}")
448
+ click.echo(f" Response: {error_body}")
449
+
450
+ if elapsed >= timeout:
451
+ raise click.ClickException(
452
+ f"Training file {file_id} not ready after {timeout:.0f}s (last status: {resp.status_code})"
453
+ )
454
+ sleep(interval)
455
+ elapsed += interval
456
+
457
+
458
+ def handle_rl(
459
+ *,
460
+ cfg_path: Path,
461
+ backend_base: str,
462
+ synth_key: str,
463
+ task_url_override: str | None,
464
+ model_override: str | None,
465
+ idempotency: str | None,
466
+ allow_experimental: bool | None,
467
+ dry_run: bool,
468
+ poll: bool,
469
+ poll_timeout: float,
470
+ poll_interval: float,
471
+ stream_format: str,
472
+ ) -> None:
473
+ overrides: dict[str, Any] = {
474
+ "backend": backend_base,
475
+ "task_url": task_url_override,
476
+ "model": model_override,
477
+ }
478
+ build = build_rl_payload(
479
+ config_path=cfg_path,
480
+ task_url=task_url_override or os.environ.get("TASK_APP_URL", ""),
481
+ overrides=overrides,
482
+ idempotency=idempotency,
483
+ allow_experimental=allow_experimental,
484
+ )
485
+
486
+ # Backend-side verification: try ALL org environment keys against /health and /task_info
487
+ verify_url = f"{backend_base}/rl/verify_task_app"
488
+ verify_headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
489
+ try:
490
+ vresp = http_post(
491
+ verify_url, headers=verify_headers, json_body={"endpoint_base_url": build.task_url}
492
+ )
493
+ try:
494
+ parsed_json = vresp.json()
495
+ except json.JSONDecodeError:
496
+ parsed_json = None
497
+
498
+ if isinstance(parsed_json, Mapping):
499
+ vjs: dict[str, Any] = dict(parsed_json)
500
+ else:
501
+ vjs = {
502
+ "status": vresp.status_code,
503
+ "text": (vresp.text or "")[:400],
504
+ }
505
+ if parsed_json is not None:
506
+ vjs["body"] = parsed_json
507
+ except Exception as _ve:
508
+ raise click.ClickException(
509
+ f"Task app verification call failed: {type(_ve).__name__}: {_ve}"
510
+ ) from _ve
511
+ if vresp.status_code is not None and vresp.status_code >= 400:
512
+ click.echo("Task app verification error:\n" + preview_json(vjs, limit=800))
513
+ raise click.ClickException(f"Verification failed with status {vresp.status_code}")
514
+ if not bool(vjs.get("any_ok")):
515
+ click.echo("Task app verification failed; no auth combination succeeded. Full report:")
516
+ click.echo(preview_json(vjs, limit=1200))
517
+ raise click.ClickException("Task app verification failed (auth)")
518
+ else:
519
+ # Print concise summary
520
+ try:
521
+ cands = vjs.get("candidates_first15") or []
522
+ attempts_raw = vjs.get("attempts")
523
+ attempts: list[Mapping[str, Any]] = (
524
+ [a for a in attempts_raw if isinstance(a, Mapping)]
525
+ if isinstance(attempts_raw, list)
526
+ else []
527
+ )
528
+ statuses = [attempt.get("status") for attempt in attempts]
529
+ click.echo(f"Verification OK (candidates={cands}, statuses={statuses})")
530
+ except (KeyError, ValueError, AttributeError):
531
+ # Parsing verification summary failed, but verification itself succeeded
532
+ click.echo("Verification OK")
533
+
534
+ env_key = get_required_value(
535
+ "environment_api_key",
536
+ env_value=os.environ.get("ENVIRONMENT_API_KEY"),
537
+ )
538
+ os.environ["ENVIRONMENT_API_KEY"] = env_key
539
+
540
+ click.echo("Performing task app health check…")
541
+ health = check_task_app_health(build.task_url, env_key)
542
+ if not health.ok:
543
+ click.echo(f"Task app health check failed: {health.detail}")
544
+ raise click.ClickException("Aborting due to failing health check")
545
+ else:
546
+ click.echo("Task app healthy")
547
+
548
+ create_url = f"{backend_base}/rl/jobs"
549
+ headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
550
+ if build.idempotency:
551
+ headers["Idempotency-Key"] = build.idempotency
552
+
553
+ click.echo(f"POST {create_url}")
554
+ click.echo("Payload preview:\n" + preview_json(build.payload, limit=800))
555
+
556
+ resp = http_post(create_url, headers=headers, json_body=build.payload)
557
+ try:
558
+ js = resp.json()
559
+ except json.JSONDecodeError as e:
560
+ click.echo(f"⚠️ Failed to parse JSON response: {e}")
561
+ js = {"status": resp.status_code, "text": resp.text[:400]}
562
+ click.echo(f"Response {resp.status_code}: {preview_json(js, limit=400)}")
563
+ if resp.status_code not in (200, 201):
564
+ raise click.ClickException("Job creation failed")
565
+ job_id = js.get("job_id") or js.get("id")
566
+ if not job_id:
567
+ raise click.ClickException("Response missing job id")
568
+
569
+ if not poll:
570
+ click.echo(f"Created job {job_id} (polling disabled)")
571
+ return
572
+
573
+ click.echo("\n=== Streaming Job Progress ===")
574
+
575
+ # Enable metrics for prompt learning
576
+ if stream_format == "chart":
577
+ config = StreamConfig(
578
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
579
+ event_types={
580
+ "prompt.learning.progress",
581
+ "prompt.learning.gepa.start",
582
+ "prompt.learning.gepa.complete",
583
+ },
584
+ metric_names={"gepa.transformation.mean_score"},
585
+ )
586
+ handlers = [LossCurveHandler()]
587
+ click.echo("Using live chart (metric=gepa.transformation.mean_score)")
588
+ else:
589
+ config = StreamConfig(
590
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
591
+ metric_names={"gepa.transformation.mean_score"},
592
+ )
593
+ handlers = [CLIHandler(hidden_event_substrings=_DEFAULT_RL_HIDDEN_SUBSTRINGS)]
594
+
595
+ streamer = JobStreamer(
596
+ base_url=backend_base,
597
+ api_key=synth_key,
598
+ job_id=job_id,
599
+ endpoints=StreamEndpoints.rl(job_id),
600
+ config=config,
601
+ handlers=handlers,
602
+ interval_seconds=poll_interval,
603
+ timeout_seconds=poll_timeout,
604
+ )
605
+ final_status = asyncio.run(streamer.stream_until_terminal())
606
+ click.echo(f"Final status: {final_status.get('status', 'unknown')}")
607
+ click.echo(preview_json(final_status, limit=600))
608
+
609
+
610
+ def handle_sft(
611
+ *,
612
+ cfg_path: Path,
613
+ backend_base: str,
614
+ synth_key: str,
615
+ dataset_override: Path | None,
616
+ allow_experimental: bool | None,
617
+ dry_run: bool,
618
+ poll: bool,
619
+ poll_timeout: float,
620
+ poll_interval: float,
621
+ stream_format: str,
622
+ examples_limit: int | None,
623
+ ) -> None:
624
+ try:
625
+ build = build_sft_payload(
626
+ config_path=cfg_path,
627
+ dataset_override=dataset_override,
628
+ allow_experimental=allow_experimental,
629
+ )
630
+ except TrainError as exc:
631
+ _raise_sft_usage_error(exc)
632
+
633
+ limited_path: Path | None = None
634
+
635
+ try:
636
+ if examples_limit is not None:
637
+ limited_path = limit_jsonl_examples(build.train_file, examples_limit)
638
+ click.echo(
639
+ f"Using first {examples_limit} examples from {build.train_file} -> {limited_path}"
640
+ )
641
+ build.train_file = limited_path
642
+
643
+ click.echo("Validating training dataset…")
644
+ validate_sft_jsonl(build.train_file)
645
+ if build.validation_file and build.validation_file.suffix == ".jsonl":
646
+ click.echo("Validating validation dataset…")
647
+ validate_sft_jsonl(build.validation_file)
648
+
649
+ upload_url = f"{backend_base.rstrip('/')}/files"
650
+ click.echo("\n=== Uploading Training Data ===")
651
+ click.echo(f"Dataset: {build.train_file}")
652
+ click.echo(f"Destination: {upload_url}")
653
+ resp = post_multipart(
654
+ upload_url, api_key=synth_key, file_field="file", file_path=build.train_file
655
+ )
656
+ js = (
657
+ resp.json()
658
+ if resp.headers.get("content-type", "").startswith("application/json")
659
+ else {}
660
+ )
661
+ if resp.status_code is not None and resp.status_code >= 400 or "id" not in js:
662
+ click.echo("\n[ERROR] Training file upload failed:")
663
+ click.echo(f" URL: {upload_url}")
664
+ click.echo(f" Status: {resp.status_code}")
665
+ click.echo(f" Response: {js or resp.text[:400]}")
666
+ click.echo(f" File: {build.train_file}")
667
+ raise click.ClickException(
668
+ f"Training file upload failed with status {resp.status_code}"
669
+ )
670
+ train_file_id = js["id"]
671
+ click.echo(f"✓ Training file uploaded (id={train_file_id})")
672
+ val_file_id = None
673
+ if build.validation_file:
674
+ click.echo(f"Uploading validation dataset: {build.validation_file}")
675
+ vresp = post_multipart(
676
+ upload_url,
677
+ api_key=synth_key,
678
+ file_field="file",
679
+ file_path=build.validation_file,
680
+ )
681
+ vjs = (
682
+ vresp.json()
683
+ if vresp.headers.get("content-type", "").startswith("application/json")
684
+ else {}
685
+ )
686
+ if vresp.status_code is not None and vresp.status_code < 400 and "id" in vjs:
687
+ val_file_id = vjs["id"]
688
+ click.echo(f"✓ Validation file uploaded (id={val_file_id})")
689
+ else:
690
+ click.echo(
691
+ f"[WARN] Validation upload failed ({vresp.status_code}): {vjs or vresp.text[:200]}"
692
+ )
693
+ payload = dict(build.payload)
694
+ payload["training_file_id"] = train_file_id
695
+ if val_file_id:
696
+ payload.setdefault("metadata", {}).setdefault("effective_config", {}).setdefault(
697
+ "data", {}
698
+ )["validation_files"] = [val_file_id]
699
+
700
+ click.echo("\n=== Checking File Processing Status ===")
701
+ try:
702
+ _wait_for_training_file(backend_base, synth_key, train_file_id)
703
+ except click.ClickException as exc:
704
+ click.echo(f"[WARN] File readiness check failed: {exc}")
705
+ click.echo("Proceeding anyway - backend will validate file during job creation...")
706
+
707
+ click.echo("\n=== Creating Training Job ===")
708
+ click.echo("Job payload preview:")
709
+ click.echo(preview_json(payload, limit=800))
710
+
711
+ create_url = f"{backend_base}/learning/jobs"
712
+ headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
713
+ click.echo(f"\nPOST {create_url}")
714
+ resp = http_post(create_url, headers=headers, json_body=payload)
715
+ js = (
716
+ resp.json()
717
+ if resp.headers.get("content-type", "").startswith("application/json")
718
+ else {}
719
+ )
720
+ if resp.status_code not in (200, 201):
721
+ click.echo("\n[ERROR] Job creation failed:")
722
+ click.echo(f" URL: {create_url}")
723
+ click.echo(f" Status: {resp.status_code}")
724
+ click.echo(f" Response: {preview_json(js, limit=600)}")
725
+ raise click.ClickException(f"Job creation failed with status {resp.status_code}")
726
+ job_id = js.get("job_id") or js.get("id")
727
+ if not job_id:
728
+ raise click.ClickException("Response missing job id")
729
+ click.echo(f"✓ Job created (id={job_id})")
730
+
731
+ click.echo("\n=== Starting Training Job ===")
732
+ start_url = f"{backend_base}/learning/jobs/{job_id}/start"
733
+ click.echo(f"POST {start_url}")
734
+ start_resp = http_post(start_url, headers=headers, json_body={})
735
+ if start_resp.status_code not in (200, 201):
736
+ click.echo(f"[WARN] Job start returned status {start_resp.status_code}")
737
+ else:
738
+ click.echo("✓ Job started")
739
+
740
+ if not poll:
741
+ click.echo(f"Started job {job_id} (polling disabled)")
742
+ return
743
+
744
+ click.echo("\n=== Streaming Job Progress ===")
745
+ config, handlers = _build_stream_components(
746
+ stream_format, hidden_event_types=_DEFAULT_SFT_HIDDEN_EVENTS
747
+ )
748
+ if stream_format == "chart":
749
+ click.echo("Using live loss chart (metric=train.loss)")
750
+ streamer = JobStreamer(
751
+ base_url=backend_base,
752
+ api_key=synth_key,
753
+ job_id=job_id,
754
+ endpoints=StreamEndpoints.learning(job_id),
755
+ config=config,
756
+ handlers=handlers,
757
+ interval_seconds=poll_interval,
758
+ timeout_seconds=poll_timeout,
759
+ )
760
+ final_status = asyncio.run(streamer.stream_until_terminal())
761
+ status = final_status.get('status') if isinstance(final_status, dict) else 'unknown'
762
+ click.echo(f"Final status: {status}")
763
+ click.echo(preview_json(final_status, limit=600))
764
+ finally:
765
+ if limited_path is not None:
766
+ with contextlib.suppress(OSError):
767
+ limited_path.unlink(missing_ok=True)
768
+ # Clean up empty parent directory if possible
769
+ with contextlib.suppress(OSError):
770
+ limited_path.parent.rmdir()
771
+
772
+
773
+ def _raise_sft_usage_error(exc: TrainError) -> NoReturn:
774
+ message = str(exc).strip()
775
+ lower_msg = message.lower()
776
+ context = "Preparing SFT training job payload"
777
+ impact = "Cannot submit training job without a valid dataset path"
778
+
779
+ if "dataset not specified" in lower_msg:
780
+ raise click.UsageError(
781
+ format_error_message(
782
+ summary="Dataset path required",
783
+ context=context,
784
+ problem="No dataset path was provided via config or CLI",
785
+ impact=impact,
786
+ solutions=[
787
+ ("Add [job].data = \"/path/to/data.jsonl\" to the config", "Persist the dataset path in the TOML file"),
788
+ ("Re-run with --dataset /path/to/data.jsonl", "Override the dataset path from the CLI"),
789
+ ("Use an absolute path accessible from the current working directory", "Relative paths are resolved from the shell cwd"),
790
+ ],
791
+ )
792
+ ) from exc
793
+
794
+ if "dataset not found" in lower_msg:
795
+ raise click.UsageError(
796
+ format_error_message(
797
+ summary="Dataset path not found",
798
+ context=context,
799
+ problem=message,
800
+ impact=impact,
801
+ solutions=[
802
+ ("Verify the dataset path exists on disk", "Double-check spelling and that the file hasn't moved"),
803
+ ("Provide an absolute path to the dataset file", "Avoid relying on relative paths that resolve incorrectly"),
804
+ ("Sync the dataset to this machine before running the CLI", "Remote paths must be accessible locally"),
805
+ ],
806
+ )
807
+ ) from exc
808
+
809
+ raise click.ClickException(message) from exc
810
+
811
+
812
+ def _save_prompt_learning_results_locally(
813
+ *,
814
+ backend_base: str,
815
+ api_key: str,
816
+ job_id: str,
817
+ config_path: Path,
818
+ ) -> None:
819
+ """Fetch events and generate results file locally after prompt learning completes."""
820
+ from datetime import datetime
821
+
822
+ try:
823
+ # Fetch all events
824
+ url = f"{backend_base}/prompt-learning/online/jobs/{job_id}/events?limit={_RESULTS_FILE_MAX_EVENTS}"
825
+ headers = {"Authorization": f"Bearer {api_key}"}
826
+ resp = http_get(url, headers=headers, timeout=30.0)
827
+
828
+ if resp.status_code != 200:
829
+ click.echo(f"⚠️ Could not fetch events to generate results file (status={resp.status_code})")
830
+ return
831
+
832
+ data = resp.json()
833
+ # Handle both list response (backend) and dict response (legacy compatibility)
834
+ if isinstance(data, list):
835
+ events = data
836
+ elif isinstance(data, dict):
837
+ events = data.get("events", [])
838
+ if not isinstance(events, list):
839
+ click.echo(f"⚠️ Events field is not a list: {type(events).__name__}")
840
+ return
841
+ else:
842
+ click.echo(f"⚠️ Unexpected response type: {type(data).__name__}")
843
+ return
844
+
845
+ if not events:
846
+ return
847
+
848
+ # Extract key data from events
849
+ best_score = None
850
+ best_prompt = None
851
+ baseline_score = None
852
+ attempted_candidates = []
853
+ optimized_candidates = []
854
+ mipro_topk_candidates = [] # Collect MIPRO top-K candidates
855
+
856
+ for event in events:
857
+ if not isinstance(event, dict):
858
+ continue # Skip malformed events
859
+
860
+ event_type = event.get("type", "")
861
+ event_data = event.get("data", {})
862
+ if not isinstance(event_data, dict):
863
+ event_data = {} # Fallback to empty dict for safety
864
+
865
+ if event_type == _PROMPT_LEARNING_EVENT_BEST_PROMPT:
866
+ best_score = event_data.get("best_score")
867
+ best_prompt = event_data.get("best_prompt")
868
+ elif event_type == _PROMPT_LEARNING_EVENT_FINAL_RESULTS:
869
+ attempted_candidates = event_data.get("attempted_candidates", [])
870
+ optimized_candidates = event_data.get("optimized_candidates", [])
871
+ elif event_type == _PROMPT_LEARNING_EVENT_VALIDATION_SCORED:
872
+ # Check if this is the baseline by checking for is_baseline flag or baseline in message
873
+ is_baseline = event_data.get("is_baseline", False)
874
+ if not is_baseline:
875
+ msg = event.get("message", "")
876
+ is_baseline = "baseline" in msg.lower()
877
+ if is_baseline:
878
+ baseline_score = event_data.get("accuracy")
879
+ elif event_type == _PROMPT_LEARNING_EVENT_GEPA_COMPLETE and best_score is None:
880
+ best_score = event_data.get("best_score")
881
+ elif event_type == _PROMPT_LEARNING_EVENT_MIPRO_COMPLETE:
882
+ # MIPRO completion event includes best_prompt and best_score
883
+ if best_score is None:
884
+ best_score = event_data.get("best_score")
885
+ if best_prompt is None:
886
+ best_prompt = event_data.get("best_prompt")
887
+ elif event_type == "mipro.topk.evaluated":
888
+ # Extract MIPRO top-K candidate data
889
+ rank = event_data.get("rank")
890
+ train_score = event_data.get("train_score")
891
+ test_score = event_data.get("test_score")
892
+ if rank is not None and train_score is not None and test_score is not None:
893
+ mipro_topk_candidates.append({
894
+ "rank": rank,
895
+ "train_score": train_score,
896
+ "test_score": test_score,
897
+ "lift_absolute": event_data.get("lift_absolute"),
898
+ "lift_percent": event_data.get("lift_percent"),
899
+ "instruction_text": event_data.get("instruction_text", ""),
900
+ "demo_indices": event_data.get("demo_indices", []),
901
+ })
902
+ elif event_type == "mipro.baseline.test":
903
+ # Extract baseline test score
904
+ if baseline_score is None:
905
+ baseline_score = event_data.get("test_score")
906
+
907
+ # Check if we have any results to display (best_prompt, best_score, or candidates)
908
+ has_results = bool(attempted_candidates or optimized_candidates or best_prompt or best_score is not None)
909
+ if not has_results:
910
+ return
911
+
912
+ # Determine algorithm name from events
913
+ algorithm_name = "PROMPT LEARNING"
914
+ for event in events:
915
+ if isinstance(event, dict):
916
+ event_type = event.get("type", "")
917
+ if "gepa" in event_type.lower():
918
+ algorithm_name = "GEPA"
919
+ break
920
+ elif "mipro" in event_type.lower():
921
+ algorithm_name = "MIPRO"
922
+ break
923
+
924
+ # Generate formatted report
925
+ lines = []
926
+ lines.append("=" * 80)
927
+ lines.append(f"{algorithm_name} PROMPT LEARNING RESULTS")
928
+ lines.append("=" * 80)
929
+ lines.append(f"Job ID: {job_id}")
930
+ lines.append(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
931
+ lines.append("")
932
+ if baseline_score is not None:
933
+ lines.append(f"📊 Baseline Score: {baseline_score:.4f} ({baseline_score*100:.1f}%)")
934
+ if best_score is not None:
935
+ lines.append(f"🏆 Best Score: {best_score:.4f} ({best_score*100:.1f}%)")
936
+ if baseline_score is not None and best_score is not None:
937
+ improvement = ((best_score - baseline_score) / baseline_score) * 100 if baseline_score > 0 else 0
938
+ lines.append(f"📈 Improvement: {improvement:+.1f}% relative ({(best_score - baseline_score)*100:+.1f} pp absolute)")
939
+ lines.append("=" * 80)
940
+ lines.append("")
941
+
942
+ # Add best prompt if available
943
+ if best_prompt and isinstance(best_prompt, dict):
944
+ lines.append("🏆 BEST PROMPT")
945
+ lines.append("-" * 80)
946
+ sections = best_prompt.get("sections", [])
947
+ if not isinstance(sections, list):
948
+ sections = []
949
+ for sec in sections:
950
+ if not isinstance(sec, dict):
951
+ continue
952
+ role = sec.get("role", "unknown")
953
+ content = sec.get("content", "")
954
+ lines.append(f"\n[{role.upper()}]:")
955
+ lines.append(content)
956
+ lines.append("")
957
+
958
+ # Add optimized candidates
959
+ if optimized_candidates and isinstance(optimized_candidates, list):
960
+ lines.append("=" * 80)
961
+ lines.append(f"✨ TOP OPTIMIZED CANDIDATES ({len(optimized_candidates)})")
962
+ lines.append("=" * 80)
963
+ lines.append("")
964
+
965
+ for idx, cand in enumerate(optimized_candidates):
966
+ if not isinstance(cand, dict):
967
+ continue
968
+ candidate_score = cand.get("score") or {}
969
+ accuracy = candidate_score.get("accuracy", 0.0)
970
+ prompt_length = candidate_score.get("prompt_length", 0)
971
+ payload_kind = cand.get("payload_kind", "unknown")
972
+
973
+ # Try score.instance_scores first, then cand.instance_scores (explicit check)
974
+ instance_scores = (
975
+ candidate_score.get('instance_scores')
976
+ if 'instance_scores' in candidate_score
977
+ else cand.get('instance_scores')
978
+ )
979
+ n_eval = len(instance_scores) if instance_scores and isinstance(instance_scores, list) else 0
980
+
981
+ lines.append(f"[{idx+1}] Accuracy: {accuracy:.4f} | Length: {prompt_length} | Type: {payload_kind} | N: {n_eval}")
982
+ lines.append("-" * 80)
983
+
984
+ obj = cand.get("object")
985
+ if obj and isinstance(obj, dict) and payload_kind == "transformation":
986
+ # For transformations, text_replacements are nested in data
987
+ data_obj = obj.get("data", {})
988
+ replacement_lines = _format_text_replacements(data_obj)
989
+ lines.extend(replacement_lines)
990
+ lines.append("")
991
+
992
+ # Add MIPRO top-K candidates
993
+ if mipro_topk_candidates and isinstance(mipro_topk_candidates, list):
994
+ # Sort by rank
995
+ mipro_topk_candidates.sort(key=lambda x: x.get("rank", 999))
996
+ lines.append("=" * 80)
997
+ lines.append(f"🎯 TOP-K CANDIDATES ({len(mipro_topk_candidates)})")
998
+ lines.append("=" * 80)
999
+ lines.append("")
1000
+
1001
+ for cand in mipro_topk_candidates:
1002
+ rank = cand.get("rank", 0)
1003
+ train_score = cand.get("train_score", 0.0)
1004
+ test_score = cand.get("test_score", 0.0)
1005
+ lift_abs = cand.get("lift_absolute")
1006
+ lift_pct = cand.get("lift_percent")
1007
+ instruction_text = cand.get("instruction_text", "")
1008
+ demo_indices = cand.get("demo_indices", [])
1009
+
1010
+ lift_str = ""
1011
+ if lift_abs is not None and lift_pct is not None:
1012
+ lift_str = f" | Lift: {lift_abs:+.3f} ({lift_pct:+.1f}%)"
1013
+
1014
+ lines.append(f"[Rank {rank}] Train: {train_score:.4f} ({train_score*100:.1f}%) | Test: {test_score:.4f} ({test_score*100:.1f}%){lift_str}")
1015
+ lines.append("-" * 80)
1016
+
1017
+ if instruction_text:
1018
+ lines.append(f"Instruction: {instruction_text[:200]}{'...' if len(instruction_text) > 200 else ''}")
1019
+ if demo_indices:
1020
+ lines.append(f"Demo Indices: {demo_indices}")
1021
+ lines.append("")
1022
+
1023
+ # Add all proposal candidates
1024
+ if attempted_candidates and isinstance(attempted_candidates, list):
1025
+ lines.append("=" * 80)
1026
+ lines.append(f"💡 ALL PROPOSAL CANDIDATES ({len(attempted_candidates)})")
1027
+ lines.append("=" * 80)
1028
+ lines.append("")
1029
+
1030
+ for idx, cand in enumerate(attempted_candidates):
1031
+ if not isinstance(cand, dict):
1032
+ continue
1033
+ accuracy = cand.get('accuracy', 0.0)
1034
+ prompt_length = cand.get('prompt_length', 0)
1035
+ tool_rate = cand.get('tool_call_rate', 0.0)
1036
+ instance_scores = cand.get('instance_scores', [])
1037
+ n_eval = len(instance_scores) if instance_scores else 0
1038
+
1039
+ lines.append(f"[{idx+1}] Accuracy: {accuracy:.4f} | Length: {prompt_length} | Tool Rate: {tool_rate:.2f} | N: {n_eval}")
1040
+ lines.append("-" * 80)
1041
+
1042
+ obj = cand.get("object")
1043
+ if obj and isinstance(obj, dict):
1044
+ # For proposals, text_replacements are at top level of object
1045
+ replacement_lines = _format_text_replacements(obj)
1046
+ lines.extend(replacement_lines)
1047
+ lines.append("")
1048
+
1049
+ lines.append("=" * 80)
1050
+ lines.append("END OF REPORT")
1051
+ lines.append("=" * 80)
1052
+
1053
+ # Determine save location
1054
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1055
+
1056
+ # Try to save in config directory first
1057
+ output_dir = config_path.parent / "results"
1058
+ output_dir.mkdir(exist_ok=True)
1059
+ output_file = output_dir / f"gepa_results_{job_id}_{timestamp}.txt"
1060
+
1061
+ with open(output_file, "w", encoding="utf-8") as f:
1062
+ f.write("\n".join(lines))
1063
+
1064
+ click.echo(f"\n📄 Results saved locally to: {output_file}")
1065
+
1066
+ except (PermissionError, OSError) as e:
1067
+ click.echo(f"⚠️ Could not save results file locally: {e}")
1068
+ except Exception as e:
1069
+ click.echo(f"⚠️ Unexpected error saving results file: {e}")
1070
+
1071
+
1072
+ def handle_prompt_learning(
1073
+ *,
1074
+ cfg_path: Path,
1075
+ backend_base: str,
1076
+ synth_key: str,
1077
+ task_url_override: str | None,
1078
+ allow_experimental: bool | None,
1079
+ dry_run: bool,
1080
+ poll: bool,
1081
+ poll_timeout: float,
1082
+ poll_interval: float,
1083
+ stream_format: str,
1084
+ ) -> None:
1085
+ """Handle prompt learning job creation (MIPRO or GEPA)."""
1086
+ env_key = get_required_value(
1087
+ "environment_api_key",
1088
+ env_value=os.environ.get("ENVIRONMENT_API_KEY"),
1089
+ )
1090
+ os.environ["ENVIRONMENT_API_KEY"] = env_key
1091
+
1092
+ overrides: dict[str, Any] = {
1093
+ "backend": backend_base,
1094
+ "task_url": task_url_override,
1095
+ }
1096
+
1097
+ build = build_prompt_learning_payload(
1098
+ config_path=cfg_path,
1099
+ task_url=task_url_override,
1100
+ overrides=overrides,
1101
+ allow_experimental=allow_experimental,
1102
+ )
1103
+
1104
+ click.echo("Performing task app health check…")
1105
+ health = check_task_app_health(build.task_url, env_key)
1106
+ if not health.ok:
1107
+ click.echo(f"Task app health check failed: {health.detail}")
1108
+ raise click.ClickException("Aborting due to failing health check")
1109
+ else:
1110
+ click.echo("Task app healthy")
1111
+
1112
+ create_url = f"{backend_base}/prompt-learning/online/jobs"
1113
+ headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
1114
+
1115
+ click.echo(f"POST {create_url}")
1116
+ click.echo("Payload preview:\n" + preview_json(build.payload, limit=800))
1117
+
1118
+ resp = http_post(create_url, headers=headers, json_body=build.payload)
1119
+ try:
1120
+ js = resp.json()
1121
+ except json.JSONDecodeError as e:
1122
+ click.echo(f"⚠️ Failed to parse JSON response: {e}")
1123
+ js = {"status": resp.status_code, "text": resp.text[:400]}
1124
+ click.echo(f"Response {resp.status_code}: {preview_json(js, limit=400)}")
1125
+ if resp.status_code not in (200, 201):
1126
+ raise click.ClickException("Job creation failed")
1127
+ job_id = js.get("job_id") or js.get("id")
1128
+ if not job_id:
1129
+ raise click.ClickException("Response missing job id")
1130
+
1131
+ if not poll:
1132
+ click.echo(f"Created job {job_id} (polling disabled)")
1133
+ return
1134
+
1135
+ click.echo("\n=== Streaming Job Progress ===")
1136
+
1137
+ # Custom config for prompt learning to enable metrics
1138
+ if stream_format == "chart":
1139
+ config = StreamConfig(
1140
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
1141
+ event_types={
1142
+ "prompt.learning.progress",
1143
+ "prompt.learning.gepa.start",
1144
+ "prompt.learning.gepa.complete",
1145
+ },
1146
+ metric_names={"gepa.transformation.mean_score"},
1147
+ )
1148
+ handlers = [LossCurveHandler()]
1149
+ click.echo("Using live loss chart (metric=gepa.transformation.mean_score)")
1150
+ else:
1151
+ # Enable metrics for CLI mode too
1152
+ config = StreamConfig(
1153
+ enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
1154
+ metric_names={"gepa.transformation.mean_score"},
1155
+ )
1156
+ handlers = [CLIHandler(
1157
+ hidden_event_types=_DEFAULT_PROMPT_LEARNING_HIDDEN_EVENTS,
1158
+ hidden_event_substrings=_DEFAULT_RL_HIDDEN_SUBSTRINGS,
1159
+ )]
1160
+
1161
+ streamer = JobStreamer(
1162
+ base_url=backend_base,
1163
+ api_key=synth_key,
1164
+ job_id=job_id,
1165
+ endpoints=StreamEndpoints.prompt_learning(job_id),
1166
+ config=config,
1167
+ handlers=handlers,
1168
+ interval_seconds=poll_interval,
1169
+ timeout_seconds=poll_timeout,
1170
+ )
1171
+ final_status = asyncio.run(streamer.stream_until_terminal())
1172
+ click.echo(f"Final status: {final_status.get('status', 'unknown')}")
1173
+ click.echo(preview_json(final_status, limit=600))
1174
+
1175
+ # Save results file locally
1176
+ _save_prompt_learning_results_locally(
1177
+ backend_base=backend_base,
1178
+ api_key=synth_key,
1179
+ job_id=job_id,
1180
+ config_path=cfg_path,
1181
+ )
1182
+
1183
+
1184
+ def register(cli: click.Group) -> None:
1185
+ cli.add_command(train_command)