synth-ai 0.2.9.dev5__py3-none-any.whl → 0.2.10__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 +23 -17
- examples/dev/qwen3_32b_qlora_4xh100.toml +40 -0
- examples/multi_step/crafter_rl_lora.md +29 -0
- 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 +65 -0
- examples/qwen_coder/infer_prod_proxy.py +73 -0
- examples/qwen_coder/infer_via_synth.py +87 -0
- examples/qwen_coder/scripts/infer_coder.sh +19 -0
- examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
- examples/qwen_coder/sft_full_17b.py +103 -0
- examples/qwen_coder/sft_lora_30b.py +110 -0
- examples/qwen_coder/subset_jsonl.py +39 -0
- examples/qwen_coder/todos.md +38 -0
- examples/qwen_coder/validate_jsonl.py +60 -0
- examples/rl/configs/eval_base_qwen.toml +1 -1
- examples/rl/configs/rl_from_base_qwen17.toml +1 -1
- examples/rl/download_dataset.py +26 -10
- examples/rl/run_eval.py +53 -52
- examples/rl/run_rl_and_save.py +29 -12
- examples/rl/task_app/math_single_step.py +180 -41
- examples/rl/task_app/math_task_app.py +14 -6
- 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/PROPOSAL.md +53 -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 +12 -10
- examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
- examples/warming_up_to_rl/export_trace_sft.py +218 -36
- examples/warming_up_to_rl/groq_test.py +15 -8
- examples/warming_up_to_rl/manage_secrets.py +29 -25
- examples/warming_up_to_rl/readme.md +9 -2
- examples/warming_up_to_rl/run_eval.py +137 -61
- examples/warming_up_to_rl/run_fft_and_save.py +131 -60
- examples/warming_up_to_rl/run_local_rollout.py +88 -39
- examples/warming_up_to_rl/run_local_rollout_modal.py +114 -28
- examples/warming_up_to_rl/run_local_rollout_parallel.py +81 -20
- examples/warming_up_to_rl/run_local_rollout_traced.py +126 -23
- examples/warming_up_to_rl/run_rl_and_save.py +35 -12
- examples/warming_up_to_rl/run_rollout_remote.py +44 -19
- examples/warming_up_to_rl/task_app/README.md +6 -2
- examples/warming_up_to_rl/task_app/grpo_crafter.py +319 -57
- examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +11 -30
- 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 +137 -182
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +150 -57
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +105 -69
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +19 -7
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +45 -42
- examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +47 -45
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +198 -92
- 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 +361 -263
- 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 +394 -274
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
- examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +56 -62
- 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 +6 -15
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
- synth_ai/__init__.py +1 -0
- synth_ai/api/models/supported.py +376 -0
- synth_ai/api/train/builders.py +157 -26
- synth_ai/api/train/cli.py +213 -57
- synth_ai/api/train/config_finder.py +65 -5
- synth_ai/api/train/env_resolver.py +33 -15
- synth_ai/api/train/pollers.py +13 -4
- synth_ai/api/train/supported_algos.py +139 -0
- synth_ai/api/train/task_app.py +5 -3
- synth_ai/api/train/utils.py +33 -48
- synth_ai/cli/__init__.py +19 -4
- synth_ai/cli/_modal_wrapper.py +28 -0
- synth_ai/cli/_typer_patch.py +49 -0
- synth_ai/cli/balance.py +2 -3
- synth_ai/cli/calc.py +1 -1
- synth_ai/cli/demo.py +21 -6
- synth_ai/cli/recent.py +2 -2
- synth_ai/cli/rl_demo.py +77 -17
- synth_ai/cli/root.py +116 -39
- synth_ai/cli/status.py +2 -2
- synth_ai/cli/task_apps.py +1699 -259
- synth_ai/cli/traces.py +7 -4
- synth_ai/cli/turso.py +73 -0
- synth_ai/cli/watch.py +12 -18
- synth_ai/core/experiment.py +0 -2
- synth_ai/demo_registry.py +68 -31
- synth_ai/demos/core/cli.py +516 -194
- synth_ai/demos/demo_task_apps/__init__.py +3 -3
- synth_ai/demos/demo_task_apps/core.py +64 -28
- synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +37 -30
- synth_ai/demos/demo_task_apps/math/_common.py +1 -2
- synth_ai/demos/demo_task_apps/math/app.py +2 -1
- synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
- synth_ai/demos/demo_task_apps/math/modal_task_app.py +183 -82
- synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -2
- synth_ai/environments/examples/bandit/engine.py +12 -4
- synth_ai/environments/examples/bandit/taskset.py +4 -4
- synth_ai/environments/examples/crafter_classic/environment.py +76 -1
- synth_ai/environments/reproducibility/tree.py +5 -6
- synth_ai/environments/service/app.py +11 -12
- synth_ai/environments/service/core_routes.py +10 -9
- synth_ai/environments/stateful/engine.py +1 -1
- synth_ai/environments/tasks/core.py +1 -0
- synth_ai/environments/tasks/filters.py +5 -6
- synth_ai/environments/tasks/utils.py +4 -5
- synth_ai/evals/base.py +0 -2
- synth_ai/handshake.py +11 -9
- synth_ai/http.py +1 -1
- synth_ai/http_client.py +43 -11
- synth_ai/inference/__init__.py +0 -2
- synth_ai/inference/client.py +20 -6
- synth_ai/jobs/client.py +103 -78
- synth_ai/learning/__init__.py +41 -6
- synth_ai/learning/algorithms.py +14 -0
- synth_ai/learning/client.py +121 -29
- synth_ai/learning/config.py +2 -40
- synth_ai/learning/constants.py +0 -2
- synth_ai/learning/ft_client.py +4 -56
- synth_ai/learning/health.py +13 -7
- synth_ai/learning/jobs.py +43 -47
- synth_ai/{rl → learning/rl}/__init__.py +14 -5
- synth_ai/learning/rl/client.py +267 -0
- synth_ai/learning/rl/config.py +31 -0
- synth_ai/{rl → learning/rl}/contracts.py +5 -10
- synth_ai/{rl → learning/rl}/env_keys.py +45 -16
- synth_ai/learning/rl/secrets.py +13 -0
- synth_ai/learning/rl_client.py +2 -253
- synth_ai/learning/sft/__init__.py +29 -0
- synth_ai/learning/sft/client.py +68 -0
- synth_ai/learning/sft/config.py +270 -0
- synth_ai/learning/sft/data.py +295 -0
- synth_ai/learning/sse.py +25 -26
- synth_ai/learning/validators.py +25 -24
- synth_ai/lm/__init__.py +21 -47
- synth_ai/task/__init__.py +26 -27
- synth_ai/task/apps/__init__.py +18 -19
- synth_ai/task/auth.py +35 -23
- synth_ai/task/client.py +15 -13
- synth_ai/task/contracts.py +37 -35
- synth_ai/task/datasets.py +9 -6
- synth_ai/task/errors.py +11 -10
- synth_ai/task/health.py +17 -11
- synth_ai/task/json.py +58 -24
- synth_ai/task/proxy.py +15 -14
- synth_ai/task/rubrics.py +22 -15
- synth_ai/task/server.py +43 -17
- synth_ai/task/tracing_utils.py +12 -7
- synth_ai/task/validators.py +0 -1
- synth_ai/task/vendors.py +5 -7
- synth_ai/tracing_v3/__init__.py +2 -0
- synth_ai/tracing_v3/abstractions.py +21 -4
- synth_ai/tracing_v3/db_config.py +26 -1
- synth_ai/tracing_v3/decorators.py +18 -15
- synth_ai/tracing_v3/examples/basic_usage.py +3 -2
- synth_ai/tracing_v3/hooks.py +6 -4
- synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
- synth_ai/tracing_v3/replica_sync.py +1 -0
- synth_ai/tracing_v3/session_tracer.py +63 -16
- synth_ai/tracing_v3/storage/base.py +89 -1
- synth_ai/tracing_v3/storage/config.py +21 -8
- synth_ai/tracing_v3/storage/factory.py +10 -8
- 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 +5 -2
- synth_ai/tracing_v3/turso/native_manager.py +1173 -0
- synth_ai/tracing_v3/utils.py +4 -3
- 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 +3 -5
- 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/exceptions.py +0 -2
- synth_ai/{lm → v0/lm}/core/main.py +19 -7
- synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
- synth_ai/{lm → v0/lm}/core/synth_models.py +2 -15
- synth_ai/{lm → v0/lm}/core/vendor_clients.py +6 -4
- synth_ai/{lm → v0/lm}/overrides.py +4 -4
- 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 +16 -16
- 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 +12 -10
- synth_ai/{lm → v0/lm}/vendors/openai_standard.py +11 -9
- synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +8 -5
- synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +4 -6
- 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 +38 -11
- synth_ai/v0/tracing/upload.py +32 -135
- 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.dev5.dist-info → synth_ai-0.2.10.dist-info}/METADATA +10 -7
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/RECORD +294 -258
- examples/common_old/backend.py +0 -21
- examples/evals_old/README.md +0 -98
- examples/evals_old/__init__.py +0 -6
- examples/evals_old/compare_models.py +0 -1037
- 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 -239
- 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 -118
- 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 -239
- 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 -37
- examples/finetuning_old/synth_qwen_v1/poll.py +0 -44
- 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 -1932
- examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -207
- examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -232
- examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
- examples/finetuning_old/synth_qwen_v1/util.py +0 -147
- examples/rl_old/task_app.py +0 -962
- synth_ai/experimental/synth_oss.py +0 -446
- synth_ai/install_sqld.sh +0 -40
- 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 -213
- synth_ai/learning/prompts/mipro.py +0 -289
- synth_ai/learning/prompts/random_search.py +0 -246
- synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
- synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
- synth_ai/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 -774
- synth_ai/zyk/__init__.py +0 -30
- /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}/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.dev5.dist-info → synth_ai-0.2.10.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/top_level.txt +0 -0
synth_ai/demos/core/cli.py
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import contextlib
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
|
-
import sys
|
|
7
|
-
import time
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any, Dict, Callable
|
|
10
7
|
import shutil
|
|
11
8
|
import stat
|
|
9
|
+
import sys
|
|
12
10
|
import textwrap
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
13
15
|
|
|
14
|
-
from synth_ai.demos.demo_task_apps import core as demo_core
|
|
15
|
-
from synth_ai.demos.demo_task_apps.core import DemoEnv, DEFAULT_TASK_APP_SECRET_NAME
|
|
16
16
|
from synth_ai.demo_registry import (
|
|
17
|
-
CopySpec,
|
|
18
17
|
DemoTemplate,
|
|
19
18
|
get_demo_template,
|
|
20
19
|
list_demo_templates,
|
|
21
20
|
)
|
|
22
|
-
from synth_ai.
|
|
21
|
+
from synth_ai.demos.demo_task_apps import core as demo_core
|
|
22
|
+
from synth_ai.demos.demo_task_apps.core import DEFAULT_TASK_APP_SECRET_NAME, DemoEnv
|
|
23
|
+
from synth_ai.handshake import HandshakeError, run_handshake
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _key_preview(value: str, label: str) -> str:
|
|
@@ -45,34 +46,70 @@ def _is_modal_public_url(u: str) -> bool:
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def cmd_setup(_args: argparse.Namespace) -> int:
|
|
48
|
-
#
|
|
49
|
+
# Change to demo directory if stored
|
|
50
|
+
demo_dir = demo_core.load_demo_dir()
|
|
51
|
+
if demo_dir and os.path.isdir(demo_dir):
|
|
52
|
+
os.chdir(demo_dir)
|
|
53
|
+
print(f"Using demo directory: {demo_dir}")
|
|
54
|
+
|
|
55
|
+
# 1) Try to fetch keys from frontend; fall back to manual input if fetch fails
|
|
56
|
+
synth_key = ""
|
|
57
|
+
rl_env_key = ""
|
|
58
|
+
org_name = "this organization"
|
|
59
|
+
|
|
49
60
|
try:
|
|
50
61
|
print("\n⏳ Connecting SDK to your browser session…")
|
|
51
62
|
res = run_handshake()
|
|
52
|
-
user = res.get("user") or {}
|
|
53
63
|
org = res.get("org") or {}
|
|
54
64
|
keys = res.get("keys") or {}
|
|
55
65
|
synth_key = str(keys.get("synth") or "").strip()
|
|
56
66
|
rl_env_key = str(keys.get("rl_env") or "").strip()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
org_name = org.get("name") or "this organization"
|
|
68
|
+
print(f"✅ Connected to {org_name}!")
|
|
69
|
+
except (HandshakeError, Exception) as e:
|
|
70
|
+
print(f"⚠️ Failed to fetch keys from frontend: {e}")
|
|
71
|
+
print("Falling back to manual entry...")
|
|
72
|
+
|
|
73
|
+
# Prompt for manual input if any key is missing
|
|
74
|
+
if not synth_key:
|
|
75
|
+
try:
|
|
76
|
+
synth_key = input(
|
|
77
|
+
"Failed to fetch your Synth API key. Please enter your Synth API key here:\n> "
|
|
78
|
+
).strip()
|
|
79
|
+
except (EOFError, KeyboardInterrupt):
|
|
80
|
+
print("\nSetup cancelled.")
|
|
81
|
+
return 1
|
|
82
|
+
if not synth_key:
|
|
83
|
+
print("Synth API key is required.")
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
if not rl_env_key:
|
|
87
|
+
try:
|
|
88
|
+
rl_env_key = input(
|
|
89
|
+
"Failed to fetch your RL Environment API key. Please enter your RL Environment API key here:\n> "
|
|
90
|
+
).strip()
|
|
91
|
+
except (EOFError, KeyboardInterrupt):
|
|
92
|
+
print("\nSetup cancelled.")
|
|
93
|
+
return 1
|
|
94
|
+
if not rl_env_key:
|
|
95
|
+
print("RL Environment API key is required.")
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
# Persist both keys to .env
|
|
99
|
+
dotenv_path = demo_core.persist_dotenv_values(
|
|
100
|
+
{
|
|
61
101
|
"SYNTH_API_KEY": synth_key,
|
|
62
102
|
"ENVIRONMENT_API_KEY": rl_env_key,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return 1
|
|
69
|
-
except Exception as e:
|
|
70
|
-
print(f"Unexpected handshake error: {e}")
|
|
71
|
-
return 1
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Store .env path for subsequent commands
|
|
107
|
+
demo_core.persist_env_file_path(dotenv_path)
|
|
72
108
|
|
|
73
109
|
# 2) Reload env after handshake to pick up values from .env (suppress env prints)
|
|
74
|
-
import io
|
|
75
110
|
import contextlib
|
|
111
|
+
import io
|
|
112
|
+
|
|
76
113
|
_buf = io.StringIO()
|
|
77
114
|
with contextlib.redirect_stdout(_buf):
|
|
78
115
|
env = demo_core.load_env()
|
|
@@ -89,22 +126,22 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
89
126
|
return
|
|
90
127
|
current = env.task_app_base_url
|
|
91
128
|
needs_lookup = False
|
|
92
|
-
if not current:
|
|
93
|
-
needs_lookup = True
|
|
94
|
-
elif not _is_modal_public_url(current):
|
|
129
|
+
if not current or not _is_modal_public_url(current):
|
|
95
130
|
needs_lookup = True
|
|
96
131
|
if not needs_lookup:
|
|
97
132
|
return
|
|
98
|
-
code, out = _popen_capture(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
code, out = _popen_capture(
|
|
134
|
+
[
|
|
135
|
+
"uv",
|
|
136
|
+
"run",
|
|
137
|
+
"python",
|
|
138
|
+
"-m",
|
|
139
|
+
"modal",
|
|
140
|
+
"app",
|
|
141
|
+
"url",
|
|
142
|
+
env.task_app_name,
|
|
143
|
+
]
|
|
144
|
+
)
|
|
108
145
|
if code != 0 or not out:
|
|
109
146
|
return
|
|
110
147
|
new_url = ""
|
|
@@ -134,15 +171,16 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
134
171
|
|
|
135
172
|
_maybe_fix_task_url()
|
|
136
173
|
|
|
137
|
-
ok_backend = False
|
|
138
|
-
ok_task = False
|
|
139
174
|
if env.dev_backend_url:
|
|
140
|
-
api = env.dev_backend_url.rstrip("/") + (
|
|
141
|
-
|
|
175
|
+
api = env.dev_backend_url.rstrip("/") + (
|
|
176
|
+
"" if env.dev_backend_url.endswith("/api") else "/api"
|
|
177
|
+
)
|
|
178
|
+
demo_core.assert_http_ok(api + "/health", method="GET")
|
|
142
179
|
# Intentionally suppress backend health print for concise output
|
|
143
180
|
if env.task_app_base_url:
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
demo_core.assert_http_ok(
|
|
182
|
+
env.task_app_base_url.rstrip("/") + "/health", method="GET"
|
|
183
|
+
) or demo_core.assert_http_ok(env.task_app_base_url.rstrip("/"), method="GET")
|
|
146
184
|
# Intentionally suppress task app health print
|
|
147
185
|
else:
|
|
148
186
|
print("\nSet your task app URL by running:\nuvx synth-ai rl_demo deploy\n")
|
|
@@ -150,13 +188,19 @@ def cmd_setup(_args: argparse.Namespace) -> int:
|
|
|
150
188
|
# Omit uv version print to keep output concise
|
|
151
189
|
|
|
152
190
|
# Keep exit code neutral; not all checks are critical for pairing
|
|
191
|
+
print(f"\nKeys saved to: {dotenv_path}")
|
|
153
192
|
return 0
|
|
154
193
|
|
|
155
194
|
|
|
156
|
-
def _popen_capture(
|
|
195
|
+
def _popen_capture(
|
|
196
|
+
cmd: list[str], cwd: str | None = None, env: dict | None = None
|
|
197
|
+
) -> tuple[int, str]:
|
|
157
198
|
import subprocess
|
|
199
|
+
|
|
158
200
|
try:
|
|
159
|
-
proc = subprocess.Popen(
|
|
201
|
+
proc = subprocess.Popen(
|
|
202
|
+
cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
|
203
|
+
)
|
|
160
204
|
out, _ = proc.communicate()
|
|
161
205
|
return int(proc.returncode or 0), out or ""
|
|
162
206
|
except Exception as e:
|
|
@@ -200,7 +244,9 @@ def _popen_stream(cmd: list[str], cwd: str | None = None, env: dict | None = Non
|
|
|
200
244
|
return int(proc.returncode or 0)
|
|
201
245
|
|
|
202
246
|
|
|
203
|
-
def _popen_stream_capture(
|
|
247
|
+
def _popen_stream_capture(
|
|
248
|
+
cmd: list[str], cwd: str | None = None, env: dict | None = None
|
|
249
|
+
) -> tuple[int, str]:
|
|
204
250
|
"""Stream subprocess output to stdout and also capture it into a buffer."""
|
|
205
251
|
import subprocess
|
|
206
252
|
import threading
|
|
@@ -251,7 +297,19 @@ def _find_asgi_apps(root: Path) -> list[Path]:
|
|
|
251
297
|
- "@modal.asgi_app()"
|
|
252
298
|
"""
|
|
253
299
|
results: list[Path] = []
|
|
254
|
-
skip_dirs = {
|
|
300
|
+
skip_dirs = {
|
|
301
|
+
".git",
|
|
302
|
+
".hg",
|
|
303
|
+
".svn",
|
|
304
|
+
"node_modules",
|
|
305
|
+
"dist",
|
|
306
|
+
"build",
|
|
307
|
+
"__pycache__",
|
|
308
|
+
".ruff_cache",
|
|
309
|
+
".mypy_cache",
|
|
310
|
+
"venv",
|
|
311
|
+
".venv",
|
|
312
|
+
}
|
|
255
313
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
256
314
|
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
|
|
257
315
|
for name in filenames:
|
|
@@ -265,16 +323,20 @@ def _find_asgi_apps(root: Path) -> list[Path]:
|
|
|
265
323
|
results.append(path)
|
|
266
324
|
except Exception:
|
|
267
325
|
continue
|
|
326
|
+
|
|
268
327
|
# Stable order: prioritize files under synth_demo/ first, then alphabetical
|
|
269
328
|
def _priority(p: Path) -> tuple[int, str]:
|
|
270
329
|
rel = str(p.resolve())
|
|
271
330
|
in_demo = "/synth_demo/" in rel or rel.endswith("/synth_demo/task_app.py")
|
|
272
331
|
return (0 if in_demo else 1, rel)
|
|
332
|
+
|
|
273
333
|
results.sort(key=_priority)
|
|
274
334
|
return results
|
|
275
335
|
|
|
276
336
|
|
|
277
|
-
def _prompt_value(
|
|
337
|
+
def _prompt_value(
|
|
338
|
+
label: str, default: str | int | float, cast: Callable[[str], Any] | None = None
|
|
339
|
+
) -> Any:
|
|
278
340
|
prompt = f"{label} [{default}]: "
|
|
279
341
|
try:
|
|
280
342
|
raw = input(prompt).strip()
|
|
@@ -293,7 +355,19 @@ def _prompt_value(label: str, default: str | int | float, cast: Callable[[str],
|
|
|
293
355
|
|
|
294
356
|
def _find_vllm_tomls(root: Path) -> list[Path]:
|
|
295
357
|
results: list[Path] = []
|
|
296
|
-
skip_dirs = {
|
|
358
|
+
skip_dirs = {
|
|
359
|
+
".git",
|
|
360
|
+
".hg",
|
|
361
|
+
".svn",
|
|
362
|
+
"node_modules",
|
|
363
|
+
"dist",
|
|
364
|
+
"build",
|
|
365
|
+
"__pycache__",
|
|
366
|
+
".ruff_cache",
|
|
367
|
+
".mypy_cache",
|
|
368
|
+
"venv",
|
|
369
|
+
".venv",
|
|
370
|
+
}
|
|
297
371
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
298
372
|
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
|
|
299
373
|
for name in filenames:
|
|
@@ -313,7 +387,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
313
387
|
default_path = os.path.join(os.getcwd(), "demo_config.toml")
|
|
314
388
|
while True:
|
|
315
389
|
try:
|
|
316
|
-
destination =
|
|
390
|
+
destination = (
|
|
391
|
+
input(f"Path to save new config [{default_path}]: ").strip() or default_path
|
|
392
|
+
)
|
|
317
393
|
except Exception:
|
|
318
394
|
destination = default_path
|
|
319
395
|
destination = os.path.abspath(destination)
|
|
@@ -322,7 +398,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
322
398
|
continue
|
|
323
399
|
if os.path.exists(destination):
|
|
324
400
|
try:
|
|
325
|
-
overwrite =
|
|
401
|
+
overwrite = (
|
|
402
|
+
input(f"{destination} exists. Overwrite? [y/N]: ").strip().lower() or "n"
|
|
403
|
+
)
|
|
326
404
|
except Exception:
|
|
327
405
|
overwrite = "n"
|
|
328
406
|
if not overwrite.startswith("y"):
|
|
@@ -334,7 +412,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
334
412
|
model_name = _prompt_value("Model name", "Qwen/Qwen3-0.6B")
|
|
335
413
|
compute_gpu_type = _prompt_value("Compute GPU type", "H100")
|
|
336
414
|
compute_gpu_count = _prompt_value("Compute GPU count", 4, int)
|
|
337
|
-
topology_gpu_type = _prompt_value(
|
|
415
|
+
topology_gpu_type = _prompt_value(
|
|
416
|
+
"Topology GPU type", f"{compute_gpu_type}:{compute_gpu_count}"
|
|
417
|
+
)
|
|
338
418
|
gpus_for_vllm = _prompt_value("Topology gpus_for_vllm", 2, int)
|
|
339
419
|
gpus_for_training = _prompt_value("Topology gpus_for_training", 1, int)
|
|
340
420
|
tensor_parallel = _prompt_value("Topology tensor_parallel", 2, int)
|
|
@@ -352,8 +432,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
352
432
|
task_url_default = env.task_app_base_url or ""
|
|
353
433
|
services_task_url = _prompt_value("services.task_url", task_url_default)
|
|
354
434
|
|
|
355
|
-
template =
|
|
356
|
-
|
|
435
|
+
template = (
|
|
436
|
+
textwrap.dedent(
|
|
437
|
+
f"""\
|
|
357
438
|
# Crafter online RL training configuration (research local copy)
|
|
358
439
|
|
|
359
440
|
[model]
|
|
@@ -495,7 +576,9 @@ def _create_new_config(env: DemoEnv) -> str:
|
|
|
495
576
|
[services]
|
|
496
577
|
task_url = \"{services_task_url}\"
|
|
497
578
|
"""
|
|
498
|
-
|
|
579
|
+
).strip()
|
|
580
|
+
+ "\n"
|
|
581
|
+
)
|
|
499
582
|
|
|
500
583
|
with open(destination, "w", encoding="utf-8") as fh:
|
|
501
584
|
fh.write(template)
|
|
@@ -514,7 +597,11 @@ def _select_or_create_config(explicit: str | None, env: DemoEnv) -> str:
|
|
|
514
597
|
discovered = _find_vllm_tomls(search_root)
|
|
515
598
|
|
|
516
599
|
extras: list[Path] = []
|
|
517
|
-
packaged = Path(
|
|
600
|
+
packaged = Path(
|
|
601
|
+
os.path.abspath(
|
|
602
|
+
os.path.join(os.path.dirname(__file__), "..", "demo_task_apps", "math", "config.toml")
|
|
603
|
+
)
|
|
604
|
+
)
|
|
518
605
|
extras.append(packaged)
|
|
519
606
|
home_cfg = Path(os.path.expanduser("~/.synth-ai/demo_config.toml"))
|
|
520
607
|
extras.append(home_cfg)
|
|
@@ -560,29 +647,36 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
560
647
|
|
|
561
648
|
env_key = (env.env_api_key or "").strip()
|
|
562
649
|
if not env_key:
|
|
563
|
-
raise RuntimeError(
|
|
650
|
+
raise RuntimeError(
|
|
651
|
+
f"[{label}] ENVIRONMENT_API_KEY missing. Run `uvx synth-ai rl_demo deploy` first."
|
|
652
|
+
)
|
|
564
653
|
|
|
565
654
|
task_url = env.task_app_base_url
|
|
566
655
|
if not task_url or not _is_modal_public_url(task_url):
|
|
567
656
|
resolved = ""
|
|
568
657
|
if env.task_app_name:
|
|
569
658
|
try:
|
|
570
|
-
choice =
|
|
571
|
-
f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: "
|
|
572
|
-
|
|
659
|
+
choice = (
|
|
660
|
+
input(f"Resolve URL from Modal for app '{env.task_app_name}'? [Y/n]: ")
|
|
661
|
+
.strip()
|
|
662
|
+
.lower()
|
|
663
|
+
or "y"
|
|
664
|
+
)
|
|
573
665
|
except Exception:
|
|
574
666
|
choice = "y"
|
|
575
667
|
if choice.startswith("y"):
|
|
576
|
-
code, out = _popen_capture(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
668
|
+
code, out = _popen_capture(
|
|
669
|
+
[
|
|
670
|
+
"uv",
|
|
671
|
+
"run",
|
|
672
|
+
"python",
|
|
673
|
+
"-m",
|
|
674
|
+
"modal",
|
|
675
|
+
"app",
|
|
676
|
+
"url",
|
|
677
|
+
env.task_app_name,
|
|
678
|
+
]
|
|
679
|
+
)
|
|
586
680
|
if code == 0 and out:
|
|
587
681
|
for tok in out.split():
|
|
588
682
|
if _is_modal_public_url(tok):
|
|
@@ -591,7 +685,9 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
591
685
|
if not resolved:
|
|
592
686
|
print(f"[{label}] Task app URL not configured or not a valid Modal public URL.")
|
|
593
687
|
print("Examples: https://<app-name>-fastapi-app.modal.run")
|
|
594
|
-
entered = input(
|
|
688
|
+
entered = input(
|
|
689
|
+
"Enter Task App base URL (must contain '.modal.run'), or press Enter to abort: "
|
|
690
|
+
).strip()
|
|
595
691
|
if not entered or not _is_modal_public_url(entered):
|
|
596
692
|
raise RuntimeError(f"[{label}] Valid Task App URL is required.")
|
|
597
693
|
task_url = entered.rstrip("/")
|
|
@@ -608,11 +704,13 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
608
704
|
demo_core.persist_task_url(task_url, name=app_name)
|
|
609
705
|
|
|
610
706
|
demo_core.persist_task_url(task_url, name=app_name)
|
|
611
|
-
demo_core.persist_dotenv_values(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
707
|
+
demo_core.persist_dotenv_values(
|
|
708
|
+
{
|
|
709
|
+
"TASK_APP_BASE_URL": task_url,
|
|
710
|
+
"TASK_APP_NAME": app_name,
|
|
711
|
+
"TASK_APP_SECRET_NAME": DEFAULT_TASK_APP_SECRET_NAME,
|
|
712
|
+
}
|
|
713
|
+
)
|
|
616
714
|
|
|
617
715
|
if synth_key:
|
|
618
716
|
os.environ["SYNTH_API_KEY"] = synth_key
|
|
@@ -621,7 +719,6 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
621
719
|
if openai_key:
|
|
622
720
|
os.environ["OPENAI_API_KEY"] = openai_key
|
|
623
721
|
|
|
624
|
-
rollout_url = task_url.rstrip("/") + "/health/rollout"
|
|
625
722
|
print(f"[{label}] Verifying rollout health:")
|
|
626
723
|
try:
|
|
627
724
|
ek = (env_key or "").strip()
|
|
@@ -636,7 +733,6 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
636
733
|
print(f"[{label}] GET", h)
|
|
637
734
|
rc, body = _http("GET", h, headers={"X-API-Key": env_key})
|
|
638
735
|
if rc == 200:
|
|
639
|
-
rollout_url = h
|
|
640
736
|
break
|
|
641
737
|
print(f"[{label}] status: {rc}")
|
|
642
738
|
try:
|
|
@@ -648,10 +744,8 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
648
744
|
print(f"[{label}] body:", preview)
|
|
649
745
|
if rc != 200:
|
|
650
746
|
print(f"[{label}] Warning: rollout health check failed ({rc}). Response: {body}")
|
|
651
|
-
|
|
747
|
+
with contextlib.suppress(Exception):
|
|
652
748
|
print(f"[{label}] Sent header X-API-Key → {_key_preview(env_key, 'X-API-Key')}")
|
|
653
|
-
except Exception:
|
|
654
|
-
pass
|
|
655
749
|
else:
|
|
656
750
|
print(f"[{label}] Task app rollout health check OK.")
|
|
657
751
|
|
|
@@ -667,6 +761,12 @@ def _ensure_task_app_ready(env: DemoEnv, synth_key: str, *, label: str) -> DemoE
|
|
|
667
761
|
|
|
668
762
|
|
|
669
763
|
def cmd_deploy(args: argparse.Namespace) -> int:
|
|
764
|
+
# Change to demo directory if stored
|
|
765
|
+
demo_dir = demo_core.load_demo_dir()
|
|
766
|
+
if demo_dir and os.path.isdir(demo_dir):
|
|
767
|
+
os.chdir(demo_dir)
|
|
768
|
+
print(f"Using demo directory: {demo_dir}")
|
|
769
|
+
|
|
670
770
|
env = demo_core.load_env()
|
|
671
771
|
os.environ["TASK_APP_SECRET_NAME"] = DEFAULT_TASK_APP_SECRET_NAME
|
|
672
772
|
cwd_env_path = os.path.join(os.getcwd(), ".env")
|
|
@@ -677,12 +777,22 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
677
777
|
if args.local:
|
|
678
778
|
print("Starting local Task App…")
|
|
679
779
|
import subprocess
|
|
680
|
-
|
|
681
|
-
|
|
780
|
+
|
|
781
|
+
subprocess.Popen(
|
|
782
|
+
[
|
|
783
|
+
sys.executable,
|
|
784
|
+
"-c",
|
|
785
|
+
"from synth_ai.demos.demo_task_apps.math.app import run; run()",
|
|
786
|
+
],
|
|
787
|
+
stdout=sys.stdout,
|
|
788
|
+
stderr=sys.stderr,
|
|
789
|
+
)
|
|
682
790
|
target = "http://127.0.0.1:8080"
|
|
683
791
|
app_name = ""
|
|
684
792
|
for _ in range(30):
|
|
685
|
-
if demo_core.assert_http_ok(
|
|
793
|
+
if demo_core.assert_http_ok(
|
|
794
|
+
target + "/health", method="GET"
|
|
795
|
+
) or demo_core.assert_http_ok(target, method="GET"):
|
|
686
796
|
url = target
|
|
687
797
|
break
|
|
688
798
|
time.sleep(1)
|
|
@@ -707,7 +817,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
707
817
|
rel = os.path.relpath(str(pth), os.getcwd())
|
|
708
818
|
print(f" [{idx}] {rel}")
|
|
709
819
|
try:
|
|
710
|
-
sel =
|
|
820
|
+
sel = (
|
|
821
|
+
input(f"Enter choice [1-{len(found)}] (default 1): ").strip() or "1"
|
|
822
|
+
)
|
|
711
823
|
except Exception:
|
|
712
824
|
sel = "1"
|
|
713
825
|
try:
|
|
@@ -719,6 +831,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
719
831
|
if not app_path and args.script:
|
|
720
832
|
# Legacy script fallback if user supplied --script explicitly
|
|
721
833
|
from synth_ai.demos.demo_task_apps.math.deploy_modal import deploy as modal_deploy
|
|
834
|
+
|
|
722
835
|
url = modal_deploy(script_path=args.script, env_api_key=env.env_api_key)
|
|
723
836
|
if args.name:
|
|
724
837
|
app_name = args.name
|
|
@@ -750,16 +863,19 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
750
863
|
env_key: str | None = existing_env_key or None
|
|
751
864
|
if existing_env_key:
|
|
752
865
|
try:
|
|
753
|
-
reuse_choice =
|
|
754
|
-
"Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: "
|
|
755
|
-
|
|
866
|
+
reuse_choice = (
|
|
867
|
+
input("Use existing ENVIRONMENT_API_KEY from state/.env? [Y/n]: ")
|
|
868
|
+
.strip()
|
|
869
|
+
.lower()
|
|
870
|
+
or "y"
|
|
871
|
+
)
|
|
756
872
|
except Exception:
|
|
757
873
|
reuse_choice = "y"
|
|
758
874
|
if not reuse_choice.startswith("y"):
|
|
759
875
|
env_key = None
|
|
760
876
|
|
|
761
877
|
if env_key is None:
|
|
762
|
-
from synth_ai.rl.secrets import mint_environment_api_key
|
|
878
|
+
from synth_ai.learning.rl.secrets import mint_environment_api_key
|
|
763
879
|
|
|
764
880
|
env_key = mint_environment_api_key()
|
|
765
881
|
demo_core.persist_env_api_key(env_key)
|
|
@@ -770,35 +886,50 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
770
886
|
print("[deploy] Minted new ENVIRONMENT_API_KEY")
|
|
771
887
|
elif env_key:
|
|
772
888
|
os.environ["ENVIRONMENT_API_KEY"] = env_key
|
|
773
|
-
|
|
889
|
+
|
|
774
890
|
# Optionally upload the new key to the backend using sealed box helper
|
|
775
891
|
backend_base = (env.dev_backend_url or "").rstrip("/")
|
|
776
|
-
synth_key = (
|
|
892
|
+
synth_key = (
|
|
893
|
+
env.synth_api_key
|
|
894
|
+
or os.environ.get("SYNTH_API_KEY")
|
|
895
|
+
or local_env.get("SYNTH_API_KEY")
|
|
896
|
+
or ""
|
|
897
|
+
).strip()
|
|
777
898
|
if backend_base and synth_key:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
899
|
+
# Pass a base WITHOUT trailing /api to setup_environment_api_key,
|
|
900
|
+
# since it appends /api/v1/... internally.
|
|
901
|
+
non_api_base = (
|
|
902
|
+
backend_base[:-4] if backend_base.endswith("/api") else backend_base
|
|
903
|
+
)
|
|
904
|
+
try:
|
|
905
|
+
choice = (
|
|
906
|
+
input(f"Upload ENVIRONMENT_API_KEY to backend {non_api_base}? [Y/n]: ")
|
|
907
|
+
.strip()
|
|
908
|
+
.lower()
|
|
909
|
+
or "y"
|
|
910
|
+
)
|
|
911
|
+
except Exception:
|
|
912
|
+
choice = "y"
|
|
913
|
+
if choice.startswith("y"):
|
|
781
914
|
try:
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
from synth_ai.rl.env_keys import setup_environment_api_key
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
synth_key = (env.synth_api_key or os.environ.get("SYNTH_API_KEY") or local_env.get("SYNTH_API_KEY") or "").strip()
|
|
915
|
+
print(f"[deploy] Uploading ENVIRONMENT_API_KEY to {non_api_base} …")
|
|
916
|
+
from synth_ai.learning.rl.env_keys import setup_environment_api_key
|
|
917
|
+
|
|
918
|
+
setup_environment_api_key(non_api_base, synth_key, token=env_key)
|
|
919
|
+
print("[deploy] Backend sealed-box upload complete.")
|
|
920
|
+
except Exception as upload_err:
|
|
921
|
+
print(f"[deploy] Failed to upload ENVIRONMENT_API_KEY: {upload_err}")
|
|
922
|
+
print(
|
|
923
|
+
'Hint: run `uvx python -c "from synth_ai.learning.rl.env_keys import setup_environment_api_key as s;'
|
|
924
|
+
" s('<backend>', '<synth_api_key>')\"` once the backend is reachable."
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
synth_key = (
|
|
928
|
+
env.synth_api_key
|
|
929
|
+
or os.environ.get("SYNTH_API_KEY")
|
|
930
|
+
or local_env.get("SYNTH_API_KEY")
|
|
931
|
+
or ""
|
|
932
|
+
).strip()
|
|
802
933
|
if not synth_key:
|
|
803
934
|
synth_key = input("Enter SYNTH_API_KEY for deployment (required): ").strip()
|
|
804
935
|
if not synth_key:
|
|
@@ -809,7 +940,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
809
940
|
env.synth_api_key = synth_key
|
|
810
941
|
os.environ["SYNTH_API_KEY"] = synth_key
|
|
811
942
|
|
|
812
|
-
openai_key = (
|
|
943
|
+
openai_key = (
|
|
944
|
+
os.environ.get("OPENAI_API_KEY") or local_env.get("OPENAI_API_KEY") or ""
|
|
945
|
+
).strip()
|
|
813
946
|
if not openai_key:
|
|
814
947
|
openai_key = input(
|
|
815
948
|
"Enter your OpenAI API key, found at https://platform.openai.com/api-keys\n> "
|
|
@@ -821,8 +954,20 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
821
954
|
local_env["OPENAI_API_KEY"] = openai_key
|
|
822
955
|
os.environ["OPENAI_API_KEY"] = openai_key
|
|
823
956
|
|
|
824
|
-
deploy_cmd = [
|
|
825
|
-
|
|
957
|
+
deploy_cmd = [
|
|
958
|
+
"uv",
|
|
959
|
+
"run",
|
|
960
|
+
"python",
|
|
961
|
+
"-m",
|
|
962
|
+
"modal",
|
|
963
|
+
"deploy",
|
|
964
|
+
"--name",
|
|
965
|
+
name_in,
|
|
966
|
+
app_path,
|
|
967
|
+
]
|
|
968
|
+
print(
|
|
969
|
+
"\nStreaming Modal build/deploy logs (this can take several minutes on first run)…\n"
|
|
970
|
+
)
|
|
826
971
|
code, deploy_logs = _popen_stream_capture(deploy_cmd)
|
|
827
972
|
if code != 0:
|
|
828
973
|
raise RuntimeError(f"modal deploy failed (exit {code})")
|
|
@@ -830,6 +975,7 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
830
975
|
if not url:
|
|
831
976
|
try:
|
|
832
977
|
import re as _re
|
|
978
|
+
|
|
833
979
|
m_all = _re.findall(r"https?://[^\s]+\.modal\.run", deploy_logs or "")
|
|
834
980
|
if m_all:
|
|
835
981
|
url = m_all[-1].strip().rstrip("/")
|
|
@@ -844,7 +990,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
844
990
|
break
|
|
845
991
|
# Fallback: try reading recent Modal logs for the app to find a URL line
|
|
846
992
|
if not url:
|
|
847
|
-
code3, out3 = _popen_capture(
|
|
993
|
+
code3, out3 = _popen_capture(
|
|
994
|
+
["uv", "run", "python", "-m", "modal", "app", "list"]
|
|
995
|
+
)
|
|
848
996
|
if code3 == 0 and out3:
|
|
849
997
|
for line in out3.splitlines():
|
|
850
998
|
if name_in in line:
|
|
@@ -857,7 +1005,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
857
1005
|
# Prompt user if still no valid URL
|
|
858
1006
|
if not url:
|
|
859
1007
|
print("\nCould not auto-detect a public Modal URL for the app.")
|
|
860
|
-
entered = input(
|
|
1008
|
+
entered = input(
|
|
1009
|
+
"Enter the Modal public URL (must contain '.modal.run'), or press Enter to abort: "
|
|
1010
|
+
).strip()
|
|
861
1011
|
if entered and _is_modal_public_url(entered):
|
|
862
1012
|
url = entered.rstrip("/")
|
|
863
1013
|
if not url:
|
|
@@ -885,8 +1035,9 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
885
1035
|
print(f"Deploy error: {e}")
|
|
886
1036
|
return 2
|
|
887
1037
|
|
|
888
|
-
|
|
889
|
-
|
|
1038
|
+
print(
|
|
1039
|
+
"`rl_demo configure` prepares environment and secrets; `synth-ai run` now handles launches."
|
|
1040
|
+
)
|
|
890
1041
|
env = demo_core.load_env()
|
|
891
1042
|
synth_key = (env.synth_api_key or "").strip()
|
|
892
1043
|
if not synth_key:
|
|
@@ -919,40 +1070,62 @@ def cmd_deploy(args: argparse.Namespace) -> int:
|
|
|
919
1070
|
|
|
920
1071
|
|
|
921
1072
|
def _ensure_modal_installed() -> None:
|
|
922
|
-
"""Install the modal package if it is not already available."""
|
|
1073
|
+
"""Install the modal package if it is not already available and check authentication."""
|
|
923
1074
|
|
|
1075
|
+
# Check if modal is installed
|
|
1076
|
+
modal_installed = False
|
|
924
1077
|
try:
|
|
925
1078
|
import importlib.util as _iu
|
|
926
1079
|
|
|
927
1080
|
if _iu.find_spec("modal") is not None:
|
|
928
|
-
|
|
929
|
-
return
|
|
1081
|
+
modal_installed = True
|
|
930
1082
|
except Exception:
|
|
931
1083
|
pass
|
|
932
1084
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1085
|
+
# Install modal if needed
|
|
1086
|
+
if not modal_installed:
|
|
1087
|
+
print("modal not found; installing…")
|
|
1088
|
+
try:
|
|
1089
|
+
if shutil.which("uv"):
|
|
1090
|
+
code, out = _popen_capture(["uv", "pip", "install", "modal>=1.1.4"])
|
|
1091
|
+
else:
|
|
1092
|
+
code, out = _popen_capture([sys.executable, "-m", "pip", "install", "modal>=1.1.4"])
|
|
1093
|
+
if code != 0:
|
|
1094
|
+
print(out)
|
|
1095
|
+
print("Failed to install modal; continuing may fail.")
|
|
1096
|
+
return
|
|
1097
|
+
else:
|
|
1098
|
+
print("✓ modal installed successfully")
|
|
1099
|
+
modal_installed = True
|
|
1100
|
+
except Exception as exc:
|
|
1101
|
+
print(f"modal install error: {exc}")
|
|
1102
|
+
return
|
|
946
1103
|
|
|
947
|
-
|
|
948
|
-
|
|
1104
|
+
# Verify modal is importable
|
|
1105
|
+
if modal_installed:
|
|
1106
|
+
try:
|
|
1107
|
+
import importlib.util as _iu
|
|
949
1108
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1109
|
+
if _iu.find_spec("modal") is None:
|
|
1110
|
+
print("Warning: modal is still not importable after install attempt.")
|
|
1111
|
+
return
|
|
1112
|
+
except Exception:
|
|
1113
|
+
print("Warning: unable to verify modal installation.")
|
|
1114
|
+
return
|
|
1115
|
+
|
|
1116
|
+
# Check modal authentication status
|
|
1117
|
+
auth_ok, auth_msg = demo_core.modal_auth_status()
|
|
1118
|
+
if auth_ok:
|
|
1119
|
+
print(f"✓ Modal authenticated: {auth_msg}")
|
|
1120
|
+
else:
|
|
1121
|
+
print("\n⚠️ Modal authentication required")
|
|
1122
|
+
print(f" Status: {auth_msg}")
|
|
1123
|
+
print("\n To authenticate Modal, run:")
|
|
1124
|
+
print(" modal setup")
|
|
1125
|
+
print("\n Or set environment variables:")
|
|
1126
|
+
print(" export MODAL_TOKEN_ID=your-token-id")
|
|
1127
|
+
print(" export MODAL_TOKEN_SECRET=your-token-secret")
|
|
1128
|
+
print("\n You can deploy later after authenticating.\n")
|
|
956
1129
|
|
|
957
1130
|
|
|
958
1131
|
def cmd_init(args: argparse.Namespace) -> int:
|
|
@@ -991,20 +1164,61 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
991
1164
|
assert selected is not None
|
|
992
1165
|
|
|
993
1166
|
default_subdir = selected.default_subdir or selected.template_id
|
|
994
|
-
|
|
1167
|
+
|
|
1168
|
+
# Check if default destination is already occupied and switch to local_demos/ if needed
|
|
1169
|
+
if args.dest:
|
|
1170
|
+
default_dest = Path(args.dest).expanduser().resolve()
|
|
1171
|
+
else:
|
|
1172
|
+
primary_dest = Path.cwd() / default_subdir
|
|
1173
|
+
if primary_dest.exists() and any(primary_dest.iterdir()):
|
|
1174
|
+
# Switch to local_demos/ automatically if primary location is occupied
|
|
1175
|
+
default_dest = (Path.cwd() / "local_demos" / default_subdir).resolve()
|
|
1176
|
+
else:
|
|
1177
|
+
default_dest = primary_dest.resolve()
|
|
1178
|
+
|
|
995
1179
|
try:
|
|
996
1180
|
dest_input = input(f"Destination directory [{default_dest}]: ").strip()
|
|
997
1181
|
except Exception:
|
|
998
1182
|
dest_input = ""
|
|
999
1183
|
destination = Path(dest_input).expanduser().resolve() if dest_input else default_dest
|
|
1000
1184
|
|
|
1185
|
+
# Track whether we should skip individual file prompts (if we already cleared the directory)
|
|
1186
|
+
directory_cleared = False
|
|
1187
|
+
|
|
1001
1188
|
if destination.exists():
|
|
1002
1189
|
if destination.is_file():
|
|
1003
1190
|
print(f"Destination {destination} is a file. Provide a directory path.")
|
|
1004
1191
|
return 1
|
|
1005
|
-
if
|
|
1006
|
-
|
|
1007
|
-
|
|
1192
|
+
if any(destination.iterdir()):
|
|
1193
|
+
try:
|
|
1194
|
+
response = (
|
|
1195
|
+
input(f"Destination {destination} is not empty. Overwrite? [y/N]: ")
|
|
1196
|
+
.strip()
|
|
1197
|
+
.lower()
|
|
1198
|
+
)
|
|
1199
|
+
except (EOFError, KeyboardInterrupt):
|
|
1200
|
+
print("\nCancelled.")
|
|
1201
|
+
return 1
|
|
1202
|
+
if response not in ("y", "yes"):
|
|
1203
|
+
print("Cancelled. Choose another directory or delete the existing one.")
|
|
1204
|
+
return 1
|
|
1205
|
+
# User agreed to overwrite - clear the entire directory including hidden files
|
|
1206
|
+
print(f"Clearing {destination}...")
|
|
1207
|
+
try:
|
|
1208
|
+
# Remove all contents including hidden files (.env, .git, etc.)
|
|
1209
|
+
shutil.rmtree(destination)
|
|
1210
|
+
except Exception as e:
|
|
1211
|
+
print(f"Error clearing directory: {e}")
|
|
1212
|
+
print("Please manually remove the directory and try again.")
|
|
1213
|
+
return 1
|
|
1214
|
+
# Recreate empty directory
|
|
1215
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
1216
|
+
# Verify it's actually empty
|
|
1217
|
+
if any(destination.iterdir()):
|
|
1218
|
+
print(f"Warning: Directory {destination} still contains files after clearing.")
|
|
1219
|
+
print("Some files may not have been removed. Please check manually.")
|
|
1220
|
+
return 1
|
|
1221
|
+
directory_cleared = True
|
|
1008
1222
|
else:
|
|
1009
1223
|
destination.mkdir(parents=True, exist_ok=True)
|
|
1010
1224
|
|
|
@@ -1018,29 +1232,83 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1018
1232
|
print(f"Template source missing: {src_path}")
|
|
1019
1233
|
return 1
|
|
1020
1234
|
dest_path = (destination / spec.destination).resolve()
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1235
|
+
|
|
1236
|
+
# Handle directory copying
|
|
1237
|
+
if src_path.is_dir():
|
|
1238
|
+
if dest_path.exists() and not directory_cleared:
|
|
1239
|
+
try:
|
|
1240
|
+
response = (
|
|
1241
|
+
input(f"Directory {dest_path.name} exists. Overwrite? [y/N]: ")
|
|
1242
|
+
.strip()
|
|
1243
|
+
.lower()
|
|
1244
|
+
)
|
|
1245
|
+
except (EOFError, KeyboardInterrupt):
|
|
1246
|
+
print("\nCancelled.")
|
|
1247
|
+
return 1
|
|
1248
|
+
if response not in ("y", "yes"):
|
|
1249
|
+
print(f"Skipping {dest_path.name}")
|
|
1250
|
+
continue
|
|
1251
|
+
shutil.rmtree(dest_path)
|
|
1252
|
+
elif dest_path.exists() and directory_cleared:
|
|
1253
|
+
shutil.rmtree(dest_path)
|
|
1254
|
+
shutil.copytree(src_path, dest_path)
|
|
1255
|
+
else:
|
|
1256
|
+
# Handle file copying
|
|
1257
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1258
|
+
if dest_path.exists() and not directory_cleared:
|
|
1259
|
+
try:
|
|
1260
|
+
response = (
|
|
1261
|
+
input(f"File {dest_path.name} exists. Overwrite? [y/N]: ")
|
|
1262
|
+
.strip()
|
|
1263
|
+
.lower()
|
|
1264
|
+
)
|
|
1265
|
+
except (EOFError, KeyboardInterrupt):
|
|
1266
|
+
print("\nCancelled.")
|
|
1267
|
+
return 1
|
|
1268
|
+
if response not in ("y", "yes"):
|
|
1269
|
+
print(f"Skipping {dest_path.name}")
|
|
1270
|
+
continue
|
|
1271
|
+
shutil.copy2(src_path, dest_path)
|
|
1272
|
+
if spec.make_executable:
|
|
1273
|
+
try:
|
|
1274
|
+
st = os.stat(dest_path)
|
|
1275
|
+
os.chmod(dest_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
1276
|
+
except Exception:
|
|
1277
|
+
pass
|
|
1032
1278
|
|
|
1033
1279
|
if selected.env_lines:
|
|
1034
1280
|
env_path = destination / ".env"
|
|
1035
|
-
|
|
1281
|
+
should_write = True
|
|
1282
|
+
if env_path.exists() and not directory_cleared:
|
|
1283
|
+
try:
|
|
1284
|
+
response = input("File .env exists. Overwrite? [y/N]: ").strip().lower()
|
|
1285
|
+
except (EOFError, KeyboardInterrupt):
|
|
1286
|
+
print("\nCancelled.")
|
|
1287
|
+
return 1
|
|
1288
|
+
should_write = response in ("y", "yes")
|
|
1289
|
+
if should_write:
|
|
1036
1290
|
_write_text(env_path, "\n".join(selected.env_lines) + "\n")
|
|
1291
|
+
elif not directory_cleared:
|
|
1292
|
+
print("Skipping .env")
|
|
1037
1293
|
|
|
1038
1294
|
config_src = selected.config_source_path()
|
|
1039
1295
|
if config_src and config_src.exists():
|
|
1040
1296
|
cfg_dst = (destination / selected.config_destination).resolve()
|
|
1041
|
-
|
|
1297
|
+
should_copy = True
|
|
1298
|
+
if cfg_dst.exists() and not directory_cleared:
|
|
1299
|
+
try:
|
|
1300
|
+
response = (
|
|
1301
|
+
input(f"File {cfg_dst.name} exists. Overwrite? [y/N]: ").strip().lower()
|
|
1302
|
+
)
|
|
1303
|
+
except (EOFError, KeyboardInterrupt):
|
|
1304
|
+
print("\nCancelled.")
|
|
1305
|
+
return 1
|
|
1306
|
+
should_copy = response in ("y", "yes")
|
|
1307
|
+
if should_copy:
|
|
1042
1308
|
cfg_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
1043
1309
|
shutil.copy2(config_src, cfg_dst)
|
|
1310
|
+
elif not directory_cleared:
|
|
1311
|
+
print(f"Skipping {cfg_dst.name}")
|
|
1044
1312
|
|
|
1045
1313
|
if selected.post_copy is not None:
|
|
1046
1314
|
try:
|
|
@@ -1049,6 +1317,14 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1049
1317
|
print(f"Post-processing failed: {post_exc}")
|
|
1050
1318
|
return 1
|
|
1051
1319
|
|
|
1320
|
+
# Store demo directory for subsequent commands
|
|
1321
|
+
demo_core.persist_demo_dir(str(destination))
|
|
1322
|
+
|
|
1323
|
+
# Store .env path if it was created
|
|
1324
|
+
env_file = destination / ".env"
|
|
1325
|
+
if env_file.exists():
|
|
1326
|
+
demo_core.persist_env_file_path(str(env_file))
|
|
1327
|
+
|
|
1052
1328
|
print(f"Demo template '{selected.name}' materialised at {destination}.")
|
|
1053
1329
|
print("Files created:")
|
|
1054
1330
|
for spec in selected.iter_copy_specs():
|
|
@@ -1057,6 +1333,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1057
1333
|
print(" - .env")
|
|
1058
1334
|
if selected.config_source_path():
|
|
1059
1335
|
print(f" - {selected.config_destination}")
|
|
1336
|
+
print("\nDemo directory stored. Subsequent commands will use this directory automatically.")
|
|
1060
1337
|
print("Review the files, edit .env, and run any provided deploy scripts when ready.")
|
|
1061
1338
|
return 0
|
|
1062
1339
|
except KeyboardInterrupt:
|
|
@@ -1067,8 +1344,14 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
1067
1344
|
return 1
|
|
1068
1345
|
|
|
1069
1346
|
|
|
1070
|
-
def _http(
|
|
1071
|
-
|
|
1347
|
+
def _http(
|
|
1348
|
+
method: str, url: str, headers: dict[str, str] | None = None, body: dict[str, Any] | None = None
|
|
1349
|
+
) -> tuple[int, dict[str, Any] | str]:
|
|
1350
|
+
import json as _json
|
|
1351
|
+
import ssl
|
|
1352
|
+
import urllib.error
|
|
1353
|
+
import urllib.request
|
|
1354
|
+
|
|
1072
1355
|
data = None
|
|
1073
1356
|
if body is not None:
|
|
1074
1357
|
data = _json.dumps(body).encode("utf-8")
|
|
@@ -1106,9 +1389,15 @@ def _write_text(path: str, content: str) -> None:
|
|
|
1106
1389
|
|
|
1107
1390
|
|
|
1108
1391
|
def cmd_run(args: argparse.Namespace) -> int:
|
|
1392
|
+
# Change to demo directory if stored
|
|
1393
|
+
demo_dir = demo_core.load_demo_dir()
|
|
1394
|
+
if demo_dir and os.path.isdir(demo_dir):
|
|
1395
|
+
os.chdir(demo_dir)
|
|
1396
|
+
print(f"Using demo directory: {demo_dir}")
|
|
1397
|
+
|
|
1109
1398
|
env = demo_core.load_env()
|
|
1110
1399
|
cwd_env_path = os.path.join(os.getcwd(), ".env")
|
|
1111
|
-
|
|
1400
|
+
demo_core.load_dotenv_file(cwd_env_path)
|
|
1112
1401
|
|
|
1113
1402
|
synth_key = (env.synth_api_key or "").strip()
|
|
1114
1403
|
if not synth_key:
|
|
@@ -1148,7 +1437,11 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1148
1437
|
# Detect monorepo launcher and delegate if available (aligns with run_clustered.sh which works)
|
|
1149
1438
|
launcher = "/Users/joshpurtell/Documents/GitHub/monorepo/tests/applications/math/rl/start_math_clustered.py"
|
|
1150
1439
|
if os.path.isfile(launcher):
|
|
1151
|
-
backend_base =
|
|
1440
|
+
backend_base = (
|
|
1441
|
+
env.dev_backend_url[:-4]
|
|
1442
|
+
if env.dev_backend_url.endswith("/api")
|
|
1443
|
+
else env.dev_backend_url
|
|
1444
|
+
)
|
|
1152
1445
|
run_env = os.environ.copy()
|
|
1153
1446
|
run_env["BACKEND_URL"] = backend_base
|
|
1154
1447
|
run_env["SYNTH_API_KEY"] = env.synth_api_key
|
|
@@ -1181,13 +1474,15 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1181
1474
|
print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
|
|
1182
1475
|
if ek:
|
|
1183
1476
|
print(f" {_key_preview(ek, 'ENVIRONMENT_API_KEY')}")
|
|
1184
|
-
print(
|
|
1477
|
+
print(
|
|
1478
|
+
"Ensure the ENVIRONMENT_API_KEY you deployed with matches the task app and remains exported."
|
|
1479
|
+
)
|
|
1185
1480
|
return code
|
|
1186
1481
|
|
|
1187
1482
|
# Fallback: legacy jobs API flow
|
|
1188
1483
|
with open(cfg_path, "rb") as fh:
|
|
1189
1484
|
inline_cfg = tomllib.load(fh)
|
|
1190
|
-
with open(cfg_path
|
|
1485
|
+
with open(cfg_path) as fh2:
|
|
1191
1486
|
toml_text = fh2.read()
|
|
1192
1487
|
if args.batch_size is not None:
|
|
1193
1488
|
inline_cfg.setdefault("training", {})["batch_size"] = int(args.batch_size)
|
|
@@ -1198,13 +1493,11 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1198
1493
|
# Print backend and key preview before request for clearer diagnostics
|
|
1199
1494
|
try:
|
|
1200
1495
|
sk = (env.synth_api_key or "").strip()
|
|
1201
|
-
sk_len = len(sk)
|
|
1202
|
-
sk_tail = sk[-5:] if sk_len >= 5 else sk
|
|
1203
1496
|
print(f"[run] Backend API: {api}")
|
|
1204
1497
|
print(f"[run] {_key_preview(sk, 'SYNTH_API_KEY')}")
|
|
1205
1498
|
except Exception:
|
|
1206
1499
|
pass
|
|
1207
|
-
data_fragment:
|
|
1500
|
+
data_fragment: dict[str, Any] = {
|
|
1208
1501
|
"model": model_name,
|
|
1209
1502
|
"endpoint_base_url": env.task_app_base_url,
|
|
1210
1503
|
"config": inline_cfg,
|
|
@@ -1222,23 +1515,28 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1222
1515
|
if inline_cfg["compute"].get("gpu_type"):
|
|
1223
1516
|
compute["gpu_type"] = str(inline_cfg["compute"]["gpu_type"]).upper()
|
|
1224
1517
|
if inline_cfg["compute"].get("gpu_count"):
|
|
1225
|
-
compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
|
|
1518
|
+
compute["gpu_count"] = int(inline_cfg["compute"]["gpu_count"])
|
|
1226
1519
|
if not compute:
|
|
1227
1520
|
topo = inline_cfg.get("topology") or {}
|
|
1228
1521
|
gshape = str(topo.get("gpu_type") or "")
|
|
1229
1522
|
if ":" in gshape:
|
|
1230
1523
|
t, c = gshape.split(":", 1)
|
|
1231
1524
|
compute = {"gpu_type": t.upper(), "gpu_count": int(c)}
|
|
1232
|
-
body:
|
|
1525
|
+
body: dict[str, Any] = {
|
|
1233
1526
|
"job_type": "rl",
|
|
1234
1527
|
"data": data_fragment,
|
|
1235
1528
|
}
|
|
1236
1529
|
if compute:
|
|
1237
1530
|
body["compute"] = compute
|
|
1238
|
-
code, js = _http(
|
|
1239
|
-
"
|
|
1240
|
-
|
|
1241
|
-
|
|
1531
|
+
code, js = _http(
|
|
1532
|
+
"POST",
|
|
1533
|
+
api + "/rl/jobs",
|
|
1534
|
+
headers={
|
|
1535
|
+
"Content-Type": "application/json",
|
|
1536
|
+
"Authorization": f"Bearer {env.synth_api_key}",
|
|
1537
|
+
},
|
|
1538
|
+
body=body,
|
|
1539
|
+
)
|
|
1242
1540
|
if code not in (200, 201) or not isinstance(js, dict):
|
|
1243
1541
|
print("Job create failed:", code)
|
|
1244
1542
|
print(f"Backend: {api}")
|
|
@@ -1276,12 +1574,14 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1276
1574
|
try:
|
|
1277
1575
|
sent_key = detail.get("sent_key")
|
|
1278
1576
|
if isinstance(sent_key, str):
|
|
1279
|
-
print(
|
|
1577
|
+
print(
|
|
1578
|
+
f"[run] Backend detail.sent_key {_key_preview(sent_key, 'detail.sent_key')}"
|
|
1579
|
+
)
|
|
1280
1580
|
except Exception:
|
|
1281
1581
|
pass
|
|
1282
1582
|
try:
|
|
1283
1583
|
sent_keys = detail.get("sent_keys")
|
|
1284
|
-
if isinstance(sent_keys,
|
|
1584
|
+
if isinstance(sent_keys, list | tuple):
|
|
1285
1585
|
previews = []
|
|
1286
1586
|
for idx, val in enumerate(sent_keys):
|
|
1287
1587
|
if isinstance(val, str):
|
|
@@ -1306,12 +1606,19 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1306
1606
|
# Extra hints for auth failures
|
|
1307
1607
|
try:
|
|
1308
1608
|
sk = (env.synth_api_key or "").strip()
|
|
1309
|
-
if int(code) == 401 or (
|
|
1609
|
+
if int(code) == 401 or (
|
|
1610
|
+
isinstance(js, dict)
|
|
1611
|
+
and any(isinstance(v, str) and "Invalid API key" in v for v in js.values())
|
|
1612
|
+
):
|
|
1310
1613
|
base_url = env.dev_backend_url
|
|
1311
|
-
print(
|
|
1614
|
+
print(
|
|
1615
|
+
"Hint: HTTP 401 Unauthorized from backend. Verify SYNTH_API_KEY for:", base_url
|
|
1616
|
+
)
|
|
1312
1617
|
if sk:
|
|
1313
1618
|
print(f" {_key_preview(sk, 'SYNTH_API_KEY')}")
|
|
1314
|
-
print(
|
|
1619
|
+
print(
|
|
1620
|
+
"Ensure the ENVIRONMENT_API_KEY and OPENAI_API_KEY used for deployment remain valid."
|
|
1621
|
+
)
|
|
1315
1622
|
except Exception:
|
|
1316
1623
|
pass
|
|
1317
1624
|
return 2
|
|
@@ -1363,9 +1670,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1363
1670
|
"rl.performance.metrics",
|
|
1364
1671
|
):
|
|
1365
1672
|
print(f"[{seq}] {typ}: {msg}")
|
|
1366
|
-
mc, mj = _http(
|
|
1367
|
-
"GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50"
|
|
1368
|
-
)
|
|
1673
|
+
mc, mj = _http("GET", api + f"/learning/jobs/{job_id}/metrics?after_step=-1&limit=50")
|
|
1369
1674
|
if mc == 200 and isinstance(mj, dict):
|
|
1370
1675
|
pts = mj.get("points") or []
|
|
1371
1676
|
for p in pts:
|
|
@@ -1384,17 +1689,23 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1384
1689
|
p = argparse.ArgumentParser(prog="synth-ai")
|
|
1385
1690
|
sub = p.add_subparsers(dest="cmd")
|
|
1386
1691
|
|
|
1387
|
-
def _add_parser(
|
|
1692
|
+
def _add_parser(
|
|
1693
|
+
names: list[str], *, configure: Callable[[argparse.ArgumentParser], None]
|
|
1694
|
+
) -> None:
|
|
1388
1695
|
for name in names:
|
|
1389
1696
|
parser = sub.add_parser(name)
|
|
1390
1697
|
configure(parser)
|
|
1391
1698
|
|
|
1392
|
-
_add_parser(
|
|
1699
|
+
_add_parser(
|
|
1700
|
+
["rl_demo.setup", "demo.setup"],
|
|
1701
|
+
configure=lambda parser: parser.set_defaults(func=cmd_setup),
|
|
1702
|
+
)
|
|
1393
1703
|
|
|
1394
1704
|
def _init_opts(parser):
|
|
1395
1705
|
parser.add_argument("--template", type=str, default=None, help="Template id to instantiate")
|
|
1396
|
-
parser.add_argument(
|
|
1397
|
-
|
|
1706
|
+
parser.add_argument(
|
|
1707
|
+
"--dest", type=str, default=None, help="Destination directory for files"
|
|
1708
|
+
)
|
|
1398
1709
|
parser.set_defaults(func=cmd_init)
|
|
1399
1710
|
|
|
1400
1711
|
_add_parser(["rl_demo.init", "demo.init"], configure=_init_opts)
|
|
@@ -1402,18 +1713,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1402
1713
|
# (prepare command removed)
|
|
1403
1714
|
|
|
1404
1715
|
def _deploy_opts(parser):
|
|
1405
|
-
parser.add_argument(
|
|
1406
|
-
|
|
1716
|
+
parser.add_argument(
|
|
1717
|
+
"--local", action="store_true", help="Run local FastAPI instead of Modal deploy"
|
|
1718
|
+
)
|
|
1719
|
+
parser.add_argument(
|
|
1720
|
+
"--app", type=str, default=None, help="Path to Modal app.py for uv run modal deploy"
|
|
1721
|
+
)
|
|
1407
1722
|
parser.add_argument("--name", type=str, default=None, help="Modal app name")
|
|
1408
|
-
parser.add_argument(
|
|
1723
|
+
parser.add_argument(
|
|
1724
|
+
"--script", type=str, default=None, help="Path to deploy_task_app.sh (optional legacy)"
|
|
1725
|
+
)
|
|
1409
1726
|
parser.set_defaults(func=cmd_deploy)
|
|
1410
1727
|
|
|
1411
1728
|
_add_parser(["rl_demo.deploy", "demo.deploy"], configure=_deploy_opts)
|
|
1412
1729
|
|
|
1413
|
-
_add_parser(
|
|
1730
|
+
_add_parser(
|
|
1731
|
+
["rl_demo.configure", "demo.configure"],
|
|
1732
|
+
configure=lambda parser: parser.set_defaults(func=cmd_run),
|
|
1733
|
+
)
|
|
1414
1734
|
|
|
1415
1735
|
def _run_opts(parser):
|
|
1416
|
-
parser.add_argument(
|
|
1736
|
+
parser.add_argument(
|
|
1737
|
+
"--config", type=str, default=None, help="Path to TOML config (skip prompt)"
|
|
1738
|
+
)
|
|
1417
1739
|
parser.add_argument("--batch-size", type=int, default=None)
|
|
1418
1740
|
parser.add_argument("--group-size", type=int, default=None)
|
|
1419
1741
|
parser.add_argument("--model", type=str, default=None)
|