synth-ai 0.2.9.dev0__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 (890) 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. {synth_ai/task/apps → examples/rl/task_app}/math_single_step.py +188 -50
  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 +60 -2
  461. synth_ai/api/train/builders.py +347 -39
  462. synth_ai/api/train/cli.py +895 -160
  463. synth_ai/api/train/config_finder.py +103 -25
  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 +70 -20
  470. synth_ai/api/train/pollers.py +29 -4
  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 +6 -4
  475. synth_ai/api/train/utils.py +64 -52
  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 +85 -63
  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 +156 -116
  554. synth_ai/cli/root.py +131 -132
  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 +2284 -257
  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 +579 -291
  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 +3 -3
  583. synth_ai/demos/demo_task_apps/core.py +64 -28
  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/deploy_modal.py +3 -6
  591. synth_ai/demos/demo_task_apps/math/modal_task_app.py +185 -83
  592. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -2
  593. synth_ai/demos/math/__init__.py +1 -0
  594. synth_ai/demos/math/_common.py +16 -0
  595. synth_ai/demos/math/app.py +38 -0
  596. synth_ai/demos/math/config.toml +76 -0
  597. synth_ai/demos/math/deploy_modal.py +54 -0
  598. synth_ai/demos/math/modal_task_app.py +703 -0
  599. synth_ai/demos/math/task_app_entry.py +51 -0
  600. synth_ai/environments/environment/core.py +7 -1
  601. synth_ai/environments/examples/bandit/engine.py +12 -5
  602. synth_ai/environments/examples/bandit/environment.py +0 -1
  603. synth_ai/environments/examples/bandit/taskset.py +4 -4
  604. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  605. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  606. synth_ai/environments/examples/crafter_classic/environment.py +93 -2
  607. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  608. synth_ai/environments/examples/enron/engine.py +7 -2
  609. synth_ai/environments/examples/enron/environment.py +68 -0
  610. synth_ai/environments/examples/red/engine.py +60 -12
  611. synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
  612. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  613. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
  614. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
  615. synth_ai/environments/examples/red/environment.py +86 -0
  616. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  617. synth_ai/environments/examples/sokoban/taskset.py +116 -0
  618. synth_ai/environments/examples/verilog/engine.py +104 -12
  619. synth_ai/environments/examples/wordle/environment.py +0 -1
  620. synth_ai/environments/reproducibility/tree.py +5 -6
  621. synth_ai/environments/service/app.py +11 -12
  622. synth_ai/environments/service/core_routes.py +10 -9
  623. synth_ai/environments/stateful/engine.py +1 -1
  624. synth_ai/environments/tasks/core.py +1 -0
  625. synth_ai/environments/tasks/filters.py +5 -6
  626. synth_ai/environments/tasks/utils.py +4 -5
  627. synth_ai/evals/__init__.py +15 -0
  628. synth_ai/evals/base.py +14 -5
  629. synth_ai/evals/client.py +82 -0
  630. synth_ai/evals/types.py +42 -0
  631. synth_ai/http.py +8 -22
  632. synth_ai/http_client.py +45 -12
  633. synth_ai/inference/__init__.py +0 -2
  634. synth_ai/inference/client.py +21 -7
  635. synth_ai/jobs/client.py +129 -80
  636. synth_ai/judge_schemas.py +127 -0
  637. synth_ai/learning/__init__.py +51 -6
  638. synth_ai/learning/algorithms.py +14 -0
  639. synth_ai/learning/client.py +122 -30
  640. synth_ai/learning/config.py +2 -40
  641. synth_ai/learning/constants.py +0 -2
  642. synth_ai/learning/ft_client.py +4 -56
  643. synth_ai/learning/health.py +14 -8
  644. synth_ai/learning/jobs.py +43 -47
  645. synth_ai/learning/prompt_learning_client.py +276 -0
  646. synth_ai/learning/prompt_learning_types.py +185 -0
  647. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  648. synth_ai/learning/rl/client.py +269 -0
  649. synth_ai/learning/rl/config.py +31 -0
  650. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  651. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  652. synth_ai/learning/rl/secrets.py +13 -0
  653. synth_ai/learning/rl_client.py +2 -253
  654. synth_ai/learning/sft/__init__.py +29 -0
  655. synth_ai/learning/sft/client.py +68 -0
  656. synth_ai/learning/sft/config.py +270 -0
  657. synth_ai/learning/sft/data.py +698 -0
  658. synth_ai/learning/sse.py +25 -26
  659. synth_ai/learning/validators.py +29 -25
  660. synth_ai/mcp/__init__.py +5 -0
  661. synth_ai/mcp/__main__.py +8 -0
  662. synth_ai/mcp/main.py +254 -0
  663. synth_ai/mcp/setup.py +100 -0
  664. synth_ai/modal.py +257 -0
  665. synth_ai/pricing/__init__.py +3 -0
  666. synth_ai/pricing/model_pricing.py +64 -0
  667. synth_ai/session/__init__.py +75 -0
  668. synth_ai/session/client.py +383 -0
  669. synth_ai/session/constants.py +63 -0
  670. synth_ai/session/exceptions.py +105 -0
  671. synth_ai/session/manager.py +139 -0
  672. synth_ai/session/models.py +89 -0
  673. synth_ai/session/query.py +110 -0
  674. synth_ai/spec/__init__.py +46 -0
  675. synth_ai/spec/dataclasses.py +149 -0
  676. synth_ai/spec/loader.py +144 -0
  677. synth_ai/spec/serializer.py +199 -0
  678. synth_ai/spec/validation.py +250 -0
  679. synth_ai/streaming/__init__.py +29 -0
  680. synth_ai/streaming/config.py +94 -0
  681. synth_ai/streaming/handlers.py +589 -0
  682. synth_ai/streaming/streamer.py +320 -0
  683. synth_ai/streaming/types.py +95 -0
  684. synth_ai/task/__init__.py +50 -30
  685. synth_ai/task/apps/__init__.py +63 -19
  686. synth_ai/task/auth.py +35 -23
  687. synth_ai/task/client.py +15 -13
  688. synth_ai/task/config.py +261 -0
  689. synth_ai/task/contracts.py +165 -64
  690. synth_ai/task/datasets.py +9 -6
  691. synth_ai/task/errors.py +11 -10
  692. synth_ai/task/health.py +17 -11
  693. synth_ai/task/inference_api.py +101 -0
  694. synth_ai/task/json.py +58 -24
  695. synth_ai/task/proxy.py +59 -66
  696. synth_ai/task/rubrics/__init__.py +55 -0
  697. synth_ai/task/rubrics/loaders.py +156 -0
  698. synth_ai/task/rubrics/models.py +57 -0
  699. synth_ai/task/rubrics/scoring.py +116 -0
  700. synth_ai/task/rubrics/strict.py +149 -0
  701. synth_ai/task/rubrics.py +22 -15
  702. synth_ai/task/server.py +65 -31
  703. synth_ai/task/trace_correlation_helpers.py +328 -0
  704. synth_ai/task/tracing_utils.py +44 -28
  705. synth_ai/task/validators.py +449 -6
  706. synth_ai/task/vendors.py +5 -7
  707. synth_ai/tracing_v3/__init__.py +4 -0
  708. synth_ai/tracing_v3/abstractions.py +21 -4
  709. synth_ai/tracing_v3/config.py +167 -22
  710. synth_ai/tracing_v3/constants.py +21 -0
  711. synth_ai/tracing_v3/db_config.py +42 -29
  712. synth_ai/tracing_v3/decorators.py +80 -45
  713. synth_ai/tracing_v3/examples/basic_usage.py +15 -9
  714. synth_ai/tracing_v3/hooks.py +6 -4
  715. synth_ai/tracing_v3/llm_call_record_helpers.py +161 -61
  716. synth_ai/tracing_v3/migration_helper.py +1 -2
  717. synth_ai/tracing_v3/replica_sync.py +12 -7
  718. synth_ai/tracing_v3/serialization.py +130 -0
  719. synth_ai/tracing_v3/session_tracer.py +73 -16
  720. synth_ai/tracing_v3/storage/base.py +89 -1
  721. synth_ai/tracing_v3/storage/config.py +63 -16
  722. synth_ai/tracing_v3/storage/factory.py +11 -9
  723. synth_ai/tracing_v3/storage/utils.py +15 -11
  724. synth_ai/tracing_v3/trace_utils.py +317 -0
  725. synth_ai/tracing_v3/turso/__init__.py +8 -21
  726. synth_ai/tracing_v3/turso/daemon.py +123 -15
  727. synth_ai/tracing_v3/turso/models.py +5 -2
  728. synth_ai/tracing_v3/turso/native_manager.py +1293 -0
  729. synth_ai/tracing_v3/utils.py +5 -4
  730. synth_ai/tunnel.py +143 -0
  731. synth_ai/tunnel_deploy.py +278 -0
  732. synth_ai/types.py +8 -0
  733. synth_ai/urls.py +11 -0
  734. synth_ai/utils/__init__.py +166 -0
  735. synth_ai/utils/agents.py +74 -0
  736. synth_ai/utils/apps.py +152 -0
  737. synth_ai/utils/base_url.py +94 -0
  738. synth_ai/utils/bin.py +39 -0
  739. synth_ai/utils/claude.py +36 -0
  740. synth_ai/utils/cli.py +284 -0
  741. synth_ai/utils/config.py +81 -0
  742. synth_ai/utils/env.py +346 -0
  743. synth_ai/utils/errors.py +85 -0
  744. synth_ai/utils/http.py +172 -0
  745. synth_ai/utils/json.py +72 -0
  746. synth_ai/utils/log_filter.py +99 -0
  747. synth_ai/utils/logging.py +198 -0
  748. synth_ai/utils/modal.py +299 -0
  749. synth_ai/utils/paths.py +95 -0
  750. synth_ai/utils/process.py +233 -0
  751. synth_ai/utils/prompts.py +39 -0
  752. synth_ai/utils/sqld.py +122 -0
  753. synth_ai/utils/ssl.py +25 -0
  754. synth_ai/utils/task_app_discovery.py +882 -0
  755. synth_ai/utils/task_app_env.py +186 -0
  756. synth_ai/utils/task_app_state.py +318 -0
  757. synth_ai/utils/tunnel/__init__.py +12 -0
  758. synth_ai/utils/tunnel/config.py +55 -0
  759. synth_ai/utils/user_config.py +137 -0
  760. synth_ai/uvicorn.py +77 -0
  761. synth_ai-0.2.23.dev3.dist-info/METADATA +357 -0
  762. synth_ai-0.2.23.dev3.dist-info/RECORD +983 -0
  763. {synth_ai-0.2.9.dev0.dist-info → synth_ai-0.2.23.dev3.dist-info}/entry_points.txt +0 -1
  764. {synth_ai-0.2.9.dev0.dist-info → synth_ai-0.2.23.dev3.dist-info}/top_level.txt +1 -0
  765. synth_ai/cli/man.py +0 -106
  766. synth_ai/core/experiment.py +0 -15
  767. synth_ai/core/system.py +0 -15
  768. synth_ai/demo_registry.py +0 -258
  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 -107
  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/task/apps/grpo_crafter.py +0 -438
  838. synth_ai/tracing/__init__.py +0 -30
  839. synth_ai/tracing_v1/__init__.py +0 -33
  840. synth_ai/tracing_v3/turso/manager.py +0 -774
  841. synth_ai/v0/tracing/abstractions.py +0 -224
  842. synth_ai/v0/tracing/base_client.py +0 -91
  843. synth_ai/v0/tracing/client_manager.py +0 -131
  844. synth_ai/v0/tracing/config.py +0 -142
  845. synth_ai/v0/tracing/context.py +0 -146
  846. synth_ai/v0/tracing/decorators.py +0 -682
  847. synth_ai/v0/tracing/events/__init__.py +0 -0
  848. synth_ai/v0/tracing/events/manage.py +0 -147
  849. synth_ai/v0/tracing/events/scope.py +0 -86
  850. synth_ai/v0/tracing/events/store.py +0 -228
  851. synth_ai/v0/tracing/immediate_client.py +0 -151
  852. synth_ai/v0/tracing/local.py +0 -18
  853. synth_ai/v0/tracing/log_client_base.py +0 -73
  854. synth_ai/v0/tracing/retry_queue.py +0 -186
  855. synth_ai/v0/tracing/trackers.py +0 -515
  856. synth_ai/v0/tracing/upload.py +0 -512
  857. synth_ai/v0/tracing/utils.py +0 -9
  858. synth_ai/v0/tracing_v1/__init__.py +0 -16
  859. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  860. synth_ai/v0/tracing_v1/base_client.py +0 -91
  861. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  862. synth_ai/v0/tracing_v1/config.py +0 -142
  863. synth_ai/v0/tracing_v1/context.py +0 -146
  864. synth_ai/v0/tracing_v1/decorators.py +0 -703
  865. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  866. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  867. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  868. synth_ai/v0/tracing_v1/events/store.py +0 -228
  869. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  870. synth_ai/v0/tracing_v1/local.py +0 -18
  871. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  872. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  873. synth_ai/v0/tracing_v1/trackers.py +0 -515
  874. synth_ai/v0/tracing_v1/upload.py +0 -527
  875. synth_ai/v0/tracing_v1/utils.py +0 -9
  876. synth_ai/zyk/__init__.py +0 -30
  877. synth_ai-0.2.9.dev0.dist-info/METADATA +0 -131
  878. synth_ai-0.2.9.dev0.dist-info/RECORD +0 -444
  879. {synth_ai/lm/caching → examples/task_apps}/__init__.py +0 -0
  880. {synth_ai/lm/cost → examples/task_apps/crafter}/__init__.py +0 -0
  881. {synth_ai/lm/structured_outputs → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server}/__init__.py +0 -0
  882. {synth_ai/lm/vendors → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests}/__init__.py +0 -0
  883. {synth_ai/lm/vendors/core → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils}/__init__.py +0 -0
  884. {synth_ai/lm/vendors/local → examples/task_apps/math}/__init__.py +0 -0
  885. {synth_ai/lm/vendors/supported → examples/workflows}/__init__.py +0 -0
  886. {synth_ai/v0/tracing → examples/workflows/math_rl}/__init__.py +0 -0
  887. /synth_ai/{compound/cais.py → cli/__main__.py} +0 -0
  888. /synth_ai/{learning/filtering.py → py.typed} +0 -0
  889. {synth_ai-0.2.9.dev0.dist-info → synth_ai-0.2.23.dev3.dist-info}/WHEEL +0 -0
  890. {synth_ai-0.2.9.dev0.dist-info → synth_ai-0.2.23.dev3.dist-info}/licenses/LICENSE +0 -0
