synth-ai 0.2.9.dev7__py3-none-any.whl → 0.2.9.dev8__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/__init__.py +16 -0
- examples/crafter_debug_render.py +8 -11
- examples/qwen_coder/README.md +102 -0
- examples/qwen_coder/_shared.py +113 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
- examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
- examples/qwen_coder/configs/coder_lora_small.toml +58 -0
- examples/qwen_coder/generate_dataset.py +98 -0
- examples/qwen_coder/infer_ft_smoke.py +64 -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 +18 -0
- examples/qwen_coder/scripts/train_coder_30b.sh +21 -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 +38 -0
- examples/qwen_coder/validate_jsonl.py +59 -0
- examples/rl/run_eval.py +36 -37
- examples/rl/run_rl_and_save.py +5 -5
- examples/rl/task_app/math_single_step.py +65 -43
- examples/rl/task_app/math_task_app.py +3 -3
- examples/sft/README.md +139 -0
- examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
- examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
- examples/sft/evaluate.py +117 -0
- examples/sft/export_dataset.py +117 -0
- examples/sft/generate_traces.py +162 -0
- examples/swe/__init__.py +12 -0
- examples/swe/task_app/README.md +105 -0
- examples/swe/task_app/__init__.py +2 -0
- examples/swe/task_app/grpo_swe_mini.py +571 -0
- examples/swe/task_app/grpo_swe_mini_task_app.py +136 -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 +1164 -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 +618 -0
- examples/swe/task_app/hosted/main.py +100 -0
- examples/swe/task_app/hosted/policy_routes.py +1079 -0
- examples/swe/task_app/hosted/registry.py +195 -0
- examples/swe/task_app/hosted/rollout.py +1869 -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 +137 -0
- examples/swe/task_app/hosted/utils.py +62 -0
- examples/vlm/README.md +68 -0
- examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
- examples/vlm/crafter_image_only_agent.py +207 -0
- examples/vlm/crafter_openai_vlm_agent.py +277 -0
- examples/vlm/filter_image_rows.py +63 -0
- examples/vlm/run_crafter_vlm_benchmark.py +316 -0
- examples/warming_up_to_rl/analyze_trace_db.py +5 -5
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
- examples/warming_up_to_rl/export_trace_sft.py +78 -21
- examples/warming_up_to_rl/groq_test.py +4 -4
- examples/warming_up_to_rl/manage_secrets.py +13 -18
- examples/warming_up_to_rl/run_eval.py +42 -44
- examples/warming_up_to_rl/run_fft_and_save.py +11 -16
- examples/warming_up_to_rl/run_local_rollout.py +1 -3
- examples/warming_up_to_rl/run_local_rollout_modal.py +2 -4
- examples/warming_up_to_rl/run_local_rollout_parallel.py +1 -4
- examples/warming_up_to_rl/run_local_rollout_traced.py +3 -5
- examples/warming_up_to_rl/run_rl_and_save.py +5 -6
- examples/warming_up_to_rl/run_rollout_remote.py +8 -10
- examples/warming_up_to_rl/task_app/README.md +6 -2
- examples/warming_up_to_rl/task_app/grpo_crafter.py +234 -35
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +2 -3
- examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
- examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +131 -114
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +101 -41
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +73 -51
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +14 -6
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +16 -16
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +32 -34
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +94 -31
- examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
- examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +303 -203
- examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
- examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +328 -225
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +13 -13
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +1 -0
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
- synth/__init__.py +14 -0
- synth_ai/__init__.py +26 -4
- synth_ai/api/models/supported.py +376 -0
- synth_ai/api/train/builders.py +128 -21
- synth_ai/api/train/cli.py +80 -64
- synth_ai/api/train/config_finder.py +7 -2
- synth_ai/api/train/env_resolver.py +1 -1
- synth_ai/api/train/pollers.py +2 -1
- synth_ai/api/train/supported_algos.py +139 -0
- synth_ai/api/train/task_app.py +1 -2
- synth_ai/api/train/utils.py +13 -44
- synth_ai/cli/__init__.py +8 -0
- synth_ai/cli/_modal_wrapper.py +28 -0
- synth_ai/cli/_typer_patch.py +49 -0
- synth_ai/cli/balance.py +1 -2
- synth_ai/cli/calc.py +1 -1
- synth_ai/cli/demo.py +2 -1
- synth_ai/cli/recent.py +2 -2
- synth_ai/cli/rl_demo.py +2 -1
- synth_ai/cli/root.py +11 -13
- synth_ai/cli/status.py +2 -2
- synth_ai/cli/task_apps.py +529 -179
- synth_ai/cli/traces.py +6 -4
- synth_ai/cli/watch.py +12 -18
- synth_ai/demo_registry.py +1 -1
- synth_ai/demos/core/cli.py +36 -43
- synth_ai/demos/demo_task_apps/__init__.py +3 -3
- synth_ai/demos/demo_task_apps/core.py +17 -25
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +3 -4
- synth_ai/demos/demo_task_apps/math/app.py +2 -1
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -4
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +16 -18
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
- synth_ai/environments/examples/crafter_classic/environment.py +76 -1
- synth_ai/environments/reproducibility/tree.py +2 -5
- synth_ai/environments/service/app.py +11 -12
- synth_ai/environments/service/core_routes.py +4 -7
- 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/handshake.py +9 -9
- synth_ai/http.py +1 -1
- synth_ai/http_client.py +18 -10
- synth_ai/inference/client.py +15 -5
- synth_ai/jobs/client.py +78 -83
- synth_ai/learning/__init__.py +41 -6
- synth_ai/learning/algorithms.py +14 -0
- synth_ai/learning/client.py +91 -24
- synth_ai/learning/config.py +2 -38
- synth_ai/learning/ft_client.py +4 -59
- synth_ai/learning/health.py +5 -6
- synth_ai/learning/jobs.py +31 -47
- synth_ai/{rl → learning/rl}/__init__.py +14 -4
- synth_ai/learning/rl/client.py +267 -0
- synth_ai/learning/rl/config.py +31 -0
- synth_ai/{rl → learning/rl}/contracts.py +5 -8
- synth_ai/{rl → learning/rl}/env_keys.py +39 -15
- synth_ai/learning/rl/secrets.py +13 -0
- synth_ai/learning/rl_client.py +2 -281
- 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 +295 -0
- synth_ai/learning/sse.py +25 -24
- synth_ai/learning/validators.py +25 -28
- synth_ai/lm/__init__.py +21 -47
- synth_ai/main.py +4 -0
- synth_ai/task/__init__.py +25 -27
- synth_ai/task/apps/__init__.py +7 -8
- synth_ai/task/auth.py +8 -8
- synth_ai/task/client.py +14 -14
- synth_ai/task/contracts.py +36 -35
- synth_ai/task/datasets.py +6 -5
- synth_ai/task/errors.py +10 -10
- synth_ai/task/health.py +17 -9
- synth_ai/task/json.py +58 -23
- synth_ai/task/proxy.py +13 -9
- synth_ai/task/rubrics.py +16 -15
- synth_ai/task/server.py +12 -12
- synth_ai/task/tracing_utils.py +4 -4
- synth_ai/task/vendors.py +5 -6
- synth_ai/tracing_v3/__init__.py +2 -0
- synth_ai/tracing_v3/abstractions.py +21 -4
- synth_ai/tracing_v3/decorators.py +18 -16
- synth_ai/tracing_v3/hooks.py +5 -5
- synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
- synth_ai/tracing_v3/session_tracer.py +40 -14
- synth_ai/tracing_v3/storage/base.py +85 -0
- synth_ai/tracing_v3/storage/config.py +21 -8
- synth_ai/tracing_v3/storage/factory.py +10 -7
- synth_ai/tracing_v3/storage/utils.py +4 -2
- synth_ai/tracing_v3/turso/daemon.py +7 -2
- synth_ai/tracing_v3/turso/models.py +2 -2
- synth_ai/tracing_v3/turso/native_manager.py +1173 -0
- synth_ai/tracing_v3/utils.py +4 -4
- synth_ai/v0/api/__init__.py +8 -0
- synth_ai/v0/api/models/__init__.py +8 -0
- synth_ai/v0/api/models/supported.py +8 -0
- synth_ai/v0/config/__init__.py +15 -0
- synth_ai/v0/config/base_url.py +12 -0
- synth_ai/v0/lm/__init__.py +51 -0
- synth_ai/{lm → v0/lm}/caching/ephemeral.py +2 -2
- synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
- synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
- synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
- synth_ai/{lm → v0/lm}/config.py +6 -1
- synth_ai/{lm → v0/lm}/core/all.py +9 -9
- synth_ai/{lm → v0/lm}/core/main.py +6 -6
- synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
- synth_ai/{lm → v0/lm}/core/synth_models.py +2 -14
- synth_ai/{lm → v0/lm}/core/vendor_clients.py +2 -2
- synth_ai/{lm → v0/lm}/overrides.py +2 -2
- synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
- synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
- synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
- synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +9 -9
- synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
- synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
- synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +10 -10
- synth_ai/{lm → v0/lm}/vendors/openai_standard.py +8 -8
- synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +2 -2
- synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +3 -3
- synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
- synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
- synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
- synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
- synth_ai/{lm → v0/lm}/vendors/synth_client.py +1 -1
- synth_ai/v0/tracing_v3/__init__.py +10 -0
- synth_ai/v0/tracing_v3/abstractions.py +3 -0
- synth_ai/v0/tracing_v3/decorators.py +3 -0
- synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
- synth_ai/v0/tracing_v3/session_tracer.py +3 -0
- synth_ai-0.2.9.dev8.dist-info/METADATA +191 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev8.dist-info}/RECORD +268 -238
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev8.dist-info}/top_level.txt +1 -0
- examples/common_old/backend.py +0 -20
- examples/evals_old/README.md +0 -98
- examples/evals_old/__init__.py +0 -6
- examples/evals_old/compare_models.py +0 -1038
- examples/evals_old/example_log.md +0 -145
- examples/evals_old/run_demo.sh +0 -126
- examples/evals_old/trace_analysis.py +0 -270
- examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
- examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
- examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
- examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -243
- examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
- examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
- examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
- examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
- examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
- examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -119
- examples/finetuning_old/synth_qwen_v1/README.md +0 -68
- examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
- examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -243
- examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
- examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
- examples/finetuning_old/synth_qwen_v1/infer.py +0 -36
- examples/finetuning_old/synth_qwen_v1/poll.py +0 -46
- examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
- examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
- examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1933
- examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -210
- examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -237
- examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
- examples/finetuning_old/synth_qwen_v1/util.py +0 -152
- examples/rl_old/task_app.py +0 -1131
- examples/warming_up_to_rl/old/event_rewards.md +0 -234
- examples/warming_up_to_rl/old/notes.md +0 -73
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
- synth_ai/experimental/synth_oss.py +0 -445
- synth_ai/learning/filtering.py +0 -0
- 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 -211
- synth_ai/learning/prompts/mipro.py +0 -289
- synth_ai/learning/prompts/random_search.py +0 -249
- synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
- synth_ai/learning/prompts/run_random_search_banking77.py +0 -329
- 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/__init__.py +0 -25
- synth_ai/tracing_v3/turso/manager.py +0 -838
- synth_ai/zyk/__init__.py +0 -30
- synth_ai-0.2.9.dev7.dist-info/METADATA +0 -131
- /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
- /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
- /synth_ai/{lm → v0/lm}/constants.py +0 -0
- /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/core/exceptions.py +0 -0
- /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
- /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
- /synth_ai/{lm → v0/lm}/injection.py +0 -0
- /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
- /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
- /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
- /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
- /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
- /synth_ai/{lm → v0/lm}/warmup.py +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev8.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev8.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.9.dev8.dist-info}/licenses/LICENSE +0 -0
synth_ai/cli/task_apps.py
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
import asyncio
|
|
4
5
|
import contextlib
|
|
5
|
-
import functools
|
|
6
6
|
import hashlib
|
|
7
7
|
import importlib
|
|
8
8
|
import importlib.util
|
|
9
9
|
import inspect
|
|
10
|
-
import os
|
|
11
10
|
import json
|
|
12
|
-
import
|
|
11
|
+
import os
|
|
13
12
|
import shutil
|
|
13
|
+
import signal
|
|
14
14
|
import subprocess
|
|
15
15
|
import sys
|
|
16
16
|
import tempfile
|
|
17
|
+
import textwrap
|
|
18
|
+
import types
|
|
19
|
+
from collections.abc import Callable, Iterable, Iterator, Sequence
|
|
17
20
|
from dataclasses import dataclass
|
|
18
21
|
from pathlib import Path
|
|
19
|
-
import
|
|
20
|
-
from typing import Any, Callable, Iterable, Sequence, Iterator, cast
|
|
22
|
+
from typing import Any, cast
|
|
21
23
|
|
|
22
24
|
try: # Python 3.11+
|
|
23
25
|
import tomllib as _toml
|
|
@@ -26,9 +28,10 @@ except Exception: # pragma: no cover - fallback
|
|
|
26
28
|
import uuid
|
|
27
29
|
|
|
28
30
|
import click
|
|
29
|
-
|
|
30
|
-
from synth_ai.task.server import run_task_app, create_task_app
|
|
31
|
+
|
|
31
32
|
from synth_ai.config.base_url import PROD_BASE_URL_DEFAULT
|
|
33
|
+
from synth_ai.task.apps import ModalDeploymentConfig, TaskAppConfig, TaskAppEntry, registry
|
|
34
|
+
from synth_ai.task.server import create_task_app, run_task_app
|
|
32
35
|
|
|
33
36
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
34
37
|
|
|
@@ -213,7 +216,7 @@ def _discover_eval_config_paths() -> list[Path]:
|
|
|
213
216
|
if not root.exists() or not root.is_dir():
|
|
214
217
|
continue
|
|
215
218
|
try:
|
|
216
|
-
|
|
219
|
+
root = root.resolve()
|
|
217
220
|
except Exception:
|
|
218
221
|
continue
|
|
219
222
|
for path in root.rglob("*.toml"):
|
|
@@ -255,11 +258,9 @@ class _TaskAppConfigVisitor(ast.NodeVisitor):
|
|
|
255
258
|
|
|
256
259
|
def _is_task_app_config_call(node: ast.Call) -> bool:
|
|
257
260
|
func = node.func
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return True
|
|
262
|
-
return False
|
|
261
|
+
return (isinstance(func, ast.Name) and func.id == "TaskAppConfig") or (
|
|
262
|
+
isinstance(func, ast.Attribute) and func.attr == "TaskAppConfig"
|
|
263
|
+
)
|
|
263
264
|
|
|
264
265
|
|
|
265
266
|
def _extract_app_id(node: ast.Call) -> str | None:
|
|
@@ -279,11 +280,9 @@ def _extract_app_id(node: ast.Call) -> str | None:
|
|
|
279
280
|
|
|
280
281
|
def _is_register_task_app_call(node: ast.Call) -> bool:
|
|
281
282
|
func = node.func
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return True
|
|
286
|
-
return False
|
|
283
|
+
return (isinstance(func, ast.Name) and func.id == "register_task_app") or (
|
|
284
|
+
isinstance(func, ast.Attribute) and func.attr == "register_task_app"
|
|
285
|
+
)
|
|
287
286
|
|
|
288
287
|
|
|
289
288
|
def _extract_register_app_id(node: ast.Call) -> str | None:
|
|
@@ -555,11 +554,7 @@ def _choice_matches_identifier(choice: AppChoice, identifier: str) -> bool:
|
|
|
555
554
|
ident = identifier.strip()
|
|
556
555
|
if not ident:
|
|
557
556
|
return False
|
|
558
|
-
|
|
559
|
-
return True
|
|
560
|
-
if ident in choice.aliases:
|
|
561
|
-
return True
|
|
562
|
-
return False
|
|
557
|
+
return ident == choice.app_id or ident == choice.label or ident in choice.aliases
|
|
563
558
|
|
|
564
559
|
|
|
565
560
|
def _choice_has_modal_support(choice: AppChoice) -> bool:
|
|
@@ -581,26 +576,23 @@ def _has_modal_support_in_file(path: Path) -> bool:
|
|
|
581
576
|
|
|
582
577
|
# Look for ModalDeploymentConfig in register_task_app calls
|
|
583
578
|
for node in ast.walk(tree):
|
|
584
|
-
if isinstance(node, ast.Call):
|
|
585
|
-
if
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
579
|
+
if isinstance(node, ast.Call) and _is_register_task_app_call(node):
|
|
580
|
+
# Check if the entry has modal=ModalDeploymentConfig(...)
|
|
581
|
+
for kw in node.keywords:
|
|
582
|
+
if kw.arg == "entry" and isinstance(kw.value, ast.Call):
|
|
583
|
+
entry_call = kw.value
|
|
584
|
+
if (
|
|
585
|
+
isinstance(entry_call.func, ast.Name)
|
|
586
|
+
and entry_call.func.id == "TaskAppEntry"
|
|
587
|
+
):
|
|
588
|
+
for entry_kw in entry_call.keywords:
|
|
589
|
+
if entry_kw.arg == "modal" and isinstance(entry_kw.value, ast.Call):
|
|
590
|
+
modal_call = entry_kw.value
|
|
591
|
+
if (
|
|
592
|
+
isinstance(modal_call.func, ast.Name)
|
|
593
|
+
and modal_call.func.id == "ModalDeploymentConfig"
|
|
597
594
|
):
|
|
598
|
-
|
|
599
|
-
if (
|
|
600
|
-
isinstance(modal_call.func, ast.Name)
|
|
601
|
-
and modal_call.func.id == "ModalDeploymentConfig"
|
|
602
|
-
):
|
|
603
|
-
return True
|
|
595
|
+
return True
|
|
604
596
|
except Exception:
|
|
605
597
|
pass
|
|
606
598
|
return False
|
|
@@ -614,27 +606,24 @@ def _extract_modal_config_from_file(path: Path) -> ModalDeploymentConfig | None:
|
|
|
614
606
|
|
|
615
607
|
# Look for ModalDeploymentConfig in register_task_app calls
|
|
616
608
|
for node in ast.walk(tree):
|
|
617
|
-
if isinstance(node, ast.Call):
|
|
618
|
-
if
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
609
|
+
if isinstance(node, ast.Call) and _is_register_task_app_call(node):
|
|
610
|
+
# Check if the entry has modal=ModalDeploymentConfig(...)
|
|
611
|
+
for kw in node.keywords:
|
|
612
|
+
if kw.arg == "entry" and isinstance(kw.value, ast.Call):
|
|
613
|
+
entry_call = kw.value
|
|
614
|
+
if (
|
|
615
|
+
isinstance(entry_call.func, ast.Name)
|
|
616
|
+
and entry_call.func.id == "TaskAppEntry"
|
|
617
|
+
):
|
|
618
|
+
for entry_kw in entry_call.keywords:
|
|
619
|
+
if entry_kw.arg == "modal" and isinstance(entry_kw.value, ast.Call):
|
|
620
|
+
modal_call = entry_kw.value
|
|
621
|
+
if (
|
|
622
|
+
isinstance(modal_call.func, ast.Name)
|
|
623
|
+
and modal_call.func.id == "ModalDeploymentConfig"
|
|
630
624
|
):
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
isinstance(modal_call.func, ast.Name)
|
|
634
|
-
and modal_call.func.id == "ModalDeploymentConfig"
|
|
635
|
-
):
|
|
636
|
-
# Extract the arguments to ModalDeploymentConfig
|
|
637
|
-
return _build_modal_config_from_ast(modal_call)
|
|
625
|
+
# Extract the arguments to ModalDeploymentConfig
|
|
626
|
+
return _build_modal_config_from_ast(modal_call)
|
|
638
627
|
except Exception:
|
|
639
628
|
pass
|
|
640
629
|
return None
|
|
@@ -648,35 +637,35 @@ def _build_modal_config_from_ast(modal_call: ast.Call) -> ModalDeploymentConfig
|
|
|
648
637
|
for kw in modal_call.keywords:
|
|
649
638
|
if kw.arg and isinstance(kw.value, ast.Constant):
|
|
650
639
|
kwargs[kw.arg] = kw.value.value
|
|
651
|
-
elif kw.arg == "pip_packages" and isinstance(kw.value,
|
|
640
|
+
elif kw.arg == "pip_packages" and isinstance(kw.value, ast.List | ast.Tuple):
|
|
652
641
|
# Handle pip_packages list/tuple
|
|
653
642
|
packages = []
|
|
654
643
|
for elt in kw.value.elts:
|
|
655
644
|
if isinstance(elt, ast.Constant):
|
|
656
645
|
packages.append(elt.value)
|
|
657
646
|
kwargs[kw.arg] = tuple(packages)
|
|
658
|
-
elif kw.arg == "extra_local_dirs" and isinstance(kw.value,
|
|
647
|
+
elif kw.arg == "extra_local_dirs" and isinstance(kw.value, ast.List | ast.Tuple):
|
|
659
648
|
# Handle extra_local_dirs list/tuple of tuples
|
|
660
649
|
dirs = []
|
|
661
650
|
for elt in kw.value.elts:
|
|
662
|
-
if isinstance(elt,
|
|
651
|
+
if isinstance(elt, ast.List | ast.Tuple) and len(elt.elts) == 2:
|
|
663
652
|
src = elt.elts[0].value if isinstance(elt.elts[0], ast.Constant) else None
|
|
664
653
|
dst = elt.elts[1].value if isinstance(elt.elts[1], ast.Constant) else None
|
|
665
654
|
if src and dst:
|
|
666
655
|
dirs.append((src, dst))
|
|
667
656
|
kwargs[kw.arg] = tuple(dirs)
|
|
668
|
-
elif kw.arg == "secret_names" and isinstance(kw.value,
|
|
657
|
+
elif kw.arg == "secret_names" and isinstance(kw.value, ast.List | ast.Tuple):
|
|
669
658
|
# Handle secret_names list/tuple
|
|
670
659
|
secrets = []
|
|
671
660
|
for elt in kw.value.elts:
|
|
672
661
|
if isinstance(elt, ast.Constant):
|
|
673
662
|
secrets.append(elt.value)
|
|
674
663
|
kwargs[kw.arg] = tuple(secrets)
|
|
675
|
-
elif kw.arg == "volume_mounts" and isinstance(kw.value,
|
|
664
|
+
elif kw.arg == "volume_mounts" and isinstance(kw.value, ast.List | ast.Tuple):
|
|
676
665
|
# Handle volume_mounts list/tuple of tuples
|
|
677
666
|
mounts = []
|
|
678
667
|
for elt in kw.value.elts:
|
|
679
|
-
if isinstance(elt,
|
|
668
|
+
if isinstance(elt, ast.List | ast.Tuple) and len(elt.elts) == 2:
|
|
680
669
|
name = elt.elts[0].value if isinstance(elt.elts[0], ast.Constant) else None
|
|
681
670
|
mount = elt.elts[1].value if isinstance(elt.elts[1], ast.Constant) else None
|
|
682
671
|
if name and mount:
|
|
@@ -724,8 +713,8 @@ def _prompt_user_for_choice(choices: list[AppChoice]) -> AppChoice:
|
|
|
724
713
|
click.echo(_format_choice(choice, idx))
|
|
725
714
|
try:
|
|
726
715
|
response = click.prompt("Enter choice", default="1", type=str).strip() or "1"
|
|
727
|
-
except (click.exceptions.Abort, EOFError, KeyboardInterrupt):
|
|
728
|
-
raise click.ClickException("Task app selection cancelled by user")
|
|
716
|
+
except (click.exceptions.Abort, EOFError, KeyboardInterrupt) as exc:
|
|
717
|
+
raise click.ClickException("Task app selection cancelled by user") from exc
|
|
729
718
|
if not response.isdigit():
|
|
730
719
|
raise click.ClickException("Selection must be a number")
|
|
731
720
|
index = int(response)
|
|
@@ -870,7 +859,11 @@ def _load_entry_from_path(
|
|
|
870
859
|
continue
|
|
871
860
|
if isinstance(attr, TaskAppConfig) and attr.app_id == app_id:
|
|
872
861
|
config_obj = attr
|
|
873
|
-
|
|
862
|
+
|
|
863
|
+
def _return_config(cfg: TaskAppConfig = attr) -> TaskAppConfig:
|
|
864
|
+
return cfg
|
|
865
|
+
|
|
866
|
+
factory_callable = _return_config
|
|
874
867
|
break
|
|
875
868
|
|
|
876
869
|
if factory_callable is None:
|
|
@@ -904,10 +897,12 @@ def _load_entry_from_path(
|
|
|
904
897
|
continue
|
|
905
898
|
if isinstance(result, TaskAppConfig) and result.app_id == app_id:
|
|
906
899
|
# Bind attr to a local and close over it without exposing parameters
|
|
907
|
-
|
|
900
|
+
bound_func: Callable[[], TaskAppConfig] = cast(Callable[[], TaskAppConfig], attr) # type: ignore[assignment]
|
|
908
901
|
|
|
909
|
-
def _factory_noargs(
|
|
910
|
-
|
|
902
|
+
def _factory_noargs(
|
|
903
|
+
func: Callable[[], TaskAppConfig] = bound_func,
|
|
904
|
+
) -> TaskAppConfig:
|
|
905
|
+
return func()
|
|
911
906
|
|
|
912
907
|
factory_callable = _factory_noargs
|
|
913
908
|
config_obj = result
|
|
@@ -919,10 +914,10 @@ def _load_entry_from_path(
|
|
|
919
914
|
# Check if the app was registered in the registry
|
|
920
915
|
entry = registry.get(app_id)
|
|
921
916
|
return entry
|
|
922
|
-
except KeyError:
|
|
917
|
+
except KeyError as exc:
|
|
923
918
|
raise click.ClickException(
|
|
924
919
|
f"Could not locate TaskAppConfig for '{app_id}' in {resolved}."
|
|
925
|
-
)
|
|
920
|
+
) from exc
|
|
926
921
|
|
|
927
922
|
modal_cfg: ModalDeploymentConfig | None = None
|
|
928
923
|
for attr_name in dir(module):
|
|
@@ -993,6 +988,103 @@ def _resolve_env_paths_for_script(script_path: Path, explicit: Sequence[str]) ->
|
|
|
993
988
|
return [env_candidates[choice - 1]]
|
|
994
989
|
|
|
995
990
|
|
|
991
|
+
def _modal_command_prefix(modal_cli: str) -> list[str]:
|
|
992
|
+
"""Resolve a command prefix for invoking the Modal CLI within the active environment."""
|
|
993
|
+
if modal_cli == "modal" and importlib.util.find_spec("modal") is not None:
|
|
994
|
+
return [sys.executable, "-m", "synth_ai.cli._modal_wrapper"]
|
|
995
|
+
|
|
996
|
+
modal_path = shutil.which(modal_cli)
|
|
997
|
+
if modal_path is not None:
|
|
998
|
+
return [modal_path]
|
|
999
|
+
|
|
1000
|
+
if modal_cli == "modal":
|
|
1001
|
+
raise click.ClickException(
|
|
1002
|
+
"Modal CLI not found. Install the 'modal' package in this environment or pass "
|
|
1003
|
+
"--modal-cli with an explicit path."
|
|
1004
|
+
)
|
|
1005
|
+
raise click.ClickException(f"Modal CLI not found (looked for '{modal_cli}')")
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _build_modal_app_wrapper(original_script: Path) -> tuple[Path, Path]:
|
|
1009
|
+
source_dir = original_script.parent.resolve()
|
|
1010
|
+
repo_root = REPO_ROOT
|
|
1011
|
+
synth_src = (repo_root / "synth_ai").resolve()
|
|
1012
|
+
temp_root = Path(tempfile.mkdtemp(prefix="synth_modal_app_"))
|
|
1013
|
+
|
|
1014
|
+
wrapper_source = textwrap.dedent(
|
|
1015
|
+
f"""
|
|
1016
|
+
from importlib import util as _util
|
|
1017
|
+
from pathlib import Path as _Path
|
|
1018
|
+
import sys as _sys
|
|
1019
|
+
|
|
1020
|
+
_source_dir = _Path({str(source_dir)!r}).resolve()
|
|
1021
|
+
_module_path = _source_dir / {original_script.name!r}
|
|
1022
|
+
_package_name = _source_dir.name
|
|
1023
|
+
_repo_root = _Path({str(repo_root)!r}).resolve()
|
|
1024
|
+
_synth_dir = _repo_root / "synth_ai"
|
|
1025
|
+
|
|
1026
|
+
for _path in (str(_source_dir), str(_source_dir.parent), str(_repo_root)):
|
|
1027
|
+
if _path not in _sys.path:
|
|
1028
|
+
_sys.path.insert(0, _path)
|
|
1029
|
+
|
|
1030
|
+
_spec = _util.spec_from_file_location("_synth_modal_target", str(_module_path))
|
|
1031
|
+
if _spec is None or _spec.loader is None:
|
|
1032
|
+
raise SystemExit("Unable to load modal task app from {original_script}")
|
|
1033
|
+
_module = _util.module_from_spec(_spec)
|
|
1034
|
+
_sys.modules.setdefault("_synth_modal_target", _module)
|
|
1035
|
+
_spec.loader.exec_module(_module)
|
|
1036
|
+
|
|
1037
|
+
try:
|
|
1038
|
+
from modal import App as _ModalApp
|
|
1039
|
+
from modal import Image as _ModalImage
|
|
1040
|
+
except Exception:
|
|
1041
|
+
_ModalApp = None # type: ignore[assignment]
|
|
1042
|
+
_ModalImage = None # type: ignore[assignment]
|
|
1043
|
+
|
|
1044
|
+
def _apply_local_mounts(image):
|
|
1045
|
+
if _ModalImage is None or not isinstance(image, _ModalImage):
|
|
1046
|
+
return image
|
|
1047
|
+
mounts = [
|
|
1048
|
+
(str(_source_dir), f"/root/{{_package_name}}"),
|
|
1049
|
+
(str(_synth_dir), "/root/synth_ai"),
|
|
1050
|
+
]
|
|
1051
|
+
for local_path, remote_path in mounts:
|
|
1052
|
+
try:
|
|
1053
|
+
image = image.add_local_dir(local_path, remote_path=remote_path)
|
|
1054
|
+
except Exception:
|
|
1055
|
+
pass
|
|
1056
|
+
return image
|
|
1057
|
+
|
|
1058
|
+
if hasattr(_module, "image"):
|
|
1059
|
+
_module.image = _apply_local_mounts(getattr(_module, "image"))
|
|
1060
|
+
|
|
1061
|
+
_candidate = getattr(_module, "app", None)
|
|
1062
|
+
if _ModalApp is None or not isinstance(_candidate, _ModalApp):
|
|
1063
|
+
candidate_modal_app = getattr(_module, "modal_app", None)
|
|
1064
|
+
if _ModalApp is not None and isinstance(candidate_modal_app, _ModalApp):
|
|
1065
|
+
_candidate = candidate_modal_app
|
|
1066
|
+
setattr(_module, "app", _candidate)
|
|
1067
|
+
|
|
1068
|
+
if _ModalApp is not None and not isinstance(_candidate, _ModalApp):
|
|
1069
|
+
raise SystemExit(
|
|
1070
|
+
"Modal task app must expose an 'app = modal.App(...)' (or modal_app) attribute."
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
for remote_path in ("/root/synth_ai", f"/root/{{_package_name}}"):
|
|
1074
|
+
if remote_path not in _sys.path:
|
|
1075
|
+
_sys.path.insert(0, remote_path)
|
|
1076
|
+
|
|
1077
|
+
globals().update({{k: v for k, v in vars(_module).items() if not k.startswith("__")}})
|
|
1078
|
+
app = getattr(_module, "app")
|
|
1079
|
+
"""
|
|
1080
|
+
).strip()
|
|
1081
|
+
|
|
1082
|
+
wrapper_path = temp_root / "__modal_wrapper__.py"
|
|
1083
|
+
wrapper_path.write_text(wrapper_source + "\n", encoding="utf-8")
|
|
1084
|
+
return wrapper_path, temp_root
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
|
|
996
1088
|
def _run_modal_script(
|
|
997
1089
|
script_path: Path,
|
|
998
1090
|
modal_cli: str,
|
|
@@ -1002,55 +1094,92 @@ def _run_modal_script(
|
|
|
1002
1094
|
modal_name: str | None = None,
|
|
1003
1095
|
dry_run: bool = False,
|
|
1004
1096
|
) -> None:
|
|
1005
|
-
modal_path = shutil.which(modal_cli)
|
|
1006
|
-
if modal_path is None:
|
|
1007
|
-
raise click.ClickException(f"Modal CLI not found (looked for '{modal_cli}')")
|
|
1008
|
-
|
|
1009
1097
|
env_paths_list = [Path(p).resolve() for p in env_paths]
|
|
1010
1098
|
path_strings = [str(p) for p in env_paths_list]
|
|
1011
1099
|
_load_env_files_into_process(path_strings)
|
|
1012
1100
|
_ensure_env_values(env_paths_list, script_path.parent)
|
|
1013
1101
|
_load_env_values(env_paths_list)
|
|
1102
|
+
# Ensure ENVIRONMENT_API_KEY is uploaded to backend for this org (matches registry path behavior)
|
|
1103
|
+
try:
|
|
1104
|
+
_preflight_env_key(env_paths_list, crash_on_failure=True)
|
|
1105
|
+
except Exception as _pf_err:
|
|
1106
|
+
raise click.ClickException(str(_pf_err))
|
|
1014
1107
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1108
|
+
proc_env = os.environ.copy()
|
|
1109
|
+
pythonpath_entries: list[str] = []
|
|
1110
|
+
script_dir = script_path.parent.resolve()
|
|
1111
|
+
pythonpath_entries.append(str(script_dir))
|
|
1112
|
+
if (script_dir / "__init__.py").exists():
|
|
1113
|
+
# Script lives inside a package; ensure the parent package directory is importable.
|
|
1114
|
+
pythonpath_entries.append(str(script_dir.parent.resolve()))
|
|
1115
|
+
pythonpath_entries.append(str(REPO_ROOT))
|
|
1116
|
+
existing_pp = proc_env.get("PYTHONPATH")
|
|
1117
|
+
if existing_pp:
|
|
1118
|
+
pythonpath_entries.append(existing_pp)
|
|
1119
|
+
unique_paths = list(dict.fromkeys(pythonpath_entries))
|
|
1120
|
+
proc_env["PYTHONPATH"] = os.pathsep.join(unique_paths)
|
|
1121
|
+
|
|
1122
|
+
wrapper_info: tuple[Path, Path] | None = None
|
|
1123
|
+
target_script = script_path
|
|
1124
|
+
if command in {"serve", "deploy"}:
|
|
1125
|
+
wrapper_path, temp_root = _build_modal_app_wrapper(script_path)
|
|
1126
|
+
wrapper_info = (wrapper_path, temp_root)
|
|
1127
|
+
target_script = wrapper_path
|
|
1128
|
+
|
|
1129
|
+
# Ensure the wrapper has access to the Synth AI source for intra-repo imports
|
|
1130
|
+
if "PYTHONPATH" in proc_env:
|
|
1131
|
+
proc_env["PYTHONPATH"] = os.pathsep.join(
|
|
1132
|
+
[str(REPO_ROOT)] + proc_env["PYTHONPATH"].split(os.pathsep)
|
|
1133
|
+
)
|
|
1134
|
+
else:
|
|
1135
|
+
proc_env["PYTHONPATH"] = str(REPO_ROOT)
|
|
1136
|
+
|
|
1137
|
+
cmd = [*_modal_command_prefix(modal_cli), command, str(target_script)]
|
|
1138
|
+
if modal_name and command == "deploy":
|
|
1017
1139
|
cmd.extend(["--name", modal_name])
|
|
1018
1140
|
if dry_run:
|
|
1019
1141
|
click.echo("Dry run: " + " ".join(cmd))
|
|
1020
1142
|
return
|
|
1021
1143
|
try:
|
|
1022
|
-
#
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1144
|
+
# Stream output live for better diagnostics
|
|
1145
|
+
proc = subprocess.Popen(
|
|
1146
|
+
cmd,
|
|
1147
|
+
stdout=subprocess.PIPE,
|
|
1148
|
+
stderr=subprocess.STDOUT,
|
|
1149
|
+
text=True,
|
|
1150
|
+
bufsize=1,
|
|
1151
|
+
env=proc_env,
|
|
1152
|
+
)
|
|
1031
1153
|
task_app_url = None
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1154
|
+
assert proc.stdout is not None
|
|
1155
|
+
for line in proc.stdout:
|
|
1156
|
+
click.echo(line, nl=False)
|
|
1157
|
+
if task_app_url is None and ("modal.run" in line and "=>" in line):
|
|
1036
1158
|
parts = line.split("=>")
|
|
1037
1159
|
if len(parts) >= 2:
|
|
1038
1160
|
task_app_url = parts[-1].strip()
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1161
|
+
if task_app_url and env_paths_list:
|
|
1162
|
+
env_file = env_paths_list[0]
|
|
1163
|
+
_save_to_env_file(env_file, "TASK_APP_BASE_URL", task_app_url)
|
|
1164
|
+
click.echo(f"\n✓ Task app URL: {task_app_url}\n")
|
|
1165
|
+
rc = proc.wait()
|
|
1166
|
+
if rc != 0:
|
|
1167
|
+
raise subprocess.CalledProcessError(rc, cmd)
|
|
1047
1168
|
except subprocess.CalledProcessError as exc:
|
|
1048
1169
|
raise click.ClickException(
|
|
1049
1170
|
f"modal {command} failed with exit code {exc.returncode}"
|
|
1050
1171
|
) from exc
|
|
1172
|
+
finally:
|
|
1173
|
+
if wrapper_info is not None:
|
|
1174
|
+
wrapper_path, temp_root = wrapper_info
|
|
1175
|
+
try:
|
|
1176
|
+
wrapper_path.unlink(missing_ok=True)
|
|
1177
|
+
except Exception:
|
|
1178
|
+
pass
|
|
1179
|
+
shutil.rmtree(temp_root, ignore_errors=True)
|
|
1051
1180
|
|
|
1052
1181
|
|
|
1053
|
-
def _preflight_env_key(crash_on_failure: bool = False) -> None:
|
|
1182
|
+
def _preflight_env_key(env_paths: Sequence[Path] | None = None, *, crash_on_failure: bool = False) -> None:
|
|
1054
1183
|
try:
|
|
1055
1184
|
raw_backend = (
|
|
1056
1185
|
os.environ.get("BACKEND_BASE_URL")
|
|
@@ -1062,13 +1191,46 @@ def _preflight_env_key(crash_on_failure: bool = False) -> None:
|
|
|
1062
1191
|
backend_base = backend_base + "/api"
|
|
1063
1192
|
synth_key = os.environ.get("SYNTH_API_KEY") or ""
|
|
1064
1193
|
env_api_key = (
|
|
1065
|
-
os.environ.get("ENVIRONMENT_API_KEY")
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1194
|
+
os.environ.get("ENVIRONMENT_API_KEY") or os.environ.get("DEV_ENVIRONMENT_API_KEY") or ""
|
|
1195
|
+
).strip()
|
|
1196
|
+
|
|
1197
|
+
def _preview(value: str) -> str:
|
|
1198
|
+
if len(value) <= 10:
|
|
1199
|
+
return value
|
|
1200
|
+
return f"{value[:6]}...{value[-4:]}"
|
|
1201
|
+
|
|
1202
|
+
minted = False
|
|
1203
|
+
if not env_api_key:
|
|
1204
|
+
try:
|
|
1205
|
+
from synth_ai.learning.rl.secrets import mint_environment_api_key
|
|
1206
|
+
|
|
1207
|
+
env_api_key = mint_environment_api_key()
|
|
1208
|
+
os.environ["ENVIRONMENT_API_KEY"] = env_api_key
|
|
1209
|
+
os.environ.setdefault("DEV_ENVIRONMENT_API_KEY", env_api_key)
|
|
1210
|
+
minted = True
|
|
1211
|
+
click.echo(
|
|
1212
|
+
f"[preflight] minted ENVIRONMENT_API_KEY ({_preview(env_api_key)})"
|
|
1213
|
+
)
|
|
1214
|
+
except Exception as mint_err:
|
|
1215
|
+
if crash_on_failure:
|
|
1216
|
+
raise click.ClickException(
|
|
1217
|
+
f"[CRITICAL] Failed to mint ENVIRONMENT_API_KEY: {mint_err}"
|
|
1218
|
+
) from mint_err
|
|
1219
|
+
click.echo(
|
|
1220
|
+
f"[WARN] Failed to mint ENVIRONMENT_API_KEY automatically ({mint_err}); proceeding without upload"
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
if env_api_key and not os.environ.get("ENVIRONMENT_API_KEY"):
|
|
1224
|
+
os.environ["ENVIRONMENT_API_KEY"] = env_api_key
|
|
1225
|
+
if env_api_key and not os.environ.get("DEV_ENVIRONMENT_API_KEY"):
|
|
1226
|
+
os.environ["DEV_ENVIRONMENT_API_KEY"] = env_api_key
|
|
1227
|
+
|
|
1228
|
+
if minted:
|
|
1229
|
+
_persist_env_api_key(env_api_key, env_paths)
|
|
1230
|
+
|
|
1070
1231
|
if synth_key and env_api_key:
|
|
1071
1232
|
import base64
|
|
1233
|
+
|
|
1072
1234
|
import httpx
|
|
1073
1235
|
|
|
1074
1236
|
click.echo(f"[preflight] backend={backend_base}")
|
|
@@ -1080,10 +1242,50 @@ def _preflight_env_key(crash_on_failure: bool = False) -> None:
|
|
|
1080
1242
|
try:
|
|
1081
1243
|
from nacl.public import PublicKey, SealedBox
|
|
1082
1244
|
|
|
1083
|
-
|
|
1245
|
+
# Decode public key and build sealed box
|
|
1246
|
+
pk_bytes = base64.b64decode(pk, validate=True)
|
|
1247
|
+
pub = PublicKey(pk_bytes)
|
|
1084
1248
|
sb = SealedBox(pub)
|
|
1249
|
+
|
|
1250
|
+
# Encrypt plaintext key
|
|
1085
1251
|
ct_b64 = base64.b64encode(sb.encrypt(env_api_key.encode("utf-8"))).decode()
|
|
1086
1252
|
payload = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ct_b64}
|
|
1253
|
+
|
|
1254
|
+
# Emit diagnostic logging (safe previews + hashes only)
|
|
1255
|
+
try:
|
|
1256
|
+
import hashlib as _hash
|
|
1257
|
+
|
|
1258
|
+
# Backend URL context
|
|
1259
|
+
click.echo(f"[preflight] posting to {backend_base.rstrip('/')}/v1/env-keys")
|
|
1260
|
+
|
|
1261
|
+
# Public key diagnostics
|
|
1262
|
+
pk_sha256 = _hash.sha256(pk_bytes).hexdigest()
|
|
1263
|
+
click.echo(
|
|
1264
|
+
f"[preflight] public_key: b64_len={len(pk)} sha256={pk_sha256} head={pk[:16]} tail={pk[-16:]}"
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
# Plaintext diagnostics (never print full secret)
|
|
1268
|
+
_plain = env_api_key
|
|
1269
|
+
_plen = len(_plain)
|
|
1270
|
+
_ppref = (_plain[:6] + "…") if _plen > 10 else _plain
|
|
1271
|
+
_psuf = ("…" + _plain[-4:]) if _plen > 10 else ""
|
|
1272
|
+
_has_ws = any(ch.isspace() for ch in _plain)
|
|
1273
|
+
click.echo(
|
|
1274
|
+
f"[preflight] plaintext: len={_plen} preview={_ppref}{_psuf} has_ws={bool(_has_ws)}"
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
# Ciphertext diagnostics
|
|
1278
|
+
try:
|
|
1279
|
+
_ct_bytes = base64.b64decode(ct_b64, validate=True)
|
|
1280
|
+
_ct_sha256 = _hash.sha256(_ct_bytes).hexdigest()
|
|
1281
|
+
click.echo(
|
|
1282
|
+
f"[preflight] ciphertext: b64_len={len(ct_b64)} sha256={_ct_sha256} head={ct_b64[:16]} tail={ct_b64[-16:]}"
|
|
1283
|
+
)
|
|
1284
|
+
except Exception:
|
|
1285
|
+
click.echo("[preflight] ciphertext: invalid base64 (unexpected)")
|
|
1286
|
+
except Exception:
|
|
1287
|
+
# Best-effort logging only
|
|
1288
|
+
pass
|
|
1087
1289
|
with httpx.Client(
|
|
1088
1290
|
timeout=15.0,
|
|
1089
1291
|
headers={
|
|
@@ -1093,15 +1295,18 @@ def _preflight_env_key(crash_on_failure: bool = False) -> None:
|
|
|
1093
1295
|
) as c:
|
|
1094
1296
|
click.echo("[preflight] upserting env key…")
|
|
1095
1297
|
up = c.post(f"{backend_base.rstrip('/')}/v1/env-keys", json=payload)
|
|
1096
|
-
|
|
1298
|
+
body_snip = ""
|
|
1299
|
+
try:
|
|
1300
|
+
body_snip = up.text[:400] if up.text else ""
|
|
1301
|
+
except Exception:
|
|
1302
|
+
body_snip = ""
|
|
1303
|
+
click.echo(f"[preflight] upsert status={up.status_code}{(' body='+body_snip) if body_snip else ''}")
|
|
1097
1304
|
|
|
1098
1305
|
# If upload succeeded (2xx), consider it successful even if verification fails
|
|
1099
1306
|
# This handles cases where verification endpoint has issues
|
|
1100
1307
|
if 200 <= up.status_code < 300:
|
|
1101
1308
|
key_preview = (
|
|
1102
|
-
|
|
1103
|
-
if len(env_api_key) > 10
|
|
1104
|
-
else env_api_key
|
|
1309
|
+
_preview(env_api_key)
|
|
1105
1310
|
)
|
|
1106
1311
|
click.echo(
|
|
1107
1312
|
f"✅ ENVIRONMENT_API_KEY uploaded successfully ({key_preview})"
|
|
@@ -1124,6 +1329,7 @@ def _preflight_env_key(crash_on_failure: bool = False) -> None:
|
|
|
1124
1329
|
else:
|
|
1125
1330
|
error_msg = (
|
|
1126
1331
|
f"ENVIRONMENT_API_KEY upload failed with status {up.status_code}"
|
|
1332
|
+
+ (f" body={body_snip}" if body_snip else "")
|
|
1127
1333
|
)
|
|
1128
1334
|
if crash_on_failure:
|
|
1129
1335
|
raise click.ClickException(f"[CRITICAL] {error_msg}")
|
|
@@ -1131,12 +1337,12 @@ def _preflight_env_key(crash_on_failure: bool = False) -> None:
|
|
|
1131
1337
|
except Exception as e:
|
|
1132
1338
|
error_msg = f"Failed to encrypt/upload ENVIRONMENT_API_KEY: {e}"
|
|
1133
1339
|
if crash_on_failure:
|
|
1134
|
-
raise click.ClickException(f"[CRITICAL] {error_msg}")
|
|
1340
|
+
raise click.ClickException(f"[CRITICAL] {error_msg}") from e
|
|
1135
1341
|
click.echo(f"[WARN] {error_msg}; proceeding anyway")
|
|
1136
1342
|
except Exception as e:
|
|
1137
1343
|
error_msg = f"Backend preflight for ENVIRONMENT_API_KEY failed: {e}"
|
|
1138
1344
|
if crash_on_failure:
|
|
1139
|
-
raise click.ClickException(f"[CRITICAL] {error_msg}")
|
|
1345
|
+
raise click.ClickException(f"[CRITICAL] {error_msg}") from e
|
|
1140
1346
|
click.echo(f"[WARN] {error_msg}; proceeding anyway")
|
|
1141
1347
|
|
|
1142
1348
|
|
|
@@ -1151,17 +1357,33 @@ def _run_modal_with_entry(
|
|
|
1151
1357
|
dry_run: bool = False,
|
|
1152
1358
|
original_path: Path | None = None,
|
|
1153
1359
|
) -> None:
|
|
1154
|
-
modal_path = shutil.which(modal_cli)
|
|
1155
|
-
if modal_path is None:
|
|
1156
|
-
raise click.ClickException(f"Modal CLI not found (looked for '{modal_cli}')")
|
|
1157
|
-
|
|
1158
1360
|
env_paths_list = [Path(p).resolve() for p in env_paths]
|
|
1159
1361
|
dotenv_paths = [str(p) for p in env_paths_list]
|
|
1160
1362
|
_load_env_files_into_process(dotenv_paths)
|
|
1161
1363
|
fallback_dir = env_paths_list[0].parent if env_paths_list else Path.cwd()
|
|
1162
1364
|
_ensure_env_values(env_paths_list, fallback_dir)
|
|
1163
1365
|
_load_env_values(env_paths_list)
|
|
1164
|
-
_preflight_env_key(crash_on_failure=True)
|
|
1366
|
+
_preflight_env_key(env_paths_list, crash_on_failure=True)
|
|
1367
|
+
|
|
1368
|
+
inline_secret_values: dict[str, str] = {}
|
|
1369
|
+
env_key = os.environ.get("ENVIRONMENT_API_KEY", "").strip()
|
|
1370
|
+
if env_key:
|
|
1371
|
+
inline_secret_values["ENVIRONMENT_API_KEY"] = env_key
|
|
1372
|
+
inline_secret_values.setdefault("DEV_ENVIRONMENT_API_KEY", env_key)
|
|
1373
|
+
aliases = os.environ.get("ENVIRONMENT_API_KEY_ALIASES", "").strip()
|
|
1374
|
+
if aliases:
|
|
1375
|
+
inline_secret_values["ENVIRONMENT_API_KEY_ALIASES"] = aliases
|
|
1376
|
+
for vendor_key in ("GROQ_API_KEY", "OPENAI_API_KEY"):
|
|
1377
|
+
val = os.environ.get(vendor_key, "").strip()
|
|
1378
|
+
if val:
|
|
1379
|
+
inline_secret_values[vendor_key] = val
|
|
1380
|
+
|
|
1381
|
+
if inline_secret_values:
|
|
1382
|
+
preview = inline_secret_values.get("ENVIRONMENT_API_KEY", "")
|
|
1383
|
+
shown = f"{preview[:6]}...{preview[-4:]}" if preview and len(preview) > 10 else preview
|
|
1384
|
+
click.echo(f"[deploy] inline ENVIRONMENT_API_KEY prepared ({shown})")
|
|
1385
|
+
else:
|
|
1386
|
+
click.echo("[deploy] no inline ENVIRONMENT_API_KEY found; relying on Modal secrets/dotenv")
|
|
1165
1387
|
|
|
1166
1388
|
script_path = _write_modal_entrypoint(
|
|
1167
1389
|
entry,
|
|
@@ -1169,8 +1391,22 @@ def _run_modal_with_entry(
|
|
|
1169
1391
|
modal_name,
|
|
1170
1392
|
dotenv_paths=dotenv_paths,
|
|
1171
1393
|
original_path=original_path,
|
|
1394
|
+
inline_secret_values=inline_secret_values,
|
|
1172
1395
|
)
|
|
1173
|
-
cmd = [
|
|
1396
|
+
cmd = [*_modal_command_prefix(modal_cli), command, str(script_path)]
|
|
1397
|
+
|
|
1398
|
+
if modal_name and command == "deploy":
|
|
1399
|
+
cmd.extend(["--name", modal_name])
|
|
1400
|
+
|
|
1401
|
+
proc_env = os.environ.copy()
|
|
1402
|
+
pythonpath_entries: list[str] = [str(REPO_ROOT)]
|
|
1403
|
+
if original_path is not None:
|
|
1404
|
+
source_dir = Path(original_path).resolve().parent
|
|
1405
|
+
pythonpath_entries.insert(0, str(source_dir))
|
|
1406
|
+
existing_pp = proc_env.get("PYTHONPATH")
|
|
1407
|
+
if existing_pp:
|
|
1408
|
+
pythonpath_entries.append(existing_pp)
|
|
1409
|
+
proc_env["PYTHONPATH"] = os.pathsep.join(list(dict.fromkeys(pythonpath_entries)))
|
|
1174
1410
|
|
|
1175
1411
|
if dry_run:
|
|
1176
1412
|
click.echo("Dry run: " + " ".join(cmd))
|
|
@@ -1178,31 +1414,33 @@ def _run_modal_with_entry(
|
|
|
1178
1414
|
return
|
|
1179
1415
|
|
|
1180
1416
|
try:
|
|
1181
|
-
#
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1417
|
+
# Stream output live for better diagnostics
|
|
1418
|
+
proc = subprocess.Popen(
|
|
1419
|
+
cmd,
|
|
1420
|
+
stdout=subprocess.PIPE,
|
|
1421
|
+
stderr=subprocess.STDOUT,
|
|
1422
|
+
text=True,
|
|
1423
|
+
bufsize=1,
|
|
1424
|
+
env=proc_env,
|
|
1425
|
+
)
|
|
1190
1426
|
task_app_url = None
|
|
1191
|
-
|
|
1427
|
+
assert proc.stdout is not None
|
|
1428
|
+
for line in proc.stdout:
|
|
1429
|
+
# Echo lines as they arrive
|
|
1430
|
+
click.echo(line, nl=False)
|
|
1192
1431
|
# Look for lines containing modal.run URLs
|
|
1193
|
-
if "modal.run" in line and "=>" in line:
|
|
1194
|
-
# Extract URL from lines like: "└── 🔨 Created web function fastapi_app => https://...modal.run"
|
|
1432
|
+
if task_app_url is None and ("modal.run" in line and "=>" in line):
|
|
1195
1433
|
parts = line.split("=>")
|
|
1196
1434
|
if len(parts) >= 2:
|
|
1197
1435
|
task_app_url = parts[-1].strip()
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1436
|
+
# Save URL immediately for convenience
|
|
1437
|
+
if task_app_url and env_paths_list:
|
|
1438
|
+
env_file = env_paths_list[0]
|
|
1439
|
+
_save_to_env_file(env_file, "TASK_APP_BASE_URL", task_app_url)
|
|
1440
|
+
click.echo(f"\n✓ Task app URL: {task_app_url}\n")
|
|
1441
|
+
rc = proc.wait()
|
|
1442
|
+
if rc != 0:
|
|
1443
|
+
raise subprocess.CalledProcessError(rc, cmd)
|
|
1206
1444
|
except subprocess.CalledProcessError as exc:
|
|
1207
1445
|
raise click.ClickException(
|
|
1208
1446
|
f"modal {command} failed with exit code {exc.returncode}"
|
|
@@ -1485,6 +1723,76 @@ def serve_command(
|
|
|
1485
1723
|
)
|
|
1486
1724
|
|
|
1487
1725
|
|
|
1726
|
+
@task_app_group.command("info")
|
|
1727
|
+
@click.option(
|
|
1728
|
+
"--base",
|
|
1729
|
+
"base_url",
|
|
1730
|
+
default=None,
|
|
1731
|
+
help="Task app base URL (default: TASK_APP_BASE_URL or http://127.0.0.1:8001)",
|
|
1732
|
+
)
|
|
1733
|
+
@click.option(
|
|
1734
|
+
"--api-key",
|
|
1735
|
+
default=None,
|
|
1736
|
+
help="Environment API key (default: ENVIRONMENT_API_KEY or dev fallbacks)",
|
|
1737
|
+
)
|
|
1738
|
+
@click.option(
|
|
1739
|
+
"--seed",
|
|
1740
|
+
"seeds",
|
|
1741
|
+
multiple=True,
|
|
1742
|
+
type=int,
|
|
1743
|
+
help="Optional seed(s) to request specific instances (repeatable)",
|
|
1744
|
+
)
|
|
1745
|
+
def info_command(base_url: str | None, api_key: str | None, seeds: tuple[int, ...]) -> None:
|
|
1746
|
+
"""Fetch Task App /task_info with authentication and print JSON."""
|
|
1747
|
+
import json as _json
|
|
1748
|
+
import os as _os
|
|
1749
|
+
|
|
1750
|
+
import requests as _requests
|
|
1751
|
+
|
|
1752
|
+
base = (base_url or _os.getenv("TASK_APP_BASE_URL") or "http://127.0.0.1:8001").rstrip("/")
|
|
1753
|
+
|
|
1754
|
+
# Resolve API key, permitting dev fallbacks
|
|
1755
|
+
try:
|
|
1756
|
+
from synth_ai.task.auth import normalize_environment_api_key as _norm_key
|
|
1757
|
+
except Exception:
|
|
1758
|
+
_norm_key = lambda: _os.getenv("ENVIRONMENT_API_KEY") # noqa: E731
|
|
1759
|
+
key = (api_key or _norm_key() or "").strip()
|
|
1760
|
+
if not key:
|
|
1761
|
+
raise click.ClickException("Missing API key. Provide --api-key or set ENVIRONMENT_API_KEY.")
|
|
1762
|
+
|
|
1763
|
+
headers: dict[str, str] = {"X-API-Key": key, "Authorization": f"Bearer {key}"}
|
|
1764
|
+
aliases = (_os.getenv("ENVIRONMENT_API_KEY_ALIASES") or "").strip()
|
|
1765
|
+
keys_csv = (
|
|
1766
|
+
",".join([key] + [p.strip() for p in aliases.split(",") if p.strip()]) if aliases else key
|
|
1767
|
+
)
|
|
1768
|
+
if keys_csv:
|
|
1769
|
+
headers["X-API-Keys"] = keys_csv
|
|
1770
|
+
|
|
1771
|
+
params: list[tuple[str, str]] = []
|
|
1772
|
+
for s in seeds:
|
|
1773
|
+
params.append(("seed", str(int(s))))
|
|
1774
|
+
|
|
1775
|
+
url = f"{base}/task_info"
|
|
1776
|
+
try:
|
|
1777
|
+
r = _requests.get(url, headers=headers, params=params or None, timeout=30)
|
|
1778
|
+
except Exception as exc:
|
|
1779
|
+
raise click.ClickException(f"Request failed: {exc}") from exc
|
|
1780
|
+
if not (200 <= r.status_code < 300):
|
|
1781
|
+
ct = r.headers.get("content-type", "")
|
|
1782
|
+
detail = r.text
|
|
1783
|
+
if ct.startswith("application/json"):
|
|
1784
|
+
with contextlib.suppress(Exception):
|
|
1785
|
+
detail = _json.dumps(r.json(), indent=2)
|
|
1786
|
+
raise click.ClickException(f"{url} returned {r.status_code}:\n{detail}")
|
|
1787
|
+
|
|
1788
|
+
data = (
|
|
1789
|
+
r.json()
|
|
1790
|
+
if r.headers.get("content-type", "").startswith("application/json")
|
|
1791
|
+
else {"raw": r.text}
|
|
1792
|
+
)
|
|
1793
|
+
click.echo(_json.dumps(data, indent=2, sort_keys=True))
|
|
1794
|
+
|
|
1795
|
+
|
|
1488
1796
|
@task_app_group.command("serve")
|
|
1489
1797
|
@click.argument("app_id", type=str, required=False)
|
|
1490
1798
|
@click.option("--host", default="0.0.0.0", show_default=True)
|
|
@@ -1638,7 +1946,7 @@ def _ensure_port_free(port: int, host: str, *, force: bool) -> None:
|
|
|
1638
1946
|
try:
|
|
1639
1947
|
os.kill(int(pid), signal.SIGTERM)
|
|
1640
1948
|
except Exception as exc:
|
|
1641
|
-
raise click.ClickException(f"Failed to terminate PID {pid}: {exc}")
|
|
1949
|
+
raise click.ClickException(f"Failed to terminate PID {pid}: {exc}") from exc
|
|
1642
1950
|
|
|
1643
1951
|
time.sleep(0.5)
|
|
1644
1952
|
|
|
@@ -1650,7 +1958,7 @@ def _ensure_port_free(port: int, host: str, *, force: bool) -> None:
|
|
|
1650
1958
|
try:
|
|
1651
1959
|
os.kill(int(pid), signal.SIGKILL)
|
|
1652
1960
|
except Exception as exc:
|
|
1653
|
-
raise click.ClickException(f"Failed to force terminate PID {pid}: {exc}")
|
|
1961
|
+
raise click.ClickException(f"Failed to force terminate PID {pid}: {exc}") from exc
|
|
1654
1962
|
time.sleep(0.5)
|
|
1655
1963
|
|
|
1656
1964
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
@@ -1668,6 +1976,8 @@ def _save_to_env_file(env_path: Path, key: str, value: str) -> None:
|
|
|
1668
1976
|
existing_lines = []
|
|
1669
1977
|
if env_path.exists():
|
|
1670
1978
|
existing_lines = env_path.read_text().splitlines()
|
|
1979
|
+
else:
|
|
1980
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1671
1981
|
|
|
1672
1982
|
# Check if key already exists and update it
|
|
1673
1983
|
key_updated = False
|
|
@@ -1693,11 +2003,33 @@ def _save_to_env_file(env_path: Path, key: str, value: str) -> None:
|
|
|
1693
2003
|
# Add newline before appending
|
|
1694
2004
|
f.write("\n")
|
|
1695
2005
|
f.write(f"{key}={value}\n")
|
|
1696
|
-
|
|
2006
|
+
click.echo(f"Saved {key} to {env_path}")
|
|
1697
2007
|
except Exception as e:
|
|
1698
2008
|
click.echo(f"Warning: Could not save {key} to .env: {e}", err=True)
|
|
1699
2009
|
|
|
1700
2010
|
|
|
2011
|
+
def _persist_env_api_key(env_api_key: str, env_paths: Sequence[Path] | None) -> None:
|
|
2012
|
+
"""Persist ENVIRONMENT_API_KEY to provided env files (or default .env)."""
|
|
2013
|
+
targets: list[Path] = []
|
|
2014
|
+
seen: set[Path] = set()
|
|
2015
|
+
for path in env_paths or ():
|
|
2016
|
+
try:
|
|
2017
|
+
resolved = Path(path).resolve()
|
|
2018
|
+
except Exception:
|
|
2019
|
+
continue
|
|
2020
|
+
if resolved in seen:
|
|
2021
|
+
continue
|
|
2022
|
+
seen.add(resolved)
|
|
2023
|
+
targets.append(resolved)
|
|
2024
|
+
|
|
2025
|
+
if not targets:
|
|
2026
|
+
demo_dir = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
|
|
2027
|
+
targets.append((demo_dir / ".env").resolve())
|
|
2028
|
+
|
|
2029
|
+
for target in targets:
|
|
2030
|
+
_save_to_env_file(target, "ENVIRONMENT_API_KEY", env_api_key)
|
|
2031
|
+
|
|
2032
|
+
|
|
1701
2033
|
def _validate_required_env_keys() -> None:
|
|
1702
2034
|
"""Validate required environment keys are set, prompting if missing."""
|
|
1703
2035
|
# Use demo directory .env file if set, otherwise current directory
|
|
@@ -1742,17 +2074,19 @@ def _print_demo_next_steps_if_applicable() -> None:
|
|
|
1742
2074
|
demo_dir = load_demo_dir()
|
|
1743
2075
|
|
|
1744
2076
|
# Check if we're in the demo directory
|
|
1745
|
-
if
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2077
|
+
if (
|
|
2078
|
+
demo_dir
|
|
2079
|
+
and Path(demo_dir).resolve() == cwd
|
|
2080
|
+
and (cwd / "run_local_rollout_traced.py").exists()
|
|
2081
|
+
):
|
|
2082
|
+
click.echo("\n" + "=" * 60)
|
|
2083
|
+
click.echo("Next step: Collect traced rollouts")
|
|
2084
|
+
click.echo("=" * 60)
|
|
2085
|
+
click.echo("\nIn another terminal, run:")
|
|
2086
|
+
click.echo(f" cd {cwd}")
|
|
2087
|
+
click.echo(" uv run python run_local_rollout_traced.py")
|
|
2088
|
+
click.echo("\nRun this 5-10 times to collect diverse traces.")
|
|
2089
|
+
click.echo("=" * 60 + "\n")
|
|
1756
2090
|
except Exception:
|
|
1757
2091
|
# Silently fail - this is just a helpful printout
|
|
1758
2092
|
pass
|
|
@@ -1813,7 +2147,8 @@ def _serve_entry(
|
|
|
1813
2147
|
_ensure_port_free(port, host, force=force)
|
|
1814
2148
|
|
|
1815
2149
|
_validate_required_env_keys()
|
|
1816
|
-
|
|
2150
|
+
env_path_objs = [Path(p) for p in env_files if p]
|
|
2151
|
+
_preflight_env_key(env_path_objs)
|
|
1817
2152
|
|
|
1818
2153
|
# Print next steps if in demo context
|
|
1819
2154
|
if trace_enabled:
|
|
@@ -1912,6 +2247,7 @@ def _write_modal_entrypoint(
|
|
|
1912
2247
|
*,
|
|
1913
2248
|
dotenv_paths: Sequence[str] | None = None,
|
|
1914
2249
|
original_path: Path | None = None,
|
|
2250
|
+
inline_secret_values: dict[str, str] | None = None,
|
|
1915
2251
|
) -> Path:
|
|
1916
2252
|
modal_name = override_name or modal_cfg.app_name
|
|
1917
2253
|
|
|
@@ -1987,6 +2323,7 @@ def _write_modal_entrypoint(
|
|
|
1987
2323
|
local_dirs.append((discovered_dir, mount_dst))
|
|
1988
2324
|
secret_names = list(modal_cfg.secret_names)
|
|
1989
2325
|
volume_mounts = [(name, mount) for name, mount in modal_cfg.volume_mounts]
|
|
2326
|
+
inline_secret_values = {k: v for k, v in (inline_secret_values or {}).items() if v}
|
|
1990
2327
|
|
|
1991
2328
|
script = f"""from __future__ import annotations
|
|
1992
2329
|
|
|
@@ -2009,6 +2346,7 @@ MODAL_APP_NAME = {modal_name!r}
|
|
|
2009
2346
|
MODULE_NAME = {module_name!r}
|
|
2010
2347
|
MODULE_FILE = {guaranteed_file_str or remote_file_str!r}
|
|
2011
2348
|
DOTENV_PATHS = {dotenv_paths!r}
|
|
2349
|
+
INLINE_SECRET_VALUES = {inline_secret_values!r}
|
|
2012
2350
|
|
|
2013
2351
|
image = Image.debian_slim(python_version={modal_cfg.python_version!r})
|
|
2014
2352
|
|
|
@@ -2052,6 +2390,9 @@ for local_src, remote_dst in local_dirs:
|
|
|
2052
2390
|
secrets = {secret_names!r}
|
|
2053
2391
|
secret_objs = [Secret.from_name(name) for name in secrets]
|
|
2054
2392
|
|
|
2393
|
+
if INLINE_SECRET_VALUES:
|
|
2394
|
+
secret_objs.append(Secret.from_dict(INLINE_SECRET_VALUES))
|
|
2395
|
+
|
|
2055
2396
|
if DOTENV_PATHS:
|
|
2056
2397
|
secret_objs.extend(Secret.from_dotenv(path) for path in DOTENV_PATHS)
|
|
2057
2398
|
|
|
@@ -2119,11 +2460,11 @@ def fastapi_app():
|
|
|
2119
2460
|
return create_task_app(config)
|
|
2120
2461
|
"""
|
|
2121
2462
|
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
return Path(
|
|
2463
|
+
with tempfile.NamedTemporaryFile("w", suffix=f"_{entry.app_id}_modal.py", delete=False) as tmp:
|
|
2464
|
+
tmp.write(script)
|
|
2465
|
+
tmp.flush()
|
|
2466
|
+
name = tmp.name
|
|
2467
|
+
return Path(name)
|
|
2127
2468
|
|
|
2128
2469
|
|
|
2129
2470
|
def register(cli: click.Group) -> None:
|
|
@@ -2178,12 +2519,9 @@ def eval_command(
|
|
|
2178
2519
|
parsed = _toml.loads(data.decode("utf-8"))
|
|
2179
2520
|
if isinstance(parsed, dict):
|
|
2180
2521
|
section = parsed.get("eval")
|
|
2181
|
-
if isinstance(section, dict)
|
|
2182
|
-
cfg = dict(section)
|
|
2183
|
-
else:
|
|
2184
|
-
cfg = dict(parsed)
|
|
2522
|
+
cfg = dict(section) if isinstance(section, dict) else dict(parsed)
|
|
2185
2523
|
except Exception as exc:
|
|
2186
|
-
raise click.ClickException(f"Failed to parse TOML '{config_path}': {exc}")
|
|
2524
|
+
raise click.ClickException(f"Failed to parse TOML '{config_path}': {exc}") from exc
|
|
2187
2525
|
|
|
2188
2526
|
app_id = app_id or (cfg.get("app_id") if isinstance(cfg.get("app_id"), str) else None) # type: ignore
|
|
2189
2527
|
|
|
@@ -2193,10 +2531,8 @@ def eval_command(
|
|
|
2193
2531
|
if cfg.get("seeds") and seeds == "0,1,2,3,4":
|
|
2194
2532
|
val = cfg["seeds"]
|
|
2195
2533
|
if isinstance(val, list):
|
|
2196
|
-
|
|
2534
|
+
with contextlib.suppress(Exception):
|
|
2197
2535
|
seeds = ",".join(str(int(x)) for x in val)
|
|
2198
|
-
except Exception:
|
|
2199
|
-
pass
|
|
2200
2536
|
elif isinstance(val, str):
|
|
2201
2537
|
seeds = val
|
|
2202
2538
|
elif isinstance(val, int):
|
|
@@ -2297,8 +2633,8 @@ def eval_command(
|
|
|
2297
2633
|
|
|
2298
2634
|
try:
|
|
2299
2635
|
seed_values = [int(s.strip()) for s in seeds.split(",") if s.strip()]
|
|
2300
|
-
except Exception:
|
|
2301
|
-
raise click.ClickException("Invalid --seeds; expected comma-separated integers")
|
|
2636
|
+
except Exception as exc:
|
|
2637
|
+
raise click.ClickException("Invalid --seeds; expected comma-separated integers") from exc
|
|
2302
2638
|
|
|
2303
2639
|
import httpx
|
|
2304
2640
|
|
|
@@ -2324,11 +2660,9 @@ def eval_command(
|
|
|
2324
2660
|
)
|
|
2325
2661
|
else:
|
|
2326
2662
|
client = httpx.Client(base_url=task_app_url, timeout=60.0, headers=headers)
|
|
2327
|
-
|
|
2328
|
-
|
|
2663
|
+
try:
|
|
2664
|
+
with contextlib.suppress(Exception):
|
|
2329
2665
|
client.get("/task_info")
|
|
2330
|
-
except Exception:
|
|
2331
|
-
pass
|
|
2332
2666
|
# Precompute optional policy overrides from TOML
|
|
2333
2667
|
policy_overrides: dict[str, Any] = {}
|
|
2334
2668
|
try:
|
|
@@ -2406,16 +2740,32 @@ def eval_command(
|
|
|
2406
2740
|
summary.append(f"tool_calls={len(tool_calls)}")
|
|
2407
2741
|
click.echo(" ".join(summary))
|
|
2408
2742
|
# Print the full response JSON (trace, trajectories, metrics)
|
|
2409
|
-
|
|
2743
|
+
with contextlib.suppress(Exception):
|
|
2410
2744
|
click.echo(json.dumps(data, indent=2))
|
|
2411
|
-
except Exception:
|
|
2412
|
-
pass
|
|
2413
2745
|
else:
|
|
2414
2746
|
click.echo(" ".join(summary))
|
|
2415
2747
|
except Exception as exc:
|
|
2416
2748
|
failures += 1
|
|
2417
2749
|
click.echo(f"seed={seed_val} error={exc}")
|
|
2418
2750
|
|
|
2751
|
+
finally:
|
|
2752
|
+
try:
|
|
2753
|
+
client.close()
|
|
2754
|
+
except AttributeError:
|
|
2755
|
+
transport_obj = getattr(client, "_transport", None)
|
|
2756
|
+
if transport_obj and hasattr(transport_obj, "aclose"):
|
|
2757
|
+
try:
|
|
2758
|
+
asyncio.run(transport_obj.aclose())
|
|
2759
|
+
except RuntimeError:
|
|
2760
|
+
# Fallback when already inside a running loop (rare for CLI).
|
|
2761
|
+
new_loop = asyncio.new_event_loop()
|
|
2762
|
+
try:
|
|
2763
|
+
new_loop.run_until_complete(transport_obj.aclose())
|
|
2764
|
+
finally:
|
|
2765
|
+
new_loop.close()
|
|
2766
|
+
except Exception:
|
|
2767
|
+
pass
|
|
2768
|
+
|
|
2419
2769
|
click.echo(
|
|
2420
2770
|
f"Eval complete: {successes} ok, {failures} failed; model={selected_model}, split={split}"
|
|
2421
2771
|
)
|