synth-ai 0.2.8.dev4__py3-none-any.whl → 0.2.23.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/README.md +1 -0
- examples/__init__.py +16 -0
- examples/analyze_semantic_words.sh +17 -0
- examples/baseline/banking77_baseline.py +243 -0
- examples/baseline/banking77_pipeline_baseline.py +294 -0
- examples/baseline/crafter_baseline.py +407 -0
- examples/baseline/pokemon_red_baseline.py +326 -0
- examples/baseline/simple_baseline.py +56 -0
- examples/baseline/warming_up_to_rl_baseline.py +239 -0
- examples/blog_posts/gepa/README.md +355 -0
- examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
- examples/blog_posts/gepa/configs/banking77_gepa_test.toml +80 -0
- examples/blog_posts/gepa/configs/banking77_mipro_local.toml +50 -0
- examples/blog_posts/gepa/configs/banking77_pipeline_gepa_local.toml +101 -0
- examples/blog_posts/gepa/configs/banking77_pipeline_gepa_test.toml +96 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +57 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +35 -0
- examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +51 -0
- examples/blog_posts/gepa/configs/hover_gepa_local.toml +57 -0
- examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +35 -0
- examples/blog_posts/gepa/configs/hover_mipro_local.toml +51 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +57 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +35 -0
- examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +51 -0
- examples/blog_posts/gepa/configs/pupa_gepa_local.toml +58 -0
- examples/blog_posts/gepa/configs/pupa_mipro_local.toml +52 -0
- examples/blog_posts/gepa/deploy_banking77_task_app.sh +54 -0
- examples/blog_posts/gepa/gepa_baseline.py +204 -0
- examples/blog_posts/gepa/query_prompts_example.py +97 -0
- examples/blog_posts/gepa/run_gepa_banking77.sh +112 -0
- examples/blog_posts/gepa/run_gepa_banking77_pipeline.sh +163 -0
- examples/blog_posts/gepa/task_apps.py +105 -0
- examples/blog_posts/gepa/test_gepa_local.sh +67 -0
- examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
- examples/blog_posts/mipro/README.md +415 -0
- examples/blog_posts/mipro/configs/banking77_mipro_local.toml +91 -0
- examples/blog_posts/mipro/configs/banking77_mipro_test.toml +87 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gemini_flash_lite_local.toml +98 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gpt41mini_local.toml +96 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_local.toml +94 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_test.toml +170 -0
- examples/blog_posts/mipro/deploy_banking77_pipeline_task_app.sh +59 -0
- examples/blog_posts/mipro/deploy_banking77_task_app.sh +41 -0
- examples/blog_posts/mipro/multi_step.md +79 -0
- examples/blog_posts/mipro/run_mipro_banking77.sh +191 -0
- examples/blog_posts/mipro/run_mipro_banking77_pipeline.sh +171 -0
- examples/blog_posts/mipro/run_mipro_banking77_pipeline_gemini_flash_lite.sh +177 -0
- examples/blog_posts/mipro/run_mipro_banking77_pipeline_gpt41mini.sh +173 -0
- examples/blog_posts/mipro/verify_banking77_setup.sh +117 -0
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/pokemon_vl/extract_images.py +239 -0
- examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
- examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
- examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
- examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
- examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
- examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
- examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
- examples/crafter_debug_render.py +186 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +45 -0
- examples/gepa/banking77_pipeline_gepa.toml +96 -0
- examples/gepa/multi_stage_gepa_example.toml +84 -0
- examples/gepa/run_gepa_banking77_pipeline.sh +157 -0
- examples/multi_step/SFT_README.md +147 -0
- examples/multi_step/configs/README_verilog_rl.md +77 -0
- examples/multi_step/configs/VERILOG_REWARDS.md +103 -0
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +196 -0
- examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
- examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +75 -0
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +145 -0
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +84 -0
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +79 -0
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
- examples/multi_step/configs/crafter_synth_backend.md +40 -0
- examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
- examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
- examples/multi_step/configs/verilog_rl_lora.toml +147 -0
- examples/multi_step/convert_traces_to_sft.py +84 -0
- examples/multi_step/crafter_rl_lora.md +70 -0
- examples/multi_step/judges/crafter_backend_judge.py +220 -0
- examples/multi_step/judges/verilog_backend_judge.py +234 -0
- examples/multi_step/readme.md +48 -0
- examples/multi_step/run_sft_qwen30b.sh +45 -0
- examples/multi_step/sse_metrics_streaming_notes.md +357 -0
- examples/multi_step/task_app_config_notes.md +494 -0
- examples/multi_step/verilog_rl_lora.md +218 -0
- examples/qwen_coder/README.md +102 -0
- examples/qwen_coder/_shared.py +113 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +60 -0
- examples/qwen_coder/configs/coder_lora_4b.toml +61 -0
- examples/qwen_coder/configs/coder_lora_small.toml +57 -0
- examples/qwen_coder/generate_dataset.py +98 -0
- examples/qwen_coder/infer_ft_smoke.py +65 -0
- examples/qwen_coder/infer_prod_proxy.py +73 -0
- examples/qwen_coder/infer_via_synth.py +87 -0
- examples/qwen_coder/scripts/infer_coder.sh +19 -0
- examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
- examples/qwen_coder/sft_full_17b.py +103 -0
- examples/qwen_coder/sft_lora_30b.py +110 -0
- examples/qwen_coder/subset_jsonl.py +39 -0
- examples/qwen_coder/todos.md +38 -0
- examples/qwen_coder/validate_jsonl.py +60 -0
- examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
- examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
- examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
- examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
- examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
- examples/qwen_vl/QUICKSTART.md +327 -0
- examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
- examples/qwen_vl/README.md +152 -0
- examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
- examples/qwen_vl/RL_VISION_TESTING.md +333 -0
- examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
- examples/qwen_vl/SETUP_COMPLETE.md +274 -0
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
- examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
- examples/qwen_vl/__init__.py +2 -0
- examples/qwen_vl/collect_data_via_cli.md +415 -0
- examples/qwen_vl/collect_vision_traces.py +368 -0
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
- examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
- examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
- examples/qwen_vl/configs/filter_vision_test.toml +8 -0
- examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
- examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
- examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
- examples/qwen_vl/run_vision_comparison.sh +61 -0
- examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
- examples/qwen_vl/test_image_validation.py +201 -0
- examples/qwen_vl/test_sft_vision_data.py +110 -0
- examples/rl/README.md +169 -0
- examples/rl/configs/eval_base_qwen.toml +17 -0
- examples/rl/configs/eval_rl_qwen.toml +13 -0
- examples/rl/configs/rl_from_base_qwen.toml +62 -0
- examples/rl/configs/rl_from_base_qwen17.toml +80 -0
- examples/rl/configs/rl_from_ft_qwen.toml +37 -0
- examples/rl/download_dataset.py +80 -0
- examples/rl/run_eval.py +436 -0
- examples/rl/run_rl_and_save.py +111 -0
- examples/rl/task_app/README.md +21 -0
- examples/rl/task_app/math_single_step.py +990 -0
- examples/rl/task_app/math_task_app.py +111 -0
- examples/run_crafter_demo.sh +10 -0
- examples/sdk_prompt_learning_example.py +55 -0
- examples/sft/README.md +139 -0
- examples/sft/configs/crafter_fft_qwen0p6b.toml +49 -0
- examples/sft/configs/crafter_lora_qwen0p6b.toml +49 -0
- examples/sft/evaluate.py +117 -0
- examples/sft/export_dataset.py +120 -0
- examples/sft/generate_traces.py +164 -0
- examples/swe/__init__.py +12 -0
- examples/swe/task_app/README.md +135 -0
- examples/swe/task_app/__init__.py +2 -0
- examples/swe/task_app/grpo_swe_mini.py +604 -0
- examples/swe/task_app/grpo_swe_mini_task_app.py +124 -0
- examples/swe/task_app/hosted/README.md +173 -0
- examples/swe/task_app/hosted/__init__.py +5 -0
- examples/swe/task_app/hosted/branching.py +143 -0
- examples/swe/task_app/hosted/environment_routes.py +1289 -0
- examples/swe/task_app/hosted/envs/__init__.py +1 -0
- examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
- examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
- examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
- examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
- examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
- examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
- examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +1191 -0
- examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
- examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
- examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
- examples/swe/task_app/hosted/hosted_app.py +204 -0
- examples/swe/task_app/hosted/inference/__init__.py +5 -0
- examples/swe/task_app/hosted/inference/openai_client.py +584 -0
- examples/swe/task_app/hosted/main.py +100 -0
- examples/swe/task_app/hosted/policy_routes.py +1094 -0
- examples/swe/task_app/hosted/registry.py +195 -0
- examples/swe/task_app/hosted/rollout.py +1905 -0
- examples/swe/task_app/hosted/storage/__init__.py +5 -0
- examples/swe/task_app/hosted/storage/volume.py +211 -0
- examples/swe/task_app/hosted/test_agents.py +161 -0
- examples/swe/task_app/hosted/test_service.py +136 -0
- examples/swe/task_app/hosted/utils.py +62 -0
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
- examples/task_apps/TESTING.md +275 -0
- examples/task_apps/banking77/__init__.py +6 -0
- examples/task_apps/banking77/banking77_task_app.py +912 -0
- examples/task_apps/banking77/deploy_wrapper.py +46 -0
- examples/task_apps/banking77_pipeline/__init__.py +6 -0
- examples/task_apps/banking77_pipeline/banking77_pipeline_task_app.py +489 -0
- examples/task_apps/banking77_pipeline/deploy_wrapper.py +50 -0
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +286 -0
- examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +187 -0
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +281 -0
- examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
- examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
- examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
- examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
- examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
- examples/task_apps/crafter/task_app/README.md +42 -0
- examples/task_apps/crafter/task_app/__init__.py +5 -0
- examples/task_apps/crafter/task_app/grpo_crafter.py +1055 -0
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +146 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +173 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +5 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +143 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +1226 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +1 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +532 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +583 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +122 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +253 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +5 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +999 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +100 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +1252 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +195 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +2233 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +5 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +211 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +161 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +136 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +411 -0
- examples/task_apps/dev/pokemon_emerald/__init__.py +2 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +811 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +120 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +160 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +155 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +69 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +96 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +1502 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +4 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +68 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +216 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +35 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +631 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +1544 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +1428 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +4848 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +41 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +298 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +95 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +204 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +2152 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +429 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +155 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +78 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +122 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +76 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +413 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +204 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +133 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +229 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +300 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +205 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +200 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +284 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +468 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +575 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +311 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +259 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +372 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +296 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +275 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +22 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +44 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +514 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +415 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +1763 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +33 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +106 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +334 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +1020 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +188 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +1481 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +862 -0
- examples/task_apps/dev/pokemon_emerald/modal_app.py +114 -0
- examples/task_apps/dev/pokemon_emerald/task_app/README.md +81 -0
- examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +6 -0
- examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +685 -0
- examples/task_apps/enron/__init__.py +2 -0
- examples/task_apps/enron/eval_groq_qwen32.toml +16 -0
- examples/task_apps/enron/filter_sft.toml +5 -0
- examples/task_apps/enron/task_app/README.md +14 -0
- examples/task_apps/enron/task_app/__init__.py +1 -0
- examples/task_apps/enron/task_app/grpo_enron.py +906 -0
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +146 -0
- examples/task_apps/enron/tests/__init__.py +4 -0
- examples/task_apps/enron/tests/conftest.py +115 -0
- examples/task_apps/enron/tests/integration/__init__.py +4 -0
- examples/task_apps/enron/tests/integration/test_enron_eval.py +179 -0
- examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
- examples/task_apps/enron/tests/unit/__init__.py +4 -0
- examples/task_apps/enron/tests/unit/test_enron_environment.py +126 -0
- examples/task_apps/gepa_benchmarks/__init__.py +7 -0
- examples/task_apps/gepa_benchmarks/common.py +260 -0
- examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
- examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
- examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
- examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
- examples/task_apps/math/README.md +21 -0
- examples/task_apps/math/math_single_step.py +1000 -0
- examples/task_apps/math/math_task_app.py +115 -0
- examples/task_apps/pokemon_battle/__init__.py +2 -0
- examples/task_apps/pokemon_battle/modal_app.py +104 -0
- examples/task_apps/pokemon_battle/task_app/README.md +68 -0
- examples/task_apps/pokemon_battle/task_app/__init__.py +6 -0
- examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +932 -0
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
- examples/task_apps/pokemon_red/README.md +356 -0
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +428 -0
- examples/task_apps/pokemon_red/__init__.py +3 -0
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +30 -0
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +224 -0
- examples/task_apps/pokemon_red/pallet_town_rl_config.toml +75 -0
- examples/task_apps/pokemon_red/task_app.py +1048 -0
- examples/task_apps/pokemon_red/test_pallet_town_rewards.py +193 -0
- examples/task_apps/sokoban/README.md +306 -0
- examples/task_apps/sokoban/__init__.py +3 -0
- examples/task_apps/sokoban/eval_groq_qwen32.toml +16 -0
- examples/task_apps/sokoban/eval_openai_gpt5.toml +16 -0
- examples/task_apps/sokoban/filter_sft.toml +5 -0
- examples/task_apps/sokoban/task_app.py +1058 -0
- examples/task_apps/sokoban/tests/__init__.py +4 -0
- examples/task_apps/sokoban/tests/conftest.py +113 -0
- examples/task_apps/sokoban/tests/integration/__init__.py +4 -0
- examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +57 -0
- examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +198 -0
- examples/task_apps/sokoban/tests/unit/__init__.py +4 -0
- examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +114 -0
- examples/task_apps/verilog/__init__.py +1 -0
- examples/task_apps/verilog/eval_groq_qwen32b.toml +22 -0
- examples/task_apps/verilog/filter_sft.toml +5 -0
- examples/task_apps/verilog/task_app/README.md +12 -0
- examples/task_apps/verilog/task_app/__init__.py +1 -0
- examples/task_apps/verilog/task_app/grpo_verilog.py +1166 -0
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
- examples/task_apps/verilog/tests/__init__.py +4 -0
- examples/task_apps/verilog/tests/conftest.py +115 -0
- examples/task_apps/verilog/tests/integration/__init__.py +4 -0
- examples/task_apps/verilog/tests/integration/test_verilog_eval.py +181 -0
- examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
- examples/task_apps/verilog/tests/unit/__init__.py +4 -0
- examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +118 -0
- examples/tunnel_gepa_banking77/README.md +106 -0
- examples/tunnel_gepa_banking77/banking77_gepa_tunnel.toml +95 -0
- examples/tunnel_gepa_banking77/keep_tunnel_running.py +60 -0
- examples/tunnel_gepa_banking77/run_gepa_with_tunnel.sh +226 -0
- examples/vlm/PROPOSAL.md +53 -0
- examples/vlm/README.md +68 -0
- examples/vlm/configs/crafter_vlm_gpt4o.toml +49 -0
- examples/vlm/crafter_image_only_agent.py +207 -0
- examples/vlm/crafter_openai_vlm_agent.py +275 -0
- examples/vlm/filter_image_rows.py +63 -0
- examples/vlm/run_crafter_vlm_benchmark.py +316 -0
- examples/warming_up_to_rl/_utils.py +92 -0
- examples/warming_up_to_rl/analyze_trace_db.py +422 -0
- examples/warming_up_to_rl/configs/crafter_fft.toml +53 -0
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +54 -0
- examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +22 -0
- examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +15 -0
- examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +24 -0
- examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +35 -0
- examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +26 -0
- examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +36 -0
- examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +32 -0
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +85 -0
- examples/warming_up_to_rl/configs/rl_from_ft.toml +58 -0
- examples/warming_up_to_rl/export_trace_sft.py +837 -0
- examples/warming_up_to_rl/groq_test.py +97 -0
- examples/warming_up_to_rl/manage_secrets.py +131 -0
- examples/warming_up_to_rl/old/event_rewards.md +234 -0
- examples/warming_up_to_rl/old/notes.md +73 -0
- examples/warming_up_to_rl/readme.md +110 -0
- examples/warming_up_to_rl/run_eval.py +736 -0
- examples/warming_up_to_rl/run_fft_and_save.py +380 -0
- examples/warming_up_to_rl/run_local_rollout.py +239 -0
- examples/warming_up_to_rl/run_local_rollout_modal.py +248 -0
- examples/warming_up_to_rl/run_local_rollout_parallel.py +405 -0
- examples/warming_up_to_rl/run_local_rollout_traced.py +477 -0
- examples/warming_up_to_rl/run_rl_and_save.py +124 -0
- examples/warming_up_to_rl/run_rollout_remote.py +156 -0
- examples/warming_up_to_rl/task_app/README.md +42 -0
- examples/warming_up_to_rl/task_app/grpo_crafter.py +876 -0
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +253 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +729 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1114 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1891 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +129 -0
- examples/workflows/math_rl/configs/eval_base_qwen.toml +15 -0
- examples/workflows/math_rl/configs/eval_rl_qwen.toml +11 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +62 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +80 -0
- examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +35 -0
- examples/workflows/math_rl/download_dataset.py +80 -0
- examples/workflows/math_rl/run_eval.py +436 -0
- examples/workflows/math_rl/run_rl_and_save.py +111 -0
- synth_ai/__init__.py +47 -23
- synth_ai/_utils/__init__.py +47 -0
- synth_ai/_utils/base_url.py +10 -0
- synth_ai/_utils/http.py +10 -0
- synth_ai/_utils/prompts.py +10 -0
- synth_ai/_utils/task_app_state.py +12 -0
- synth_ai/_utils/user_config.py +10 -0
- synth_ai/api/models/supported.py +514 -0
- synth_ai/api/train/__init__.py +63 -0
- synth_ai/api/train/builders.py +473 -0
- synth_ai/api/train/cli.py +1185 -0
- synth_ai/api/train/config_finder.py +246 -0
- synth_ai/api/train/configs/__init__.py +65 -0
- synth_ai/api/train/configs/prompt_learning.py +496 -0
- synth_ai/api/train/configs/rl.py +188 -0
- synth_ai/api/train/configs/sft.py +99 -0
- synth_ai/api/train/configs/shared.py +81 -0
- synth_ai/api/train/env_resolver.py +352 -0
- synth_ai/api/train/pollers.py +91 -0
- synth_ai/api/train/prompt_learning.py +425 -0
- synth_ai/api/train/sft.py +390 -0
- synth_ai/api/train/supported_algos.py +147 -0
- synth_ai/api/train/task_app.py +195 -0
- synth_ai/api/train/utils.py +244 -0
- synth_ai/api/train/validators.py +1117 -0
- synth_ai/api/tunnel.py +49 -0
- synth_ai/auth/credentials.py +94 -0
- synth_ai/baseline/__init__.py +25 -0
- synth_ai/baseline/config.py +209 -0
- synth_ai/baseline/discovery.py +214 -0
- synth_ai/baseline/execution.py +146 -0
- synth_ai/cfgs.py +227 -0
- synth_ai/cli/__init__.py +90 -45
- synth_ai/cli/_modal_wrapper.py +31 -0
- synth_ai/cli/_storage.py +20 -0
- synth_ai/cli/_typer_patch.py +47 -0
- synth_ai/cli/_validate_task_app.py +29 -0
- synth_ai/cli/balance.py +16 -4
- synth_ai/cli/calc.py +36 -21
- synth_ai/cli/claude.py +70 -0
- synth_ai/cli/codex.py +267 -0
- synth_ai/cli/commands/__init__.py +18 -0
- synth_ai/cli/commands/baseline/__init__.py +12 -0
- synth_ai/cli/commands/baseline/core.py +637 -0
- synth_ai/cli/commands/baseline/list.py +93 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1112 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +424 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +185 -0
- synth_ai/cli/commands/help/core.py +72 -0
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1437 -0
- synth_ai/cli/commands/status/__init__.py +66 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/session.py +183 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/subcommands/usage.py +203 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +200 -0
- synth_ai/cli/commands/train/judge_validation.py +305 -0
- synth_ai/cli/commands/train/validation.py +386 -0
- synth_ai/cli/demo.py +32 -140
- synth_ai/cli/deploy.py +233 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/legacy_root_backup.py +28 -22
- synth_ai/cli/lib/__init__.py +10 -0
- synth_ai/cli/lib/task_app_discovery.py +7 -0
- synth_ai/cli/lib/task_app_env.py +518 -0
- synth_ai/cli/mcp.py +34 -0
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/opencode.py +256 -0
- synth_ai/cli/recent.py +13 -7
- synth_ai/cli/rl_demo.py +166 -114
- synth_ai/cli/root.py +143 -112
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +49 -0
- synth_ai/cli/status.py +7 -125
- synth_ai/cli/task_app_deploy.py +7 -0
- synth_ai/cli/task_app_list.py +25 -0
- synth_ai/cli/task_app_modal_serve.py +11 -0
- synth_ai/cli/task_app_serve.py +11 -0
- synth_ai/cli/task_apps.py +3134 -0
- synth_ai/cli/traces.py +9 -5
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +5 -0
- synth_ai/cli/turso.py +73 -0
- synth_ai/cli/watch.py +13 -18
- synth_ai/demos/__init__.py +10 -0
- synth_ai/demos/core/__init__.py +28 -1
- synth_ai/demos/core/cli.py +745 -416
- synth_ai/demos/crafter/__init__.py +1 -0
- synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
- synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
- synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
- synth_ai/demos/demo_registry.py +176 -0
- synth_ai/demos/demo_task_apps/__init__.py +7 -1
- synth_ai/demos/demo_task_apps/core.py +75 -37
- synth_ai/demos/demo_task_apps/crafter/__init__.py +1 -0
- synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +53 -0
- synth_ai/demos/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +73 -0
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +184 -0
- synth_ai/demos/demo_task_apps/math/_common.py +1 -2
- synth_ai/demos/demo_task_apps/math/app.py +2 -1
- synth_ai/demos/demo_task_apps/math/config.toml +55 -110
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +491 -166
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +37 -0
- synth_ai/demos/math/__init__.py +1 -0
- synth_ai/demos/math/_common.py +16 -0
- synth_ai/demos/math/app.py +38 -0
- synth_ai/demos/math/config.toml +76 -0
- synth_ai/demos/math/deploy_modal.py +54 -0
- synth_ai/demos/math/modal_task_app.py +703 -0
- synth_ai/demos/math/task_app_entry.py +51 -0
- synth_ai/environments/environment/core.py +7 -1
- synth_ai/environments/examples/bandit/engine.py +12 -5
- synth_ai/environments/examples/bandit/environment.py +0 -1
- synth_ai/environments/examples/bandit/taskset.py +4 -4
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
- synth_ai/environments/examples/crafter_classic/environment.py +93 -2
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
- synth_ai/environments/examples/enron/engine.py +7 -2
- synth_ai/environments/examples/enron/environment.py +68 -0
- synth_ai/environments/examples/red/engine.py +60 -12
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
- synth_ai/environments/examples/red/environment.py +86 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/environments/examples/sokoban/taskset.py +116 -0
- synth_ai/environments/examples/verilog/engine.py +104 -12
- synth_ai/environments/examples/wordle/environment.py +0 -1
- synth_ai/environments/reproducibility/tree.py +5 -6
- synth_ai/environments/service/app.py +11 -12
- synth_ai/environments/service/core_routes.py +10 -9
- synth_ai/environments/stateful/engine.py +1 -1
- synth_ai/environments/tasks/core.py +1 -0
- synth_ai/environments/tasks/filters.py +5 -6
- synth_ai/environments/tasks/utils.py +4 -5
- synth_ai/evals/__init__.py +15 -0
- synth_ai/evals/base.py +14 -5
- synth_ai/evals/client.py +82 -0
- synth_ai/evals/types.py +42 -0
- synth_ai/http.py +8 -22
- synth_ai/http_client.py +45 -12
- synth_ai/inference/__init__.py +0 -2
- synth_ai/inference/client.py +21 -7
- synth_ai/jobs/client.py +129 -80
- synth_ai/judge_schemas.py +127 -0
- synth_ai/learning/__init__.py +51 -6
- synth_ai/learning/algorithms.py +14 -0
- synth_ai/learning/client.py +122 -30
- synth_ai/learning/config.py +2 -40
- synth_ai/learning/constants.py +0 -2
- synth_ai/learning/ft_client.py +4 -56
- synth_ai/learning/health.py +14 -8
- synth_ai/learning/jobs.py +43 -47
- synth_ai/learning/prompt_learning_client.py +276 -0
- synth_ai/learning/prompt_learning_types.py +185 -0
- synth_ai/{rl → learning/rl}/__init__.py +14 -5
- synth_ai/learning/rl/client.py +269 -0
- synth_ai/learning/rl/config.py +31 -0
- synth_ai/{rl → learning/rl}/contracts.py +5 -10
- synth_ai/{rl → learning/rl}/env_keys.py +45 -16
- synth_ai/learning/rl/secrets.py +13 -0
- synth_ai/learning/rl_client.py +2 -253
- synth_ai/learning/sft/__init__.py +29 -0
- synth_ai/learning/sft/client.py +68 -0
- synth_ai/learning/sft/config.py +270 -0
- synth_ai/learning/sft/data.py +698 -0
- synth_ai/learning/sse.py +25 -26
- synth_ai/learning/validators.py +29 -25
- synth_ai/mcp/__init__.py +5 -0
- synth_ai/mcp/__main__.py +8 -0
- synth_ai/mcp/main.py +254 -0
- synth_ai/mcp/setup.py +100 -0
- synth_ai/modal.py +257 -0
- synth_ai/pricing/__init__.py +3 -0
- synth_ai/pricing/model_pricing.py +64 -0
- synth_ai/session/__init__.py +75 -0
- synth_ai/session/client.py +383 -0
- synth_ai/session/constants.py +63 -0
- synth_ai/session/exceptions.py +105 -0
- synth_ai/session/manager.py +139 -0
- synth_ai/session/models.py +89 -0
- synth_ai/session/query.py +110 -0
- synth_ai/spec/__init__.py +46 -0
- synth_ai/spec/dataclasses.py +149 -0
- synth_ai/spec/loader.py +144 -0
- synth_ai/spec/serializer.py +199 -0
- synth_ai/spec/validation.py +250 -0
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +589 -0
- synth_ai/streaming/streamer.py +320 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/__init__.py +116 -3
- synth_ai/task/apps/__init__.py +132 -0
- synth_ai/task/auth.py +165 -0
- synth_ai/task/client.py +167 -0
- synth_ai/task/config.py +261 -0
- synth_ai/task/contracts.py +173 -57
- synth_ai/task/datasets.py +108 -0
- synth_ai/task/errors.py +50 -0
- synth_ai/task/health.py +17 -11
- synth_ai/task/inference_api.py +101 -0
- synth_ai/task/json.py +111 -0
- synth_ai/task/proxy.py +251 -0
- synth_ai/task/rubrics/__init__.py +55 -0
- synth_ai/task/rubrics/loaders.py +156 -0
- synth_ai/task/rubrics/models.py +57 -0
- synth_ai/task/rubrics/scoring.py +116 -0
- synth_ai/task/rubrics/strict.py +149 -0
- synth_ai/task/rubrics.py +219 -0
- synth_ai/task/server.py +432 -0
- synth_ai/task/trace_correlation_helpers.py +328 -0
- synth_ai/task/tracing_utils.py +95 -0
- synth_ai/task/validators.py +449 -6
- synth_ai/task/vendors.py +59 -0
- synth_ai/tracing_v3/__init__.py +4 -0
- synth_ai/tracing_v3/abstractions.py +21 -4
- synth_ai/tracing_v3/config.py +167 -22
- synth_ai/tracing_v3/constants.py +21 -0
- synth_ai/tracing_v3/db_config.py +42 -29
- synth_ai/tracing_v3/decorators.py +80 -45
- synth_ai/tracing_v3/examples/basic_usage.py +15 -9
- synth_ai/tracing_v3/hooks.py +6 -4
- synth_ai/tracing_v3/llm_call_record_helpers.py +161 -61
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/replica_sync.py +12 -7
- synth_ai/tracing_v3/serialization.py +130 -0
- synth_ai/tracing_v3/session_tracer.py +86 -21
- synth_ai/tracing_v3/storage/base.py +98 -12
- synth_ai/tracing_v3/storage/config.py +63 -16
- synth_ai/tracing_v3/storage/factory.py +11 -9
- synth_ai/tracing_v3/storage/utils.py +15 -11
- synth_ai/tracing_v3/trace_utils.py +317 -0
- synth_ai/tracing_v3/turso/__init__.py +8 -21
- synth_ai/tracing_v3/turso/daemon.py +123 -15
- synth_ai/tracing_v3/turso/models.py +5 -2
- synth_ai/tracing_v3/turso/native_manager.py +1293 -0
- synth_ai/tracing_v3/utils.py +5 -4
- synth_ai/tunnel.py +143 -0
- synth_ai/tunnel_deploy.py +278 -0
- synth_ai/types.py +8 -0
- synth_ai/urls.py +11 -0
- synth_ai/utils/__init__.py +166 -0
- synth_ai/utils/agents.py +74 -0
- synth_ai/utils/apps.py +152 -0
- synth_ai/utils/base_url.py +94 -0
- synth_ai/utils/bin.py +39 -0
- synth_ai/utils/claude.py +36 -0
- synth_ai/utils/cli.py +284 -0
- synth_ai/utils/config.py +81 -0
- synth_ai/utils/env.py +346 -0
- synth_ai/utils/errors.py +85 -0
- synth_ai/utils/http.py +172 -0
- synth_ai/utils/json.py +72 -0
- synth_ai/utils/log_filter.py +99 -0
- synth_ai/utils/logging.py +198 -0
- synth_ai/utils/modal.py +299 -0
- synth_ai/utils/paths.py +95 -0
- synth_ai/utils/process.py +233 -0
- synth_ai/utils/prompts.py +39 -0
- synth_ai/utils/sqld.py +122 -0
- synth_ai/utils/ssl.py +25 -0
- synth_ai/utils/task_app_discovery.py +882 -0
- synth_ai/utils/task_app_env.py +186 -0
- synth_ai/utils/task_app_state.py +318 -0
- synth_ai/utils/tunnel/__init__.py +12 -0
- synth_ai/utils/tunnel/config.py +55 -0
- synth_ai/utils/user_config.py +137 -0
- synth_ai/uvicorn.py +77 -0
- synth_ai-0.2.23.dev3.dist-info/METADATA +357 -0
- synth_ai-0.2.23.dev3.dist-info/RECORD +983 -0
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/entry_points.txt +0 -1
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/top_level.txt +1 -0
- synth_ai/cli/man.py +0 -106
- synth_ai/core/experiment.py +0 -15
- synth_ai/core/system.py +0 -15
- synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
- synth_ai/experimental/synth_oss.py +0 -446
- synth_ai/handshake.py +0 -63
- synth_ai/install_sqld.sh +0 -40
- synth_ai/learning/offline/dpo.py +0 -0
- synth_ai/learning/offline/providers.py +0 -7
- synth_ai/learning/offline/sft.py +0 -0
- synth_ai/learning/offline/shared.py +0 -0
- synth_ai/learning/online/grpo.py +0 -0
- synth_ai/learning/online/irft.py +0 -0
- synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
- synth_ai/learning/prompts/gepa.py +0 -0
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
- synth_ai/learning/prompts/mipro.py +0 -289
- synth_ai/learning/prompts/random_search.py +0 -246
- synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
- synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
- synth_ai/lm/__init__.py +0 -51
- synth_ai/lm/caching/constants.py +0 -6
- synth_ai/lm/caching/dbs.py +0 -0
- synth_ai/lm/caching/ephemeral.py +0 -102
- synth_ai/lm/caching/handler.py +0 -137
- synth_ai/lm/caching/initialize.py +0 -11
- synth_ai/lm/caching/persistent.py +0 -114
- synth_ai/lm/config.py +0 -110
- synth_ai/lm/constants.py +0 -32
- synth_ai/lm/core/__init__.py +0 -8
- synth_ai/lm/core/all.py +0 -73
- synth_ai/lm/core/exceptions.py +0 -7
- synth_ai/lm/core/main.py +0 -319
- synth_ai/lm/core/main_v3.py +0 -594
- synth_ai/lm/core/synth_models.py +0 -48
- synth_ai/lm/core/vendor_clients.py +0 -188
- synth_ai/lm/cost/monitor.py +0 -1
- synth_ai/lm/cost/statefulness.py +0 -1
- synth_ai/lm/injection.py +0 -80
- synth_ai/lm/overrides.py +0 -206
- synth_ai/lm/provider_support/__init__.py +0 -8
- synth_ai/lm/provider_support/anthropic.py +0 -972
- synth_ai/lm/provider_support/openai.py +0 -1139
- synth_ai/lm/provider_support/suppress_logging.py +0 -31
- synth_ai/lm/structured_outputs/handler.py +0 -440
- synth_ai/lm/structured_outputs/inject.py +0 -297
- synth_ai/lm/structured_outputs/rehabilitate.py +0 -185
- synth_ai/lm/tools/__init__.py +0 -3
- synth_ai/lm/tools/base.py +0 -172
- synth_ai/lm/unified_interface.py +0 -202
- synth_ai/lm/vendors/base.py +0 -81
- synth_ai/lm/vendors/core/anthropic_api.py +0 -387
- synth_ai/lm/vendors/core/gemini_api.py +0 -292
- synth_ai/lm/vendors/core/mistral_api.py +0 -322
- synth_ai/lm/vendors/core/openai_api.py +0 -225
- synth_ai/lm/vendors/core/synth_dev_api.py +0 -0
- synth_ai/lm/vendors/local/ollama.py +0 -0
- synth_ai/lm/vendors/openai_standard.py +0 -780
- synth_ai/lm/vendors/openai_standard_responses.py +0 -256
- synth_ai/lm/vendors/retries.py +0 -22
- synth_ai/lm/vendors/supported/custom_endpoint.py +0 -417
- synth_ai/lm/vendors/supported/deepseek.py +0 -69
- synth_ai/lm/vendors/supported/grok.py +0 -75
- synth_ai/lm/vendors/supported/groq.py +0 -16
- synth_ai/lm/vendors/supported/ollama.py +0 -15
- synth_ai/lm/vendors/supported/openrouter.py +0 -74
- synth_ai/lm/vendors/supported/together.py +0 -11
- synth_ai/lm/vendors/synth_client.py +0 -808
- synth_ai/lm/warmup.py +0 -186
- synth_ai/rl/secrets.py +0 -19
- synth_ai/scripts/verify_rewards.py +0 -100
- synth_ai/tracing/__init__.py +0 -30
- synth_ai/tracing_v1/__init__.py +0 -33
- synth_ai/tracing_v3/turso/manager.py +0 -760
- synth_ai/v0/tracing/abstractions.py +0 -224
- synth_ai/v0/tracing/base_client.py +0 -91
- synth_ai/v0/tracing/client_manager.py +0 -131
- synth_ai/v0/tracing/config.py +0 -142
- synth_ai/v0/tracing/context.py +0 -146
- synth_ai/v0/tracing/decorators.py +0 -682
- synth_ai/v0/tracing/events/__init__.py +0 -0
- synth_ai/v0/tracing/events/manage.py +0 -147
- synth_ai/v0/tracing/events/scope.py +0 -86
- synth_ai/v0/tracing/events/store.py +0 -228
- synth_ai/v0/tracing/immediate_client.py +0 -151
- synth_ai/v0/tracing/local.py +0 -18
- synth_ai/v0/tracing/log_client_base.py +0 -73
- synth_ai/v0/tracing/retry_queue.py +0 -186
- synth_ai/v0/tracing/trackers.py +0 -515
- synth_ai/v0/tracing/upload.py +0 -512
- synth_ai/v0/tracing/utils.py +0 -9
- synth_ai/v0/tracing_v1/__init__.py +0 -16
- synth_ai/v0/tracing_v1/abstractions.py +0 -224
- synth_ai/v0/tracing_v1/base_client.py +0 -91
- synth_ai/v0/tracing_v1/client_manager.py +0 -131
- synth_ai/v0/tracing_v1/config.py +0 -142
- synth_ai/v0/tracing_v1/context.py +0 -146
- synth_ai/v0/tracing_v1/decorators.py +0 -703
- synth_ai/v0/tracing_v1/events/__init__.py +0 -0
- synth_ai/v0/tracing_v1/events/manage.py +0 -147
- synth_ai/v0/tracing_v1/events/scope.py +0 -86
- synth_ai/v0/tracing_v1/events/store.py +0 -228
- synth_ai/v0/tracing_v1/immediate_client.py +0 -151
- synth_ai/v0/tracing_v1/local.py +0 -18
- synth_ai/v0/tracing_v1/log_client_base.py +0 -73
- synth_ai/v0/tracing_v1/retry_queue.py +0 -186
- synth_ai/v0/tracing_v1/trackers.py +0 -515
- synth_ai/v0/tracing_v1/upload.py +0 -527
- synth_ai/v0/tracing_v1/utils.py +0 -9
- synth_ai/zyk/__init__.py +0 -30
- synth_ai-0.2.8.dev4.dist-info/METADATA +0 -129
- synth_ai-0.2.8.dev4.dist-info/RECORD +0 -420
- {synth_ai/lm/caching → examples/task_apps}/__init__.py +0 -0
- {synth_ai/lm/cost → examples/task_apps/crafter}/__init__.py +0 -0
- {synth_ai/lm/structured_outputs → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server}/__init__.py +0 -0
- {synth_ai/lm/vendors → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests}/__init__.py +0 -0
- {synth_ai/lm/vendors/core → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils}/__init__.py +0 -0
- {synth_ai/lm/vendors/local → examples/task_apps/math}/__init__.py +0 -0
- {synth_ai/lm/vendors/supported → examples/workflows}/__init__.py +0 -0
- {synth_ai/v0/tracing → examples/workflows/math_rl}/__init__.py +0 -0
- /synth_ai/{compound/cais.py → cli/__main__.py} +0 -0
- /synth_ai/{learning/filtering.py → py.typed} +0 -0
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, NoReturn, cast
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
_config_module = cast(
|
|
16
|
+
Any, importlib.import_module("synth_ai.config.base_url")
|
|
17
|
+
)
|
|
18
|
+
get_backend_from_env = cast(Callable[[], str], _config_module.get_backend_from_env)
|
|
19
|
+
except Exception as exc: # pragma: no cover - critical dependency
|
|
20
|
+
raise RuntimeError("Unable to load backend configuration helpers") from exc
|
|
21
|
+
|
|
22
|
+
from synth_ai.streaming import (
|
|
23
|
+
CLIHandler,
|
|
24
|
+
JobStreamer,
|
|
25
|
+
LossCurveHandler,
|
|
26
|
+
StreamConfig,
|
|
27
|
+
StreamEndpoints,
|
|
28
|
+
StreamType,
|
|
29
|
+
)
|
|
30
|
+
from synth_ai.utils.env import load_env_file
|
|
31
|
+
from synth_ai.utils.errors import format_error_message, get_required_value
|
|
32
|
+
|
|
33
|
+
from .builders import build_prompt_learning_payload, build_rl_payload, build_sft_payload
|
|
34
|
+
from .config_finder import discover_configs, prompt_for_config
|
|
35
|
+
from .env_resolver import KeySpec, resolve_env
|
|
36
|
+
from .task_app import check_task_app_health
|
|
37
|
+
from .utils import (
|
|
38
|
+
TrainError,
|
|
39
|
+
ensure_api_base,
|
|
40
|
+
http_get,
|
|
41
|
+
http_post,
|
|
42
|
+
limit_jsonl_examples,
|
|
43
|
+
mask_value,
|
|
44
|
+
post_multipart,
|
|
45
|
+
preview_json,
|
|
46
|
+
sleep,
|
|
47
|
+
validate_sft_jsonl,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Constants for prompt learning event types
|
|
51
|
+
_PROMPT_LEARNING_EVENT_BEST_PROMPT = "prompt.learning.best.prompt"
|
|
52
|
+
_PROMPT_LEARNING_EVENT_FINAL_RESULTS = "prompt.learning.final.results"
|
|
53
|
+
_PROMPT_LEARNING_EVENT_VALIDATION_SCORED = "prompt.learning.validation.scored"
|
|
54
|
+
_PROMPT_LEARNING_EVENT_GEPA_COMPLETE = "prompt.learning.gepa.complete"
|
|
55
|
+
_PROMPT_LEARNING_EVENT_MIPRO_COMPLETE = "prompt.learning.mipro.complete"
|
|
56
|
+
|
|
57
|
+
# Constants for formatting
|
|
58
|
+
_MAX_TEXT_REPLACEMENTS_DISPLAY = 3 # Max number of text replacements to show in output
|
|
59
|
+
_RESULTS_FILE_MAX_EVENTS = 10000 # Max events to fetch for results file generation
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _format_text_replacements(obj: dict[str, Any] | None, max_display: int = _MAX_TEXT_REPLACEMENTS_DISPLAY) -> list[str]:
|
|
63
|
+
"""Extract and format text replacements from a candidate object.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
obj: Candidate object dictionary containing text_replacements
|
|
67
|
+
max_display: Maximum number of replacements to display
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of formatted lines showing role and replacement text
|
|
71
|
+
"""
|
|
72
|
+
lines = []
|
|
73
|
+
if not obj or not isinstance(obj, dict):
|
|
74
|
+
return lines
|
|
75
|
+
|
|
76
|
+
text_replacements = obj.get("text_replacements", [])
|
|
77
|
+
if not text_replacements or not isinstance(text_replacements, list):
|
|
78
|
+
return lines
|
|
79
|
+
|
|
80
|
+
for replacement in text_replacements[:max_display]:
|
|
81
|
+
if isinstance(replacement, dict):
|
|
82
|
+
new_text = replacement.get("new_text", "")
|
|
83
|
+
role = replacement.get("apply_to_role", "system")
|
|
84
|
+
if new_text:
|
|
85
|
+
lines.append(f" [{role.upper()}]: {new_text}")
|
|
86
|
+
lines.append("")
|
|
87
|
+
|
|
88
|
+
return lines
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _default_backend() -> str:
|
|
92
|
+
"""Resolve backend URL with proper production default."""
|
|
93
|
+
# Check explicit override first
|
|
94
|
+
explicit = os.getenv("BACKEND_BASE_URL", "").strip()
|
|
95
|
+
if explicit:
|
|
96
|
+
return explicit
|
|
97
|
+
# Use standard resolution logic
|
|
98
|
+
base, _ = get_backend_from_env()
|
|
99
|
+
return f"{base}/api" if not base.endswith("/api") else base
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_DEFAULT_SFT_HIDDEN_EVENTS = {
|
|
103
|
+
"sft.created",
|
|
104
|
+
"sft.pricing.check.requested",
|
|
105
|
+
"sft.pricing.check.allowed",
|
|
106
|
+
"sft.stage",
|
|
107
|
+
"snapshot.fetch",
|
|
108
|
+
"hatchet.preflight",
|
|
109
|
+
"hatchet.submission.attempt",
|
|
110
|
+
"hatchet.submission.result",
|
|
111
|
+
"sft.running",
|
|
112
|
+
"sft.status",
|
|
113
|
+
"sft.worker.alive",
|
|
114
|
+
"sft.dispatch.selected",
|
|
115
|
+
"sft.config.prepared",
|
|
116
|
+
"sft.strategy.selected",
|
|
117
|
+
"sft.training.args",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_DEFAULT_RL_HIDDEN_SUBSTRINGS = {"modal", "hatchet"}
|
|
121
|
+
|
|
122
|
+
_DEFAULT_PROMPT_LEARNING_HIDDEN_EVENTS = {
|
|
123
|
+
"prompt.learning.policy.tokens",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_stream_components(
|
|
128
|
+
stream_format: str,
|
|
129
|
+
*,
|
|
130
|
+
hidden_event_types: set[str] | None = None,
|
|
131
|
+
hidden_event_substrings: set[str] | None = None,
|
|
132
|
+
) -> tuple[StreamConfig, list]:
|
|
133
|
+
"""Return stream configuration and handlers for the requested format."""
|
|
134
|
+
if stream_format == "chart":
|
|
135
|
+
config = StreamConfig(
|
|
136
|
+
enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
|
|
137
|
+
event_types={
|
|
138
|
+
"sft.progress",
|
|
139
|
+
"sft.training.started",
|
|
140
|
+
"sft.training.finish",
|
|
141
|
+
"sft.validation.summary",
|
|
142
|
+
"rl.train.step",
|
|
143
|
+
"rl.train.started",
|
|
144
|
+
"rl.train.completed",
|
|
145
|
+
"workflow.completed",
|
|
146
|
+
"workflow.failed",
|
|
147
|
+
},
|
|
148
|
+
metric_names={"train.loss"},
|
|
149
|
+
)
|
|
150
|
+
handlers = [LossCurveHandler()]
|
|
151
|
+
else:
|
|
152
|
+
config = StreamConfig.default()
|
|
153
|
+
handlers = [
|
|
154
|
+
CLIHandler(
|
|
155
|
+
hidden_event_types=hidden_event_types or set(),
|
|
156
|
+
hidden_event_substrings=hidden_event_substrings or set(),
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
return config, handlers
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@click.command("train")
|
|
163
|
+
@click.option(
|
|
164
|
+
"--config",
|
|
165
|
+
"config_paths",
|
|
166
|
+
multiple=True,
|
|
167
|
+
type=click.Path(),
|
|
168
|
+
help="Path to training TOML (repeatable)",
|
|
169
|
+
)
|
|
170
|
+
@click.option("--type", "train_type", type=click.Choice(["auto", "rl", "sft", "prompt_learning"]), default="auto")
|
|
171
|
+
@click.option(
|
|
172
|
+
"--env-file",
|
|
173
|
+
"env_files",
|
|
174
|
+
multiple=True,
|
|
175
|
+
type=click.Path(),
|
|
176
|
+
help=".env file(s) to preload (skips selection prompt)",
|
|
177
|
+
)
|
|
178
|
+
@click.option("--task-url", default=None, help="Override task app base URL (RL only)")
|
|
179
|
+
@click.option(
|
|
180
|
+
"--dataset",
|
|
181
|
+
"dataset_path",
|
|
182
|
+
type=click.Path(),
|
|
183
|
+
default=None,
|
|
184
|
+
help="Override dataset JSONL path (SFT)",
|
|
185
|
+
)
|
|
186
|
+
@click.option("--backend", default=_default_backend, help="Backend base URL")
|
|
187
|
+
@click.option("--model", default=None, help="Override model identifier")
|
|
188
|
+
@click.option(
|
|
189
|
+
"--allow-experimental",
|
|
190
|
+
"allow_experimental",
|
|
191
|
+
is_flag=True,
|
|
192
|
+
flag_value=True,
|
|
193
|
+
default=None,
|
|
194
|
+
help="Allow experimental models (overrides SDK_EXPERIMENTAL env)",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"--no-allow-experimental",
|
|
198
|
+
"allow_experimental",
|
|
199
|
+
is_flag=True,
|
|
200
|
+
flag_value=False,
|
|
201
|
+
help="Disallow experimental models (overrides SDK_EXPERIMENTAL env)",
|
|
202
|
+
)
|
|
203
|
+
@click.option("--idempotency", default=None, help="Idempotency-Key header for job creation")
|
|
204
|
+
@click.option("--dry-run", is_flag=True, hidden=True, help="Deprecated: no-op")
|
|
205
|
+
@click.option("--poll/--no-poll", default=True, help="Poll job status until terminal state")
|
|
206
|
+
@click.option(
|
|
207
|
+
"--poll-timeout", default=3600.0, type=float, help="Maximum seconds to poll before timing out"
|
|
208
|
+
)
|
|
209
|
+
@click.option("--poll-interval", default=5.0, type=float, help="Seconds between poll attempts")
|
|
210
|
+
@click.option(
|
|
211
|
+
"--stream-format",
|
|
212
|
+
type=click.Choice(["cli", "chart"]),
|
|
213
|
+
default="cli",
|
|
214
|
+
show_default=True,
|
|
215
|
+
help="Streaming output style (cli = line updates, chart = live loss panel)",
|
|
216
|
+
)
|
|
217
|
+
@click.option(
|
|
218
|
+
"--examples",
|
|
219
|
+
"examples_limit",
|
|
220
|
+
type=int,
|
|
221
|
+
default=None,
|
|
222
|
+
help="Limit SFT training to the first N examples",
|
|
223
|
+
)
|
|
224
|
+
def train_command(
|
|
225
|
+
config_paths: tuple[str, ...],
|
|
226
|
+
train_type: str,
|
|
227
|
+
env_files: tuple[str, ...],
|
|
228
|
+
task_url: str | None,
|
|
229
|
+
dataset_path: str | None,
|
|
230
|
+
backend: str,
|
|
231
|
+
model: str | None,
|
|
232
|
+
allow_experimental: bool | None,
|
|
233
|
+
idempotency: str | None,
|
|
234
|
+
dry_run: bool,
|
|
235
|
+
poll: bool,
|
|
236
|
+
poll_timeout: float,
|
|
237
|
+
poll_interval: float,
|
|
238
|
+
stream_format: str,
|
|
239
|
+
examples_limit: int | None,
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Interactive launcher for RL / SFT / Prompt Learning jobs."""
|
|
242
|
+
load_env_file()
|
|
243
|
+
|
|
244
|
+
candidates = discover_configs(
|
|
245
|
+
list(config_paths), requested_type=train_type if train_type != "auto" else None
|
|
246
|
+
)
|
|
247
|
+
selection = prompt_for_config(
|
|
248
|
+
candidates,
|
|
249
|
+
requested_type=train_type if train_type != "auto" else None,
|
|
250
|
+
allow_autoselect=bool(config_paths),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
effective_type = train_type if train_type != "auto" else selection.train_type
|
|
254
|
+
if effective_type not in {"rl", "sft", "prompt_learning"}:
|
|
255
|
+
raise click.UsageError(
|
|
256
|
+
format_error_message(
|
|
257
|
+
summary="Training type required",
|
|
258
|
+
context="Determining which trainer to invoke",
|
|
259
|
+
problem="Config metadata did not specify rl / sft / prompt_learning and no --type flag was provided",
|
|
260
|
+
impact="CLI cannot select the correct builder without a type",
|
|
261
|
+
solutions=[
|
|
262
|
+
("Pass --type rl|sft|prompt_learning", "Explicitly tell the CLI which workflow to run"),
|
|
263
|
+
("Add algorithm.type metadata to the config", "Include algorithm.type or prompt_learning markers in the TOML"),
|
|
264
|
+
("Use separate config files per training mode", "Keeps intent unambiguous for automation"),
|
|
265
|
+
],
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
cfg_path = selection.path
|
|
270
|
+
click.echo(f"Using config: {cfg_path} ({effective_type})")
|
|
271
|
+
|
|
272
|
+
required_keys: list[KeySpec] = []
|
|
273
|
+
if effective_type == "rl" or effective_type == "prompt_learning":
|
|
274
|
+
required_keys.append(KeySpec("SYNTH_API_KEY", "Synth API key for backend"))
|
|
275
|
+
required_keys.append(
|
|
276
|
+
KeySpec(
|
|
277
|
+
"ENVIRONMENT_API_KEY",
|
|
278
|
+
"Environment API key for task app",
|
|
279
|
+
allow_modal_secret=True,
|
|
280
|
+
modal_secret_pattern="env",
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
required_keys.append(
|
|
284
|
+
KeySpec(
|
|
285
|
+
"TASK_APP_URL",
|
|
286
|
+
"Task app base URL",
|
|
287
|
+
secret=False,
|
|
288
|
+
allow_modal_app=True,
|
|
289
|
+
optional=bool(task_url),
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
else: # sft
|
|
293
|
+
required_keys.append(KeySpec("SYNTH_API_KEY", "Synth API key for backend"))
|
|
294
|
+
|
|
295
|
+
env_path, env_values = resolve_env(
|
|
296
|
+
config_path=cfg_path,
|
|
297
|
+
explicit_env_paths=env_files,
|
|
298
|
+
required_keys=required_keys,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
missing_keys = [
|
|
302
|
+
spec.name
|
|
303
|
+
for spec in required_keys
|
|
304
|
+
if not spec.optional and not (env_values.get(spec.name) or os.environ.get(spec.name))
|
|
305
|
+
]
|
|
306
|
+
if missing_keys:
|
|
307
|
+
try:
|
|
308
|
+
_task_apps_module = cast(
|
|
309
|
+
Any, importlib.import_module("synth_ai.cli.task_apps")
|
|
310
|
+
)
|
|
311
|
+
_interactive_fill_env = cast(
|
|
312
|
+
Callable[[Path], Path | None], _task_apps_module._interactive_fill_env
|
|
313
|
+
)
|
|
314
|
+
except Exception as exc: # pragma: no cover - protective fallback
|
|
315
|
+
raise click.ClickException(f"Unable to prompt for env values: {exc}") from exc
|
|
316
|
+
|
|
317
|
+
target_dir = cfg_path.parent
|
|
318
|
+
generated = _interactive_fill_env(target_dir / ".env")
|
|
319
|
+
if generated is None:
|
|
320
|
+
raise click.ClickException("Required environment values missing; aborting.")
|
|
321
|
+
env_path, env_values = resolve_env(
|
|
322
|
+
config_path=cfg_path,
|
|
323
|
+
explicit_env_paths=(str(generated),),
|
|
324
|
+
required_keys=required_keys,
|
|
325
|
+
)
|
|
326
|
+
click.echo(f"Using env file: {env_path}")
|
|
327
|
+
|
|
328
|
+
synth_key = get_required_value(
|
|
329
|
+
"synth_api_key",
|
|
330
|
+
env_value=env_values.get("SYNTH_API_KEY") or os.environ.get("SYNTH_API_KEY"),
|
|
331
|
+
)
|
|
332
|
+
os.environ["SYNTH_API_KEY"] = synth_key
|
|
333
|
+
|
|
334
|
+
backend_base = ensure_api_base(backend)
|
|
335
|
+
click.echo(f"Backend base: {backend_base} (key {mask_value(synth_key)})")
|
|
336
|
+
|
|
337
|
+
if effective_type == "rl":
|
|
338
|
+
handle_rl(
|
|
339
|
+
cfg_path=cfg_path,
|
|
340
|
+
backend_base=backend_base,
|
|
341
|
+
synth_key=synth_key,
|
|
342
|
+
task_url_override=task_url,
|
|
343
|
+
model_override=model,
|
|
344
|
+
idempotency=idempotency,
|
|
345
|
+
allow_experimental=allow_experimental,
|
|
346
|
+
dry_run=dry_run,
|
|
347
|
+
poll=poll,
|
|
348
|
+
poll_timeout=poll_timeout,
|
|
349
|
+
poll_interval=poll_interval,
|
|
350
|
+
stream_format=stream_format,
|
|
351
|
+
)
|
|
352
|
+
elif effective_type == "prompt_learning":
|
|
353
|
+
handle_prompt_learning(
|
|
354
|
+
cfg_path=cfg_path,
|
|
355
|
+
backend_base=backend_base,
|
|
356
|
+
synth_key=synth_key,
|
|
357
|
+
task_url_override=task_url,
|
|
358
|
+
allow_experimental=allow_experimental,
|
|
359
|
+
dry_run=dry_run,
|
|
360
|
+
poll=poll,
|
|
361
|
+
poll_timeout=poll_timeout,
|
|
362
|
+
poll_interval=poll_interval,
|
|
363
|
+
stream_format=stream_format,
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
dataset_override_path = Path(dataset_path).expanduser().resolve() if dataset_path else None
|
|
367
|
+
handle_sft(
|
|
368
|
+
cfg_path=cfg_path,
|
|
369
|
+
backend_base=backend_base,
|
|
370
|
+
synth_key=synth_key,
|
|
371
|
+
dataset_override=dataset_override_path,
|
|
372
|
+
allow_experimental=allow_experimental,
|
|
373
|
+
dry_run=dry_run,
|
|
374
|
+
poll=poll,
|
|
375
|
+
poll_timeout=poll_timeout,
|
|
376
|
+
poll_interval=poll_interval,
|
|
377
|
+
stream_format=stream_format,
|
|
378
|
+
examples_limit=examples_limit,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _wait_for_training_file(
|
|
383
|
+
backend_base: str, api_key: str, file_id: str, *, timeout: float = 10.0
|
|
384
|
+
) -> None:
|
|
385
|
+
"""Wait for training file to be visible after upload.
|
|
386
|
+
|
|
387
|
+
Reduced from 120s to 10s because:
|
|
388
|
+
- POST response already confirms file is uploaded
|
|
389
|
+
- Backend now forces read-your-writes consistency
|
|
390
|
+
- By job creation time, replica lag has resolved
|
|
391
|
+
- Quick sanity check only, not critical path
|
|
392
|
+
"""
|
|
393
|
+
url = f"{backend_base.rstrip('/')}/files/{file_id}"
|
|
394
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
395
|
+
elapsed = 0.0
|
|
396
|
+
interval = 2.0
|
|
397
|
+
first_check = True
|
|
398
|
+
while True:
|
|
399
|
+
resp = http_get(url, headers=headers, timeout=30.0)
|
|
400
|
+
if resp.status_code == 200:
|
|
401
|
+
try:
|
|
402
|
+
data = resp.json()
|
|
403
|
+
except json.JSONDecodeError:
|
|
404
|
+
data = {}
|
|
405
|
+
status = str(
|
|
406
|
+
data.get("status") or data.get("state") or data.get("storage_state") or "ready"
|
|
407
|
+
).lower()
|
|
408
|
+
if first_check:
|
|
409
|
+
click.echo(f"File uploaded successfully (id={file_id}, status={status})")
|
|
410
|
+
first_check = False
|
|
411
|
+
if status in {"ready", "uploaded", "stored", "complete"}:
|
|
412
|
+
click.echo(f"✓ Training file ready (status={status})")
|
|
413
|
+
return
|
|
414
|
+
# Show progress for processing states
|
|
415
|
+
if status in {"processing", "pending", "validating"}:
|
|
416
|
+
click.echo(
|
|
417
|
+
f" Waiting for file processing... (status={status}, {elapsed:.0f}s elapsed)"
|
|
418
|
+
)
|
|
419
|
+
elif resp.status_code == 404:
|
|
420
|
+
# Keep polling; object may not be visible yet
|
|
421
|
+
if first_check:
|
|
422
|
+
click.echo(f"Waiting for file {file_id} to become visible...")
|
|
423
|
+
first_check = False
|
|
424
|
+
elif resp.status_code in {401, 403}:
|
|
425
|
+
# Auth errors won't resolve by polling - fail immediately
|
|
426
|
+
try:
|
|
427
|
+
error_body = resp.json()
|
|
428
|
+
except json.JSONDecodeError:
|
|
429
|
+
error_body = resp.text[:400]
|
|
430
|
+
click.echo("\n[ERROR] Authentication failed when checking training file:")
|
|
431
|
+
click.echo(f" URL: {url}")
|
|
432
|
+
click.echo(f" Status: {resp.status_code}")
|
|
433
|
+
click.echo(f" Response: {error_body}")
|
|
434
|
+
click.echo(f" API key: {mask_value(api_key)}")
|
|
435
|
+
raise click.ClickException(
|
|
436
|
+
f"Authentication error ({resp.status_code}). "
|
|
437
|
+
"Check that your SYNTH_API_KEY is valid and has permission to access this organization's files."
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
# Other errors - show details but keep polling
|
|
441
|
+
try:
|
|
442
|
+
error_body = resp.json()
|
|
443
|
+
except json.JSONDecodeError:
|
|
444
|
+
error_body = resp.text[:400]
|
|
445
|
+
click.echo(f"[WARN] Unexpected response checking file {file_id}:")
|
|
446
|
+
click.echo(f" URL: {url}")
|
|
447
|
+
click.echo(f" Status: {resp.status_code}")
|
|
448
|
+
click.echo(f" Response: {error_body}")
|
|
449
|
+
|
|
450
|
+
if elapsed >= timeout:
|
|
451
|
+
raise click.ClickException(
|
|
452
|
+
f"Training file {file_id} not ready after {timeout:.0f}s (last status: {resp.status_code})"
|
|
453
|
+
)
|
|
454
|
+
sleep(interval)
|
|
455
|
+
elapsed += interval
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def handle_rl(
|
|
459
|
+
*,
|
|
460
|
+
cfg_path: Path,
|
|
461
|
+
backend_base: str,
|
|
462
|
+
synth_key: str,
|
|
463
|
+
task_url_override: str | None,
|
|
464
|
+
model_override: str | None,
|
|
465
|
+
idempotency: str | None,
|
|
466
|
+
allow_experimental: bool | None,
|
|
467
|
+
dry_run: bool,
|
|
468
|
+
poll: bool,
|
|
469
|
+
poll_timeout: float,
|
|
470
|
+
poll_interval: float,
|
|
471
|
+
stream_format: str,
|
|
472
|
+
) -> None:
|
|
473
|
+
overrides: dict[str, Any] = {
|
|
474
|
+
"backend": backend_base,
|
|
475
|
+
"task_url": task_url_override,
|
|
476
|
+
"model": model_override,
|
|
477
|
+
}
|
|
478
|
+
build = build_rl_payload(
|
|
479
|
+
config_path=cfg_path,
|
|
480
|
+
task_url=task_url_override or os.environ.get("TASK_APP_URL", ""),
|
|
481
|
+
overrides=overrides,
|
|
482
|
+
idempotency=idempotency,
|
|
483
|
+
allow_experimental=allow_experimental,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Backend-side verification: try ALL org environment keys against /health and /task_info
|
|
487
|
+
verify_url = f"{backend_base}/rl/verify_task_app"
|
|
488
|
+
verify_headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
|
|
489
|
+
try:
|
|
490
|
+
vresp = http_post(
|
|
491
|
+
verify_url, headers=verify_headers, json_body={"endpoint_base_url": build.task_url}
|
|
492
|
+
)
|
|
493
|
+
try:
|
|
494
|
+
parsed_json = vresp.json()
|
|
495
|
+
except json.JSONDecodeError:
|
|
496
|
+
parsed_json = None
|
|
497
|
+
|
|
498
|
+
if isinstance(parsed_json, Mapping):
|
|
499
|
+
vjs: dict[str, Any] = dict(parsed_json)
|
|
500
|
+
else:
|
|
501
|
+
vjs = {
|
|
502
|
+
"status": vresp.status_code,
|
|
503
|
+
"text": (vresp.text or "")[:400],
|
|
504
|
+
}
|
|
505
|
+
if parsed_json is not None:
|
|
506
|
+
vjs["body"] = parsed_json
|
|
507
|
+
except Exception as _ve:
|
|
508
|
+
raise click.ClickException(
|
|
509
|
+
f"Task app verification call failed: {type(_ve).__name__}: {_ve}"
|
|
510
|
+
) from _ve
|
|
511
|
+
if vresp.status_code is not None and vresp.status_code >= 400:
|
|
512
|
+
click.echo("Task app verification error:\n" + preview_json(vjs, limit=800))
|
|
513
|
+
raise click.ClickException(f"Verification failed with status {vresp.status_code}")
|
|
514
|
+
if not bool(vjs.get("any_ok")):
|
|
515
|
+
click.echo("Task app verification failed; no auth combination succeeded. Full report:")
|
|
516
|
+
click.echo(preview_json(vjs, limit=1200))
|
|
517
|
+
raise click.ClickException("Task app verification failed (auth)")
|
|
518
|
+
else:
|
|
519
|
+
# Print concise summary
|
|
520
|
+
try:
|
|
521
|
+
cands = vjs.get("candidates_first15") or []
|
|
522
|
+
attempts_raw = vjs.get("attempts")
|
|
523
|
+
attempts: list[Mapping[str, Any]] = (
|
|
524
|
+
[a for a in attempts_raw if isinstance(a, Mapping)]
|
|
525
|
+
if isinstance(attempts_raw, list)
|
|
526
|
+
else []
|
|
527
|
+
)
|
|
528
|
+
statuses = [attempt.get("status") for attempt in attempts]
|
|
529
|
+
click.echo(f"Verification OK (candidates={cands}, statuses={statuses})")
|
|
530
|
+
except (KeyError, ValueError, AttributeError):
|
|
531
|
+
# Parsing verification summary failed, but verification itself succeeded
|
|
532
|
+
click.echo("Verification OK")
|
|
533
|
+
|
|
534
|
+
env_key = get_required_value(
|
|
535
|
+
"environment_api_key",
|
|
536
|
+
env_value=os.environ.get("ENVIRONMENT_API_KEY"),
|
|
537
|
+
)
|
|
538
|
+
os.environ["ENVIRONMENT_API_KEY"] = env_key
|
|
539
|
+
|
|
540
|
+
click.echo("Performing task app health check…")
|
|
541
|
+
health = check_task_app_health(build.task_url, env_key)
|
|
542
|
+
if not health.ok:
|
|
543
|
+
click.echo(f"Task app health check failed: {health.detail}")
|
|
544
|
+
raise click.ClickException("Aborting due to failing health check")
|
|
545
|
+
else:
|
|
546
|
+
click.echo("Task app healthy")
|
|
547
|
+
|
|
548
|
+
create_url = f"{backend_base}/rl/jobs"
|
|
549
|
+
headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
|
|
550
|
+
if build.idempotency:
|
|
551
|
+
headers["Idempotency-Key"] = build.idempotency
|
|
552
|
+
|
|
553
|
+
click.echo(f"POST {create_url}")
|
|
554
|
+
click.echo("Payload preview:\n" + preview_json(build.payload, limit=800))
|
|
555
|
+
|
|
556
|
+
resp = http_post(create_url, headers=headers, json_body=build.payload)
|
|
557
|
+
try:
|
|
558
|
+
js = resp.json()
|
|
559
|
+
except json.JSONDecodeError as e:
|
|
560
|
+
click.echo(f"⚠️ Failed to parse JSON response: {e}")
|
|
561
|
+
js = {"status": resp.status_code, "text": resp.text[:400]}
|
|
562
|
+
click.echo(f"Response {resp.status_code}: {preview_json(js, limit=400)}")
|
|
563
|
+
if resp.status_code not in (200, 201):
|
|
564
|
+
raise click.ClickException("Job creation failed")
|
|
565
|
+
job_id = js.get("job_id") or js.get("id")
|
|
566
|
+
if not job_id:
|
|
567
|
+
raise click.ClickException("Response missing job id")
|
|
568
|
+
|
|
569
|
+
if not poll:
|
|
570
|
+
click.echo(f"Created job {job_id} (polling disabled)")
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
click.echo("\n=== Streaming Job Progress ===")
|
|
574
|
+
|
|
575
|
+
# Enable metrics for prompt learning
|
|
576
|
+
if stream_format == "chart":
|
|
577
|
+
config = StreamConfig(
|
|
578
|
+
enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
|
|
579
|
+
event_types={
|
|
580
|
+
"prompt.learning.progress",
|
|
581
|
+
"prompt.learning.gepa.start",
|
|
582
|
+
"prompt.learning.gepa.complete",
|
|
583
|
+
},
|
|
584
|
+
metric_names={"gepa.transformation.mean_score"},
|
|
585
|
+
)
|
|
586
|
+
handlers = [LossCurveHandler()]
|
|
587
|
+
click.echo("Using live chart (metric=gepa.transformation.mean_score)")
|
|
588
|
+
else:
|
|
589
|
+
config = StreamConfig(
|
|
590
|
+
enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
|
|
591
|
+
metric_names={"gepa.transformation.mean_score"},
|
|
592
|
+
)
|
|
593
|
+
handlers = [CLIHandler(hidden_event_substrings=_DEFAULT_RL_HIDDEN_SUBSTRINGS)]
|
|
594
|
+
|
|
595
|
+
streamer = JobStreamer(
|
|
596
|
+
base_url=backend_base,
|
|
597
|
+
api_key=synth_key,
|
|
598
|
+
job_id=job_id,
|
|
599
|
+
endpoints=StreamEndpoints.rl(job_id),
|
|
600
|
+
config=config,
|
|
601
|
+
handlers=handlers,
|
|
602
|
+
interval_seconds=poll_interval,
|
|
603
|
+
timeout_seconds=poll_timeout,
|
|
604
|
+
)
|
|
605
|
+
final_status = asyncio.run(streamer.stream_until_terminal())
|
|
606
|
+
click.echo(f"Final status: {final_status.get('status', 'unknown')}")
|
|
607
|
+
click.echo(preview_json(final_status, limit=600))
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def handle_sft(
|
|
611
|
+
*,
|
|
612
|
+
cfg_path: Path,
|
|
613
|
+
backend_base: str,
|
|
614
|
+
synth_key: str,
|
|
615
|
+
dataset_override: Path | None,
|
|
616
|
+
allow_experimental: bool | None,
|
|
617
|
+
dry_run: bool,
|
|
618
|
+
poll: bool,
|
|
619
|
+
poll_timeout: float,
|
|
620
|
+
poll_interval: float,
|
|
621
|
+
stream_format: str,
|
|
622
|
+
examples_limit: int | None,
|
|
623
|
+
) -> None:
|
|
624
|
+
try:
|
|
625
|
+
build = build_sft_payload(
|
|
626
|
+
config_path=cfg_path,
|
|
627
|
+
dataset_override=dataset_override,
|
|
628
|
+
allow_experimental=allow_experimental,
|
|
629
|
+
)
|
|
630
|
+
except TrainError as exc:
|
|
631
|
+
_raise_sft_usage_error(exc)
|
|
632
|
+
|
|
633
|
+
limited_path: Path | None = None
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
if examples_limit is not None:
|
|
637
|
+
limited_path = limit_jsonl_examples(build.train_file, examples_limit)
|
|
638
|
+
click.echo(
|
|
639
|
+
f"Using first {examples_limit} examples from {build.train_file} -> {limited_path}"
|
|
640
|
+
)
|
|
641
|
+
build.train_file = limited_path
|
|
642
|
+
|
|
643
|
+
click.echo("Validating training dataset…")
|
|
644
|
+
validate_sft_jsonl(build.train_file)
|
|
645
|
+
if build.validation_file and build.validation_file.suffix == ".jsonl":
|
|
646
|
+
click.echo("Validating validation dataset…")
|
|
647
|
+
validate_sft_jsonl(build.validation_file)
|
|
648
|
+
|
|
649
|
+
upload_url = f"{backend_base.rstrip('/')}/files"
|
|
650
|
+
click.echo("\n=== Uploading Training Data ===")
|
|
651
|
+
click.echo(f"Dataset: {build.train_file}")
|
|
652
|
+
click.echo(f"Destination: {upload_url}")
|
|
653
|
+
resp = post_multipart(
|
|
654
|
+
upload_url, api_key=synth_key, file_field="file", file_path=build.train_file
|
|
655
|
+
)
|
|
656
|
+
js = (
|
|
657
|
+
resp.json()
|
|
658
|
+
if resp.headers.get("content-type", "").startswith("application/json")
|
|
659
|
+
else {}
|
|
660
|
+
)
|
|
661
|
+
if resp.status_code is not None and resp.status_code >= 400 or "id" not in js:
|
|
662
|
+
click.echo("\n[ERROR] Training file upload failed:")
|
|
663
|
+
click.echo(f" URL: {upload_url}")
|
|
664
|
+
click.echo(f" Status: {resp.status_code}")
|
|
665
|
+
click.echo(f" Response: {js or resp.text[:400]}")
|
|
666
|
+
click.echo(f" File: {build.train_file}")
|
|
667
|
+
raise click.ClickException(
|
|
668
|
+
f"Training file upload failed with status {resp.status_code}"
|
|
669
|
+
)
|
|
670
|
+
train_file_id = js["id"]
|
|
671
|
+
click.echo(f"✓ Training file uploaded (id={train_file_id})")
|
|
672
|
+
val_file_id = None
|
|
673
|
+
if build.validation_file:
|
|
674
|
+
click.echo(f"Uploading validation dataset: {build.validation_file}")
|
|
675
|
+
vresp = post_multipart(
|
|
676
|
+
upload_url,
|
|
677
|
+
api_key=synth_key,
|
|
678
|
+
file_field="file",
|
|
679
|
+
file_path=build.validation_file,
|
|
680
|
+
)
|
|
681
|
+
vjs = (
|
|
682
|
+
vresp.json()
|
|
683
|
+
if vresp.headers.get("content-type", "").startswith("application/json")
|
|
684
|
+
else {}
|
|
685
|
+
)
|
|
686
|
+
if vresp.status_code is not None and vresp.status_code < 400 and "id" in vjs:
|
|
687
|
+
val_file_id = vjs["id"]
|
|
688
|
+
click.echo(f"✓ Validation file uploaded (id={val_file_id})")
|
|
689
|
+
else:
|
|
690
|
+
click.echo(
|
|
691
|
+
f"[WARN] Validation upload failed ({vresp.status_code}): {vjs or vresp.text[:200]}"
|
|
692
|
+
)
|
|
693
|
+
payload = dict(build.payload)
|
|
694
|
+
payload["training_file_id"] = train_file_id
|
|
695
|
+
if val_file_id:
|
|
696
|
+
payload.setdefault("metadata", {}).setdefault("effective_config", {}).setdefault(
|
|
697
|
+
"data", {}
|
|
698
|
+
)["validation_files"] = [val_file_id]
|
|
699
|
+
|
|
700
|
+
click.echo("\n=== Checking File Processing Status ===")
|
|
701
|
+
try:
|
|
702
|
+
_wait_for_training_file(backend_base, synth_key, train_file_id)
|
|
703
|
+
except click.ClickException as exc:
|
|
704
|
+
click.echo(f"[WARN] File readiness check failed: {exc}")
|
|
705
|
+
click.echo("Proceeding anyway - backend will validate file during job creation...")
|
|
706
|
+
|
|
707
|
+
click.echo("\n=== Creating Training Job ===")
|
|
708
|
+
click.echo("Job payload preview:")
|
|
709
|
+
click.echo(preview_json(payload, limit=800))
|
|
710
|
+
|
|
711
|
+
create_url = f"{backend_base}/learning/jobs"
|
|
712
|
+
headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
|
|
713
|
+
click.echo(f"\nPOST {create_url}")
|
|
714
|
+
resp = http_post(create_url, headers=headers, json_body=payload)
|
|
715
|
+
js = (
|
|
716
|
+
resp.json()
|
|
717
|
+
if resp.headers.get("content-type", "").startswith("application/json")
|
|
718
|
+
else {}
|
|
719
|
+
)
|
|
720
|
+
if resp.status_code not in (200, 201):
|
|
721
|
+
click.echo("\n[ERROR] Job creation failed:")
|
|
722
|
+
click.echo(f" URL: {create_url}")
|
|
723
|
+
click.echo(f" Status: {resp.status_code}")
|
|
724
|
+
click.echo(f" Response: {preview_json(js, limit=600)}")
|
|
725
|
+
raise click.ClickException(f"Job creation failed with status {resp.status_code}")
|
|
726
|
+
job_id = js.get("job_id") or js.get("id")
|
|
727
|
+
if not job_id:
|
|
728
|
+
raise click.ClickException("Response missing job id")
|
|
729
|
+
click.echo(f"✓ Job created (id={job_id})")
|
|
730
|
+
|
|
731
|
+
click.echo("\n=== Starting Training Job ===")
|
|
732
|
+
start_url = f"{backend_base}/learning/jobs/{job_id}/start"
|
|
733
|
+
click.echo(f"POST {start_url}")
|
|
734
|
+
start_resp = http_post(start_url, headers=headers, json_body={})
|
|
735
|
+
if start_resp.status_code not in (200, 201):
|
|
736
|
+
click.echo(f"[WARN] Job start returned status {start_resp.status_code}")
|
|
737
|
+
else:
|
|
738
|
+
click.echo("✓ Job started")
|
|
739
|
+
|
|
740
|
+
if not poll:
|
|
741
|
+
click.echo(f"Started job {job_id} (polling disabled)")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
click.echo("\n=== Streaming Job Progress ===")
|
|
745
|
+
config, handlers = _build_stream_components(
|
|
746
|
+
stream_format, hidden_event_types=_DEFAULT_SFT_HIDDEN_EVENTS
|
|
747
|
+
)
|
|
748
|
+
if stream_format == "chart":
|
|
749
|
+
click.echo("Using live loss chart (metric=train.loss)")
|
|
750
|
+
streamer = JobStreamer(
|
|
751
|
+
base_url=backend_base,
|
|
752
|
+
api_key=synth_key,
|
|
753
|
+
job_id=job_id,
|
|
754
|
+
endpoints=StreamEndpoints.learning(job_id),
|
|
755
|
+
config=config,
|
|
756
|
+
handlers=handlers,
|
|
757
|
+
interval_seconds=poll_interval,
|
|
758
|
+
timeout_seconds=poll_timeout,
|
|
759
|
+
)
|
|
760
|
+
final_status = asyncio.run(streamer.stream_until_terminal())
|
|
761
|
+
status = final_status.get('status') if isinstance(final_status, dict) else 'unknown'
|
|
762
|
+
click.echo(f"Final status: {status}")
|
|
763
|
+
click.echo(preview_json(final_status, limit=600))
|
|
764
|
+
finally:
|
|
765
|
+
if limited_path is not None:
|
|
766
|
+
with contextlib.suppress(OSError):
|
|
767
|
+
limited_path.unlink(missing_ok=True)
|
|
768
|
+
# Clean up empty parent directory if possible
|
|
769
|
+
with contextlib.suppress(OSError):
|
|
770
|
+
limited_path.parent.rmdir()
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _raise_sft_usage_error(exc: TrainError) -> NoReturn:
|
|
774
|
+
message = str(exc).strip()
|
|
775
|
+
lower_msg = message.lower()
|
|
776
|
+
context = "Preparing SFT training job payload"
|
|
777
|
+
impact = "Cannot submit training job without a valid dataset path"
|
|
778
|
+
|
|
779
|
+
if "dataset not specified" in lower_msg:
|
|
780
|
+
raise click.UsageError(
|
|
781
|
+
format_error_message(
|
|
782
|
+
summary="Dataset path required",
|
|
783
|
+
context=context,
|
|
784
|
+
problem="No dataset path was provided via config or CLI",
|
|
785
|
+
impact=impact,
|
|
786
|
+
solutions=[
|
|
787
|
+
("Add [job].data = \"/path/to/data.jsonl\" to the config", "Persist the dataset path in the TOML file"),
|
|
788
|
+
("Re-run with --dataset /path/to/data.jsonl", "Override the dataset path from the CLI"),
|
|
789
|
+
("Use an absolute path accessible from the current working directory", "Relative paths are resolved from the shell cwd"),
|
|
790
|
+
],
|
|
791
|
+
)
|
|
792
|
+
) from exc
|
|
793
|
+
|
|
794
|
+
if "dataset not found" in lower_msg:
|
|
795
|
+
raise click.UsageError(
|
|
796
|
+
format_error_message(
|
|
797
|
+
summary="Dataset path not found",
|
|
798
|
+
context=context,
|
|
799
|
+
problem=message,
|
|
800
|
+
impact=impact,
|
|
801
|
+
solutions=[
|
|
802
|
+
("Verify the dataset path exists on disk", "Double-check spelling and that the file hasn't moved"),
|
|
803
|
+
("Provide an absolute path to the dataset file", "Avoid relying on relative paths that resolve incorrectly"),
|
|
804
|
+
("Sync the dataset to this machine before running the CLI", "Remote paths must be accessible locally"),
|
|
805
|
+
],
|
|
806
|
+
)
|
|
807
|
+
) from exc
|
|
808
|
+
|
|
809
|
+
raise click.ClickException(message) from exc
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _save_prompt_learning_results_locally(
|
|
813
|
+
*,
|
|
814
|
+
backend_base: str,
|
|
815
|
+
api_key: str,
|
|
816
|
+
job_id: str,
|
|
817
|
+
config_path: Path,
|
|
818
|
+
) -> None:
|
|
819
|
+
"""Fetch events and generate results file locally after prompt learning completes."""
|
|
820
|
+
from datetime import datetime
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
# Fetch all events
|
|
824
|
+
url = f"{backend_base}/prompt-learning/online/jobs/{job_id}/events?limit={_RESULTS_FILE_MAX_EVENTS}"
|
|
825
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
826
|
+
resp = http_get(url, headers=headers, timeout=30.0)
|
|
827
|
+
|
|
828
|
+
if resp.status_code != 200:
|
|
829
|
+
click.echo(f"⚠️ Could not fetch events to generate results file (status={resp.status_code})")
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
data = resp.json()
|
|
833
|
+
# Handle both list response (backend) and dict response (legacy compatibility)
|
|
834
|
+
if isinstance(data, list):
|
|
835
|
+
events = data
|
|
836
|
+
elif isinstance(data, dict):
|
|
837
|
+
events = data.get("events", [])
|
|
838
|
+
if not isinstance(events, list):
|
|
839
|
+
click.echo(f"⚠️ Events field is not a list: {type(events).__name__}")
|
|
840
|
+
return
|
|
841
|
+
else:
|
|
842
|
+
click.echo(f"⚠️ Unexpected response type: {type(data).__name__}")
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
if not events:
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
# Extract key data from events
|
|
849
|
+
best_score = None
|
|
850
|
+
best_prompt = None
|
|
851
|
+
baseline_score = None
|
|
852
|
+
attempted_candidates = []
|
|
853
|
+
optimized_candidates = []
|
|
854
|
+
mipro_topk_candidates = [] # Collect MIPRO top-K candidates
|
|
855
|
+
|
|
856
|
+
for event in events:
|
|
857
|
+
if not isinstance(event, dict):
|
|
858
|
+
continue # Skip malformed events
|
|
859
|
+
|
|
860
|
+
event_type = event.get("type", "")
|
|
861
|
+
event_data = event.get("data", {})
|
|
862
|
+
if not isinstance(event_data, dict):
|
|
863
|
+
event_data = {} # Fallback to empty dict for safety
|
|
864
|
+
|
|
865
|
+
if event_type == _PROMPT_LEARNING_EVENT_BEST_PROMPT:
|
|
866
|
+
best_score = event_data.get("best_score")
|
|
867
|
+
best_prompt = event_data.get("best_prompt")
|
|
868
|
+
elif event_type == _PROMPT_LEARNING_EVENT_FINAL_RESULTS:
|
|
869
|
+
attempted_candidates = event_data.get("attempted_candidates", [])
|
|
870
|
+
optimized_candidates = event_data.get("optimized_candidates", [])
|
|
871
|
+
elif event_type == _PROMPT_LEARNING_EVENT_VALIDATION_SCORED:
|
|
872
|
+
# Check if this is the baseline by checking for is_baseline flag or baseline in message
|
|
873
|
+
is_baseline = event_data.get("is_baseline", False)
|
|
874
|
+
if not is_baseline:
|
|
875
|
+
msg = event.get("message", "")
|
|
876
|
+
is_baseline = "baseline" in msg.lower()
|
|
877
|
+
if is_baseline:
|
|
878
|
+
baseline_score = event_data.get("accuracy")
|
|
879
|
+
elif event_type == _PROMPT_LEARNING_EVENT_GEPA_COMPLETE and best_score is None:
|
|
880
|
+
best_score = event_data.get("best_score")
|
|
881
|
+
elif event_type == _PROMPT_LEARNING_EVENT_MIPRO_COMPLETE:
|
|
882
|
+
# MIPRO completion event includes best_prompt and best_score
|
|
883
|
+
if best_score is None:
|
|
884
|
+
best_score = event_data.get("best_score")
|
|
885
|
+
if best_prompt is None:
|
|
886
|
+
best_prompt = event_data.get("best_prompt")
|
|
887
|
+
elif event_type == "mipro.topk.evaluated":
|
|
888
|
+
# Extract MIPRO top-K candidate data
|
|
889
|
+
rank = event_data.get("rank")
|
|
890
|
+
train_score = event_data.get("train_score")
|
|
891
|
+
test_score = event_data.get("test_score")
|
|
892
|
+
if rank is not None and train_score is not None and test_score is not None:
|
|
893
|
+
mipro_topk_candidates.append({
|
|
894
|
+
"rank": rank,
|
|
895
|
+
"train_score": train_score,
|
|
896
|
+
"test_score": test_score,
|
|
897
|
+
"lift_absolute": event_data.get("lift_absolute"),
|
|
898
|
+
"lift_percent": event_data.get("lift_percent"),
|
|
899
|
+
"instruction_text": event_data.get("instruction_text", ""),
|
|
900
|
+
"demo_indices": event_data.get("demo_indices", []),
|
|
901
|
+
})
|
|
902
|
+
elif event_type == "mipro.baseline.test":
|
|
903
|
+
# Extract baseline test score
|
|
904
|
+
if baseline_score is None:
|
|
905
|
+
baseline_score = event_data.get("test_score")
|
|
906
|
+
|
|
907
|
+
# Check if we have any results to display (best_prompt, best_score, or candidates)
|
|
908
|
+
has_results = bool(attempted_candidates or optimized_candidates or best_prompt or best_score is not None)
|
|
909
|
+
if not has_results:
|
|
910
|
+
return
|
|
911
|
+
|
|
912
|
+
# Determine algorithm name from events
|
|
913
|
+
algorithm_name = "PROMPT LEARNING"
|
|
914
|
+
for event in events:
|
|
915
|
+
if isinstance(event, dict):
|
|
916
|
+
event_type = event.get("type", "")
|
|
917
|
+
if "gepa" in event_type.lower():
|
|
918
|
+
algorithm_name = "GEPA"
|
|
919
|
+
break
|
|
920
|
+
elif "mipro" in event_type.lower():
|
|
921
|
+
algorithm_name = "MIPRO"
|
|
922
|
+
break
|
|
923
|
+
|
|
924
|
+
# Generate formatted report
|
|
925
|
+
lines = []
|
|
926
|
+
lines.append("=" * 80)
|
|
927
|
+
lines.append(f"{algorithm_name} PROMPT LEARNING RESULTS")
|
|
928
|
+
lines.append("=" * 80)
|
|
929
|
+
lines.append(f"Job ID: {job_id}")
|
|
930
|
+
lines.append(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
931
|
+
lines.append("")
|
|
932
|
+
if baseline_score is not None:
|
|
933
|
+
lines.append(f"📊 Baseline Score: {baseline_score:.4f} ({baseline_score*100:.1f}%)")
|
|
934
|
+
if best_score is not None:
|
|
935
|
+
lines.append(f"🏆 Best Score: {best_score:.4f} ({best_score*100:.1f}%)")
|
|
936
|
+
if baseline_score is not None and best_score is not None:
|
|
937
|
+
improvement = ((best_score - baseline_score) / baseline_score) * 100 if baseline_score > 0 else 0
|
|
938
|
+
lines.append(f"📈 Improvement: {improvement:+.1f}% relative ({(best_score - baseline_score)*100:+.1f} pp absolute)")
|
|
939
|
+
lines.append("=" * 80)
|
|
940
|
+
lines.append("")
|
|
941
|
+
|
|
942
|
+
# Add best prompt if available
|
|
943
|
+
if best_prompt and isinstance(best_prompt, dict):
|
|
944
|
+
lines.append("🏆 BEST PROMPT")
|
|
945
|
+
lines.append("-" * 80)
|
|
946
|
+
sections = best_prompt.get("sections", [])
|
|
947
|
+
if not isinstance(sections, list):
|
|
948
|
+
sections = []
|
|
949
|
+
for sec in sections:
|
|
950
|
+
if not isinstance(sec, dict):
|
|
951
|
+
continue
|
|
952
|
+
role = sec.get("role", "unknown")
|
|
953
|
+
content = sec.get("content", "")
|
|
954
|
+
lines.append(f"\n[{role.upper()}]:")
|
|
955
|
+
lines.append(content)
|
|
956
|
+
lines.append("")
|
|
957
|
+
|
|
958
|
+
# Add optimized candidates
|
|
959
|
+
if optimized_candidates and isinstance(optimized_candidates, list):
|
|
960
|
+
lines.append("=" * 80)
|
|
961
|
+
lines.append(f"✨ TOP OPTIMIZED CANDIDATES ({len(optimized_candidates)})")
|
|
962
|
+
lines.append("=" * 80)
|
|
963
|
+
lines.append("")
|
|
964
|
+
|
|
965
|
+
for idx, cand in enumerate(optimized_candidates):
|
|
966
|
+
if not isinstance(cand, dict):
|
|
967
|
+
continue
|
|
968
|
+
candidate_score = cand.get("score") or {}
|
|
969
|
+
accuracy = candidate_score.get("accuracy", 0.0)
|
|
970
|
+
prompt_length = candidate_score.get("prompt_length", 0)
|
|
971
|
+
payload_kind = cand.get("payload_kind", "unknown")
|
|
972
|
+
|
|
973
|
+
# Try score.instance_scores first, then cand.instance_scores (explicit check)
|
|
974
|
+
instance_scores = (
|
|
975
|
+
candidate_score.get('instance_scores')
|
|
976
|
+
if 'instance_scores' in candidate_score
|
|
977
|
+
else cand.get('instance_scores')
|
|
978
|
+
)
|
|
979
|
+
n_eval = len(instance_scores) if instance_scores and isinstance(instance_scores, list) else 0
|
|
980
|
+
|
|
981
|
+
lines.append(f"[{idx+1}] Accuracy: {accuracy:.4f} | Length: {prompt_length} | Type: {payload_kind} | N: {n_eval}")
|
|
982
|
+
lines.append("-" * 80)
|
|
983
|
+
|
|
984
|
+
obj = cand.get("object")
|
|
985
|
+
if obj and isinstance(obj, dict) and payload_kind == "transformation":
|
|
986
|
+
# For transformations, text_replacements are nested in data
|
|
987
|
+
data_obj = obj.get("data", {})
|
|
988
|
+
replacement_lines = _format_text_replacements(data_obj)
|
|
989
|
+
lines.extend(replacement_lines)
|
|
990
|
+
lines.append("")
|
|
991
|
+
|
|
992
|
+
# Add MIPRO top-K candidates
|
|
993
|
+
if mipro_topk_candidates and isinstance(mipro_topk_candidates, list):
|
|
994
|
+
# Sort by rank
|
|
995
|
+
mipro_topk_candidates.sort(key=lambda x: x.get("rank", 999))
|
|
996
|
+
lines.append("=" * 80)
|
|
997
|
+
lines.append(f"🎯 TOP-K CANDIDATES ({len(mipro_topk_candidates)})")
|
|
998
|
+
lines.append("=" * 80)
|
|
999
|
+
lines.append("")
|
|
1000
|
+
|
|
1001
|
+
for cand in mipro_topk_candidates:
|
|
1002
|
+
rank = cand.get("rank", 0)
|
|
1003
|
+
train_score = cand.get("train_score", 0.0)
|
|
1004
|
+
test_score = cand.get("test_score", 0.0)
|
|
1005
|
+
lift_abs = cand.get("lift_absolute")
|
|
1006
|
+
lift_pct = cand.get("lift_percent")
|
|
1007
|
+
instruction_text = cand.get("instruction_text", "")
|
|
1008
|
+
demo_indices = cand.get("demo_indices", [])
|
|
1009
|
+
|
|
1010
|
+
lift_str = ""
|
|
1011
|
+
if lift_abs is not None and lift_pct is not None:
|
|
1012
|
+
lift_str = f" | Lift: {lift_abs:+.3f} ({lift_pct:+.1f}%)"
|
|
1013
|
+
|
|
1014
|
+
lines.append(f"[Rank {rank}] Train: {train_score:.4f} ({train_score*100:.1f}%) | Test: {test_score:.4f} ({test_score*100:.1f}%){lift_str}")
|
|
1015
|
+
lines.append("-" * 80)
|
|
1016
|
+
|
|
1017
|
+
if instruction_text:
|
|
1018
|
+
lines.append(f"Instruction: {instruction_text[:200]}{'...' if len(instruction_text) > 200 else ''}")
|
|
1019
|
+
if demo_indices:
|
|
1020
|
+
lines.append(f"Demo Indices: {demo_indices}")
|
|
1021
|
+
lines.append("")
|
|
1022
|
+
|
|
1023
|
+
# Add all proposal candidates
|
|
1024
|
+
if attempted_candidates and isinstance(attempted_candidates, list):
|
|
1025
|
+
lines.append("=" * 80)
|
|
1026
|
+
lines.append(f"💡 ALL PROPOSAL CANDIDATES ({len(attempted_candidates)})")
|
|
1027
|
+
lines.append("=" * 80)
|
|
1028
|
+
lines.append("")
|
|
1029
|
+
|
|
1030
|
+
for idx, cand in enumerate(attempted_candidates):
|
|
1031
|
+
if not isinstance(cand, dict):
|
|
1032
|
+
continue
|
|
1033
|
+
accuracy = cand.get('accuracy', 0.0)
|
|
1034
|
+
prompt_length = cand.get('prompt_length', 0)
|
|
1035
|
+
tool_rate = cand.get('tool_call_rate', 0.0)
|
|
1036
|
+
instance_scores = cand.get('instance_scores', [])
|
|
1037
|
+
n_eval = len(instance_scores) if instance_scores else 0
|
|
1038
|
+
|
|
1039
|
+
lines.append(f"[{idx+1}] Accuracy: {accuracy:.4f} | Length: {prompt_length} | Tool Rate: {tool_rate:.2f} | N: {n_eval}")
|
|
1040
|
+
lines.append("-" * 80)
|
|
1041
|
+
|
|
1042
|
+
obj = cand.get("object")
|
|
1043
|
+
if obj and isinstance(obj, dict):
|
|
1044
|
+
# For proposals, text_replacements are at top level of object
|
|
1045
|
+
replacement_lines = _format_text_replacements(obj)
|
|
1046
|
+
lines.extend(replacement_lines)
|
|
1047
|
+
lines.append("")
|
|
1048
|
+
|
|
1049
|
+
lines.append("=" * 80)
|
|
1050
|
+
lines.append("END OF REPORT")
|
|
1051
|
+
lines.append("=" * 80)
|
|
1052
|
+
|
|
1053
|
+
# Determine save location
|
|
1054
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1055
|
+
|
|
1056
|
+
# Try to save in config directory first
|
|
1057
|
+
output_dir = config_path.parent / "results"
|
|
1058
|
+
output_dir.mkdir(exist_ok=True)
|
|
1059
|
+
output_file = output_dir / f"gepa_results_{job_id}_{timestamp}.txt"
|
|
1060
|
+
|
|
1061
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
1062
|
+
f.write("\n".join(lines))
|
|
1063
|
+
|
|
1064
|
+
click.echo(f"\n📄 Results saved locally to: {output_file}")
|
|
1065
|
+
|
|
1066
|
+
except (PermissionError, OSError) as e:
|
|
1067
|
+
click.echo(f"⚠️ Could not save results file locally: {e}")
|
|
1068
|
+
except Exception as e:
|
|
1069
|
+
click.echo(f"⚠️ Unexpected error saving results file: {e}")
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def handle_prompt_learning(
|
|
1073
|
+
*,
|
|
1074
|
+
cfg_path: Path,
|
|
1075
|
+
backend_base: str,
|
|
1076
|
+
synth_key: str,
|
|
1077
|
+
task_url_override: str | None,
|
|
1078
|
+
allow_experimental: bool | None,
|
|
1079
|
+
dry_run: bool,
|
|
1080
|
+
poll: bool,
|
|
1081
|
+
poll_timeout: float,
|
|
1082
|
+
poll_interval: float,
|
|
1083
|
+
stream_format: str,
|
|
1084
|
+
) -> None:
|
|
1085
|
+
"""Handle prompt learning job creation (MIPRO or GEPA)."""
|
|
1086
|
+
env_key = get_required_value(
|
|
1087
|
+
"environment_api_key",
|
|
1088
|
+
env_value=os.environ.get("ENVIRONMENT_API_KEY"),
|
|
1089
|
+
)
|
|
1090
|
+
os.environ["ENVIRONMENT_API_KEY"] = env_key
|
|
1091
|
+
|
|
1092
|
+
overrides: dict[str, Any] = {
|
|
1093
|
+
"backend": backend_base,
|
|
1094
|
+
"task_url": task_url_override,
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
build = build_prompt_learning_payload(
|
|
1098
|
+
config_path=cfg_path,
|
|
1099
|
+
task_url=task_url_override,
|
|
1100
|
+
overrides=overrides,
|
|
1101
|
+
allow_experimental=allow_experimental,
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
click.echo("Performing task app health check…")
|
|
1105
|
+
health = check_task_app_health(build.task_url, env_key)
|
|
1106
|
+
if not health.ok:
|
|
1107
|
+
click.echo(f"Task app health check failed: {health.detail}")
|
|
1108
|
+
raise click.ClickException("Aborting due to failing health check")
|
|
1109
|
+
else:
|
|
1110
|
+
click.echo("Task app healthy")
|
|
1111
|
+
|
|
1112
|
+
create_url = f"{backend_base}/prompt-learning/online/jobs"
|
|
1113
|
+
headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
|
|
1114
|
+
|
|
1115
|
+
click.echo(f"POST {create_url}")
|
|
1116
|
+
click.echo("Payload preview:\n" + preview_json(build.payload, limit=800))
|
|
1117
|
+
|
|
1118
|
+
resp = http_post(create_url, headers=headers, json_body=build.payload)
|
|
1119
|
+
try:
|
|
1120
|
+
js = resp.json()
|
|
1121
|
+
except json.JSONDecodeError as e:
|
|
1122
|
+
click.echo(f"⚠️ Failed to parse JSON response: {e}")
|
|
1123
|
+
js = {"status": resp.status_code, "text": resp.text[:400]}
|
|
1124
|
+
click.echo(f"Response {resp.status_code}: {preview_json(js, limit=400)}")
|
|
1125
|
+
if resp.status_code not in (200, 201):
|
|
1126
|
+
raise click.ClickException("Job creation failed")
|
|
1127
|
+
job_id = js.get("job_id") or js.get("id")
|
|
1128
|
+
if not job_id:
|
|
1129
|
+
raise click.ClickException("Response missing job id")
|
|
1130
|
+
|
|
1131
|
+
if not poll:
|
|
1132
|
+
click.echo(f"Created job {job_id} (polling disabled)")
|
|
1133
|
+
return
|
|
1134
|
+
|
|
1135
|
+
click.echo("\n=== Streaming Job Progress ===")
|
|
1136
|
+
|
|
1137
|
+
# Custom config for prompt learning to enable metrics
|
|
1138
|
+
if stream_format == "chart":
|
|
1139
|
+
config = StreamConfig(
|
|
1140
|
+
enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
|
|
1141
|
+
event_types={
|
|
1142
|
+
"prompt.learning.progress",
|
|
1143
|
+
"prompt.learning.gepa.start",
|
|
1144
|
+
"prompt.learning.gepa.complete",
|
|
1145
|
+
},
|
|
1146
|
+
metric_names={"gepa.transformation.mean_score"},
|
|
1147
|
+
)
|
|
1148
|
+
handlers = [LossCurveHandler()]
|
|
1149
|
+
click.echo("Using live loss chart (metric=gepa.transformation.mean_score)")
|
|
1150
|
+
else:
|
|
1151
|
+
# Enable metrics for CLI mode too
|
|
1152
|
+
config = StreamConfig(
|
|
1153
|
+
enabled_streams={StreamType.STATUS, StreamType.EVENTS, StreamType.METRICS},
|
|
1154
|
+
metric_names={"gepa.transformation.mean_score"},
|
|
1155
|
+
)
|
|
1156
|
+
handlers = [CLIHandler(
|
|
1157
|
+
hidden_event_types=_DEFAULT_PROMPT_LEARNING_HIDDEN_EVENTS,
|
|
1158
|
+
hidden_event_substrings=_DEFAULT_RL_HIDDEN_SUBSTRINGS,
|
|
1159
|
+
)]
|
|
1160
|
+
|
|
1161
|
+
streamer = JobStreamer(
|
|
1162
|
+
base_url=backend_base,
|
|
1163
|
+
api_key=synth_key,
|
|
1164
|
+
job_id=job_id,
|
|
1165
|
+
endpoints=StreamEndpoints.prompt_learning(job_id),
|
|
1166
|
+
config=config,
|
|
1167
|
+
handlers=handlers,
|
|
1168
|
+
interval_seconds=poll_interval,
|
|
1169
|
+
timeout_seconds=poll_timeout,
|
|
1170
|
+
)
|
|
1171
|
+
final_status = asyncio.run(streamer.stream_until_terminal())
|
|
1172
|
+
click.echo(f"Final status: {final_status.get('status', 'unknown')}")
|
|
1173
|
+
click.echo(preview_json(final_status, limit=600))
|
|
1174
|
+
|
|
1175
|
+
# Save results file locally
|
|
1176
|
+
_save_prompt_learning_results_locally(
|
|
1177
|
+
backend_base=backend_base,
|
|
1178
|
+
api_key=synth_key,
|
|
1179
|
+
job_id=job_id,
|
|
1180
|
+
config_path=cfg_path,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def register(cli: click.Group) -> None:
|
|
1185
|
+
cli.add_command(train_command)
|