synth_ai/api/train/cli.py CHANGED
@@ -1,22 +1,44 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
4
+ import contextlib
5
+ import importlib
6
+ import json
3
7
  import os
8
+ from collections.abc import Callable, Mapping
4
9
  from pathlib import Path
5
- from typing import Any, Dict
10
+ from typing import Any, NoReturn, cast
6
11
 
7
12
  import click
8
13
 
9
- from .builders import RLBuildResult, SFTBuildResult, build_rl_payload, build_sft_payload
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
10
34
  from .config_finder import discover_configs, prompt_for_config
11
35
  from .env_resolver import KeySpec, resolve_env
12
- from .pollers import RLJobPoller, SFTJobPoller
13
36
  from .task_app import check_task_app_health
14
37
  from .utils import (
15
38
  TrainError,
16
- REPO_ROOT,
17
39
  ensure_api_base,
18
- http_post,
19
40
  http_get,
41
+ http_post,
20
42
  limit_jsonl_examples,
21
43
  mask_value,
22
44
  post_multipart,
@@ -25,87 +47,180 @@ from .utils import (
25
47
  validate_sft_jsonl,
26
48
  )
27
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"
28
56
 
29
- def _discover_dataset_candidates(config_path: Path, limit: int = 50) -> list[Path]:
30
- search_dirs: list[Path] = [
31
- config_path.parent,
32
- config_path.parent / "datasets",
33
- REPO_ROOT / "traces",
34
- REPO_ROOT / "datasets",
35
- ]
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
36
60
 
37
- candidates: list[Path] = []
38
- seen: set[Path] = set()
39
- for directory in search_dirs:
40
- if not directory.exists() or not directory.is_dir():
41
- continue
42
- for path in directory.rglob("*.jsonl"):
43
- try:
44
- resolved = path.resolve()
45
- except OSError:
46
- continue
47
- if resolved in seen:
48
- continue
49
- seen.add(resolved)
50
- if resolved.stat().st_size == 0:
51
- continue
52
- candidates.append(resolved)
53
- if len(candidates) >= limit:
54
- return candidates
55
- return candidates
56
-
57
-
58
- def prompt_for_dataset(config_path: Path) -> Path:
59
- candidates = _discover_dataset_candidates(config_path)
60
- while True:
61
- if candidates:
62
- click.echo("Select dataset JSONL file:")
63
- for idx, candidate in enumerate(candidates, start=1):
64
- click.echo(f" {idx}) {candidate}")
65
- click.echo(" m) Enter path manually")
66
- click.echo(" 0) Abort")
67
- choice = click.prompt("Choice", default="m").strip().lower()
68
- if choice == "0":
69
- raise click.ClickException("Aborted by user")
70
- if choice in {"m", "manual"}:
71
- selected = _prompt_manual_dataset()
72
- else:
73
- try:
74
- idx = int(choice)
75
- except ValueError:
76
- click.echo("Invalid selection; try again")
77
- continue
78
- if idx < 1 or idx > len(candidates):
79
- click.echo("Invalid selection; try again")
80
- continue
81
- selected = candidates[idx - 1]
82
- else:
83
- selected = _prompt_manual_dataset()
84
61
 
85
- if selected.exists() and selected.suffix == ".jsonl":
86
- return selected.resolve()
87
- click.echo("File not found or not a .jsonl; please try again.")
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
+ }
88
125
 
