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