synth-ai 0.2.14__py3-none-any.whl → 0.2.17__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.
Potentially problematic release.
This version of synth-ai might be problematic. Click here for more details.
- examples/README.md +1 -0
- examples/analyze_semantic_words.sh +2 -2
- examples/blog_posts/pokemon_vl/README.md +98 -0
- examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +25 -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 +42 -0
- examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
- examples/blog_posts/warming_up_to_rl/README.md +158 -0
- examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -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/train_rl_from_sft.toml +41 -0
- examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
- examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
- examples/multi_step/SFT_README.md +147 -0
- examples/multi_step/configs/crafter_rl_outcome.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +73 -115
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +1 -1
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +1 -1
- 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/verilog_rl_lora.toml +80 -123
- examples/multi_step/convert_traces_to_sft.py +84 -0
- examples/multi_step/run_sft_qwen30b.sh +45 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -2
- examples/qwen_coder/configs/coder_lora_4b.toml +5 -1
- examples/qwen_coder/configs/coder_lora_small.toml +1 -2
- 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 +6 -6
- 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 +79 -0
- examples/rl/configs/rl_from_ft_qwen.toml +37 -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 +2 -2
- examples/sft/README.md +6 -6
- examples/sft/configs/crafter_fft_qwen0p6b.toml +7 -2
- examples/sft/configs/crafter_lora_qwen0p6b.toml +7 -3
- examples/sft/evaluate.py +2 -4
- examples/sft/export_dataset.py +7 -4
- examples/swe/task_app/README.md +33 -3
- examples/swe/task_app/grpo_swe_mini.py +4 -1
- examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
- examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
- examples/swe/task_app/hosted/envs/mini_swe/environment.py +50 -23
- examples/swe/task_app/hosted/inference/openai_client.py +4 -4
- examples/swe/task_app/hosted/policy_routes.py +0 -2
- examples/swe/task_app/hosted/rollout.py +0 -8
- examples/swe/task_app/morph_backend.py +178 -0
- examples/task_apps/crafter/task_app/README.md +1 -1
- examples/task_apps/crafter/task_app/grpo_crafter.py +70 -10
- examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +63 -27
- examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
- examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +48 -50
- examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +75 -36
- examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +31 -15
- examples/task_apps/enron/__init__.py +1 -0
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
- examples/task_apps/math/README.md +1 -2
- examples/task_apps/pokemon_red/README.md +3 -4
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
- examples/task_apps/pokemon_red/task_app.py +36 -5
- examples/task_apps/sokoban/README.md +2 -3
- examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
- examples/vlm/README.md +3 -3
- examples/vlm/configs/crafter_vlm_gpt4o.toml +5 -0
- examples/vlm/crafter_openai_vlm_agent.py +3 -5
- examples/vlm/filter_image_rows.py +1 -1
- examples/vlm/run_crafter_vlm_benchmark.py +2 -2
- examples/warming_up_to_rl/_utils.py +92 -0
- examples/warming_up_to_rl/analyze_trace_db.py +1 -1
- examples/warming_up_to_rl/configs/crafter_fft.toml +5 -0
- examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
- examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
- examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
- examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
- examples/warming_up_to_rl/export_trace_sft.py +174 -60
- examples/warming_up_to_rl/readme.md +63 -132
- examples/warming_up_to_rl/run_fft_and_save.py +1 -1
- examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
- examples/warming_up_to_rl/run_rl_and_save.py +1 -1
- examples/warming_up_to_rl/task_app/README.md +42 -0
- examples/warming_up_to_rl/task_app/grpo_crafter.py +827 -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 +204 -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 +618 -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 +1084 -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 +1861 -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 +62 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
- examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +5 -0
- synth_ai/__init__.py +44 -30
- 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 +144 -7
- synth_ai/api/train/__init__.py +13 -1
- synth_ai/api/train/builders.py +9 -3
- synth_ai/api/train/cli.py +155 -17
- synth_ai/api/train/config_finder.py +18 -11
- synth_ai/api/train/configs/__init__.py +8 -1
- synth_ai/api/train/configs/rl.py +32 -7
- synth_ai/api/train/configs/sft.py +6 -2
- synth_ai/api/train/configs/shared.py +59 -2
- synth_ai/api/train/env_resolver.py +13 -10
- synth_ai/auth/credentials.py +119 -0
- synth_ai/cli/__init__.py +61 -69
- synth_ai/cli/_modal_wrapper.py +7 -5
- synth_ai/cli/_typer_patch.py +0 -2
- synth_ai/cli/_validate_task_app.py +22 -4
- synth_ai/cli/commands/__init__.py +17 -0
- synth_ai/cli/commands/demo/__init__.py +6 -0
- synth_ai/cli/commands/demo/core.py +163 -0
- synth_ai/cli/commands/deploy/__init__.py +23 -0
- synth_ai/cli/commands/deploy/core.py +614 -0
- synth_ai/cli/commands/deploy/errors.py +72 -0
- synth_ai/cli/commands/deploy/validation.py +11 -0
- synth_ai/cli/commands/eval/__init__.py +19 -0
- synth_ai/cli/commands/eval/core.py +1109 -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 +388 -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 +177 -0
- synth_ai/cli/commands/help/core.py +73 -0
- synth_ai/cli/commands/status/__init__.py +64 -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/runs.py +81 -0
- synth_ai/cli/commands/status/subcommands/summary.py +47 -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 +199 -0
- synth_ai/cli/commands/train/judge_validation.py +304 -0
- synth_ai/cli/commands/train/validation.py +443 -0
- synth_ai/cli/demo.py +2 -162
- synth_ai/cli/deploy/__init__.py +28 -0
- synth_ai/cli/deploy/core.py +5 -0
- synth_ai/cli/deploy/errors.py +23 -0
- synth_ai/cli/deploy/validation.py +5 -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 +3 -1
- 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/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/recent.py +2 -1
- 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 +21 -0
- synth_ai/cli/status.py +7 -126
- 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 +110 -1499
- synth_ai/cli/traces.py +1 -1
- 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 +1 -1
- synth_ai/cli/watch.py +1 -1
- synth_ai/demos/__init__.py +10 -0
- synth_ai/demos/core/__init__.py +28 -1
- 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/crafter/grpo_crafter_task_app.py +1 -1
- 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 +702 -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 +0 -1
- synth_ai/environments/examples/bandit/environment.py +0 -1
- synth_ai/environments/examples/red/engine.py +33 -12
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
- synth_ai/environments/examples/red/environment.py +26 -0
- synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
- synth_ai/environments/examples/wordle/environment.py +0 -1
- synth_ai/evals/base.py +16 -5
- synth_ai/evals/client.py +1 -1
- synth_ai/http.py +8 -22
- synth_ai/inference/client.py +1 -1
- synth_ai/judge_schemas.py +4 -5
- synth_ai/learning/client.py +1 -1
- synth_ai/learning/health.py +1 -1
- synth_ai/learning/jobs.py +1 -1
- synth_ai/learning/rl/client.py +4 -2
- synth_ai/learning/rl/env_keys.py +1 -1
- synth_ai/learning/rl/secrets.py +1 -1
- synth_ai/learning/sft/client.py +1 -1
- synth_ai/learning/sft/data.py +407 -4
- synth_ai/learning/validators.py +4 -1
- synth_ai/streaming/__init__.py +29 -0
- synth_ai/streaming/config.py +94 -0
- synth_ai/streaming/handlers.py +469 -0
- synth_ai/streaming/streamer.py +301 -0
- synth_ai/streaming/types.py +95 -0
- synth_ai/task/apps/__init__.py +4 -2
- synth_ai/task/config.py +6 -4
- synth_ai/task/rubrics/__init__.py +1 -2
- synth_ai/task/rubrics/loaders.py +14 -10
- synth_ai/task/rubrics.py +219 -0
- synth_ai/task/trace_correlation_helpers.py +24 -11
- synth_ai/task/tracing_utils.py +14 -3
- synth_ai/task/validators.py +0 -1
- synth_ai/tracing_v3/abstractions.py +3 -3
- synth_ai/tracing_v3/config.py +15 -13
- synth_ai/tracing_v3/constants.py +21 -0
- synth_ai/tracing_v3/db_config.py +3 -1
- synth_ai/tracing_v3/decorators.py +10 -7
- synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
- synth_ai/tracing_v3/migration_helper.py +1 -2
- synth_ai/tracing_v3/session_tracer.py +7 -7
- synth_ai/tracing_v3/storage/base.py +29 -29
- synth_ai/tracing_v3/storage/config.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +8 -9
- synth_ai/tracing_v3/turso/native_manager.py +80 -72
- synth_ai/tracing_v3/utils.py +2 -2
- synth_ai/utils/__init__.py +101 -0
- synth_ai/utils/base_url.py +94 -0
- synth_ai/utils/cli.py +131 -0
- synth_ai/utils/env.py +294 -0
- synth_ai/utils/http.py +172 -0
- synth_ai/utils/modal.py +308 -0
- synth_ai/utils/process.py +212 -0
- synth_ai/utils/prompts.py +39 -0
- synth_ai/utils/sqld.py +122 -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/user_config.py +137 -0
- synth_ai/v0/config/__init__.py +1 -5
- synth_ai/v0/config/base_url.py +1 -7
- synth_ai/v0/tracing/config.py +1 -1
- synth_ai/v0/tracing/decorators.py +1 -1
- synth_ai/v0/tracing/upload.py +1 -1
- synth_ai/v0/tracing_v1/config.py +1 -1
- synth_ai/v0/tracing_v1/decorators.py +1 -1
- synth_ai/v0/tracing_v1/upload.py +1 -1
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/METADATA +91 -32
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/RECORD +341 -154
- synth_ai/cli/man.py +0 -106
- synth_ai/cli/tui.py +0 -57
- synth_ai/compound/cais.py +0 -0
- synth_ai/core/experiment.py +0 -13
- synth_ai/core/system.py +0 -15
- synth_ai/demo_registry.py +0 -295
- synth_ai/handshake.py +0 -109
- synth_ai/tui/__init__.py +0 -5
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -164
- synth_ai/tui/cli/query_experiments_v3.py +0 -164
- synth_ai/tui/dashboard.py +0 -906
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.14.dist-info → synth_ai-0.2.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from click.exceptions import Abort
|
|
13
|
+
from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT
|
|
14
|
+
from synth_ai.task.apps import TaskAppEntry
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_env_files_into_process(paths: Sequence[str]) -> None:
|
|
20
|
+
"""Load key/value pairs from .env-style files into the current process."""
|
|
21
|
+
|
|
22
|
+
for path_str in paths:
|
|
23
|
+
try:
|
|
24
|
+
content = Path(path_str).expanduser().read_text()
|
|
25
|
+
except Exception:
|
|
26
|
+
continue
|
|
27
|
+
for line in content.splitlines():
|
|
28
|
+
if not line or line.lstrip().startswith("#") or "=" not in line:
|
|
29
|
+
continue
|
|
30
|
+
key, value = line.split("=", 1)
|
|
31
|
+
key = key.strip()
|
|
32
|
+
val = value.strip().strip('"').strip("'")
|
|
33
|
+
if not key:
|
|
34
|
+
continue
|
|
35
|
+
current = os.environ.get(key, "")
|
|
36
|
+
if not current.strip():
|
|
37
|
+
os.environ[key] = val
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _collect_env_candidates(base_dir: Path) -> list[Path]:
|
|
41
|
+
cwd = Path.cwd()
|
|
42
|
+
candidates: list[Path] = []
|
|
43
|
+
|
|
44
|
+
candidates.extend(sorted(cwd.glob("**/*.env")))
|
|
45
|
+
|
|
46
|
+
repo_candidates = sorted(REPO_ROOT.glob("**/*.env"))
|
|
47
|
+
for candidate in repo_candidates:
|
|
48
|
+
if candidate not in candidates:
|
|
49
|
+
candidates.append(candidate)
|
|
50
|
+
|
|
51
|
+
if base_dir not in (cwd, REPO_ROOT):
|
|
52
|
+
base_candidates = sorted(base_dir.glob("**/*.env"))
|
|
53
|
+
for candidate in base_candidates:
|
|
54
|
+
if candidate not in candidates:
|
|
55
|
+
candidates.append(candidate)
|
|
56
|
+
|
|
57
|
+
return candidates
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def determine_env_files(entry: TaskAppEntry, user_env_files: Sequence[str]) -> list[Path]:
|
|
61
|
+
"""Resolve env file paths for a task app invocation."""
|
|
62
|
+
|
|
63
|
+
resolved: list[Path] = []
|
|
64
|
+
for candidate in user_env_files:
|
|
65
|
+
path = Path(candidate).expanduser()
|
|
66
|
+
if not path.exists():
|
|
67
|
+
raise click.ClickException(f"Env file not found: {path}")
|
|
68
|
+
resolved.append(path)
|
|
69
|
+
if resolved:
|
|
70
|
+
return resolved
|
|
71
|
+
|
|
72
|
+
candidates = _collect_env_candidates(Path.cwd())
|
|
73
|
+
if not candidates:
|
|
74
|
+
raise click.ClickException("No env file found. Pass --env-file explicitly.")
|
|
75
|
+
|
|
76
|
+
click.echo("Select env file to load:")
|
|
77
|
+
for idx, path in enumerate(candidates, start=1):
|
|
78
|
+
click.echo(f" {idx}) {path.resolve()}")
|
|
79
|
+
choice = click.prompt("Enter choice", type=click.IntRange(1, len(candidates)), default=1)
|
|
80
|
+
selected = candidates[choice - 1]
|
|
81
|
+
return [selected]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_env_paths_for_script(script_path: Path, explicit: Sequence[str]) -> list[Path]:
|
|
85
|
+
"""Resolve env files for a standalone Modal script."""
|
|
86
|
+
|
|
87
|
+
if explicit:
|
|
88
|
+
resolved = []
|
|
89
|
+
for candidate in explicit:
|
|
90
|
+
path = Path(candidate).expanduser()
|
|
91
|
+
if not path.exists():
|
|
92
|
+
raise click.ClickException(f"Env file not found: {path}")
|
|
93
|
+
resolved.append(path)
|
|
94
|
+
return resolved
|
|
95
|
+
|
|
96
|
+
candidates = _collect_env_candidates(script_path.parent.resolve())
|
|
97
|
+
if not candidates:
|
|
98
|
+
created = interactive_create_env(script_path.parent)
|
|
99
|
+
if created is None:
|
|
100
|
+
raise click.ClickException("Env file required (--env-file) for this task app")
|
|
101
|
+
return [created]
|
|
102
|
+
|
|
103
|
+
click.echo("Select env file to load:")
|
|
104
|
+
for idx, path in enumerate(candidates, start=1):
|
|
105
|
+
click.echo(f" {idx}) {path.resolve()}")
|
|
106
|
+
choice = click.prompt("Enter choice", type=click.IntRange(1, len(candidates)), default=1)
|
|
107
|
+
return [candidates[choice - 1]]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ensure_port_free(port: int, host: str, *, force: bool) -> None:
|
|
111
|
+
"""Ensure a TCP port is not in use, optionally killing processes if --force."""
|
|
112
|
+
|
|
113
|
+
import socket # local import to avoid unnecessary dependency during CLI import
|
|
114
|
+
|
|
115
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
116
|
+
in_use = sock.connect_ex((host, port)) == 0
|
|
117
|
+
if not in_use:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
out = subprocess.run(
|
|
122
|
+
["lsof", "-ti", f"TCP:{port}"],
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
check=False,
|
|
126
|
+
)
|
|
127
|
+
pids = [pid for pid in out.stdout.strip().splitlines() if pid]
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
pids = []
|
|
130
|
+
|
|
131
|
+
if not force:
|
|
132
|
+
message = f"Port {port} appears to be in use"
|
|
133
|
+
if pids:
|
|
134
|
+
message += f" (PIDs: {', '.join(pids)})"
|
|
135
|
+
raise click.ClickException(message)
|
|
136
|
+
|
|
137
|
+
for pid in pids:
|
|
138
|
+
try:
|
|
139
|
+
os.kill(int(pid), signal.SIGTERM)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
raise click.ClickException(f"Failed to terminate PID {pid}: {exc}") from exc
|
|
142
|
+
|
|
143
|
+
time.sleep(0.5)
|
|
144
|
+
|
|
145
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
146
|
+
still_in_use = sock.connect_ex((host, port)) == 0
|
|
147
|
+
|
|
148
|
+
if still_in_use:
|
|
149
|
+
for pid in pids:
|
|
150
|
+
try:
|
|
151
|
+
os.kill(int(pid), signal.SIGKILL)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
raise click.ClickException(f"Failed to force terminate PID {pid}: {exc}") from exc
|
|
154
|
+
time.sleep(0.5)
|
|
155
|
+
|
|
156
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
157
|
+
if sock.connect_ex((host, port)) == 0:
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
f"Port {port} is still in use after attempting to terminate processes."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def save_to_env_file(env_path: Path, key: str, value: str) -> None:
|
|
164
|
+
"""Save or update a key/value pair in a .env file."""
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
existing_lines = env_path.read_text().splitlines() if env_path.exists() else []
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
raise click.ClickException(f"Failed to read {env_path}: {exc}") from exc
|
|
170
|
+
|
|
171
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
key_updated = False
|
|
174
|
+
updated_lines: list[str] = []
|
|
175
|
+
for line in existing_lines:
|
|
176
|
+
if line.strip().startswith(f"{key}="):
|
|
177
|
+
updated_lines.append(f"{key}={value}")
|
|
178
|
+
key_updated = True
|
|
179
|
+
else:
|
|
180
|
+
updated_lines.append(line)
|
|
181
|
+
|
|
182
|
+
if key_updated:
|
|
183
|
+
env_path.write_text("\n".join(updated_lines) + "\n")
|
|
184
|
+
click.echo(f"Updated {key} in {env_path}")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
with env_path.open("a", encoding="utf-8") as handle:
|
|
188
|
+
if existing_lines and existing_lines[-1].strip():
|
|
189
|
+
handle.write("\n")
|
|
190
|
+
handle.write(f"{key}={value}\n")
|
|
191
|
+
click.echo(f"Saved {key} to {env_path}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def persist_env_api_key(env_api_key: str, env_paths: Sequence[Path] | None) -> None:
|
|
195
|
+
"""Persist ENVIRONMENT_API_KEY to provided .env files (or demo directory .env)."""
|
|
196
|
+
|
|
197
|
+
targets: list[Path] = []
|
|
198
|
+
seen: set[Path] = set()
|
|
199
|
+
for path in env_paths or ():
|
|
200
|
+
try:
|
|
201
|
+
resolved = Path(path).resolve()
|
|
202
|
+
except Exception:
|
|
203
|
+
continue
|
|
204
|
+
if resolved in seen:
|
|
205
|
+
continue
|
|
206
|
+
seen.add(resolved)
|
|
207
|
+
targets.append(resolved)
|
|
208
|
+
|
|
209
|
+
if not targets:
|
|
210
|
+
demo_dir = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
|
|
211
|
+
targets.append((demo_dir / ".env").resolve())
|
|
212
|
+
|
|
213
|
+
for target in targets:
|
|
214
|
+
save_to_env_file(target, "ENVIRONMENT_API_KEY", env_api_key)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _load_dotenv_if_present(env_file: Path) -> None:
|
|
218
|
+
try:
|
|
219
|
+
from dotenv import load_dotenv
|
|
220
|
+
except Exception:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
with contextlib.suppress(Exception):
|
|
224
|
+
load_dotenv(env_file, override=False)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def validate_required_env_keys() -> None:
|
|
228
|
+
"""Ensure ENVIRONMENT_API_KEY (and optional Groq key) are set, prompting if needed."""
|
|
229
|
+
|
|
230
|
+
demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
|
|
231
|
+
env_file = demo_base / ".env"
|
|
232
|
+
|
|
233
|
+
if env_file.exists():
|
|
234
|
+
_load_dotenv_if_present(env_file)
|
|
235
|
+
|
|
236
|
+
env_api_key = os.environ.get("ENVIRONMENT_API_KEY", "").strip()
|
|
237
|
+
if not env_api_key:
|
|
238
|
+
env_api_key = click.prompt(
|
|
239
|
+
"Please enter your RL Environment API key",
|
|
240
|
+
type=str,
|
|
241
|
+
).strip()
|
|
242
|
+
if not env_api_key:
|
|
243
|
+
raise click.ClickException("RL Environment API key is required to start the server")
|
|
244
|
+
os.environ["ENVIRONMENT_API_KEY"] = env_api_key
|
|
245
|
+
save_to_env_file(env_file, "ENVIRONMENT_API_KEY", env_api_key)
|
|
246
|
+
|
|
247
|
+
groq_api_key = os.environ.get("GROQ_API_KEY", "").strip()
|
|
248
|
+
if not groq_api_key:
|
|
249
|
+
click.echo("\nInference API key configuration:")
|
|
250
|
+
click.echo("This workflow requires a Groq API key.")
|
|
251
|
+
groq_api_key = click.prompt(
|
|
252
|
+
"Groq API key (or press Enter to skip)",
|
|
253
|
+
type=str,
|
|
254
|
+
default="",
|
|
255
|
+
show_default=False,
|
|
256
|
+
).strip()
|
|
257
|
+
if groq_api_key:
|
|
258
|
+
os.environ["GROQ_API_KEY"] = groq_api_key
|
|
259
|
+
save_to_env_file(env_file, "GROQ_API_KEY", groq_api_key)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def print_demo_next_steps_if_applicable() -> None:
|
|
263
|
+
"""Print helpful instructions when operating inside a demo directory."""
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
from synth_ai.demos.demo_task_apps.core import load_demo_dir
|
|
267
|
+
|
|
268
|
+
cwd = Path.cwd().resolve()
|
|
269
|
+
demo_dir = load_demo_dir()
|
|
270
|
+
|
|
271
|
+
if demo_dir and Path(demo_dir).resolve() == cwd and (cwd / "run_local_rollout_traced.py").exists():
|
|
272
|
+
click.echo("\n" + "=" * 60)
|
|
273
|
+
click.echo("Next step: Collect traced rollouts")
|
|
274
|
+
click.echo("=" * 60)
|
|
275
|
+
click.echo("\nIn another terminal, run:")
|
|
276
|
+
click.echo(f" cd {cwd}")
|
|
277
|
+
click.echo(" uv run python run_local_rollout_traced.py")
|
|
278
|
+
click.echo("\nRun this 5-10 times to collect diverse traces.")
|
|
279
|
+
click.echo("=" * 60 + "\n")
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _preview_secret(value: str) -> str:
|
|
285
|
+
if len(value) <= 10:
|
|
286
|
+
return value
|
|
287
|
+
return f"{value[:6]}...{value[-4:]}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def preflight_env_key(env_paths: Sequence[Path] | None = None, *, crash_on_failure: bool = False) -> None:
|
|
291
|
+
"""Ensure ENVIRONMENT_API_KEY exists and attempt to upload it to the backend."""
|
|
292
|
+
|
|
293
|
+
raw_backend = (
|
|
294
|
+
os.environ.get("BACKEND_BASE_URL")
|
|
295
|
+
or os.environ.get("SYNTH_BASE_URL")
|
|
296
|
+
or f"{PROD_BASE_URL_DEFAULT}/api"
|
|
297
|
+
)
|
|
298
|
+
backend_base = raw_backend.rstrip("/")
|
|
299
|
+
if not backend_base.endswith("/api"):
|
|
300
|
+
backend_base += "/api"
|
|
301
|
+
|
|
302
|
+
synth_key = os.environ.get("SYNTH_API_KEY") or ""
|
|
303
|
+
env_api_key = (
|
|
304
|
+
os.environ.get("ENVIRONMENT_API_KEY")
|
|
305
|
+
or os.environ.get("DEV_ENVIRONMENT_API_KEY")
|
|
306
|
+
or ""
|
|
307
|
+
).strip()
|
|
308
|
+
|
|
309
|
+
def _mint_key() -> str | None:
|
|
310
|
+
try:
|
|
311
|
+
from synth_ai.learning.rl.secrets import mint_environment_api_key
|
|
312
|
+
|
|
313
|
+
key = mint_environment_api_key()
|
|
314
|
+
os.environ["ENVIRONMENT_API_KEY"] = key
|
|
315
|
+
os.environ.setdefault("DEV_ENVIRONMENT_API_KEY", key)
|
|
316
|
+
click.echo(f"[preflight] minted ENVIRONMENT_API_KEY ({_preview_secret(key)})")
|
|
317
|
+
return key
|
|
318
|
+
except Exception as exc: # pragma: no cover - defensive fallback
|
|
319
|
+
if crash_on_failure:
|
|
320
|
+
raise click.ClickException(
|
|
321
|
+
f"[CRITICAL] Failed to mint ENVIRONMENT_API_KEY: {exc}"
|
|
322
|
+
) from exc
|
|
323
|
+
click.echo(
|
|
324
|
+
f"[WARN] Failed to mint ENVIRONMENT_API_KEY automatically ({exc}); proceeding without upload"
|
|
325
|
+
)
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
minted = False
|
|
329
|
+
if not env_api_key:
|
|
330
|
+
env_api_key = _mint_key() or ""
|
|
331
|
+
minted = bool(env_api_key)
|
|
332
|
+
|
|
333
|
+
if env_api_key and minted and env_paths:
|
|
334
|
+
persist_env_api_key(env_api_key, env_paths)
|
|
335
|
+
|
|
336
|
+
if not synth_key.strip():
|
|
337
|
+
click.echo("[preflight] SYNTH_API_KEY not set; skipping backend preflight.")
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
if not env_api_key:
|
|
341
|
+
click.echo("[preflight] ENVIRONMENT_API_KEY missing; continuing without verification.")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
import base64
|
|
346
|
+
|
|
347
|
+
import httpx
|
|
348
|
+
from nacl.public import PublicKey, SealedBox
|
|
349
|
+
except Exception: # pragma: no cover - optional deps
|
|
350
|
+
click.echo("[preflight] Optional crypto dependencies missing; skipping upload.")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
with httpx.Client(timeout=15.0, headers={"Authorization": f"Bearer {synth_key}"}) as client:
|
|
355
|
+
click.echo(f"[preflight] backend={backend_base}")
|
|
356
|
+
click.echo("[preflight] fetching public key…")
|
|
357
|
+
rpk = client.get(f"{backend_base.rstrip('/')}/v1/crypto/public-key")
|
|
358
|
+
if rpk.status_code != 200:
|
|
359
|
+
click.echo(f"[preflight] public key fetch failed with {rpk.status_code}; skipping upload")
|
|
360
|
+
return
|
|
361
|
+
pk = (rpk.json() or {}).get("public_key")
|
|
362
|
+
if not pk:
|
|
363
|
+
click.echo("[preflight] no public key returned; skipping upload")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
pk_bytes = base64.b64decode(pk, validate=True)
|
|
367
|
+
sealed_box = SealedBox(PublicKey(pk_bytes))
|
|
368
|
+
ciphertext = sealed_box.encrypt(env_api_key.encode("utf-8"))
|
|
369
|
+
ct_b64 = base64.b64encode(ciphertext).decode()
|
|
370
|
+
payload = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ct_b64}
|
|
371
|
+
|
|
372
|
+
click.echo(f"[preflight] posting to {backend_base.rstrip('/')}/v1/env-keys")
|
|
373
|
+
response = client.post(f"{backend_base.rstrip('/')}/v1/env-keys", json=payload)
|
|
374
|
+
if 200 <= response.status_code < 300:
|
|
375
|
+
click.echo(
|
|
376
|
+
f"✅ ENVIRONMENT_API_KEY uploaded successfully ({_preview_secret(env_api_key)})"
|
|
377
|
+
)
|
|
378
|
+
try:
|
|
379
|
+
ver = client.get(f"{backend_base.rstrip('/')}/v1/env-keys/verify")
|
|
380
|
+
if ver.status_code == 200 and (ver.json() or {}).get("present"):
|
|
381
|
+
click.echo("✅ Key verified in backend")
|
|
382
|
+
else:
|
|
383
|
+
click.echo(
|
|
384
|
+
f"⚠️ Verification returned {ver.status_code}, but upload succeeded - proceeding"
|
|
385
|
+
)
|
|
386
|
+
except Exception as verify_err: # pragma: no cover - verification optional
|
|
387
|
+
click.echo(
|
|
388
|
+
f"⚠️ Verification check failed ({verify_err}), but upload succeeded - proceeding"
|
|
389
|
+
)
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
snippet = response.text[:400] if response.text else ""
|
|
393
|
+
message = (
|
|
394
|
+
f"ENVIRONMENT_API_KEY upload failed with status {response.status_code}"
|
|
395
|
+
+ (f" body={snippet}" if snippet else "")
|
|
396
|
+
)
|
|
397
|
+
if crash_on_failure:
|
|
398
|
+
raise click.ClickException(f"[CRITICAL] {message}")
|
|
399
|
+
click.echo(f"[WARN] {message}; proceeding anyway")
|
|
400
|
+
except Exception as exc: # pragma: no cover - network failures
|
|
401
|
+
message = f"Backend preflight for ENVIRONMENT_API_KEY failed: {exc}"
|
|
402
|
+
if crash_on_failure:
|
|
403
|
+
raise click.ClickException(f"[CRITICAL] {message}") from exc
|
|
404
|
+
click.echo(f"[WARN] {message}; proceeding anyway")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def load_env_values(paths: Sequence[Path], *, allow_empty: bool = False) -> dict[str, str]:
|
|
408
|
+
"""Load values from a sequence of env files, returning a merged dictionary."""
|
|
409
|
+
|
|
410
|
+
values: dict[str, str] = {}
|
|
411
|
+
for path in paths:
|
|
412
|
+
try:
|
|
413
|
+
content = Path(path).read_text(encoding="utf-8")
|
|
414
|
+
except FileNotFoundError:
|
|
415
|
+
continue
|
|
416
|
+
for line in content.splitlines():
|
|
417
|
+
if not line or line.lstrip().startswith("#") or "=" not in line:
|
|
418
|
+
continue
|
|
419
|
+
key, value = line.split("=", 1)
|
|
420
|
+
key = key.strip()
|
|
421
|
+
value = value.strip()
|
|
422
|
+
if key and key not in values:
|
|
423
|
+
values[key] = value
|
|
424
|
+
if not allow_empty and not values:
|
|
425
|
+
raise click.ClickException("No environment values found")
|
|
426
|
+
os.environ.update({k: v for k, v in values.items() if k and v})
|
|
427
|
+
return values
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _parse_env_file(path: Path) -> dict[str, str]:
|
|
431
|
+
data: dict[str, str] = {}
|
|
432
|
+
try:
|
|
433
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
434
|
+
if not line or line.lstrip().startswith("#") or "=" not in line:
|
|
435
|
+
continue
|
|
436
|
+
key, value = line.split("=", 1)
|
|
437
|
+
data[key.strip()] = value.strip()
|
|
438
|
+
except FileNotFoundError:
|
|
439
|
+
pass
|
|
440
|
+
return data
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def interactive_fill_env(env_path: Path) -> Path | None:
|
|
444
|
+
"""Interactively collect credentials and write them to a .env file."""
|
|
445
|
+
|
|
446
|
+
existing = _parse_env_file(env_path) if env_path.exists() else {}
|
|
447
|
+
|
|
448
|
+
def _prompt(label: str, *, default: str = "", required: bool) -> str | None:
|
|
449
|
+
while True:
|
|
450
|
+
try:
|
|
451
|
+
value = click.prompt(
|
|
452
|
+
label,
|
|
453
|
+
default=default,
|
|
454
|
+
show_default=bool(default) or not required,
|
|
455
|
+
).strip()
|
|
456
|
+
except (Abort, EOFError, KeyboardInterrupt):
|
|
457
|
+
click.echo("Aborted env creation.")
|
|
458
|
+
return None
|
|
459
|
+
if value or not required:
|
|
460
|
+
return value
|
|
461
|
+
click.echo("This field is required.")
|
|
462
|
+
|
|
463
|
+
env_default = existing.get("ENVIRONMENT_API_KEY", "").strip()
|
|
464
|
+
env_api_key = _prompt("ENVIRONMENT_API_KEY", default=env_default, required=True)
|
|
465
|
+
if env_api_key is None:
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
synth_default = existing.get("SYNTH_API_KEY", "").strip()
|
|
469
|
+
openai_default = existing.get("OPENAI_API_KEY", "").strip()
|
|
470
|
+
synth_key = _prompt("SYNTH_API_KEY (optional)", default=synth_default, required=False) or ""
|
|
471
|
+
openai_key = _prompt("OPENAI_API_KEY (optional)", default=openai_default, required=False) or ""
|
|
472
|
+
|
|
473
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
474
|
+
env_path.write_text(
|
|
475
|
+
"\n".join(
|
|
476
|
+
[
|
|
477
|
+
f"ENVIRONMENT_API_KEY={env_api_key}",
|
|
478
|
+
f"SYNTH_API_KEY={synth_key}",
|
|
479
|
+
f"OPENAI_API_KEY={openai_key}",
|
|
480
|
+
]
|
|
481
|
+
)
|
|
482
|
+
+ "\n",
|
|
483
|
+
encoding="utf-8",
|
|
484
|
+
)
|
|
485
|
+
click.echo(f"Wrote credentials to {env_path}")
|
|
486
|
+
return env_path
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def interactive_create_env(target_dir: Path) -> Path | None:
|
|
490
|
+
"""Create a .env file for the provided directory if one does not exist."""
|
|
491
|
+
|
|
492
|
+
env_path = (target_dir / ".env").resolve()
|
|
493
|
+
if env_path.exists():
|
|
494
|
+
existing = _parse_env_file(env_path)
|
|
495
|
+
env_api = (existing.get("ENVIRONMENT_API_KEY") or "").strip()
|
|
496
|
+
if env_api:
|
|
497
|
+
return env_path
|
|
498
|
+
click.echo(f"Existing {env_path} is missing ENVIRONMENT_API_KEY. Let's update it.")
|
|
499
|
+
return interactive_fill_env(env_path)
|
|
500
|
+
|
|
501
|
+
click.echo("No .env found for this task app. Let's create one.")
|
|
502
|
+
return interactive_fill_env(env_path)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def ensure_env_values(env_paths: list[Path], fallback_dir: Path) -> None:
|
|
506
|
+
"""Ensure required env values are present, prompting to create .env if needed."""
|
|
507
|
+
|
|
508
|
+
if (os.environ.get("ENVIRONMENT_API_KEY") or "").strip():
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
target = env_paths[0] if env_paths else (fallback_dir / ".env").resolve()
|
|
512
|
+
result = interactive_fill_env(target)
|
|
513
|
+
if result is None:
|
|
514
|
+
raise click.ClickException("ENVIRONMENT_API_KEY required to continue")
|
|
515
|
+
|
|
516
|
+
load_env_values([result])
|
|
517
|
+
if not (os.environ.get("ENVIRONMENT_API_KEY") or "").strip():
|
|
518
|
+
raise click.ClickException("Failed to load ENVIRONMENT_API_KEY from generated .env")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .core import command, get_command
|
|
4
|
+
from .errors import ModalServeCliError
|
|
5
|
+
from .validation import validate_modal_serve_options
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"command",
|
|
9
|
+
"get_command",
|
|
10
|
+
"ModalServeCliError",
|
|
11
|
+
"validate_modal_serve_options",
|
|
12
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from synth_ai.cli.task_apps import task_app_group
|
|
5
|
+
|
|
6
|
+
__all__ = ["command", "get_command"]
|
|
7
|
+
|
|
8
|
+
command = task_app_group.commands.get("modal-serve")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_command() -> click.Command:
|
|
12
|
+
if command is None:
|
|
13
|
+
raise RuntimeError("modal-serve command is not registered on task_app_group")
|
|
14
|
+
return command
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import MutableMapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = ["validate_modal_serve_options"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_modal_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
|
|
10
|
+
"""Validate parameters passed to the modal-serve CLI command."""
|
|
11
|
+
return options
|
synth_ai/cli/recent.py
CHANGED
|
@@ -12,12 +12,13 @@ from rich import box
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
|
-
from
|
|
15
|
+
from ._storage import load_storage
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
18
18
|
import pandas as pd
|
|
19
19
|
else:
|
|
20
20
|
pd = Any # type: ignore[assignment]
|
|
21
|
+
|
|
21
22
|
def _fmt_int(v: Any) -> str:
|
|
22
23
|
try:
|
|
23
24
|
return f"{int(v):,}"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .core import command, get_command
|
|
4
|
+
from .errors import ServeCliError
|
|
5
|
+
from .validation import validate_serve_options
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"command",
|
|
9
|
+
"get_command",
|
|
10
|
+
"ServeCliError",
|
|
11
|
+
"validate_serve_options",
|
|
12
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from synth_ai.cli.task_apps import task_app_group
|
|
5
|
+
|
|
6
|
+
__all__ = ["command", "get_command"]
|
|
7
|
+
|
|
8
|
+
command = task_app_group.commands.get("serve")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_command() -> click.Command:
|
|
12
|
+
if command is None:
|
|
13
|
+
raise RuntimeError("Serve command is not registered on task_app_group")
|
|
14
|
+
return command
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import MutableMapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = ["validate_serve_options"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_serve_options(options: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
|
|
10
|
+
"""Validate parameters passed to the serve CLI command."""
|
|
11
|
+
return options
|
synth_ai/cli/setup.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Instructions on Docs → https://usesynth.ai/cli-cmds/setup"""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from synth_ai.auth.credentials import fetch_credentials_from_web_browser_session
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command("setup")
|
|
8
|
+
@click.option(
|
|
9
|
+
"--local",
|
|
10
|
+
is_flag=True,
|
|
11
|
+
help="Load your credentials from your local machine"
|
|
12
|
+
)
|
|
13
|
+
@click.option(
|
|
14
|
+
"--dev",
|
|
15
|
+
is_flag=True
|
|
16
|
+
)
|
|
17
|
+
def setup_cmd(local: bool, dev: bool) -> None:
|
|
18
|
+
fetch_credentials_from_web_browser_session(
|
|
19
|
+
browser=not local,
|
|
20
|
+
prod=not dev
|
|
21
|
+
)
|