89
126
 
90
- def _prompt_manual_dataset() -> Path:
91
- manual = click.prompt("Enter dataset JSONL path", type=str).strip()
92
- return Path(manual).expanduser()
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
93
160
 
94
161
 
95
162
  @click.command("train")
96
- @click.option("--config", "config_paths", multiple=True, type=click.Path(), help="Path to training TOML (repeatable)")
97
- @click.option("--type", "train_type", type=click.Choice(["auto", "rl", "sft"]), default="auto")
98
- @click.option("--env-file", "env_files", multiple=True, type=click.Path(), help=".env file(s) to preload (skips selection prompt)")
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
+ )
99
178
  @click.option("--task-url", default=None, help="Override task app base URL (RL only)")
100
- @click.option("--dataset", "dataset_path", type=click.Path(), default=None, help="Override dataset JSONL path (SFT)")
101
- @click.option("--backend", default=lambda: os.getenv("BACKEND_BASE_URL", "http://localhost:8000/api"), help="Backend base URL")
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")
102
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
+ )
103
203
  @click.option("--idempotency", default=None, help="Idempotency-Key header for job creation")
104
- @click.option("--dry-run", is_flag=True, help="Preview payload without submitting")
204
+ @click.option("--dry-run", is_flag=True, hidden=True, help="Deprecated: no-op")
105
205
  @click.option("--poll/--no-poll", default=True, help="Poll job status until terminal state")
