synth-ai 0.2.8.dev4__py3-none-any.whl → 0.2.23.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/README.md +1 -0
- examples/__init__.py +16 -0
- examples/analyze_semantic_words.sh +17 -0
- examples/baseline/banking77_baseline.py +243 -0
- examples/baseline/banking77_pipeline_baseline.py +294 -0
- examples/baseline/crafter_baseline.py +407 -0
- examples/baseline/pokemon_red_baseline.py +326 -0
- examples/baseline/simple_baseline.py +56 -0
- examples/baseline/warming_up_to_rl_baseline.py +239 -0
- examples/blog_posts/gepa/README.md +355 -0
- examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
- examples/blog_posts/gepa/configs/banking77_gepa_test.toml +80 -0
- examples/blog_posts/gepa/configs/banking77_mipro_local.toml +50 -0
- examples/blog_posts/gepa/configs/banking77_pipeline_gepa_local.toml +101 -0
- examples/blog_posts/gepa/configs/banking77_pipeline_gepa_test.toml +96 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +57 -0
- examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +35 -0
- examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +51 -0
- examples/blog_posts/gepa/configs/hover_gepa_local.toml +57 -0
- examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +35 -0
- examples/blog_posts/gepa/configs/hover_mipro_local.toml +51 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +57 -0
- examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +35 -0
- examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +51 -0
- examples/blog_posts/gepa/configs/pupa_gepa_local.toml +58 -0
- examples/blog_posts/gepa/configs/pupa_mipro_local.toml +52 -0
- examples/blog_posts/gepa/deploy_banking77_task_app.sh +54 -0
- examples/blog_posts/gepa/gepa_baseline.py +204 -0
- examples/blog_posts/gepa/query_prompts_example.py +97 -0
- examples/blog_posts/gepa/run_gepa_banking77.sh +112 -0
- examples/blog_posts/gepa/run_gepa_banking77_pipeline.sh +163 -0
- examples/blog_posts/gepa/task_apps.py +105 -0
- examples/blog_posts/gepa/test_gepa_local.sh +67 -0
- examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
- examples/blog_posts/mipro/README.md +415 -0
- examples/blog_posts/mipro/configs/banking77_mipro_local.toml +91 -0
- examples/blog_posts/mipro/configs/banking77_mipro_test.toml +87 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gemini_flash_lite_local.toml +98 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gpt41mini_local.toml +96 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_local.toml +94 -0
- examples/blog_posts/mipro/configs/banking77_pipeline_mipro_test.toml +170 -0
- examples/blog_posts/mipro/deploy_banking77_pipeline_task_app.sh +59 -0
- examples/blog_posts/mipro/deploy_banking77_task_app.sh +41 -0
- examples/blog_posts/mipro/multi_step.md +79 -0
- examples/blog_posts/mipro/run_mipro_banking77.sh +191 -0
- examples/blog_posts/mipro/run_mipro_banking77_pipeline.sh +171 -0
- examples/blog_posts/mipro/run_mipro_banking77_pipeline_gemini_flash_lite.sh +177 -0
- examples/blog_posts/mipro/run_mipro_banking77_pipeline_gpt41mini.sh +173 -0
- examples/blog_posts/mipro/verify_banking77_setup.sh +117 -0
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
- examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
- examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
- examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/pokemon_vl/extract_images.py +239 -0
- examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
- examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
- examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
- examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
- examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
- examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
- examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
- examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
- examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
- examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
- examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
- examples/crafter_debug_render.py +186 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +45 -0
- examples/gepa/banking77_pipeline_gepa.toml +96 -0
- examples/gepa/multi_stage_gepa_example.toml +84 -0
- examples/gepa/run_gepa_banking77_pipeline.sh +157 -0
- examples/multi_step/SFT_README.md +147 -0
- examples/multi_step/configs/README_verilog_rl.md +77 -0
- examples/multi_step/configs/VERILOG_REWARDS.md +103 -0
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +196 -0
- examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
- examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +75 -0
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +145 -0
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +84 -0
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +79 -0
- examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
- examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
- examples/multi_step/configs/crafter_synth_backend.md +40 -0
- examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
- examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
- examples/multi_step/configs/verilog_rl_lora.toml +147 -0
- examples/multi_step/convert_traces_to_sft.py +84 -0
- examples/multi_step/crafter_rl_lora.md +70 -0
- examples/multi_step/judges/crafter_backend_judge.py +220 -0
- examples/multi_step/judges/verilog_backend_judge.py +234 -0
- examples/multi_step/readme.md +48 -0
- examples/multi_step/run_sft_qwen30b.sh +45 -0
- examples/multi_step/sse_metrics_streaming_notes.md +357 -0
- examples/multi_step/task_app_config_notes.md +494 -0
- examples/multi_step/verilog_rl_lora.md +218 -0
- examples/qwen_coder/README.md +102 -0
- examples/qwen_coder/_shared.py +113 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +60 -0
- examples/qwen_coder/configs/coder_lora_4b.toml +61 -0
- examples/qwen_coder/configs/coder_lora_small.toml +57 -0
- examples/qwen_coder/generate_dataset.py +98 -0
- examples/qwen_coder/infer_ft_smoke.py +65 -0
- examples/qwen_coder/infer_prod_proxy.py +73 -0
- examples/qwen_coder/infer_via_synth.py +87 -0
- examples/qwen_coder/scripts/infer_coder.sh +19 -0
- examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
- examples/qwen_coder/sft_full_17b.py +103 -0
- examples/qwen_coder/sft_lora_30b.py +110 -0
- examples/qwen_coder/subset_jsonl.py +39 -0
- examples/qwen_coder/todos.md +38 -0
- examples/qwen_coder/validate_jsonl.py +60 -0
- examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
- examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
- examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
- examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
- examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
- examples/qwen_vl/QUICKSTART.md +327 -0
- examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
- examples/qwen_vl/README.md +152 -0
- examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
- examples/qwen_vl/RL_VISION_TESTING.md +333 -0
- examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
- examples/qwen_vl/SETUP_COMPLETE.md +274 -0
- examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
- examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
- examples/qwen_vl/__init__.py +2 -0
- examples/qwen_vl/collect_data_via_cli.md +415 -0
- examples/qwen_vl/collect_vision_traces.py +368 -0
- examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
- examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
- examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
- examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
- examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
- examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
- examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
- examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
- examples/qwen_vl/configs/filter_vision_test.toml +8 -0
- examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
- examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
- examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
- examples/qwen_vl/run_vision_comparison.sh +61 -0
- examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
- examples/qwen_vl/test_image_validation.py +201 -0
- examples/qwen_vl/test_sft_vision_data.py +110 -0
- examples/rl/README.md +169 -0
- examples/rl/configs/eval_base_qwen.toml +17 -0
- examples/rl/configs/eval_rl_qwen.toml +13 -0
- examples/rl/configs/rl_from_base_qwen.toml +62 -0
- examples/rl/configs/rl_from_base_qwen17.toml +80 -0
- examples/rl/configs/rl_from_ft_qwen.toml +37 -0
- examples/rl/download_dataset.py +80 -0
- examples/rl/run_eval.py +436 -0
- examples/rl/run_rl_and_save.py +111 -0
- examples/rl/task_app/README.md +21 -0
- examples/rl/task_app/math_single_step.py +990 -0
- examples/rl/task_app/math_task_app.py +111 -0
- examples/run_crafter_demo.sh +10 -0
- examples/sdk_prompt_learning_example.py +55 -0
- examples/sft/README.md +139 -0
- examples/sft/configs/crafter_fft_qwen0p6b.toml +49 -0
- examples/sft/configs/crafter_lora_qwen0p6b.toml +49 -0
- examples/sft/evaluate.py +117 -0
- examples/sft/export_dataset.py +120 -0
- examples/sft/generate_traces.py +164 -0
- examples/swe/__init__.py +12 -0
- examples/swe/task_app/README.md +135 -0
- examples/swe/task_app/__init__.py +2 -0
- examples/swe/task_app/grpo_swe_mini.py +604 -0
- examples/swe/task_app/grpo_swe_mini_task_app.py +124 -0
- examples/swe/task_app/hosted/README.md +173 -0
- examples/swe/task_app/hosted/__init__.py +5 -0
- examples/swe/task_app/hosted/branching.py +143 -0
- examples/swe/task_app/hosted/environment_routes.py +1289 -0
- examples/swe/task_app/hosted/envs/__init__.py +1 -0
- examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
- examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
- examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
- examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
- examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
- examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
- examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +1191 -0
- examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
- examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
- examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
- examples/swe/task_app/hosted/hosted_app.py +204 -0
- examples/swe/task_app/hosted/inference/__init__.py +5 -0
- examples/swe/task_app/hosted/inference/openai_client.py +584 -0
- examples/swe/task_app/hosted/main.py +100 -0
- examples/swe/task_app/hosted/policy_routes.py +1094 -0
- examples/swe/task_app/hosted/registry.py +195 -0
- examples/swe/task_app/hosted/rollout.py +1905 -0
- examples/swe/task_app/hosted/storage/__init__.py +5 -0
- examples/swe/task_app/hosted/storage/volume.py +211 -0
- examples/swe/task_app/hosted/test_agents.py +161 -0
- examples/swe/task_app/hosted/test_service.py +136 -0
- examples/swe/task_app/hosted/utils.py +62 -0
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
- examples/task_apps/TESTING.md +275 -0
- examples/task_apps/banking77/__init__.py +6 -0
- examples/task_apps/banking77/banking77_task_app.py +912 -0
- examples/task_apps/banking77/deploy_wrapper.py +46 -0
- examples/task_apps/banking77_pipeline/__init__.py +6 -0
- examples/task_apps/banking77_pipeline/banking77_pipeline_task_app.py +489 -0
- examples/task_apps/banking77_pipeline/deploy_wrapper.py +50 -0
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +286 -0
- examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +187 -0
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +281 -0
- examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
- examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
- examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
- examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
- examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
- examples/task_apps/crafter/task_app/README.md +42 -0
- examples/task_apps/crafter/task_app/__init__.py +5 -0
- examples/task_apps/crafter/task_app/grpo_crafter.py +1055 -0
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +146 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +173 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +5 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +143 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +1226 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +1 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +532 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +583 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +122 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +253 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +5 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +999 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +100 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +1252 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +195 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +2233 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +5 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +211 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +161 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +136 -0
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +411 -0
- examples/task_apps/dev/pokemon_emerald/__init__.py +2 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +811 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +120 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +160 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +155 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +69 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +96 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +1502 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +4 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +68 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +216 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +35 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +631 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +1544 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +1428 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +4848 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +41 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +298 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +95 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +204 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +2152 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +429 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +155 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +78 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +122 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +76 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +413 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +204 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +133 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +229 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +300 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +205 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +200 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +284 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +468 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +575 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +311 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +259 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +372 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +296 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +275 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +22 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +44 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +514 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +415 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +1763 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +33 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +106 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +334 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +1020 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +188 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +1481 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +862 -0
- examples/task_apps/dev/pokemon_emerald/modal_app.py +114 -0
- examples/task_apps/dev/pokemon_emerald/task_app/README.md +81 -0
- examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +6 -0
- examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +685 -0
- examples/task_apps/enron/__init__.py +2 -0
- examples/task_apps/enron/eval_groq_qwen32.toml +16 -0
- examples/task_apps/enron/filter_sft.toml +5 -0
- examples/task_apps/enron/task_app/README.md +14 -0
- examples/task_apps/enron/task_app/__init__.py +1 -0
- examples/task_apps/enron/task_app/grpo_enron.py +906 -0
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +146 -0
- examples/task_apps/enron/tests/__init__.py +4 -0
- examples/task_apps/enron/tests/conftest.py +115 -0
- examples/task_apps/enron/tests/integration/__init__.py +4 -0
- examples/task_apps/enron/tests/integration/test_enron_eval.py +179 -0
- examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
- examples/task_apps/enron/tests/unit/__init__.py +4 -0
- examples/task_apps/enron/tests/unit/test_enron_environment.py +126 -0
- examples/task_apps/gepa_benchmarks/__init__.py +7 -0
- examples/task_apps/gepa_benchmarks/common.py +260 -0
- examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
- examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
- examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
- examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
- examples/task_apps/math/README.md +21 -0
- examples/task_apps/math/math_single_step.py +1000 -0
- examples/task_apps/math/math_task_app.py +115 -0
- examples/task_apps/pokemon_battle/__init__.py +2 -0
- examples/task_apps/pokemon_battle/modal_app.py +104 -0
- examples/task_apps/pokemon_battle/task_app/README.md +68 -0
- examples/task_apps/pokemon_battle/task_app/__init__.py +6 -0
- examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +932 -0
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
- examples/task_apps/pokemon_red/README.md +356 -0
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +428 -0
- examples/task_apps/pokemon_red/__init__.py +3 -0
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +30 -0
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +224 -0
- examples/task_apps/pokemon_red/pallet_town_rl_config.toml +75 -0
- examples/task_apps/pokemon_red/task_app.py +1048 -0
- examples/task_apps/pokemon_red/test_pallet_town_rewards.py +193 -0
- examples/task_apps/sokoban/README.md +306 -0
- examples/task_apps/sokoban/__init__.py +3 -0
- examples/task_apps/sokoban/eval_groq_qwen32.toml +16 -0
- examples/task_apps/sokoban/eval_openai_gpt5.toml +16 -0
- examples/task_apps/sokoban/filter_sft.toml +5 -0
- examples/task_apps/sokoban/task_app.py +1058 -0
- examples/task_apps/sokoban/tests/__init__.py +4 -0
- examples/task_apps/sokoban/tests/conftest.py +113 -0
- examples/task_apps/sokoban/tests/integration/__init__.py +4 -0
- examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +57 -0
- examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +198 -0
- examples/task_apps/sokoban/tests/unit/__init__.py +4 -0
- examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +114 -0
- examples/task_apps/verilog/__init__.py +1 -0
- examples/task_apps/verilog/eval_groq_qwen32b.toml +22 -0
- examples/task_apps/verilog/filter_sft.toml +5 -0
- examples/task_apps/verilog/task_app/README.md +12 -0
- examples/task_apps/verilog/task_app/__init__.py +1 -0
- examples/task_apps/verilog/task_app/grpo_verilog.py +1166 -0
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
- examples/task_apps/verilog/tests/__init__.py +4 -0
- examples/task_apps/verilog/tests/conftest.py +115 -0
- examples/task_apps/verilog/tests/integration/__init__.py +4 -0
- examples/task_apps/verilog/tests/integration/test_verilog_eval.py +181 -0
- examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
- examples/task_apps/verilog/tests/unit/__init__.py +4 -0
- examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +118 -0
- examples/tunnel_gepa_banking77/README.md +106 -0
- examples/tunnel_gepa_banking77/banking77_gepa_tunnel.toml +95 -0
- examples/tunnel_gepa_banking77/keep_tunnel_running.py +60 -0
- examples/tunnel_gepa_banking77/run_gepa_with_tunnel.sh +226 -0
- examples/vlm/PROPOSAL.md +53 -0
- examples/vlm/README.md +68 -0
- examples/vlm/configs/crafter_vlm_gpt4o.toml +49 -0
- examples/vlm/crafter_image_only_agent.py +207 -0
- examples/vlm/crafter_openai_vlm_agent.py +275 -0
- examples/vlm/filter_image_rows.py +63 -0
- examples/vlm/run_crafter_vlm_benchmark.py +316 -0
- examples/warming_up_to_rl/_utils.py +92 -0
- examples/warming_up_to_rl/analyze_trace_db.py +422 -0
- examples/warming_up_to_rl/configs/crafter_fft.toml +53 -0
- examples/warming_up_to_rl/configs/crafter_fft_4b.toml +54 -0
- examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +22 -0
- examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +15 -0
- examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +24 -0
- examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +35 -0
- examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +26 -0
- examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +36 -0
- examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +32 -0
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +85 -0
- examples/warming_up_to_rl/configs/rl_from_ft.toml +58 -0
- examples/warming_up_to_rl/export_trace_sft.py +837 -0
- examples/warming_up_to_rl/groq_test.py +97 -0
- examples/warming_up_to_rl/manage_secrets.py +131 -0
- examples/warming_up_to_rl/old/event_rewards.md +234 -0
- examples/warming_up_to_rl/old/notes.md +73 -0
- examples/warming_up_to_rl/readme.md +110 -0
- examples/warming_up_to_rl/run_eval.py +736 -0
- examples/warming_up_to_rl/run_fft_and_save.py +380 -0
- examples/warming_up_to_rl/run_local_rollout.py +239 -0
- examples/warming_up_to_rl/run_local_rollout_modal.py +248 -0
- examples/warming_up_to_rl/run_local_rollout_parallel.py +405 -0
- examples/warming_up_to_rl/run_local_rollout_traced.py +477 -0
- examples/warming_up_to_rl/run_rl_and_save.py +124 -0
- examples/warming_up_to_rl/run_rollout_remote.py +156 -0
- examples/warming_up_to_rl/task_app/README.md +42 -0
- examples/warming_up_to_rl/task_app/grpo_crafter.py +876 -0
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +253 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +729 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1114 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1891 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +129 -0
- examples/workflows/math_rl/configs/eval_base_qwen.toml +15 -0
- examples/workflows/math_rl/configs/eval_rl_qwen.toml +11 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +62 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +80 -0
- examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +35 -0
- examples/workflows/math_rl/download_dataset.py +80 -0
- examples/workflows/math_rl/run_eval.py +436 -0
- examples/workflows/math_rl/run_rl_and_save.py +111 -0
- synth_ai/__init__.py +47 -23
- synth_ai/_utils/__init__.py +47 -0
- synth_ai/_utils/base_url.py +10 -0
- synth_ai/_utils/http.py +10 -0
- synth_ai/_utils/prompts.py +10 -0
- synth_ai/_utils/task_app_state.py +12 -0
- synth_ai/_utils/user_config.py +10 -0
- synth_ai/api/models/supported.py +514 -0
- synth_ai/api/train/__init__.py +63 -0
- synth_ai/api/train/builders.py +473 -0
- synth_ai/api/train/cli.py +1185 -0
- synth_ai/api/train/config_finder.py +246 -0
- synth_ai/api/train/configs/__init__.py +65 -0
- synth_ai/api/train/configs/prompt_learning.py +496 -0
- synth_ai/api/train/configs/rl.py +188 -0
- synth_ai/api/train/configs/sft.py +99 -0
- synth_ai/api/train/configs/shared.py +81 -0
- synth_ai/api/train/env_resolver.py +352 -0
- synth_ai/api/train/pollers.py +91 -0
- synth_ai/api/train/prompt_learning.py +425 -0
- synth_ai/api/train/sft.py +390 -0
- synth_ai/api/train/supported_algos.py +147 -0
- synth_ai/api/train/task_app.py +195 -0
- synth_ai/api/train/utils.py +244 -0
- synth_ai/api/train/validators.py +1117 -0
- synth_ai/api/tunnel.py +49 -0
- synth_ai/auth/credentials.py +94 -0
- synth_ai/baseline/__init__.py +25 -0
- synth_ai/baseline/config.py +209 -0
- synth_ai/baseline/discovery.py +214 -0
- synth_ai/baseline/execution.py +146 -0
- synth_ai/cfgs.py +227 -0
- synth_ai/cli/__init__.py +90 -45
- synth_ai/cli/_modal_wrapper.py +31 -0
- synth_ai/cli/_storage.py +20 -0
- synth_ai/cli/_typer_patch.py +47 -0
- synth_ai/cli/_validate_task_app.py +29 -0
- synth_ai/cli/balance.py +16 -4
- synth_ai/cli/calc.py +36 -21
- synth_ai/cli/claude.py +70 -0
- synth_ai/cli/codex.py +267 -0
- synth_ai/cli/commands/__init__.py +18 -0
- synth_ai/cli/commands/baseline/__init__.py +12 -0
- synth_ai/cli/commands/baseline/core.py +637 -0
- synth_ai/cli/commands/baseline/list.py +93 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1112 -0
- synth_ai/cli/commands/eval/errors.py +81 -0
- synth_ai/cli/commands/eval/validation.py +133 -0
- synth_ai/cli/commands/filter/__init__.py +12 -0
- synth_ai/cli/commands/filter/core.py +424 -0
- synth_ai/cli/commands/filter/errors.py +55 -0
- synth_ai/cli/commands/filter/validation.py +77 -0
- synth_ai/cli/commands/help/__init__.py +185 -0
- synth_ai/cli/commands/help/core.py +72 -0
- synth_ai/cli/commands/smoke/__init__.py +7 -0
- synth_ai/cli/commands/smoke/core.py +1437 -0
- synth_ai/cli/commands/status/__init__.py +66 -0
- synth_ai/cli/commands/status/client.py +192 -0
- synth_ai/cli/commands/status/config.py +92 -0
- synth_ai/cli/commands/status/errors.py +20 -0
- synth_ai/cli/commands/status/formatters.py +164 -0
- synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
- synth_ai/cli/commands/status/subcommands/files.py +79 -0
- synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
- synth_ai/cli/commands/status/subcommands/models.py +79 -0
- synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
- synth_ai/cli/commands/status/subcommands/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/session.py +183 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -0
- synth_ai/cli/commands/status/subcommands/usage.py +203 -0
- synth_ai/cli/commands/status/utils.py +114 -0
- synth_ai/cli/commands/train/__init__.py +53 -0
- synth_ai/cli/commands/train/core.py +21 -0
- synth_ai/cli/commands/train/errors.py +117 -0
- synth_ai/cli/commands/train/judge_schemas.py +200 -0
- synth_ai/cli/commands/train/judge_validation.py +305 -0
- synth_ai/cli/commands/train/validation.py +386 -0
- synth_ai/cli/demo.py +32 -140
- synth_ai/cli/deploy.py +233 -0
- synth_ai/cli/eval/__init__.py +36 -0
- synth_ai/cli/eval/core.py +5 -0
- synth_ai/cli/eval/errors.py +31 -0
- synth_ai/cli/eval/validation.py +5 -0
- synth_ai/cli/filter/__init__.py +28 -0
- synth_ai/cli/filter/core.py +5 -0
- synth_ai/cli/filter/errors.py +23 -0
- synth_ai/cli/filter/validation.py +5 -0
- synth_ai/cli/legacy_root_backup.py +28 -22
- synth_ai/cli/lib/__init__.py +10 -0
- synth_ai/cli/lib/task_app_discovery.py +7 -0
- synth_ai/cli/lib/task_app_env.py +518 -0
- synth_ai/cli/mcp.py +34 -0
- synth_ai/cli/modal_serve/__init__.py +12 -0
- synth_ai/cli/modal_serve/core.py +14 -0
- synth_ai/cli/modal_serve/errors.py +8 -0
- synth_ai/cli/modal_serve/validation.py +11 -0
- synth_ai/cli/opencode.py +256 -0
- synth_ai/cli/recent.py +13 -7
- synth_ai/cli/rl_demo.py +166 -114
- synth_ai/cli/root.py +143 -112
- synth_ai/cli/serve/__init__.py +12 -0
- synth_ai/cli/serve/core.py +14 -0
- synth_ai/cli/serve/errors.py +8 -0
- synth_ai/cli/serve/validation.py +11 -0
- synth_ai/cli/setup.py +49 -0
- synth_ai/cli/status.py +7 -125
- synth_ai/cli/task_app_deploy.py +7 -0
- synth_ai/cli/task_app_list.py +25 -0
- synth_ai/cli/task_app_modal_serve.py +11 -0
- synth_ai/cli/task_app_serve.py +11 -0
- synth_ai/cli/task_apps.py +3134 -0
- synth_ai/cli/traces.py +9 -5
- synth_ai/cli/train/__init__.py +12 -0
- synth_ai/cli/train/core.py +21 -0
- synth_ai/cli/train/errors.py +8 -0
- synth_ai/cli/train/validation.py +24 -0
- synth_ai/cli/train.py +5 -0
- synth_ai/cli/turso.py +73 -0
- synth_ai/cli/watch.py +13 -18
- synth_ai/demos/__init__.py +10 -0
- synth_ai/demos/core/__init__.py +28 -1
- synth_ai/demos/core/cli.py +745 -416
- synth_ai/demos/crafter/__init__.py +1 -0
- synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
- synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
- synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
- synth_ai/demos/demo_registry.py +176 -0
- synth_ai/demos/demo_task_apps/__init__.py +7 -1
- synth_ai/demos/demo_task_apps/core.py +75 -37
- synth_ai/demos/demo_task_apps/crafter/__init__.py +1 -0
- synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +53 -0
- synth_ai/demos/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +73 -0
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +184 -0
- synth_ai/demos/demo_task_apps/math/_common.py +1 -2
- synth_ai/demos/demo_task_apps/math/app.py +2 -1
- synth_ai/demos/demo_task_apps/math/config.toml +55 -110
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +491 -166
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +37 -0
- synth_ai/demos/math/__init__.py +1 -0
- synth_ai/demos/math/_common.py +16 -0
- synth_ai/demos/math/app.py +38 -0
- synth_ai/demos/math/config.toml +76 -0
- synth_ai/demos/math/deploy_modal.py +54 -0
- synth_ai/demos/math/modal_task_app.py +703 -0
- synth_ai/demos/math/task_app_entry.py +51 -0
- synth_ai/environments/environment/core.py +7 -1
- synth_ai/environments/examples/bandit/engine.py +12 -5
- synth_ai/environments/examples/bandit/environment.py +0 -1
- synth_ai/environments/examples/bandit/taskset.py +4 -4
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
- synth_ai/environments/examples/crafter_classic/environment.py +93 -2
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
- synth_ai/environments/examples/enron/engine.py +7 -2
- synth_ai/environments/examples/enron/environment.py +68 -0
- synth_ai/environments/examples/red/engine.py +60 -12
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
- synth_ai/environments/examples/red/environment.py +86 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/environments/examples/sokoban/taskset.py +116 -0
- synth_ai/environments/examples/verilog/engine.py +104 -12
- synth_ai/environments/examples/wordle/environment.py +0 -1
- synth_ai/environments/reproducibility/tree.py +5 -6
- synth_ai/environments/service/app.py +11 -12
- synth_ai/environments/service/core_routes.py +10 -9
- synth_ai/environments/stateful/engine.py +1 -1
- synth_ai/environments/tasks/core.py +1 -0
- synth_ai/environments/tasks/filters.py +5 -6
- synth_ai/environments/tasks/utils.py +4 -5
- synth_ai/evals/__init__.py +15 -0
- synth_ai/evals/base.py +14 -5
- synth_ai/evals/client.py +82 -0
- synth_ai/evals/types.py +42 -0
- synth_ai/http.py +8 -22
- synth_ai/http_client.py +45 -12
- synth_ai/inference/__init__.py +0 -2
- synth_ai/inference/client.py +21 -7
- synth_ai/jobs/client.py +129 -80
- synth_ai/judge_schemas.py +127 -0
- synth_ai/learning/__init__.py +51 -6
- synth_ai/learning/algorithms.py +14 -0
- synth_ai/learning/client.py +122 -30
- synth_ai/learning/config.py +2 -40
- synth_ai/learning/constants.py +0 -2
- synth_ai/learning/ft_client.py +4 -56
- synth_ai/learning/health.py +14 -8
- synth_ai/learning/jobs.py +43 -47
- synth_ai/learning/prompt_learning_client.py +276 -0
- synth_ai/learning/prompt_learning_types.py +185 -0
- synth_ai/{rl → learning/rl}/__init__.py +14 -5
- synth_ai/learning/rl/client.py +269 -0
- synth_ai/learning/rl/config.py +31 -0
- synth_ai/{rl → learning/rl}/contracts.py +5 -10
- synth_ai/{rl → learning/rl}/env_keys.py +45 -16
- synth_ai/learning/rl/secrets.py +13 -0
- synth_ai/learning/rl_client.py +2 -253
- synth_ai/learning/sft/__init__.py +29 -0
- synth_ai/learning/sft/client.py +68 -0
- synth_ai/learning/sft/config.py +270 -0
- synth_ai/learning/sft/data.py +698 -0
- synth_ai/learning/sse.py +25 -26
- synth_ai/learning/validators.py +29 -25
- synth_ai/mcp/__init__.py +5 -0
- synth_ai/mcp/__main__.py +8 -0
- synth_ai/mcp/main.py +254 -0
- synth_ai/mcp/setup.py +100 -0
- synth_ai/modal.py +257 -0
- synth_ai/pricing/__init__.py +3 -0
- synth_ai/pricing/model_pricing.py +64 -0
- synth_ai/session/__init__.py +75 -0
- synth_ai/session/client.py +383 -0
- synth_ai/session/constants.py +63 -0
- synth_ai/session/exceptions.py +105 -0
- synth_ai/session/manager.py +139 -0
- synth_ai/session/models.py +89 -0
- synth_ai/session/query.py +110 -0
- synth_ai/spec/__init__.py +46 -0
- synth_ai/spec/dataclasses.py +149 -0
- synth_ai/spec/loader.py +144 -0
- synth_ai/spec/serializer.py +199 -0
- synth_ai/spec/validation.py +250 -0
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +589 -0
- synth_ai/streaming/streamer.py +320 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/__init__.py +116 -3
- synth_ai/task/apps/__init__.py +132 -0
- synth_ai/task/auth.py +165 -0
- synth_ai/task/client.py +167 -0
- synth_ai/task/config.py +261 -0
- synth_ai/task/contracts.py +173 -57
- synth_ai/task/datasets.py +108 -0
- synth_ai/task/errors.py +50 -0
- synth_ai/task/health.py +17 -11
- synth_ai/task/inference_api.py +101 -0
- synth_ai/task/json.py +111 -0
- synth_ai/task/proxy.py +251 -0
- synth_ai/task/rubrics/__init__.py +55 -0
- synth_ai/task/rubrics/loaders.py +156 -0
- synth_ai/task/rubrics/models.py +57 -0
- synth_ai/task/rubrics/scoring.py +116 -0
- synth_ai/task/rubrics/strict.py +149 -0
- synth_ai/task/rubrics.py +219 -0
- synth_ai/task/server.py +432 -0
- synth_ai/task/trace_correlation_helpers.py +328 -0
- synth_ai/task/tracing_utils.py +95 -0
- synth_ai/task/validators.py +449 -6
- synth_ai/task/vendors.py +59 -0
- synth_ai/tracing_v3/__init__.py +4 -0
- synth_ai/tracing_v3/abstractions.py +21 -4
- synth_ai/tracing_v3/config.py +167 -22
- synth_ai/tracing_v3/constants.py +21 -0
- synth_ai/tracing_v3/db_config.py +42 -29
- synth_ai/tracing_v3/decorators.py +80 -45
- synth_ai/tracing_v3/examples/basic_usage.py +15 -9
- synth_ai/tracing_v3/hooks.py +6 -4
- synth_ai/tracing_v3/llm_call_record_helpers.py +161 -61
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/replica_sync.py +12 -7
- synth_ai/tracing_v3/serialization.py +130 -0
- synth_ai/tracing_v3/session_tracer.py +86 -21
- synth_ai/tracing_v3/storage/base.py +98 -12
- synth_ai/tracing_v3/storage/config.py +63 -16
- synth_ai/tracing_v3/storage/factory.py +11 -9
- synth_ai/tracing_v3/storage/utils.py +15 -11
- synth_ai/tracing_v3/trace_utils.py +317 -0
- synth_ai/tracing_v3/turso/__init__.py +8 -21
- synth_ai/tracing_v3/turso/daemon.py +123 -15
- synth_ai/tracing_v3/turso/models.py +5 -2
- synth_ai/tracing_v3/turso/native_manager.py +1293 -0
- synth_ai/tracing_v3/utils.py +5 -4
- synth_ai/tunnel.py +143 -0
- synth_ai/tunnel_deploy.py +278 -0
- synth_ai/types.py +8 -0
- synth_ai/urls.py +11 -0
- synth_ai/utils/__init__.py +166 -0
- synth_ai/utils/agents.py +74 -0
- synth_ai/utils/apps.py +152 -0
- synth_ai/utils/base_url.py +94 -0
- synth_ai/utils/bin.py +39 -0
- synth_ai/utils/claude.py +36 -0
- synth_ai/utils/cli.py +284 -0
- synth_ai/utils/config.py +81 -0
- synth_ai/utils/env.py +346 -0
- synth_ai/utils/errors.py +85 -0
- synth_ai/utils/http.py +172 -0
- synth_ai/utils/json.py +72 -0
- synth_ai/utils/log_filter.py +99 -0
- synth_ai/utils/logging.py +198 -0
- synth_ai/utils/modal.py +299 -0
- synth_ai/utils/paths.py +95 -0
- synth_ai/utils/process.py +233 -0
- synth_ai/utils/prompts.py +39 -0
- synth_ai/utils/sqld.py +122 -0
- synth_ai/utils/ssl.py +25 -0
- synth_ai/utils/task_app_discovery.py +882 -0
- synth_ai/utils/task_app_env.py +186 -0
- synth_ai/utils/task_app_state.py +318 -0
- synth_ai/utils/tunnel/__init__.py +12 -0
- synth_ai/utils/tunnel/config.py +55 -0
- synth_ai/utils/user_config.py +137 -0
- synth_ai/uvicorn.py +77 -0
- synth_ai-0.2.23.dev3.dist-info/METADATA +357 -0
- synth_ai-0.2.23.dev3.dist-info/RECORD +983 -0
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/entry_points.txt +0 -1
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/top_level.txt +1 -0
- synth_ai/cli/man.py +0 -106
- synth_ai/core/experiment.py +0 -15
- synth_ai/core/system.py +0 -15
- synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
- synth_ai/experimental/synth_oss.py +0 -446
- synth_ai/handshake.py +0 -63
- synth_ai/install_sqld.sh +0 -40
- synth_ai/learning/offline/dpo.py +0 -0
- synth_ai/learning/offline/providers.py +0 -7
- synth_ai/learning/offline/sft.py +0 -0
- synth_ai/learning/offline/shared.py +0 -0
- synth_ai/learning/online/grpo.py +0 -0
- synth_ai/learning/online/irft.py +0 -0
- synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
- synth_ai/learning/prompts/gepa.py +0 -0
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
- synth_ai/learning/prompts/mipro.py +0 -289
- synth_ai/learning/prompts/random_search.py +0 -246
- synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
- synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
- synth_ai/lm/__init__.py +0 -51
- synth_ai/lm/caching/constants.py +0 -6
- synth_ai/lm/caching/dbs.py +0 -0
- synth_ai/lm/caching/ephemeral.py +0 -102
- synth_ai/lm/caching/handler.py +0 -137
- synth_ai/lm/caching/initialize.py +0 -11
- synth_ai/lm/caching/persistent.py +0 -114
- synth_ai/lm/config.py +0 -110
- synth_ai/lm/constants.py +0 -32
- synth_ai/lm/core/__init__.py +0 -8
- synth_ai/lm/core/all.py +0 -73
- synth_ai/lm/core/exceptions.py +0 -7
- synth_ai/lm/core/main.py +0 -319
- synth_ai/lm/core/main_v3.py +0 -594
- synth_ai/lm/core/synth_models.py +0 -48
- synth_ai/lm/core/vendor_clients.py +0 -188
- synth_ai/lm/cost/monitor.py +0 -1
- synth_ai/lm/cost/statefulness.py +0 -1
- synth_ai/lm/injection.py +0 -80
- synth_ai/lm/overrides.py +0 -206
- synth_ai/lm/provider_support/__init__.py +0 -8
- synth_ai/lm/provider_support/anthropic.py +0 -972
- synth_ai/lm/provider_support/openai.py +0 -1139
- synth_ai/lm/provider_support/suppress_logging.py +0 -31
- synth_ai/lm/structured_outputs/handler.py +0 -440
- synth_ai/lm/structured_outputs/inject.py +0 -297
- synth_ai/lm/structured_outputs/rehabilitate.py +0 -185
- synth_ai/lm/tools/__init__.py +0 -3
- synth_ai/lm/tools/base.py +0 -172
- synth_ai/lm/unified_interface.py +0 -202
- synth_ai/lm/vendors/base.py +0 -81
- synth_ai/lm/vendors/core/anthropic_api.py +0 -387
- synth_ai/lm/vendors/core/gemini_api.py +0 -292
- synth_ai/lm/vendors/core/mistral_api.py +0 -322
- synth_ai/lm/vendors/core/openai_api.py +0 -225
- synth_ai/lm/vendors/core/synth_dev_api.py +0 -0
- synth_ai/lm/vendors/local/ollama.py +0 -0
- synth_ai/lm/vendors/openai_standard.py +0 -780
- synth_ai/lm/vendors/openai_standard_responses.py +0 -256
- synth_ai/lm/vendors/retries.py +0 -22
- synth_ai/lm/vendors/supported/custom_endpoint.py +0 -417
- synth_ai/lm/vendors/supported/deepseek.py +0 -69
- synth_ai/lm/vendors/supported/grok.py +0 -75
- synth_ai/lm/vendors/supported/groq.py +0 -16
- synth_ai/lm/vendors/supported/ollama.py +0 -15
- synth_ai/lm/vendors/supported/openrouter.py +0 -74
- synth_ai/lm/vendors/supported/together.py +0 -11
- synth_ai/lm/vendors/synth_client.py +0 -808
- synth_ai/lm/warmup.py +0 -186
- synth_ai/rl/secrets.py +0 -19
- synth_ai/scripts/verify_rewards.py +0 -100
- synth_ai/tracing/__init__.py +0 -30
- synth_ai/tracing_v1/__init__.py +0 -33
- synth_ai/tracing_v3/turso/manager.py +0 -760
- synth_ai/v0/tracing/abstractions.py +0 -224
- synth_ai/v0/tracing/base_client.py +0 -91
- synth_ai/v0/tracing/client_manager.py +0 -131
- synth_ai/v0/tracing/config.py +0 -142
- synth_ai/v0/tracing/context.py +0 -146
- synth_ai/v0/tracing/decorators.py +0 -682
- synth_ai/v0/tracing/events/__init__.py +0 -0
- synth_ai/v0/tracing/events/manage.py +0 -147
- synth_ai/v0/tracing/events/scope.py +0 -86
- synth_ai/v0/tracing/events/store.py +0 -228
- synth_ai/v0/tracing/immediate_client.py +0 -151
- synth_ai/v0/tracing/local.py +0 -18
- synth_ai/v0/tracing/log_client_base.py +0 -73
- synth_ai/v0/tracing/retry_queue.py +0 -186
- synth_ai/v0/tracing/trackers.py +0 -515
- synth_ai/v0/tracing/upload.py +0 -512
- synth_ai/v0/tracing/utils.py +0 -9
- synth_ai/v0/tracing_v1/__init__.py +0 -16
- synth_ai/v0/tracing_v1/abstractions.py +0 -224
- synth_ai/v0/tracing_v1/base_client.py +0 -91
- synth_ai/v0/tracing_v1/client_manager.py +0 -131
- synth_ai/v0/tracing_v1/config.py +0 -142
- synth_ai/v0/tracing_v1/context.py +0 -146
- synth_ai/v0/tracing_v1/decorators.py +0 -703
- synth_ai/v0/tracing_v1/events/__init__.py +0 -0
- synth_ai/v0/tracing_v1/events/manage.py +0 -147
- synth_ai/v0/tracing_v1/events/scope.py +0 -86
- synth_ai/v0/tracing_v1/events/store.py +0 -228
- synth_ai/v0/tracing_v1/immediate_client.py +0 -151
- synth_ai/v0/tracing_v1/local.py +0 -18
- synth_ai/v0/tracing_v1/log_client_base.py +0 -73
- synth_ai/v0/tracing_v1/retry_queue.py +0 -186
- synth_ai/v0/tracing_v1/trackers.py +0 -515
- synth_ai/v0/tracing_v1/upload.py +0 -527
- synth_ai/v0/tracing_v1/utils.py +0 -9
- synth_ai/zyk/__init__.py +0 -30
- synth_ai-0.2.8.dev4.dist-info/METADATA +0 -129
- synth_ai-0.2.8.dev4.dist-info/RECORD +0 -420
- {synth_ai/lm/caching → examples/task_apps}/__init__.py +0 -0
- {synth_ai/lm/cost → examples/task_apps/crafter}/__init__.py +0 -0
- {synth_ai/lm/structured_outputs → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server}/__init__.py +0 -0
- {synth_ai/lm/vendors → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests}/__init__.py +0 -0
- {synth_ai/lm/vendors/core → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils}/__init__.py +0 -0
- {synth_ai/lm/vendors/local → examples/task_apps/math}/__init__.py +0 -0
- {synth_ai/lm/vendors/supported → examples/workflows}/__init__.py +0 -0
- {synth_ai/v0/tracing → examples/workflows/math_rl}/__init__.py +0 -0
- /synth_ai/{compound/cais.py → cli/__main__.py} +0 -0
- /synth_ai/{learning/filtering.py → py.typed} +0 -0
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1437 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import httpx
|
|
17
|
+
import tomllib
|
|
18
|
+
from synth_ai.task.client import TaskAppClient
|
|
19
|
+
from synth_ai.task.contracts import (
|
|
20
|
+
RolloutEnvSpec,
|
|
21
|
+
RolloutMode,
|
|
22
|
+
RolloutPolicySpec,
|
|
23
|
+
RolloutRecordConfig,
|
|
24
|
+
RolloutRequest,
|
|
25
|
+
RolloutSafetyConfig,
|
|
26
|
+
)
|
|
27
|
+
from synth_ai.task.validators import (
|
|
28
|
+
normalize_inference_url,
|
|
29
|
+
validate_rollout_response_for_rl,
|
|
30
|
+
validate_task_app_url,
|
|
31
|
+
)
|
|
32
|
+
from synth_ai.tracing_v3.config import resolve_trace_db_settings
|
|
33
|
+
from synth_ai.tracing_v3.turso.daemon import start_sqld
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _append_query_param(url: str, key: str, value: str) -> str:
|
|
37
|
+
parsed = urlparse(url)
|
|
38
|
+
params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
|
39
|
+
params[key] = value
|
|
40
|
+
new_query = urlencode(params)
|
|
41
|
+
result = urlunparse(parsed._replace(query=new_query))
|
|
42
|
+
return str(result)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_local_libsql() -> None:
|
|
46
|
+
"""Start a local sqld/libSQL instance or abort the smoke test."""
|
|
47
|
+
|
|
48
|
+
traces_root = Path(os.getenv("SYNTH_TRACES_DIR", str((Path.cwd() / "traces" / "v3").resolve())))
|
|
49
|
+
traces_root.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
local_db_path = Path(os.getenv("SQLD_DB_PATH", str(traces_root / "local.db"))).resolve()
|
|
52
|
+
local_db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
hrana_port = int(os.getenv("SQLD_HTTP_PORT", "8080"))
|
|
55
|
+
http_port = hrana_port + 1
|
|
56
|
+
os.environ["SQLD_DB_PATH"] = str(local_db_path)
|
|
57
|
+
os.environ["SQLD_HTTP_PORT"] = str(hrana_port)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
start_sqld(db_path=str(local_db_path), hrana_port=hrana_port, http_port=http_port)
|
|
61
|
+
started_new = True
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
# If address in use, assume an existing sqld instance; verify health below
|
|
64
|
+
if "Address already in use" in str(exc):
|
|
65
|
+
started_new = False
|
|
66
|
+
click.echo(
|
|
67
|
+
f"[libsql] sqld already running on 127.0.0.1:{hrana_port} (hrana) and 127.0.0.1:{http_port} (http); attempting to reuse", err=True
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
raise click.ClickException(
|
|
71
|
+
f"Failed to start local sqld on 127.0.0.1:{hrana_port}: {exc}"
|
|
72
|
+
) from exc
|
|
73
|
+
|
|
74
|
+
health_url = f"http://127.0.0.1:{http_port}/health"
|
|
75
|
+
deadline = time.time() + 5.0
|
|
76
|
+
healthy = False
|
|
77
|
+
while time.time() < deadline:
|
|
78
|
+
try:
|
|
79
|
+
resp = httpx.get(health_url, timeout=0.5)
|
|
80
|
+
if resp.status_code == 200:
|
|
81
|
+
healthy = True
|
|
82
|
+
break
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
time.sleep(0.1)
|
|
86
|
+
|
|
87
|
+
if not healthy:
|
|
88
|
+
msg = (
|
|
89
|
+
f"Tracing backend not reachable at {health_url}. "
|
|
90
|
+
"Start sqld manually or disable tracing (TASKAPP_TRACING_ENABLED=0)."
|
|
91
|
+
)
|
|
92
|
+
raise click.ClickException(msg)
|
|
93
|
+
|
|
94
|
+
click.echo(
|
|
95
|
+
f"[libsql] sqld ready on libsql://127.0.0.1:{hrana_port} with HTTP API on :{http_port} (started_new={started_new})",
|
|
96
|
+
err=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Python libsql client uses HTTP API port, not Hrana WebSocket port
|
|
100
|
+
local_dsn = f"http://127.0.0.1:{http_port}"
|
|
101
|
+
os.environ["LIBSQL_URL"] = local_dsn
|
|
102
|
+
os.environ["SYNTH_TRACES_DB"] = local_dsn
|
|
103
|
+
os.environ.pop("LIBSQL_AUTH_TOKEN", None)
|
|
104
|
+
os.environ.pop("TURSO_AUTH_TOKEN", None)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _refresh_tracing_config() -> None:
|
|
108
|
+
"""Rebuild global tracing configuration so new env vars take effect."""
|
|
109
|
+
|
|
110
|
+
from synth_ai.tracing_v3 import config as tracing_config_module
|
|
111
|
+
from synth_ai.tracing_v3.storage import config as storage_config_module
|
|
112
|
+
|
|
113
|
+
tracing_config_module.CONFIG = tracing_config_module.TursoConfig() # type: ignore[assignment]
|
|
114
|
+
storage_config_module.STORAGE_CONFIG = storage_config_module.StorageConfig( # type: ignore[assignment]
|
|
115
|
+
connection_string=os.environ["SYNTH_TRACES_DB"],
|
|
116
|
+
backend=storage_config_module.StorageBackend.TURSO_NATIVE,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _load_smoke_config(config_path: Path | None) -> dict[str, Any]:
|
|
121
|
+
"""Load [smoke] section from TOML config file.
|
|
122
|
+
|
|
123
|
+
Returns an empty dict if no config file or no [smoke] section.
|
|
124
|
+
"""
|
|
125
|
+
if not config_path:
|
|
126
|
+
return {}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with open(config_path, "rb") as f:
|
|
130
|
+
full_config = tomllib.load(f)
|
|
131
|
+
|
|
132
|
+
smoke_config = full_config.get("smoke", {})
|
|
133
|
+
|
|
134
|
+
if smoke_config:
|
|
135
|
+
click.echo(f"[smoke] Loaded configuration from {config_path}", err=True)
|
|
136
|
+
click.echo(f"[smoke] Config keys: {', '.join(smoke_config.keys())}", err=True)
|
|
137
|
+
|
|
138
|
+
return smoke_config
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
click.echo(f"[smoke] Warning: Failed to load config from {config_path}: {exc}", err=True)
|
|
141
|
+
return {}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _kill_process_on_port(port: int) -> None:
|
|
145
|
+
"""Kill any process listening on the given port."""
|
|
146
|
+
try:
|
|
147
|
+
# Use lsof to find and kill process on port
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
["lsof", "-ti", f":{port}"],
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
timeout=2,
|
|
153
|
+
)
|
|
154
|
+
if result.stdout.strip():
|
|
155
|
+
pids = result.stdout.strip().split('\n')
|
|
156
|
+
for pid in pids:
|
|
157
|
+
try:
|
|
158
|
+
subprocess.run(["kill", "-9", pid], timeout=2)
|
|
159
|
+
click.echo(f"[smoke] Killed existing process {pid} on port {port}", err=True)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
time.sleep(2.0) # Give OS time to release port
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
click.echo(f"[smoke] Warning: Could not check/kill port {port}: {exc}", err=True)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _start_task_app_server(
|
|
168
|
+
task_app_name: str,
|
|
169
|
+
port: int,
|
|
170
|
+
env_file: str | None,
|
|
171
|
+
force: bool
|
|
172
|
+
) -> tuple[Any, str]:
|
|
173
|
+
"""Start a task app server in the background using task-app serve.
|
|
174
|
+
|
|
175
|
+
Returns (process, url) tuple.
|
|
176
|
+
"""
|
|
177
|
+
import subprocess
|
|
178
|
+
import time as time_module
|
|
179
|
+
|
|
180
|
+
# Build command using task-app serve (for TaskAppConfig-based apps)
|
|
181
|
+
cmd = [
|
|
182
|
+
"nohup",
|
|
183
|
+
"uvx", "synth-ai",
|
|
184
|
+
"task-app", "serve", task_app_name,
|
|
185
|
+
"--port", str(port),
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
if env_file:
|
|
189
|
+
cmd.extend(["--env-file", env_file])
|
|
190
|
+
|
|
191
|
+
if force:
|
|
192
|
+
cmd.append("--force")
|
|
193
|
+
|
|
194
|
+
# Resolve the synth-ai root directory
|
|
195
|
+
import synth_ai
|
|
196
|
+
synth_ai_root = Path(synth_ai.__file__).resolve().parent.parent
|
|
197
|
+
|
|
198
|
+
click.echo(f"[smoke] Starting task app '{task_app_name}' on port {port}...", err=True)
|
|
199
|
+
click.echo(f"[smoke] Command: {' '.join(cmd)}", err=True)
|
|
200
|
+
click.echo(f"[smoke] Working directory: {synth_ai_root}", err=True)
|
|
201
|
+
|
|
202
|
+
# nohup requires output redirection to a file
|
|
203
|
+
# Open file, start process, then close file handle so process is fully detached
|
|
204
|
+
# Run from synth-ai root so task app discovery works
|
|
205
|
+
nohup_log = Path(synth_ai_root) / "nohup_task_app.out"
|
|
206
|
+
|
|
207
|
+
# Inherit SYNTH_QUIET environment variable to suppress patch messages
|
|
208
|
+
env = os.environ.copy()
|
|
209
|
+
if os.getenv("SYNTH_QUIET"):
|
|
210
|
+
env["SYNTH_QUIET"] = "1"
|
|
211
|
+
|
|
212
|
+
with open(nohup_log, "w") as log_file:
|
|
213
|
+
proc = subprocess.Popen(
|
|
214
|
+
cmd,
|
|
215
|
+
stdout=log_file,
|
|
216
|
+
stderr=subprocess.STDOUT,
|
|
217
|
+
text=True,
|
|
218
|
+
cwd=str(synth_ai_root),
|
|
219
|
+
env=env,
|
|
220
|
+
)
|
|
221
|
+
# File is closed immediately so process is detached
|
|
222
|
+
|
|
223
|
+
# Wait for server to be ready
|
|
224
|
+
url = f"http://localhost:{port}"
|
|
225
|
+
click.echo(f"[smoke] Waiting for task app to be ready at {url}...", err=True)
|
|
226
|
+
|
|
227
|
+
import httpx
|
|
228
|
+
deadline = time.time() + 120.0 # Give it 2 minutes for initial setup
|
|
229
|
+
attempt = 0
|
|
230
|
+
last_log_line = None
|
|
231
|
+
while time.time() < deadline:
|
|
232
|
+
attempt += 1
|
|
233
|
+
try:
|
|
234
|
+
resp = httpx.get(f"{url}/health", timeout=1.0)
|
|
235
|
+
# Accept both 200 and 400 - 400 means server is up but auth is failing (which is fine for smoke test)
|
|
236
|
+
if resp.status_code in (200, 400):
|
|
237
|
+
click.echo(f"[smoke] Task app ready at {url} (status={resp.status_code})", err=True)
|
|
238
|
+
return proc, url
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# Show polling progress every 5 seconds with last log line
|
|
243
|
+
if attempt % 10 == 0:
|
|
244
|
+
elapsed = int(time.time() - (deadline - 120.0))
|
|
245
|
+
# Try to read last line from nohup log
|
|
246
|
+
try:
|
|
247
|
+
if nohup_log.exists():
|
|
248
|
+
with open(nohup_log) as f:
|
|
249
|
+
lines = f.readlines()
|
|
250
|
+
if lines:
|
|
251
|
+
# Get last non-empty line
|
|
252
|
+
for line in reversed(lines[-10:]):
|
|
253
|
+
stripped = line.strip()
|
|
254
|
+
if stripped and stripped != last_log_line:
|
|
255
|
+
last_log_line = stripped
|
|
256
|
+
# Truncate if too long
|
|
257
|
+
if len(stripped) > 80:
|
|
258
|
+
stripped = stripped[:77] + "..."
|
|
259
|
+
click.echo(f"[smoke] Waiting ({elapsed}s): {stripped}", err=True)
|
|
260
|
+
break
|
|
261
|
+
else:
|
|
262
|
+
click.echo(f"[smoke] Still waiting for task app... ({elapsed}s elapsed)", err=True)
|
|
263
|
+
else:
|
|
264
|
+
click.echo(f"[smoke] Still waiting for task app... ({elapsed}s elapsed)", err=True)
|
|
265
|
+
except Exception:
|
|
266
|
+
click.echo(f"[smoke] Still waiting for task app... ({elapsed}s elapsed)", err=True)
|
|
267
|
+
|
|
268
|
+
# Check if process died
|
|
269
|
+
if proc.poll() is not None:
|
|
270
|
+
# Build a manual command that the user can copy-paste
|
|
271
|
+
manual_cmd_parts = ["uvx", "synth-ai", "task-app", "serve", task_app_name, "--port", str(port)]
|
|
272
|
+
if env_file:
|
|
273
|
+
manual_cmd_parts.extend(["--env-file", env_file])
|
|
274
|
+
if force:
|
|
275
|
+
manual_cmd_parts.append("--force")
|
|
276
|
+
|
|
277
|
+
raise click.ClickException(
|
|
278
|
+
f"Task app '{task_app_name}' process exited unexpectedly (code={proc.returncode}). "
|
|
279
|
+
f"Check that the task app name is correct and .env has required keys. "
|
|
280
|
+
f"Try running manually: {' '.join(manual_cmd_parts)}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
time_module.sleep(0.5)
|
|
284
|
+
|
|
285
|
+
proc.kill()
|
|
286
|
+
raise click.ClickException("Task app failed to start within 120 seconds")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _start_sqld_server(
|
|
290
|
+
db_path: str,
|
|
291
|
+
hrana_port: int,
|
|
292
|
+
http_port: int
|
|
293
|
+
) -> Any:
|
|
294
|
+
"""Start sqld server in the background.
|
|
295
|
+
|
|
296
|
+
Returns the process handle.
|
|
297
|
+
"""
|
|
298
|
+
import shutil
|
|
299
|
+
import subprocess
|
|
300
|
+
|
|
301
|
+
# Check if sqld is available
|
|
302
|
+
sqld_bin = shutil.which("sqld")
|
|
303
|
+
if not sqld_bin:
|
|
304
|
+
click.echo("[smoke] Warning: sqld not found in PATH, skipping auto-start", err=True)
|
|
305
|
+
click.echo("[smoke] Install sqld: brew install sqld", err=True)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# Ensure db directory exists
|
|
309
|
+
db_path_obj = Path(db_path).expanduser().resolve()
|
|
310
|
+
db_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
|
|
312
|
+
# Kill any existing processes on these ports
|
|
313
|
+
for port in [hrana_port, http_port]:
|
|
314
|
+
_kill_process_on_port(port)
|
|
315
|
+
|
|
316
|
+
cmd = [
|
|
317
|
+
sqld_bin,
|
|
318
|
+
"--db-path", str(db_path_obj),
|
|
319
|
+
"--hrana-listen-addr", f"127.0.0.1:{hrana_port}",
|
|
320
|
+
"--http-listen-addr", f"127.0.0.1:{http_port}",
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
click.echo("[smoke] Starting sqld server...", err=True)
|
|
324
|
+
click.echo(f"[smoke] DB path: {db_path_obj}", err=True)
|
|
325
|
+
click.echo(f"[smoke] Hrana port: {hrana_port}, HTTP port: {http_port}", err=True)
|
|
326
|
+
click.echo(f"[smoke] Command: {' '.join(cmd)}", err=True)
|
|
327
|
+
|
|
328
|
+
# Redirect to devnull to avoid process dying from pipe buffer issues
|
|
329
|
+
proc = subprocess.Popen(
|
|
330
|
+
cmd,
|
|
331
|
+
stdout=subprocess.DEVNULL,
|
|
332
|
+
stderr=subprocess.DEVNULL,
|
|
333
|
+
text=True,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Wait for server to be ready
|
|
337
|
+
health_url = f"http://127.0.0.1:{http_port}/health"
|
|
338
|
+
click.echo(f"[smoke] Waiting for sqld to be ready at {health_url}...", err=True)
|
|
339
|
+
|
|
340
|
+
deadline = time.time() + 10.0
|
|
341
|
+
while time.time() < deadline:
|
|
342
|
+
try:
|
|
343
|
+
resp = httpx.get(health_url, timeout=0.5)
|
|
344
|
+
if resp.status_code == 200:
|
|
345
|
+
click.echo("[smoke] sqld ready", err=True)
|
|
346
|
+
# Set environment variables for tracing
|
|
347
|
+
os.environ["SQLD_DB_PATH"] = str(db_path_obj)
|
|
348
|
+
os.environ["SQLD_HTTP_PORT"] = str(hrana_port)
|
|
349
|
+
os.environ["LIBSQL_URL"] = f"http://127.0.0.1:{http_port}"
|
|
350
|
+
os.environ["SYNTH_TRACES_DB"] = f"http://127.0.0.1:{http_port}"
|
|
351
|
+
return proc
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
# Check if process died
|
|
356
|
+
if proc.poll() is not None:
|
|
357
|
+
click.echo(f"[smoke] Warning: sqld process exited with code {proc.returncode}", err=True)
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
time.sleep(0.2)
|
|
361
|
+
|
|
362
|
+
click.echo("[smoke] Warning: sqld health check timed out, continuing anyway...", err=True)
|
|
363
|
+
return proc
|
|
364
|
+
|
|
365
|
+
class MockRLTrainer:
|
|
366
|
+
"""Minimal trainer emulator with a local FastAPI mock for GPT-5-Nano.
|
|
367
|
+
|
|
368
|
+
In ``synthetic`` mode it emits deterministic tool calls so the rollout can
|
|
369
|
+
progress without relying on external inference. In ``openai`` mode it acts
|
|
370
|
+
as a thin proxy around the real OpenAI chat completions endpoint (useful to
|
|
371
|
+
reproduce production behaviour locally).
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
def __init__(self, *, port: int = 0, backend: str = "synthetic") -> None:
|
|
375
|
+
self.port = port
|
|
376
|
+
self.backend = backend.lower().strip() or "synthetic"
|
|
377
|
+
self._server = None
|
|
378
|
+
self._task: asyncio.Task | None = None
|
|
379
|
+
self._openai_endpoint = os.getenv(
|
|
380
|
+
"SMOKE_OPENAI_ENDPOINT", "https://api.openai.com/v1/chat/completions"
|
|
381
|
+
)
|
|
382
|
+
self._openai_api_key = (
|
|
383
|
+
os.getenv("SMOKE_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY") or ""
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _build_app(self):
|
|
387
|
+
import json
|
|
388
|
+
|
|
389
|
+
from fastapi import Body, FastAPI
|
|
390
|
+
from fastapi.responses import JSONResponse
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
logger = logging.getLogger(__name__)
|
|
394
|
+
except Exception: # pragma: no cover - logging failures should not crash
|
|
395
|
+
logger = None
|
|
396
|
+
|
|
397
|
+
app = FastAPI()
|
|
398
|
+
backend = self.backend
|
|
399
|
+
|
|
400
|
+
@app.post("/v1/chat/completions")
|
|
401
|
+
async def chat_completions(body: dict = Body(...), cid: str | None = None):
|
|
402
|
+
log = logger or logging.getLogger("MockRLTrainer")
|
|
403
|
+
try:
|
|
404
|
+
msg_count = len(body.get("messages") or [])
|
|
405
|
+
except Exception:
|
|
406
|
+
msg_count = -1
|
|
407
|
+
click.echo(
|
|
408
|
+
f"[mock-rl] ← request backend={backend} model={body.get('model')} messages={msg_count} cid={cid}",
|
|
409
|
+
err=True,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Explicit Body(...) avoids FastAPI interpreting parameters as query args
|
|
413
|
+
model = (body.get("model") or "gpt-5-nano")
|
|
414
|
+
messages = body.get("messages") or []
|
|
415
|
+
tools = body.get("tools") or []
|
|
416
|
+
|
|
417
|
+
# Decide whether to emit a tool call (to drive env steps) or plain text
|
|
418
|
+
emit_tool = False
|
|
419
|
+
tool_name = ""
|
|
420
|
+
for t in tools:
|
|
421
|
+
try:
|
|
422
|
+
if (t or {}).get("type") == "function":
|
|
423
|
+
fn = (t or {}).get("function") or {}
|
|
424
|
+
name = (fn or {}).get("name") or ""
|
|
425
|
+
if name:
|
|
426
|
+
tool_name = name
|
|
427
|
+
emit_tool = True
|
|
428
|
+
break
|
|
429
|
+
except Exception:
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
# Simple heuristic actions to move/explore then interact
|
|
433
|
+
actions = ["move_right", "move_right", "move_down", "move_left", "do"]
|
|
434
|
+
|
|
435
|
+
correlation = cid
|
|
436
|
+
|
|
437
|
+
if backend == "openai":
|
|
438
|
+
if not self._openai_api_key:
|
|
439
|
+
return JSONResponse(
|
|
440
|
+
{
|
|
441
|
+
"error": "OPENAI_API_KEY (or SMOKE_OPENAI_API_KEY) is required for mock backend 'openai'"
|
|
442
|
+
},
|
|
443
|
+
status_code=500,
|
|
444
|
+
)
|
|
445
|
+
try:
|
|
446
|
+
from examples.task_apps.crafter.task_app.synth_envs_hosted.inference.openai_client import (
|
|
447
|
+
OpenAIClient as _HostedOpenAIClient,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
hosted_client = _HostedOpenAIClient(
|
|
451
|
+
base_url=self._openai_endpoint,
|
|
452
|
+
api_key=self._openai_api_key,
|
|
453
|
+
)
|
|
454
|
+
except Exception as exc:
|
|
455
|
+
if logger is not None:
|
|
456
|
+
logger.error("MockRLTrainer failed to import HostedOpenAIClient: %s", exc)
|
|
457
|
+
return JSONResponse(
|
|
458
|
+
{"error": f"OpenAI proxy unavailable: {exc}"},
|
|
459
|
+
status_code=500,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
result = await hosted_client.generate_with_retries( # type: ignore[attr-defined]
|
|
464
|
+
request=body,
|
|
465
|
+
base_url=self._openai_endpoint,
|
|
466
|
+
max_retries=0,
|
|
467
|
+
)
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
if logger is not None:
|
|
470
|
+
logger.error("MockRLTrainer OpenAI generate failed: %s", exc)
|
|
471
|
+
return JSONResponse(
|
|
472
|
+
{"error": f"OpenAI proxy request failed: {exc}"},
|
|
473
|
+
status_code=502,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if isinstance(result, dict):
|
|
477
|
+
data_typed = dict(result)
|
|
478
|
+
synth_meta = data_typed.get("synth")
|
|
479
|
+
if not isinstance(synth_meta, dict):
|
|
480
|
+
synth_meta = {}
|
|
481
|
+
data_typed["synth"] = synth_meta
|
|
482
|
+
if correlation:
|
|
483
|
+
synth_meta.setdefault("cid", correlation)
|
|
484
|
+
|
|
485
|
+
# Fallback: if the upstream response failed to emit tool calls,
|
|
486
|
+
# synthesize a deterministic action plan so the rollout can proceed.
|
|
487
|
+
try:
|
|
488
|
+
choices = data_typed.get("choices") or []
|
|
489
|
+
first = choices[0] if choices else {}
|
|
490
|
+
message = first.get("message") if isinstance(first, dict) else {}
|
|
491
|
+
tc = message.get("tool_calls") if isinstance(message, dict) else None
|
|
492
|
+
if not tc:
|
|
493
|
+
if logger is not None:
|
|
494
|
+
logger.warning(
|
|
495
|
+
"MockRLTrainer fallback: OpenAI returned no tool calls; injecting deterministic actions."
|
|
496
|
+
)
|
|
497
|
+
fallback_message = dict(message or {})
|
|
498
|
+
fallback_message.setdefault("role", "assistant")
|
|
499
|
+
fallback_message["content"] = ""
|
|
500
|
+
fallback_message["tool_calls"] = [
|
|
501
|
+
{
|
|
502
|
+
"id": f"call_{uuid.uuid4().hex[:8]}",
|
|
503
|
+
"type": "function",
|
|
504
|
+
"function": {
|
|
505
|
+
"name": tool_name or "interact_many",
|
|
506
|
+
"arguments": json.dumps({"actions": actions}),
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
fallback_message["function_call"] = {
|
|
511
|
+
"name": tool_name or "interact_many",
|
|
512
|
+
"arguments": json.dumps({"actions": actions}),
|
|
513
|
+
}
|
|
514
|
+
if choices:
|
|
515
|
+
choices[0]["message"] = fallback_message
|
|
516
|
+
else:
|
|
517
|
+
data_typed["choices"] = [
|
|
518
|
+
{
|
|
519
|
+
"index": 0,
|
|
520
|
+
"message": fallback_message,
|
|
521
|
+
"finish_reason": "tool_calls",
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
except Exception as exc:
|
|
525
|
+
if logger is not None:
|
|
526
|
+
logger.debug("MockRLTrainer fallback injection failed: %s", exc)
|
|
527
|
+
|
|
528
|
+
tool_call_count = 0
|
|
529
|
+
try:
|
|
530
|
+
choices = data_typed.get("choices") or []
|
|
531
|
+
first = choices[0] if choices else {}
|
|
532
|
+
message = first.get("message") if isinstance(first, dict) else {}
|
|
533
|
+
if isinstance(message, dict):
|
|
534
|
+
tool_call_count = len(message.get("tool_calls") or [])
|
|
535
|
+
except Exception:
|
|
536
|
+
tool_call_count = 0
|
|
537
|
+
|
|
538
|
+
log.info(
|
|
539
|
+
"MockRLTrainer proxy returning response with %s tool calls (cid=%s)",
|
|
540
|
+
tool_call_count,
|
|
541
|
+
cid,
|
|
542
|
+
)
|
|
543
|
+
if tool_call_count == 0:
|
|
544
|
+
log.error(
|
|
545
|
+
"MockRLTrainer proxy still missing tool_calls after fallback injection (cid=%s)",
|
|
546
|
+
cid,
|
|
547
|
+
)
|
|
548
|
+
click.echo(
|
|
549
|
+
"[mock-rl] ✗ proxy response missing tool_calls; failing request", err=True
|
|
550
|
+
)
|
|
551
|
+
return JSONResponse(data_typed)
|
|
552
|
+
return JSONResponse(result)
|
|
553
|
+
|
|
554
|
+
if emit_tool:
|
|
555
|
+
# Emit BOTH legacy function_call and modern tool_calls for broad compatibility
|
|
556
|
+
message_payload = {
|
|
557
|
+
"role": "assistant",
|
|
558
|
+
"content": "",
|
|
559
|
+
"function_call": {
|
|
560
|
+
"name": tool_name,
|
|
561
|
+
"arguments": json.dumps({"actions": actions}),
|
|
562
|
+
},
|
|
563
|
+
"tool_calls": [
|
|
564
|
+
{
|
|
565
|
+
"id": f"call_{uuid.uuid4().hex[:8]}",
|
|
566
|
+
"type": "function",
|
|
567
|
+
"function": {
|
|
568
|
+
"name": tool_name,
|
|
569
|
+
"arguments": json.dumps({"actions": actions}),
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
],
|
|
573
|
+
}
|
|
574
|
+
finish_reason = "tool_calls"
|
|
575
|
+
else:
|
|
576
|
+
# Fallback: echo last user content as plain text
|
|
577
|
+
click.echo(
|
|
578
|
+
f"[mock-rl] ! no tool schema supplied; returning text response (cid={cid})",
|
|
579
|
+
err=True,
|
|
580
|
+
)
|
|
581
|
+
log.warning(
|
|
582
|
+
"MockRLTrainer received request without tool schema; responding with text content (cid=%s)",
|
|
583
|
+
cid,
|
|
584
|
+
)
|
|
585
|
+
last_user = next((m.get("content", "") for m in reversed(messages) if m.get("role") == "user"), "")
|
|
586
|
+
text = (last_user or "").strip()
|
|
587
|
+
if len(text) > 160:
|
|
588
|
+
text = text[:160] + "..."
|
|
589
|
+
message_payload = {"role": "assistant", "content": f"MOCK(gpt-5-nano): {text or 'ack'}"}
|
|
590
|
+
finish_reason = "stop"
|
|
591
|
+
|
|
592
|
+
response = {
|
|
593
|
+
"id": f"cmpl_{uuid.uuid4().hex[:12]}",
|
|
594
|
+
"object": "chat.completion",
|
|
595
|
+
"created": int(asyncio.get_event_loop().time()),
|
|
596
|
+
"model": model,
|
|
597
|
+
"choices": [{"index": 0, "message": message_payload, "finish_reason": finish_reason}],
|
|
598
|
+
"usage": {"prompt_tokens": 32, "completion_tokens": 16, "total_tokens": 48},
|
|
599
|
+
"synth": {"cid": correlation},
|
|
600
|
+
}
|
|
601
|
+
if finish_reason == "tool_calls":
|
|
602
|
+
# Type-safe extraction of tool call count
|
|
603
|
+
tc = 0
|
|
604
|
+
try:
|
|
605
|
+
choices = response.get("choices")
|
|
606
|
+
if isinstance(choices, list) and choices:
|
|
607
|
+
first_choice = choices[0]
|
|
608
|
+
if isinstance(first_choice, dict):
|
|
609
|
+
msg = first_choice.get("message")
|
|
610
|
+
if isinstance(msg, dict):
|
|
611
|
+
tool_calls = msg.get("tool_calls")
|
|
612
|
+
if isinstance(tool_calls, list):
|
|
613
|
+
tc = len(tool_calls)
|
|
614
|
+
except Exception:
|
|
615
|
+
pass
|
|
616
|
+
log.debug(
|
|
617
|
+
"MockRLTrainer synthetic response emitting %s tool calls (cid=%s)",
|
|
618
|
+
tc,
|
|
619
|
+
cid,
|
|
620
|
+
)
|
|
621
|
+
assert tc > 0, "MockRLTrainer synthetic response missing tool_calls"
|
|
622
|
+
click.echo(
|
|
623
|
+
f"[mock-rl] → response tool_calls={tc} backend={backend} cid={cid}",
|
|
624
|
+
err=True,
|
|
625
|
+
)
|
|
626
|
+
else:
|
|
627
|
+
click.echo(
|
|
628
|
+
f"[mock-rl] → response finish_reason={finish_reason} backend={backend} cid={cid}",
|
|
629
|
+
err=True,
|
|
630
|
+
)
|
|
631
|
+
return JSONResponse(response)
|
|
632
|
+
|
|
633
|
+
return app
|
|
634
|
+
|
|
635
|
+
async def start(self) -> None:
|
|
636
|
+
import socket
|
|
637
|
+
|
|
638
|
+
import uvicorn
|
|
639
|
+
|
|
640
|
+
def _allocate_port() -> int:
|
|
641
|
+
nonlocal socket
|
|
642
|
+
if self.port:
|
|
643
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
644
|
+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
645
|
+
try:
|
|
646
|
+
probe.bind(("127.0.0.1", self.port))
|
|
647
|
+
return self.port
|
|
648
|
+
except OSError:
|
|
649
|
+
pass
|
|
650
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
651
|
+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
652
|
+
probe.bind(("127.0.0.1", 0))
|
|
653
|
+
self.port = probe.getsockname()[1]
|
|
654
|
+
return self.port
|
|
655
|
+
|
|
656
|
+
retries = 0
|
|
657
|
+
while True:
|
|
658
|
+
selected_port = _allocate_port()
|
|
659
|
+
config = uvicorn.Config(
|
|
660
|
+
self._build_app(),
|
|
661
|
+
host="127.0.0.1",
|
|
662
|
+
port=selected_port,
|
|
663
|
+
log_level="warning",
|
|
664
|
+
)
|
|
665
|
+
self._server = uvicorn.Server(config)
|
|
666
|
+
self._task = asyncio.create_task(self._server.serve())
|
|
667
|
+
|
|
668
|
+
for _ in range(100):
|
|
669
|
+
if getattr(self._server, "started", False):
|
|
670
|
+
break
|
|
671
|
+
if self._task.done():
|
|
672
|
+
break
|
|
673
|
+
await asyncio.sleep(0.05)
|
|
674
|
+
|
|
675
|
+
if getattr(self._server, "started", False):
|
|
676
|
+
try:
|
|
677
|
+
logging.getLogger(__name__).info(
|
|
678
|
+
"MockRLTrainer started on http://127.0.0.1:%s (backend=%s)",
|
|
679
|
+
self.port,
|
|
680
|
+
self.backend,
|
|
681
|
+
)
|
|
682
|
+
click.echo(
|
|
683
|
+
f"[mock-rl] server ready http://127.0.0.1:{self.port} backend={self.backend}",
|
|
684
|
+
err=True,
|
|
685
|
+
)
|
|
686
|
+
except Exception:
|
|
687
|
+
pass
|
|
688
|
+
return
|
|
689
|
+
|
|
690
|
+
# Startup failed; stop server and retry on a new port if possible
|
|
691
|
+
await self.stop()
|
|
692
|
+
if retries >= 5:
|
|
693
|
+
raise RuntimeError("MockRLTrainer failed to start after multiple attempts")
|
|
694
|
+
self.port = 0
|
|
695
|
+
retries += 1
|
|
696
|
+
|
|
697
|
+
async def stop(self) -> None:
|
|
698
|
+
if self._server is not None:
|
|
699
|
+
self._server.should_exit = True
|
|
700
|
+
if self._task is not None:
|
|
701
|
+
with contextlib.suppress(Exception):
|
|
702
|
+
await asyncio.wait_for(self._task, timeout=2.0)
|
|
703
|
+
self._task = None
|
|
704
|
+
self._server = None
|
|
705
|
+
click.echo("[mock-rl] server stopped", err=True)
|
|
706
|
+
|
|
707
|
+
async def _run_smoke_async(
|
|
708
|
+
*,
|
|
709
|
+
task_app_url: str,
|
|
710
|
+
api_key: str | None,
|
|
711
|
+
env_name_opt: str | None,
|
|
712
|
+
policy_name: str,
|
|
713
|
+
model: str,
|
|
714
|
+
inference_url_opt: str | None,
|
|
715
|
+
inference_policy: str | None,
|
|
716
|
+
max_steps: int,
|
|
717
|
+
return_trace: bool,
|
|
718
|
+
use_mock: bool,
|
|
719
|
+
mock_port: int,
|
|
720
|
+
mock_backend: str,
|
|
721
|
+
config_path: Path | None,
|
|
722
|
+
rollouts: int = 1,
|
|
723
|
+
group_size: int = 1,
|
|
724
|
+
batch_size: int | None = None,
|
|
725
|
+
) -> int:
|
|
726
|
+
# If config is provided, derive defaults (URL/env/model)
|
|
727
|
+
cfg: Any | None = None
|
|
728
|
+
if config_path is not None:
|
|
729
|
+
try:
|
|
730
|
+
from synth_ai.api.train.configs.rl import (
|
|
731
|
+
RLConfig as _RLConfig, # lazy import to avoid heavy deps when unused
|
|
732
|
+
)
|
|
733
|
+
cfg = _RLConfig.from_path(config_path)
|
|
734
|
+
except Exception as exc:
|
|
735
|
+
click.echo(f"Failed to load RL config {config_path}: {exc}", err=True)
|
|
736
|
+
return 2
|
|
737
|
+
|
|
738
|
+
# Prefer explicit CLI --url; only use config services.task_url if URL not provided
|
|
739
|
+
try:
|
|
740
|
+
if not task_app_url and cfg.services and getattr(cfg.services, "task_url", None):
|
|
741
|
+
task_app_url = cfg.services.task_url
|
|
742
|
+
except Exception:
|
|
743
|
+
pass
|
|
744
|
+
# Fill env and model if not explicitly set
|
|
745
|
+
try:
|
|
746
|
+
if not env_name_opt and cfg.rollout and getattr(cfg.rollout, "env_name", None):
|
|
747
|
+
env_name_opt = cfg.rollout.env_name
|
|
748
|
+
except Exception:
|
|
749
|
+
pass
|
|
750
|
+
try:
|
|
751
|
+
if model == "gpt-5-nano":
|
|
752
|
+
# Prefer smoke config model over policy model for smoke tests
|
|
753
|
+
smoke_cfg = getattr(cfg, "smoke", None)
|
|
754
|
+
smoke_model = None
|
|
755
|
+
if smoke_cfg and hasattr(smoke_cfg, "model"):
|
|
756
|
+
smoke_model = smoke_cfg.model
|
|
757
|
+
if smoke_model:
|
|
758
|
+
model = str(smoke_model).strip()
|
|
759
|
+
elif cfg.policy:
|
|
760
|
+
if getattr(cfg.policy, "model_name", None):
|
|
761
|
+
model = str(cfg.policy.model_name).strip()
|
|
762
|
+
elif getattr(cfg.policy, "source", None):
|
|
763
|
+
model = str(cfg.policy.source).strip()
|
|
764
|
+
elif cfg.model and getattr(cfg.model, "source", None):
|
|
765
|
+
model = str(cfg.model.source).strip()
|
|
766
|
+
elif cfg.model and getattr(cfg.model, "base", None):
|
|
767
|
+
model = str(cfg.model.base).strip()
|
|
768
|
+
except Exception:
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
base = validate_task_app_url(task_app_url)
|
|
772
|
+
mock_backend = (mock_backend or "synthetic").strip().lower()
|
|
773
|
+
|
|
774
|
+
# Discover environment if not provided
|
|
775
|
+
async with TaskAppClient(base_url=base, api_key=api_key) as client:
|
|
776
|
+
# Probe basic info quickly
|
|
777
|
+
try:
|
|
778
|
+
_ = await client.health()
|
|
779
|
+
except Exception:
|
|
780
|
+
click.echo("Auth or connectivity check failed on /health. If this endpoint requires a key, pass --api-key or set ENVIRONMENT_API_KEY.", err=True)
|
|
781
|
+
# Continue; rollout may still clarify the error
|
|
782
|
+
|
|
783
|
+
# Fetch a sample task instance to infer environment name if not provided
|
|
784
|
+
env_name = env_name_opt
|
|
785
|
+
if not env_name:
|
|
786
|
+
try:
|
|
787
|
+
ti = await client.task_info(seeds=[0])
|
|
788
|
+
# task_info returns TaskInfo or list[TaskInfo]; normalize
|
|
789
|
+
info: Any = ti[0] if isinstance(ti, list) else ti
|
|
790
|
+
env_name = getattr(info, "environment", None) or getattr(info, "task", {}).get("name") # type: ignore[attr-defined]
|
|
791
|
+
except Exception:
|
|
792
|
+
env_name = None
|
|
793
|
+
if not env_name:
|
|
794
|
+
click.echo("Could not infer environment name; pass --env-name.", err=True)
|
|
795
|
+
return 2
|
|
796
|
+
|
|
797
|
+
# Build ops: alternating agent/env for max_steps
|
|
798
|
+
ops: list[str] = []
|
|
799
|
+
for _ in range(max_steps):
|
|
800
|
+
ops.append("agent")
|
|
801
|
+
ops.append("env")
|
|
802
|
+
|
|
803
|
+
# Inference URL: user override > preset > local mock > Synth API default
|
|
804
|
+
synth_base = (os.getenv("SYNTH_API_BASE") or os.getenv("SYNTH_BASE_URL") or "https://api.synth.run").rstrip("/")
|
|
805
|
+
# Avoid double '/api' if base already includes it
|
|
806
|
+
if synth_base.endswith("/api"):
|
|
807
|
+
default_infer = f"{synth_base}/inference/v1/chat/completions"
|
|
808
|
+
else:
|
|
809
|
+
default_infer = f"{synth_base}/api/inference/v1/chat/completions"
|
|
810
|
+
|
|
811
|
+
# Helper to execute one or more rollouts and return exit code
|
|
812
|
+
async def __do_rollouts(inference_url_raw: str) -> int:
|
|
813
|
+
successes = 0
|
|
814
|
+
total_steps = 0
|
|
815
|
+
nonzero_returns = 0
|
|
816
|
+
v3_traces = 0
|
|
817
|
+
|
|
818
|
+
# Derive sampling params from config if present
|
|
819
|
+
sampling: dict[str, Any] = {}
|
|
820
|
+
try:
|
|
821
|
+
if cfg and cfg.policy:
|
|
822
|
+
if getattr(cfg.policy, "temperature", None) is not None:
|
|
823
|
+
sampling["temperature"] = cfg.policy.temperature
|
|
824
|
+
if getattr(cfg.policy, "top_p", None) is not None:
|
|
825
|
+
sampling["top_p"] = cfg.policy.top_p
|
|
826
|
+
if getattr(cfg.policy, "max_tokens", None) is not None:
|
|
827
|
+
sampling["max_tokens"] = cfg.policy.max_tokens
|
|
828
|
+
except Exception:
|
|
829
|
+
pass
|
|
830
|
+
|
|
831
|
+
num_outer = batch_size if (batch_size is not None and batch_size > 0) else max(1, int(rollouts))
|
|
832
|
+
for i in range(num_outer):
|
|
833
|
+
for g in range(max(1, int(group_size))):
|
|
834
|
+
if inference_url_raw.startswith("/"):
|
|
835
|
+
inference_url_abs = f"{base}{inference_url_raw}"
|
|
836
|
+
else:
|
|
837
|
+
inference_url_abs = inference_url_raw
|
|
838
|
+
inference_url_norm = normalize_inference_url(inference_url_abs)
|
|
839
|
+
correlation_id = f"smoke-{uuid.uuid4()}"
|
|
840
|
+
inference_url_with_cid = _append_query_param(inference_url_norm, "cid", correlation_id)
|
|
841
|
+
|
|
842
|
+
run_id = correlation_id
|
|
843
|
+
policy_cfg: dict[str, Any] = {
|
|
844
|
+
"model": model,
|
|
845
|
+
"inference_url": inference_url_with_cid,
|
|
846
|
+
}
|
|
847
|
+
if sampling:
|
|
848
|
+
policy_cfg.update(sampling)
|
|
849
|
+
|
|
850
|
+
request = RolloutRequest(
|
|
851
|
+
run_id=run_id,
|
|
852
|
+
env=RolloutEnvSpec(env_name=env_name, config={}, seed=i),
|
|
853
|
+
policy=RolloutPolicySpec(policy_name=policy_name, config=policy_cfg),
|
|
854
|
+
ops=ops,
|
|
855
|
+
record=RolloutRecordConfig(
|
|
856
|
+
trajectories=True,
|
|
857
|
+
logprobs=False,
|
|
858
|
+
value=False,
|
|
859
|
+
return_trace=return_trace,
|
|
860
|
+
trace_format=("structured" if return_trace else "compact"),
|
|
861
|
+
),
|
|
862
|
+
on_done="reset",
|
|
863
|
+
safety=RolloutSafetyConfig(max_ops=max_steps * 4, max_time_s=900.0),
|
|
864
|
+
training_session_id=None,
|
|
865
|
+
synth_base_url=synth_base,
|
|
866
|
+
mode=RolloutMode.RL,
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
try:
|
|
870
|
+
click.echo(f">> POST /rollout run_id={run_id} env={env_name} policy={policy_name} url={inference_url_with_cid}")
|
|
871
|
+
click.echo(f" ops={ops[:10]}{'...' if len(ops) > 10 else ''}")
|
|
872
|
+
response = await client.rollout(request)
|
|
873
|
+
except Exception as exc:
|
|
874
|
+
click.echo(f"Rollout[{i}:{g}] failed: {type(exc).__name__}: {exc}", err=True)
|
|
875
|
+
import traceback
|
|
876
|
+
click.echo(f"Traceback: {traceback.format_exc()}", err=True)
|
|
877
|
+
continue
|
|
878
|
+
|
|
879
|
+
successes += 1
|
|
880
|
+
try:
|
|
881
|
+
validate_rollout_response_for_rl(response.model_dump())
|
|
882
|
+
except Exception as vexc:
|
|
883
|
+
click.echo(f" ⚠ RL response validation warning: {vexc}", err=True)
|
|
884
|
+
|
|
885
|
+
pm = response.pipeline_metadata or {}
|
|
886
|
+
inferred_url = pm.get("inference_url") if isinstance(pm, dict) else None
|
|
887
|
+
metrics = response.metrics
|
|
888
|
+
if inferred_url:
|
|
889
|
+
click.echo(f" rollout[{i}:{g}] inference_url: {inferred_url}")
|
|
890
|
+
click.echo(f" rollout[{i}:{g}] episodes={metrics.num_episodes} steps={metrics.num_steps} mean_return={metrics.mean_return:.4f}")
|
|
891
|
+
|
|
892
|
+
total_steps += int(metrics.num_steps)
|
|
893
|
+
if (metrics.mean_return or 0.0) != 0.0:
|
|
894
|
+
nonzero_returns += 1
|
|
895
|
+
if response.trace is not None and isinstance(response.trace, dict):
|
|
896
|
+
v3_traces += 1
|
|
897
|
+
|
|
898
|
+
if i == 0 and g == 0:
|
|
899
|
+
try:
|
|
900
|
+
traj0 = response.trajectories[0]
|
|
901
|
+
step_meta_url = None
|
|
902
|
+
for step in traj0.steps:
|
|
903
|
+
info = getattr(step, "info", None) or {}
|
|
904
|
+
meta = info.get("meta") if isinstance(info, dict) else None
|
|
905
|
+
if isinstance(meta, dict) and meta.get("inference_url"):
|
|
906
|
+
step_meta_url = meta.get("inference_url")
|
|
907
|
+
break
|
|
908
|
+
if step_meta_url:
|
|
909
|
+
click.echo(f" step.meta.inference_url: {str(step_meta_url)[:120]}...")
|
|
910
|
+
except Exception:
|
|
911
|
+
pass
|
|
912
|
+
|
|
913
|
+
try:
|
|
914
|
+
try:
|
|
915
|
+
metrics_dump = response.metrics.model_dump()
|
|
916
|
+
except Exception:
|
|
917
|
+
metrics_dump = {
|
|
918
|
+
"episode_returns": getattr(response.metrics, "episode_returns", None),
|
|
919
|
+
"mean_return": getattr(response.metrics, "mean_return", None),
|
|
920
|
+
"num_steps": getattr(response.metrics, "num_steps", None),
|
|
921
|
+
"num_episodes": getattr(response.metrics, "num_episodes", None),
|
|
922
|
+
"outcome_score": getattr(response.metrics, "outcome_score", None),
|
|
923
|
+
"events_score": getattr(response.metrics, "events_score", None),
|
|
924
|
+
}
|
|
925
|
+
click.echo(" reward.info (metrics): " + str(metrics_dump))
|
|
926
|
+
|
|
927
|
+
try:
|
|
928
|
+
traj = response.trajectories[0]
|
|
929
|
+
step_rewards = []
|
|
930
|
+
all_achievements = set()
|
|
931
|
+
for st in getattr(traj, "steps", []) or []:
|
|
932
|
+
try:
|
|
933
|
+
step_rewards.append(getattr(st, "reward", None))
|
|
934
|
+
except Exception:
|
|
935
|
+
step_rewards.append(None)
|
|
936
|
+
# Extract achievements from step info
|
|
937
|
+
try:
|
|
938
|
+
step_info = getattr(st, "info", None)
|
|
939
|
+
if isinstance(step_info, dict):
|
|
940
|
+
achievements_status = step_info.get("achievements_status")
|
|
941
|
+
if isinstance(achievements_status, dict):
|
|
942
|
+
for ach_name, ach_val in achievements_status.items():
|
|
943
|
+
if ach_val:
|
|
944
|
+
all_achievements.add(str(ach_name))
|
|
945
|
+
except Exception:
|
|
946
|
+
pass
|
|
947
|
+
click.echo(" reward.per_step: " + str(step_rewards))
|
|
948
|
+
if all_achievements:
|
|
949
|
+
click.echo(f" achievements: {sorted(all_achievements)}")
|
|
950
|
+
else:
|
|
951
|
+
click.echo(" achievements: none")
|
|
952
|
+
except Exception:
|
|
953
|
+
pass
|
|
954
|
+
|
|
955
|
+
# Extract and display tool calls from v3 trace
|
|
956
|
+
#
|
|
957
|
+
# IMPORTANT: Tool calls are extracted from the structured v3 trace format.
|
|
958
|
+
# The trace must be requested with return_trace=True for this to work.
|
|
959
|
+
#
|
|
960
|
+
# Trace structure:
|
|
961
|
+
# trace.event_history[] - list of events (policy calls, env steps)
|
|
962
|
+
# ├─ event.call_records[] - LLM calls made during this event
|
|
963
|
+
# ├─ call_record.output_tool_calls[] - tool calls from LLM response
|
|
964
|
+
# ├─ tool_call.name - function name (e.g., "interact_many")
|
|
965
|
+
# └─ tool_call.arguments_json - JSON string of arguments
|
|
966
|
+
#
|
|
967
|
+
# This provides visibility into what actions the policy is taking,
|
|
968
|
+
# which is critical for debugging RL training issues.
|
|
969
|
+
tr = response.trace if isinstance(response.trace, dict) else None
|
|
970
|
+
if tr:
|
|
971
|
+
event_history = tr.get("event_history", [])
|
|
972
|
+
tool_call_count = 0
|
|
973
|
+
|
|
974
|
+
# Extract tool calls from event_history call_records
|
|
975
|
+
if event_history and isinstance(event_history, list):
|
|
976
|
+
for event in event_history:
|
|
977
|
+
if not isinstance(event, dict):
|
|
978
|
+
continue
|
|
979
|
+
# Policy events contain call_records with LLM interactions
|
|
980
|
+
call_records = event.get("call_records")
|
|
981
|
+
if call_records and isinstance(call_records, list):
|
|
982
|
+
for call_record in call_records:
|
|
983
|
+
if isinstance(call_record, dict):
|
|
984
|
+
# Extract tool calls from this LLM call
|
|
985
|
+
output_tool_calls = call_record.get("output_tool_calls", [])
|
|
986
|
+
if output_tool_calls and isinstance(output_tool_calls, list):
|
|
987
|
+
for tc in output_tool_calls:
|
|
988
|
+
if isinstance(tc, dict):
|
|
989
|
+
fn_name = tc.get("name", "unknown")
|
|
990
|
+
fn_args = tc.get("arguments_json", "{}")
|
|
991
|
+
# Display tool call with truncated args for readability
|
|
992
|
+
click.echo(f" TOOL_CALL[{tool_call_count}]: {fn_name}({fn_args[:100]}{'...' if len(fn_args) > 100 else ''})")
|
|
993
|
+
tool_call_count += 1
|
|
994
|
+
|
|
995
|
+
if tool_call_count > 0:
|
|
996
|
+
click.echo(f" ✓ {tool_call_count} tool calls executed")
|
|
997
|
+
else:
|
|
998
|
+
# No tool calls found - might indicate:
|
|
999
|
+
# 1. return_trace=False (trace not requested)
|
|
1000
|
+
# 2. Policy didn't make tool calls (unlikely for most RL tasks)
|
|
1001
|
+
# 3. Trace format mismatch (structure changed)
|
|
1002
|
+
click.echo(" ⚠ No tool calls found in trace")
|
|
1003
|
+
else:
|
|
1004
|
+
click.echo(" ⚠ Trace not available")
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
click.echo(f" trace error: {e}", err=True)
|
|
1007
|
+
|
|
1008
|
+
click.echo("✓ Smoke rollouts complete")
|
|
1009
|
+
denom = num_outer * max(1, int(group_size))
|
|
1010
|
+
click.echo(f" successes={successes}/{denom} total_steps={total_steps} v3_traces={v3_traces}/{denom} nonzero_returns={nonzero_returns}/{denom}")
|
|
1011
|
+
|
|
1012
|
+
if successes == 0:
|
|
1013
|
+
click.echo(" ⚠ All rollouts failed", err=True)
|
|
1014
|
+
return 3
|
|
1015
|
+
if v3_traces < successes:
|
|
1016
|
+
click.echo(" ⚠ Some rollouts missing v3 traces (trace field)", err=True)
|
|
1017
|
+
if total_steps == 0:
|
|
1018
|
+
click.echo(" ⚠ No steps executed; check ops/policy config", err=True)
|
|
1019
|
+
|
|
1020
|
+
return 0
|
|
1021
|
+
|
|
1022
|
+
# Initialize to default; policy/flags may override below
|
|
1023
|
+
inference_url_raw = inference_url_opt or default_infer
|
|
1024
|
+
mock: MockRLTrainer | None = None
|
|
1025
|
+
preset = (inference_policy or "").strip().lower()
|
|
1026
|
+
|
|
1027
|
+
# Respect explicit preset overrides
|
|
1028
|
+
if preset == "mock":
|
|
1029
|
+
use_mock = True
|
|
1030
|
+
elif preset == "gpt-5-nano":
|
|
1031
|
+
if not inference_url_opt:
|
|
1032
|
+
inference_url_raw = default_infer
|
|
1033
|
+
if not model:
|
|
1034
|
+
model = "gpt-5-nano"
|
|
1035
|
+
elif preset == "openai":
|
|
1036
|
+
inference_url_raw = "https://api.openai.com/v1/chat/completions"
|
|
1037
|
+
elif preset == "groq":
|
|
1038
|
+
inference_url_raw = "https://api.groq.com/openai/v1/chat/completions"
|
|
1039
|
+
|
|
1040
|
+
# Start mock proxy only when explicitly requested
|
|
1041
|
+
if use_mock:
|
|
1042
|
+
backend_choice = mock_backend
|
|
1043
|
+
if backend_choice == "openai" and not (
|
|
1044
|
+
os.getenv("SMOKE_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
|
|
1045
|
+
):
|
|
1046
|
+
click.echo(
|
|
1047
|
+
" ⚠ OPENAI_API_KEY not configured; falling back to synthetic mock.",
|
|
1048
|
+
err=True,
|
|
1049
|
+
)
|
|
1050
|
+
backend_choice = "synthetic"
|
|
1051
|
+
mock = MockRLTrainer(port=mock_port, backend=backend_choice)
|
|
1052
|
+
await mock.start()
|
|
1053
|
+
inference_url_raw = f"http://127.0.0.1:{mock.port}"
|
|
1054
|
+
|
|
1055
|
+
try:
|
|
1056
|
+
result = await __do_rollouts(inference_url_raw)
|
|
1057
|
+
finally:
|
|
1058
|
+
if mock is not None:
|
|
1059
|
+
with contextlib.suppress(Exception):
|
|
1060
|
+
await mock.stop()
|
|
1061
|
+
return result
|
|
1062
|
+
async def _run_train_step(
|
|
1063
|
+
*,
|
|
1064
|
+
task_app_url: str,
|
|
1065
|
+
api_key: str | None,
|
|
1066
|
+
env_name_opt: str | None,
|
|
1067
|
+
policy_name: str,
|
|
1068
|
+
model: str,
|
|
1069
|
+
inference_policy: str | None,
|
|
1070
|
+
inference_url_opt: str | None,
|
|
1071
|
+
max_steps: int,
|
|
1072
|
+
return_trace: bool,
|
|
1073
|
+
use_mock: bool,
|
|
1074
|
+
mock_backend: str,
|
|
1075
|
+
mock_port: int,
|
|
1076
|
+
config_path: Path | None,
|
|
1077
|
+
parallel: int,
|
|
1078
|
+
) -> int:
|
|
1079
|
+
import time
|
|
1080
|
+
start = time.perf_counter()
|
|
1081
|
+
|
|
1082
|
+
async def one(seed_idx: int) -> dict[str, Any]:
|
|
1083
|
+
t0 = time.perf_counter()
|
|
1084
|
+
try:
|
|
1085
|
+
code = await _run_smoke_async(
|
|
1086
|
+
task_app_url=task_app_url,
|
|
1087
|
+
api_key=api_key,
|
|
1088
|
+
env_name_opt=env_name_opt,
|
|
1089
|
+
policy_name=policy_name,
|
|
1090
|
+
model=model,
|
|
1091
|
+
inference_policy=inference_policy,
|
|
1092
|
+
inference_url_opt=inference_url_opt,
|
|
1093
|
+
max_steps=max_steps,
|
|
1094
|
+
return_trace=return_trace,
|
|
1095
|
+
use_mock=use_mock,
|
|
1096
|
+
mock_backend=mock_backend,
|
|
1097
|
+
mock_port=mock_port,
|
|
1098
|
+
config_path=config_path,
|
|
1099
|
+
rollouts=1,
|
|
1100
|
+
group_size=1,
|
|
1101
|
+
batch_size=None,
|
|
1102
|
+
)
|
|
1103
|
+
wall_ms = (time.perf_counter() - t0) * 1000.0
|
|
1104
|
+
return {"exit": int(code), "wall_ms": wall_ms}
|
|
1105
|
+
except Exception as e:
|
|
1106
|
+
wall_ms = (time.perf_counter() - t0) * 1000.0
|
|
1107
|
+
return {"exit": 99, "wall_ms": wall_ms, "error": f"{type(e).__name__}: {e}"}
|
|
1108
|
+
|
|
1109
|
+
# Launch N rollouts concurrently
|
|
1110
|
+
tasks = [one(i) for i in range(max(1, int(parallel)))]
|
|
1111
|
+
results = await asyncio.gather(*tasks, return_exceptions=False)
|
|
1112
|
+
total_wall_ms = (time.perf_counter() - start) * 1000.0
|
|
1113
|
+
|
|
1114
|
+
# Print summary
|
|
1115
|
+
def _exit_code(result: dict[str, Any]) -> int:
|
|
1116
|
+
value = result.get("exit")
|
|
1117
|
+
if isinstance(value, int | float):
|
|
1118
|
+
return int(value)
|
|
1119
|
+
if isinstance(value, str) and value.strip():
|
|
1120
|
+
try:
|
|
1121
|
+
return int(value.strip())
|
|
1122
|
+
except ValueError:
|
|
1123
|
+
return 1
|
|
1124
|
+
return 1
|
|
1125
|
+
|
|
1126
|
+
successes = sum(1 for r in results if _exit_code(r) == 0)
|
|
1127
|
+
avg_wall = sum(float(r.get("wall_ms", 0.0)) for r in results) / max(len(results), 1)
|
|
1128
|
+
click.echo("✓ Train-step emulation complete")
|
|
1129
|
+
click.echo(f" parallel={parallel} successes={successes}/{len(results)} total_wall_ms={total_wall_ms:.1f} avg_rollout_wall_ms={avg_wall:.1f}")
|
|
1130
|
+
|
|
1131
|
+
# Show brief failure codes to aid diagnosis
|
|
1132
|
+
if successes < len(results):
|
|
1133
|
+
codes: dict[int, int] = {}
|
|
1134
|
+
for r in results:
|
|
1135
|
+
if not isinstance(r, dict):
|
|
1136
|
+
continue
|
|
1137
|
+
c = _exit_code(r)
|
|
1138
|
+
codes[c] = codes.get(c, 0) + 1
|
|
1139
|
+
click.echo(f" failure_codes={codes}")
|
|
1140
|
+
|
|
1141
|
+
return 0 if successes == len(results) else 3
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
@click.command("smoke")
|
|
1145
|
+
@click.option("--url", "task_app_url", type=str, default=lambda: os.getenv("TASK_APP_URL", "http://localhost:8765"), help="Task app base URL.")
|
|
1146
|
+
@click.option(
|
|
1147
|
+
"--api-key",
|
|
1148
|
+
type=str,
|
|
1149
|
+
default=lambda: os.getenv("ENVIRONMENT_API_KEY", ""),
|
|
1150
|
+
envvar="ENVIRONMENT_API_KEY",
|
|
1151
|
+
help="Environment API key (X-API-Key).",
|
|
1152
|
+
)
|
|
1153
|
+
@click.option("--env-name", type=str, default=None, help="Environment name to roll out (auto-detected if possible).")
|
|
1154
|
+
@click.option("--policy-name", type=str, default="react", help="Policy name to pass to task app.")
|
|
1155
|
+
@click.option("--model", type=str, default="gpt-5-nano", help="Model id to route in inference payload.")
|
|
1156
|
+
@click.option(
|
|
1157
|
+
"--policy",
|
|
1158
|
+
"inference_policy",
|
|
1159
|
+
type=click.Choice(["mock", "gpt-5-nano", "openai", "groq"], case_sensitive=False),
|
|
1160
|
+
default=None,
|
|
1161
|
+
help="Inference route preset (mock, gpt-5-nano via Synth, OpenAI or Groq).",
|
|
1162
|
+
)
|
|
1163
|
+
@click.option("--inference-url", type=str, default=None, help="Override inference URL (default: Synth API chat completions).")
|
|
1164
|
+
@click.option("--max-steps", type=int, default=3, show_default=True, help="Number of agent/env step pairs.")
|
|
1165
|
+
@click.option("--return-trace", is_flag=True, help="Request v3 trace in response if supported.")
|
|
1166
|
+
@click.option("--use-mock/--no-mock", default=True, show_default=True, help="Use local mock inference server (GPT-5-Nano emulation).")
|
|
1167
|
+
@click.option(
|
|
1168
|
+
"--mock-backend",
|
|
1169
|
+
type=click.Choice(["synthetic", "openai"], case_sensitive=False),
|
|
1170
|
+
default="synthetic",
|
|
1171
|
+
show_default=True,
|
|
1172
|
+
help="Mock inference backend: synthetic deterministic tooling or OpenAI passthrough.",
|
|
1173
|
+
)
|
|
1174
|
+
@click.option("--mock-port", type=int, default=0, show_default=True, help="Port for local mock inference server (0 = auto).")
|
|
1175
|
+
@click.option("--config", type=click.Path(exists=True, dir_okay=False, path_type=Path), default=None, help="RL TOML config to derive URL/env/model.")
|
|
1176
|
+
@click.option("--env-file", type=click.Path(exists=True, dir_okay=False, path_type=Path), default=None, help="Path to .env to load before running.")
|
|
1177
|
+
@click.option("--rollouts", type=int, default=1, show_default=True, help="Number of rollouts (seeds 0..N-1).")
|
|
1178
|
+
@click.option("--group-size", type=int, default=1, show_default=True, help="Completions per seed to emulate GRPO grouping.")
|
|
1179
|
+
@click.option("--batch-size", type=int, default=None, help="Alias for rollouts; when set, overrides --rollouts.")
|
|
1180
|
+
@click.option(
|
|
1181
|
+
"--parallel",
|
|
1182
|
+
type=int,
|
|
1183
|
+
default=0,
|
|
1184
|
+
show_default=True,
|
|
1185
|
+
help="Emulate a train step by running this many rollouts concurrently (0 = sequential).",
|
|
1186
|
+
)
|
|
1187
|
+
def command(
|
|
1188
|
+
task_app_url: str,
|
|
1189
|
+
api_key: str,
|
|
1190
|
+
env_name: str | None,
|
|
1191
|
+
policy_name: str,
|
|
1192
|
+
model: str,
|
|
1193
|
+
inference_policy: str | None,
|
|
1194
|
+
inference_url: str | None,
|
|
1195
|
+
max_steps: int,
|
|
1196
|
+
return_trace: bool,
|
|
1197
|
+
use_mock: bool,
|
|
1198
|
+
mock_backend: str,
|
|
1199
|
+
mock_port: int,
|
|
1200
|
+
config: Path | None,
|
|
1201
|
+
env_file: Path | None,
|
|
1202
|
+
rollouts: int,
|
|
1203
|
+
group_size: int,
|
|
1204
|
+
batch_size: int | None,
|
|
1205
|
+
parallel: int,
|
|
1206
|
+
) -> None:
|
|
1207
|
+
"""Smoke-test a Task App by emulating a trainer rollout using GPT-5-Nano.
|
|
1208
|
+
|
|
1209
|
+
This command posts a minimal RL rollout to the task app, with a valid
|
|
1210
|
+
OpenAI-compatible inference URL including a trace correlation id, and
|
|
1211
|
+
validates that the response contains the fields required by the RL trainer
|
|
1212
|
+
(e.g. pipeline_metadata.inference_url and per-step info.meta.inference_url).
|
|
1213
|
+
|
|
1214
|
+
If --config is provided, loads settings from the [smoke] section in the TOML file.
|
|
1215
|
+
CLI arguments override TOML values.
|
|
1216
|
+
"""
|
|
1217
|
+
|
|
1218
|
+
# Load [smoke] section from TOML if config is provided
|
|
1219
|
+
smoke_config = _load_smoke_config(config)
|
|
1220
|
+
|
|
1221
|
+
# Track background processes for cleanup
|
|
1222
|
+
background_procs: list[Any] = []
|
|
1223
|
+
|
|
1224
|
+
try:
|
|
1225
|
+
# Auto-start sqld if configured
|
|
1226
|
+
if smoke_config.get("sqld_auto_start"):
|
|
1227
|
+
sqld_db_path = smoke_config.get("sqld_db_path", "./traces/local.db")
|
|
1228
|
+
sqld_hrana_port = smoke_config.get("sqld_hrana_port", 8080)
|
|
1229
|
+
sqld_http_port = smoke_config.get("sqld_http_port", 8081)
|
|
1230
|
+
|
|
1231
|
+
sqld_proc = _start_sqld_server(
|
|
1232
|
+
db_path=sqld_db_path,
|
|
1233
|
+
hrana_port=sqld_hrana_port,
|
|
1234
|
+
http_port=sqld_http_port,
|
|
1235
|
+
)
|
|
1236
|
+
if sqld_proc:
|
|
1237
|
+
background_procs.append(("sqld", sqld_proc))
|
|
1238
|
+
|
|
1239
|
+
# Auto-start task app if configured
|
|
1240
|
+
task_app_override_url = None
|
|
1241
|
+
if smoke_config.get("task_app_name"):
|
|
1242
|
+
task_app_name = smoke_config["task_app_name"]
|
|
1243
|
+
task_app_port = smoke_config.get("task_app_port", 8765)
|
|
1244
|
+
task_app_env_file = smoke_config.get("task_app_env_file")
|
|
1245
|
+
task_app_force = smoke_config.get("task_app_force", True)
|
|
1246
|
+
|
|
1247
|
+
task_app_proc, task_app_url = _start_task_app_server(
|
|
1248
|
+
task_app_name=task_app_name,
|
|
1249
|
+
port=task_app_port,
|
|
1250
|
+
env_file=task_app_env_file,
|
|
1251
|
+
force=task_app_force,
|
|
1252
|
+
)
|
|
1253
|
+
background_procs.append(("task_app", task_app_proc))
|
|
1254
|
+
task_app_override_url = task_app_url
|
|
1255
|
+
click.echo(f"[smoke] Task app started, will use URL: {task_app_url}", err=True)
|
|
1256
|
+
except Exception as exc:
|
|
1257
|
+
# Cleanup any processes that did start
|
|
1258
|
+
for proc_name, proc in background_procs:
|
|
1259
|
+
if proc and proc.poll() is None:
|
|
1260
|
+
click.echo(f"[smoke] Cleaning up {proc_name}...", err=True)
|
|
1261
|
+
proc.terminate()
|
|
1262
|
+
try:
|
|
1263
|
+
proc.wait(timeout=3)
|
|
1264
|
+
except Exception:
|
|
1265
|
+
proc.kill()
|
|
1266
|
+
|
|
1267
|
+
click.echo(f"[smoke] ERROR: Auto-start failed: {exc}", err=True)
|
|
1268
|
+
raise click.ClickException(f"Auto-start failed: {exc}") from exc
|
|
1269
|
+
|
|
1270
|
+
# Apply TOML defaults (CLI args take precedence)
|
|
1271
|
+
# Override task_url with auto-started task app URL if applicable
|
|
1272
|
+
if task_app_override_url:
|
|
1273
|
+
task_app_url = task_app_override_url
|
|
1274
|
+
# For string/int args: use TOML value if CLI value matches the default
|
|
1275
|
+
ctx = click.get_current_context()
|
|
1276
|
+
|
|
1277
|
+
# Helper to check if a CLI param was explicitly provided or is using default
|
|
1278
|
+
def use_toml_default(param_name: str, cli_value: Any, toml_key: str) -> Any:
|
|
1279
|
+
"""Use TOML value if CLI param is at its default, otherwise use CLI value."""
|
|
1280
|
+
if not smoke_config or toml_key not in smoke_config:
|
|
1281
|
+
return cli_value
|
|
1282
|
+
|
|
1283
|
+
param = next((p for p in ctx.command.params if p.name == param_name), None)
|
|
1284
|
+
if not param:
|
|
1285
|
+
return cli_value
|
|
1286
|
+
|
|
1287
|
+
# Check if value was explicitly provided (not default)
|
|
1288
|
+
# If it matches the default, use TOML value
|
|
1289
|
+
param_default = param.default() if callable(param.default) else param.default
|
|
1290
|
+
if cli_value == param_default:
|
|
1291
|
+
toml_value = smoke_config[toml_key]
|
|
1292
|
+
click.echo(f"[smoke] Using {toml_key}={toml_value} from config", err=True)
|
|
1293
|
+
return toml_value
|
|
1294
|
+
|
|
1295
|
+
return cli_value
|
|
1296
|
+
|
|
1297
|
+
# Apply TOML defaults
|
|
1298
|
+
task_app_url = use_toml_default("task_app_url", task_app_url, "task_url")
|
|
1299
|
+
env_name = use_toml_default("env_name", env_name, "env_name")
|
|
1300
|
+
policy_name = use_toml_default("policy_name", policy_name, "policy_name")
|
|
1301
|
+
model = use_toml_default("model", model, "model")
|
|
1302
|
+
inference_policy = use_toml_default("inference_policy", inference_policy, "policy")
|
|
1303
|
+
inference_url = use_toml_default("inference_url", inference_url, "inference_url")
|
|
1304
|
+
max_steps = use_toml_default("max_steps", max_steps, "max_steps")
|
|
1305
|
+
return_trace = use_toml_default("return_trace", return_trace, "return_trace")
|
|
1306
|
+
use_mock = use_toml_default("use_mock", use_mock, "use_mock")
|
|
1307
|
+
mock_backend = use_toml_default("mock_backend", mock_backend, "mock_backend")
|
|
1308
|
+
mock_port = use_toml_default("mock_port", mock_port, "mock_port")
|
|
1309
|
+
api_key = use_toml_default("api_key", api_key, "api_key")
|
|
1310
|
+
|
|
1311
|
+
# Auto-configure tracing to avoid interactive prompts
|
|
1312
|
+
try:
|
|
1313
|
+
os.environ.setdefault("CI", "true")
|
|
1314
|
+
os.environ.setdefault("SYNTH_TRACING_AUTO_YES", "1")
|
|
1315
|
+
# Derive a default traces directory relative to CWD
|
|
1316
|
+
traces_dir = os.environ.get("SYNTH_TRACES_DIR")
|
|
1317
|
+
if not traces_dir:
|
|
1318
|
+
traces_dir = str((Path.cwd() / "traces" / "v3").resolve())
|
|
1319
|
+
os.environ["SYNTH_TRACES_DIR"] = traces_dir
|
|
1320
|
+
with contextlib.suppress(Exception):
|
|
1321
|
+
Path(traces_dir).mkdir(parents=True, exist_ok=True)
|
|
1322
|
+
_ensure_local_libsql()
|
|
1323
|
+
# Prefer a libsql/turso/sqld URL when provided to enable concurrent writes
|
|
1324
|
+
libsql_url = (
|
|
1325
|
+
os.getenv("TRACING_DB_URL")
|
|
1326
|
+
or os.getenv("LIBSQL_URL")
|
|
1327
|
+
or os.getenv("TURSO_DATABASE_URL")
|
|
1328
|
+
or os.getenv("LIBSQL_HTTP_URL")
|
|
1329
|
+
)
|
|
1330
|
+
if libsql_url:
|
|
1331
|
+
os.environ.setdefault("LIBSQL_URL", libsql_url)
|
|
1332
|
+
|
|
1333
|
+
auth_hint = (
|
|
1334
|
+
os.getenv("TRACING_DB_AUTH_TOKEN")
|
|
1335
|
+
or os.getenv("LIBSQL_AUTH_TOKEN")
|
|
1336
|
+
or os.getenv("TURSO_AUTH_TOKEN")
|
|
1337
|
+
)
|
|
1338
|
+
if auth_hint:
|
|
1339
|
+
os.environ.setdefault("LIBSQL_AUTH_TOKEN", auth_hint)
|
|
1340
|
+
|
|
1341
|
+
resolved_url, resolved_token = resolve_trace_db_settings()
|
|
1342
|
+
os.environ.setdefault("SYNTH_TRACES_DB", resolved_url)
|
|
1343
|
+
if resolved_token and not (
|
|
1344
|
+
os.getenv("LIBSQL_AUTH_TOKEN") or os.getenv("TURSO_AUTH_TOKEN")
|
|
1345
|
+
):
|
|
1346
|
+
os.environ["LIBSQL_AUTH_TOKEN"] = resolved_token
|
|
1347
|
+
|
|
1348
|
+
_refresh_tracing_config()
|
|
1349
|
+
except Exception:
|
|
1350
|
+
pass
|
|
1351
|
+
|
|
1352
|
+
# Load env file(s) before resolving API key
|
|
1353
|
+
try:
|
|
1354
|
+
# Explicit --env-file takes precedence
|
|
1355
|
+
if env_file is not None:
|
|
1356
|
+
try:
|
|
1357
|
+
from dotenv import load_dotenv as _ld
|
|
1358
|
+
_ld(env_file, override=False)
|
|
1359
|
+
except Exception:
|
|
1360
|
+
pass
|
|
1361
|
+
else:
|
|
1362
|
+
# Best-effort auto-discovery from CWD
|
|
1363
|
+
try:
|
|
1364
|
+
from dotenv import find_dotenv as _fd
|
|
1365
|
+
from dotenv import load_dotenv as _ld
|
|
1366
|
+
_ld(_fd(usecwd=True), override=False)
|
|
1367
|
+
except Exception:
|
|
1368
|
+
pass
|
|
1369
|
+
|
|
1370
|
+
# If api_key not passed, try to read from env now
|
|
1371
|
+
if not api_key:
|
|
1372
|
+
api_key = os.getenv("ENVIRONMENT_API_KEY", "")
|
|
1373
|
+
except Exception:
|
|
1374
|
+
pass
|
|
1375
|
+
|
|
1376
|
+
try:
|
|
1377
|
+
if parallel and parallel > 0:
|
|
1378
|
+
exit_code = asyncio.run(
|
|
1379
|
+
_run_train_step(
|
|
1380
|
+
task_app_url=task_app_url,
|
|
1381
|
+
api_key=(api_key or None),
|
|
1382
|
+
env_name_opt=env_name,
|
|
1383
|
+
policy_name=policy_name,
|
|
1384
|
+
model=model,
|
|
1385
|
+
inference_policy=inference_policy,
|
|
1386
|
+
inference_url_opt=inference_url,
|
|
1387
|
+
max_steps=max_steps,
|
|
1388
|
+
return_trace=return_trace,
|
|
1389
|
+
use_mock=use_mock,
|
|
1390
|
+
mock_backend=mock_backend,
|
|
1391
|
+
mock_port=mock_port,
|
|
1392
|
+
config_path=config,
|
|
1393
|
+
parallel=parallel,
|
|
1394
|
+
)
|
|
1395
|
+
)
|
|
1396
|
+
else:
|
|
1397
|
+
exit_code = asyncio.run(
|
|
1398
|
+
_run_smoke_async(
|
|
1399
|
+
task_app_url=task_app_url,
|
|
1400
|
+
api_key=(api_key or None),
|
|
1401
|
+
env_name_opt=env_name,
|
|
1402
|
+
policy_name=policy_name,
|
|
1403
|
+
model=model,
|
|
1404
|
+
inference_policy=inference_policy,
|
|
1405
|
+
inference_url_opt=inference_url,
|
|
1406
|
+
max_steps=max_steps,
|
|
1407
|
+
return_trace=return_trace,
|
|
1408
|
+
use_mock=use_mock,
|
|
1409
|
+
mock_backend=mock_backend,
|
|
1410
|
+
mock_port=mock_port,
|
|
1411
|
+
config_path=config,
|
|
1412
|
+
rollouts=rollouts,
|
|
1413
|
+
group_size=group_size,
|
|
1414
|
+
batch_size=batch_size,
|
|
1415
|
+
)
|
|
1416
|
+
)
|
|
1417
|
+
except KeyboardInterrupt:
|
|
1418
|
+
click.echo("Interrupted", err=True)
|
|
1419
|
+
sys.exit(130)
|
|
1420
|
+
finally:
|
|
1421
|
+
# Cleanup background processes
|
|
1422
|
+
for proc_name, proc in background_procs:
|
|
1423
|
+
if proc and proc.poll() is None:
|
|
1424
|
+
click.echo(f"[smoke] Stopping {proc_name}...", err=True)
|
|
1425
|
+
proc.terminate()
|
|
1426
|
+
try:
|
|
1427
|
+
proc.wait(timeout=5)
|
|
1428
|
+
except Exception:
|
|
1429
|
+
proc.kill()
|
|
1430
|
+
if background_procs:
|
|
1431
|
+
click.echo("[smoke] Background services stopped", err=True)
|
|
1432
|
+
|
|
1433
|
+
sys.exit(exit_code)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def register(cli: click.Group) -> None:
|
|
1437
|
+
cli.add_command(command)
|