106
- @click.option("--poll-timeout", default=3600.0, type=float, help="Maximum seconds to poll before timing out")
206
+ @click.option(
207
+ "--poll-timeout", default=3600.0, type=float, help="Maximum seconds to poll before timing out"
208
+ )
107
209
  @click.option("--poll-interval", default=5.0, type=float, help="Seconds between poll attempts")
108
- @click.option("--examples", "examples_limit", type=int, default=None, help="Limit SFT training to the first N examples")
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
+ )
109
224
  def train_command(
110
225
  config_paths: tuple[str, ...],
111
226
  train_type: str,
@@ -114,27 +229,48 @@ def train_command(
114
229
  dataset_path: str | None,
115
230
  backend: str,
116
231
  model: str | None,
232
+ allow_experimental: bool | None,
117
233
  idempotency: str | None,
118
234
  dry_run: bool,
119
235
  poll: bool,
120
236
  poll_timeout: float,
121
237
  poll_interval: float,
238
+ stream_format: str,
122
239
  examples_limit: int | None,
123
240
  ) -> None:
124
- """Interactive launcher for RL / SFT jobs."""
241
+ """Interactive launcher for RL / SFT / Prompt Learning jobs."""
242
+ load_env_file()
125
243
 
126
- candidates = discover_configs(list(config_paths), requested_type=train_type if train_type != "auto" else None)
127
- selection = prompt_for_config(candidates, requested_type=train_type if train_type != "auto" else None)
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
+ )
128
252
 
129
253
  effective_type = train_type if train_type != "auto" else selection.train_type
130
- if effective_type not in {"rl", "sft"}:
131
- effective_type = click.prompt("Detected config type is ambiguous. Enter type", type=click.Choice(["rl", "sft"]))
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
+ )
132
268
 
133
269
  cfg_path = selection.path
134
270
  click.echo(f"Using config: {cfg_path} ({effective_type})")
135
271
 
136
272
  required_keys: list[KeySpec] = []
137
- if effective_type == "rl":
273
+ if effective_type == "rl" or effective_type == "prompt_learning":
138
274
  required_keys.append(KeySpec("SYNTH_API_KEY", "Synth API key for backend"))
139
275
  required_keys.append(
140
276
  KeySpec(
@@ -169,7 +305,12 @@ def train_command(
169
305
  ]
170
306
  if missing_keys:
171
307
  try:
172
- from synth_ai.cli.task_apps import _interactive_fill_env
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
+ )
173
314
  except Exception as exc: # pragma: no cover - protective fallback
174
315
  raise click.ClickException(f"Unable to prompt for env values: {exc}") from exc
175
316
 
@@ -184,9 +325,11 @@ def train_command(
184
325
  )
185
326
  click.echo(f"Using env file: {env_path}")
186
327
 
187
- synth_key = env_values.get("SYNTH_API_KEY") or os.environ.get("SYNTH_API_KEY")
188
- if not synth_key:
189
- raise click.ClickException("SYNTH_API_KEY required")
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
190
333
 
191
334
  backend_base = ensure_api_base(backend)
192
335
  click.echo(f"Backend base: {backend_base} (key {mask_value(synth_key)})")
@@ -199,10 +342,25 @@ def train_command(
199
342
  task_url_override=task_url,
200
343
  model_override=model,
201
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,
202
359
  dry_run=dry_run,
203
360
  poll=poll,
204
361
  poll_timeout=poll_timeout,
205
362
  poll_interval=poll_interval,
363
+ stream_format=stream_format,
206
364
  )
207
365
  else:
208
366
  dataset_override_path = Path(dataset_path).expanduser().resolve() if dataset_path else None
@@ -211,37 +369,88 @@ def train_command(
211
369
  backend_base=backend_base,
212
370
  synth_key=synth_key,
213
371
  dataset_override=dataset_override_path,
372
+ allow_experimental=allow_experimental,
214
373
  dry_run=dry_run,
215
374
  poll=poll,
216
375
  poll_timeout=poll_timeout,
217
376
  poll_interval=poll_interval,
377
+ stream_format=stream_format,
218
378
  examples_limit=examples_limit,
219
379
  )
220
380
 
221
381
 
222
- def _wait_for_training_file(backend_base: str, api_key: str, file_id: str, *, timeout: float = 120.0) -> None:
223
- url = f"{backend_base}/learning/files/{file_id}"
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}"
224
394
  headers = {"Authorization": f"Bearer {api_key}"}
225
395
  elapsed = 0.0
226
396
  interval = 2.0
397
+ first_check = True
227
398
  while True:
228
399
  resp = http_get(url, headers=headers, timeout=30.0)
229
400
  if resp.status_code == 200:
230
401
  try:
231
402
  data = resp.json()
232
- except Exception:
403
+ except json.JSONDecodeError:
233
404
  data = {}
234
- status = str(data.get("status") or data.get("state") or data.get("storage_state") or "ready").lower()
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
235
411
  if status in {"ready", "uploaded", "stored", "complete"}:
412
+ click.echo(f"✓ Training file ready (status={status})")
236
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
+ )
237
419
  elif resp.status_code == 404:
238
420
  # Keep polling; object may not be visible yet
239
- pass
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
+ )
240
439
  else:
241
- click.echo(f"[WARN] Unexpected response while checking training file {file_id}: {resp.status_code}")
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}")
242
449
 
243
450
  if elapsed >= timeout:
244
- raise click.ClickException(f"Training file {file_id} not ready after {timeout:.0f}s")
451
+ raise click.ClickException(
452
+ f"Training file {file_id} not ready after {timeout:.0f}s (last status: {resp.status_code})"
453
+ )
245
454
  sleep(interval)
246
455
  elapsed += interval
247
456
 
@@ -254,31 +463,52 @@ def handle_rl(
254
463
  task_url_override: str | None,
255
464
  model_override: str | None,
256
465
  idempotency: str | None,
466
+ allow_experimental: bool | None,
257
467
  dry_run: bool,
258
468
  poll: bool,
259
469
  poll_timeout: float,
260
470
  poll_interval: float,
471
+ stream_format: str,
261
472
  ) -> None:
262
- overrides: Dict[str, Any] = {"backend": backend_base, "task_url": task_url_override, "model": model_override}
473
+ overrides: dict[str, Any] = {
474
+ "backend": backend_base,
475
+ "task_url": task_url_override,
476
+ "model": model_override,
477
+ }
263
478
  build = build_rl_payload(
264
479
  config_path=cfg_path,
265
480
  task_url=task_url_override or os.environ.get("TASK_APP_URL", ""),
266
481
  overrides=overrides,
267
482
  idempotency=idempotency,
483
+ allow_experimental=allow_experimental,
268
484
  )
269
485
 
270
486
  # Backend-side verification: try ALL org environment keys against /health and /task_info
271
487
  verify_url = f"{backend_base}/rl/verify_task_app"
272
488
  verify_headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
273
489
  try:
274
- vresp = http_post(verify_url, headers=verify_headers, json_body={"endpoint_base_url": build.task_url})
490
+ vresp = http_post(
491
+ verify_url, headers=verify_headers, json_body={"endpoint_base_url": build.task_url}
492
+ )
275
493
  try:
276
- vjs = vresp.json()
277
- except Exception:
278
- vjs = {"status": vresp.status_code, "text": (vresp.text or "")[:400]}
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
279
507
  except Exception as _ve:
280
- raise click.ClickException(f"Task app verification call failed: {type(_ve).__name__}: {_ve}") from _ve
281
- if vresp.status_code >= 400:
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:
282
512
  click.echo("Task app verification error:\n" + preview_json(vjs, limit=800))
283
513
  raise click.ClickException(f"Verification failed with status {vresp.status_code}")
284
514
  if not bool(vjs.get("any_ok")):
@@ -289,15 +519,23 @@ def handle_rl(
289
519
  # Print concise summary
290
520
  try:
291
521
  cands = vjs.get("candidates_first15") or []
292
- attempts = vjs.get("attempts") or []
293
- statuses = [a.get("status") for a in attempts]
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]
294
529
  click.echo(f"Verification OK (candidates={cands}, statuses={statuses})")
295
- except Exception:
296
- pass
530
+ except (KeyError, ValueError, AttributeError):
531
+ # Parsing verification summary failed, but verification itself succeeded
532
+ click.echo("Verification OK")
297
533
 
298
- env_key = os.environ.get("ENVIRONMENT_API_KEY")
299
- if not env_key:
300
- raise click.ClickException("ENVIRONMENT_API_KEY required for RL flow")
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
301
539
 
302
540
  click.echo("Performing task app health check…")
303
541
  health = check_task_app_health(build.task_url, env_key)
@@ -314,14 +552,12 @@ def handle_rl(
314
552
 
315
553
  click.echo(f"POST {create_url}")
316
554
  click.echo("Payload preview:\n" + preview_json(build.payload, limit=800))
317
- if dry_run:
318
- click.echo("Dry run enabled; skipping submission")
319
- return
320
555
 
321
556
  resp = http_post(create_url, headers=headers, json_body=build.payload)
322
557
  try:
323
558
  js = resp.json()
324
- except Exception:
559
+ except json.JSONDecodeError as e:
560
+ click.echo(f"⚠️ Failed to parse JSON response: {e}")
325
561
  js = {"status": resp.status_code, "text": resp.text[:400]}
326
562
  click.echo(f"Response {resp.status_code}: {preview_json(js, limit=400)}")
327
563
  if resp.status_code not in (200, 201):
@@ -334,10 +570,41 @@ def handle_rl(
334
570
  click.echo(f"Created job {job_id} (polling disabled)")
335
571
  return
336
572
 
337
- poller = RLJobPoller(backend_base, synth_key, interval=poll_interval, timeout=poll_timeout)
338
- outcome = poller.poll_job(job_id)
339
- click.echo(f"Final status: {outcome.status}")
340
- click.echo(preview_json(outcome.payload, limit=600))
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))
341
608
 
342
609
 
343
610
  def handle_sft(
@@ -346,21 +613,22 @@ def handle_sft(
346
613
  backend_base: str,
347
614
  synth_key: str,
348
615
  dataset_override: Path | None,
616
+ allow_experimental: bool | None,
349
617
  dry_run: bool,
350
618
  poll: bool,
351
619
  poll_timeout: float,
352
620
  poll_interval: float,
621
+ stream_format: str,
353
622
  examples_limit: int | None,
354
623
  ) -> None:
355
- dataset_path = dataset_override
356
-
357
- while True:
358
- try:
359
- build = build_sft_payload(config_path=cfg_path, dataset_override=dataset_path)
360
- break
361
- except TrainError as exc:
362
- click.echo(str(exc))
363
- dataset_path = prompt_for_dataset(cfg_path)
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)
364
632
 
365
633
  limited_path: Path | None = None
366
634
 
@@ -378,72 +646,539 @@ def handle_sft(
378
646
  click.echo("Validating validation dataset…")
379
647
  validate_sft_jsonl(build.validation_file)
380
648
 
381
- upload_url = f"{backend_base}/learning/files"
382
- click.echo(f"Uploading dataset {build.train_file}")
383
- if dry_run:
384
- click.echo("Dry run: skipping upload")
385
- train_file_id = "dry-run-train"
386
- val_file_id = None
387
- else:
388
- resp = post_multipart(upload_url, api_key=synth_key, file_field="file", file_path=build.train_file)
389
- js = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
390
- if resp.status_code >= 400 or "id" not in js:
391
- raise click.ClickException(f"Training file upload failed ({resp.status_code}): {js or resp.text[:200]}")
392
- train_file_id = js["id"]
393
- val_file_id = None
394
- if build.validation_file:
395
- click.echo(f"Uploading validation dataset {build.validation_file}")
396
- vresp = post_multipart(upload_url, api_key=synth_key, file_field="file", file_path=build.validation_file)
397
- vjs = vresp.json() if vresp.headers.get("content-type", "").startswith("application/json") else {}
398
- if vresp.status_code < 400 and "id" in vjs:
399
- val_file_id = vjs["id"]
400
- else:
401
- click.echo(f"[WARN] Validation upload failed: {vresp.status_code} {vjs or vresp.text[:200]}")
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
+ )
402
693
  payload = dict(build.payload)
403
694
  payload["training_file_id"] = train_file_id
404
695
  if val_file_id:
405
- payload.setdefault("metadata", {}).setdefault("effective_config", {}).setdefault("data", {})["validation_files"] = [val_file_id]
696
+ payload.setdefault("metadata", {}).setdefault("effective_config", {}).setdefault(
697
+ "data", {}
698
+ )["validation_files"] = [val_file_id]
406
699
 
700
+ click.echo("\n=== Checking File Processing Status ===")
407
701
  try:
408
702
  _wait_for_training_file(backend_base, synth_key, train_file_id)
409
703
  except click.ClickException as exc:
410
- raise click.ClickException(f"Training file {train_file_id} not ready: {exc}") from exc
704
+ click.echo(f"[WARN] File readiness check failed: {exc}")
705
+ click.echo("Proceeding anyway - backend will validate file during job creation...")
411
706
 
412
- click.echo("FFT job payload:\n" + preview_json(payload, limit=800))
413
- if dry_run:
414
- click.echo("Dry run: skipping job submission")
415
- return
707
+ click.echo("\n=== Creating Training Job ===")
708
+ click.echo("Job payload preview:")
709
+ click.echo(preview_json(payload, limit=800))
416
710
 
417
711
  create_url = f"{backend_base}/learning/jobs"
418
712
  headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
713
+ click.echo(f"\nPOST {create_url}")
419
714
  resp = http_post(create_url, headers=headers, json_body=payload)
420
- js = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
421
- click.echo(f"Response {resp.status_code}: {preview_json(js, limit=400)}")
715
+ js = (
716
+ resp.json()
717
+ if resp.headers.get("content-type", "").startswith("application/json")
718
+ else {}
719
+ )
422
720
  if resp.status_code not in (200, 201):
423
- raise click.ClickException("Failed to create learning job")
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}")
424
726
  job_id = js.get("job_id") or js.get("id")
425
727
  if not job_id:
426
728
  raise click.ClickException("Response missing job id")
729
+ click.echo(f"✓ Job created (id={job_id})")
427
730
 
731
+ click.echo("\n=== Starting Training Job ===")
428
732
  start_url = f"{backend_base}/learning/jobs/{job_id}/start"
429
- click.echo(f"POST {start_url} (start)")
430
- _ = http_post(start_url, headers=headers, json_body={})
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")
431
739
 
432
740
  if not poll:
433
741
  click.echo(f"Started job {job_id} (polling disabled)")
434
742
  return
435
743
 
436
- poller = SFTJobPoller(backend_base, synth_key, interval=poll_interval, timeout=poll_timeout)
437
- outcome = poller.poll_job(job_id)
438
- click.echo(f"Final status: {outcome.status}")
439
- click.echo(preview_json(outcome.payload, limit=600))
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))
440
764
  finally:
441
765
  if limited_path is not None:
442
- try:
766
+ with contextlib.suppress(OSError):
443
767
  limited_path.unlink(missing_ok=True)
768
+ # Clean up empty parent directory if possible
769
+ with contextlib.suppress(OSError):
444
770
  limited_path.parent.rmdir()
445
- except Exception:
446
- pass
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
+ )
447
1182
 
448
1183
 
449
1184
  def register(cli: click.Group) -> None: