synth-ai 0.2.9.dev4__py3-none-any.whl → 0.2.9.dev6__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/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/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/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/__init__.py +14 -0
- synth_ai/__init__.py +20 -4
- 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 +1709 -243
- 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.dev6.dist-info/METADATA +191 -0
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/RECORD +291 -264
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/top_level.txt +1 -0
- 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
- examples/warming_up_to_rl/old/event_rewards.md +0 -234
- examples/warming_up_to_rl/old/notes.md +0 -73
- examples/warming_up_to_rl/task_app/synth_envs_hosted/test_stepwise_rewards.py +0 -58
- 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/environments/examples/sokoban/units/astar_common.py +0 -95
- 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-0.2.9.dev4.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}/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.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.9.dev4.dist-info → synth_ai-0.2.9.dev6.dist-info}/licenses/LICENSE +0 -0
synth_ai/api/train/cli.py
CHANGED
|
@@ -2,21 +2,22 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
+
from synth_ai.config.base_url import get_backend_from_env
|
|
8
9
|
|
|
9
|
-
from .builders import
|
|
10
|
+
from .builders import build_rl_payload, build_sft_payload
|
|
10
11
|
from .config_finder import discover_configs, prompt_for_config
|
|
11
12
|
from .env_resolver import KeySpec, resolve_env
|
|
12
13
|
from .pollers import RLJobPoller, SFTJobPoller
|
|
13
14
|
from .task_app import check_task_app_health
|
|
14
15
|
from .utils import (
|
|
15
|
-
TrainError,
|
|
16
16
|
REPO_ROOT,
|
|
17
|
+
TrainError,
|
|
17
18
|
ensure_api_base,
|
|
18
|
-
http_post,
|
|
19
19
|
http_get,
|
|
20
|
+
http_post,
|
|
20
21
|
limit_jsonl_examples,
|
|
21
22
|
mask_value,
|
|
22
23
|
post_multipart,
|
|
@@ -92,20 +93,72 @@ def _prompt_manual_dataset() -> Path:
|
|
|
92
93
|
return Path(manual).expanduser()
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
def _default_backend() -> str:
|
|
97
|
+
"""Resolve backend URL with proper production default."""
|
|
98
|
+
# Check explicit override first
|
|
99
|
+
explicit = os.getenv("BACKEND_BASE_URL", "").strip()
|
|
100
|
+
if explicit:
|
|
101
|
+
return explicit
|
|
102
|
+
# Use standard resolution logic
|
|
103
|
+
base, _ = get_backend_from_env()
|
|
104
|
+
return f"{base}/api" if not base.endswith("/api") else base
|
|
105
|
+
|
|
106
|
+
|
|
95
107
|
@click.command("train")
|
|
96
|
-
@click.option(
|
|
108
|
+
@click.option(
|
|
109
|
+
"--config",
|
|
110
|
+
"config_paths",
|
|
111
|
+
multiple=True,
|
|
112
|
+
type=click.Path(),
|
|
113
|
+
help="Path to training TOML (repeatable)",
|
|
114
|
+
)
|
|
97
115
|
@click.option("--type", "train_type", type=click.Choice(["auto", "rl", "sft"]), default="auto")
|
|
98
|
-
@click.option(
|
|
116
|
+
@click.option(
|
|
117
|
+
"--env-file",
|
|
118
|
+
"env_files",
|
|
119
|
+
multiple=True,
|
|
120
|
+
type=click.Path(),
|
|
121
|
+
help=".env file(s) to preload (skips selection prompt)",
|
|
122
|
+
)
|
|
99
123
|
@click.option("--task-url", default=None, help="Override task app base URL (RL only)")
|
|
100
|
-
@click.option(
|
|
101
|
-
|
|
124
|
+
@click.option(
|
|
125
|
+
"--dataset",
|
|
126
|
+
"dataset_path",
|
|
127
|
+
type=click.Path(),
|
|
128
|
+
default=None,
|
|
129
|
+
help="Override dataset JSONL path (SFT)",
|
|
130
|
+
)
|
|
131
|
+
@click.option("--backend", default=_default_backend, help="Backend base URL")
|
|
102
132
|
@click.option("--model", default=None, help="Override model identifier")
|
|
133
|
+
@click.option(
|
|
134
|
+
"--allow-experimental",
|
|
135
|
+
"allow_experimental",
|
|
136
|
+
is_flag=True,
|
|
137
|
+
flag_value=True,
|
|
138
|
+
default=None,
|
|
139
|
+
help="Allow experimental models (overrides SDK_EXPERIMENTAL env)",
|
|
140
|
+
)
|
|
141
|
+
@click.option(
|
|
142
|
+
"--no-allow-experimental",
|
|
143
|
+
"allow_experimental",
|
|
144
|
+
is_flag=True,
|
|
145
|
+
flag_value=False,
|
|
146
|
+
help="Disallow experimental models (overrides SDK_EXPERIMENTAL env)",
|
|
147
|
+
)
|
|
103
148
|
@click.option("--idempotency", default=None, help="Idempotency-Key header for job creation")
|
|
104
|
-
@click.option("--dry-run", is_flag=True, help="
|
|
149
|
+
@click.option("--dry-run", is_flag=True, hidden=True, help="Deprecated: no-op")
|
|
105
150
|
@click.option("--poll/--no-poll", default=True, help="Poll job status until terminal state")
|
|
106
|
-
@click.option(
|
|
151
|
+
@click.option(
|
|
152
|
+
"--poll-timeout", default=3600.0, type=float, help="Maximum seconds to poll before timing out"
|
|
153
|
+
)
|
|
107
154
|
@click.option("--poll-interval", default=5.0, type=float, help="Seconds between poll attempts")
|
|
108
|
-
@click.option(
|
|
155
|
+
@click.option(
|
|
156
|
+
"--examples",
|
|
157
|
+
"examples_limit",
|
|
158
|
+
type=int,
|
|
159
|
+
default=None,
|
|
160
|
+
help="Limit SFT training to the first N examples",
|
|
161
|
+
)
|
|
109
162
|
def train_command(
|
|
110
163
|
config_paths: tuple[str, ...],
|
|
111
164
|
train_type: str,
|
|
@@ -114,6 +167,7 @@ def train_command(
|
|
|
114
167
|
dataset_path: str | None,
|
|
115
168
|
backend: str,
|
|
116
169
|
model: str | None,
|
|
170
|
+
allow_experimental: bool | None,
|
|
117
171
|
idempotency: str | None,
|
|
118
172
|
dry_run: bool,
|
|
119
173
|
poll: bool,
|
|
@@ -123,12 +177,20 @@ def train_command(
|
|
|
123
177
|
) -> None:
|
|
124
178
|
"""Interactive launcher for RL / SFT jobs."""
|
|
125
179
|
|
|
126
|
-
candidates = discover_configs(
|
|
127
|
-
|
|
180
|
+
candidates = discover_configs(
|
|
181
|
+
list(config_paths), requested_type=train_type if train_type != "auto" else None
|
|
182
|
+
)
|
|
183
|
+
selection = prompt_for_config(
|
|
184
|
+
candidates,
|
|
185
|
+
requested_type=train_type if train_type != "auto" else None,
|
|
186
|
+
allow_autoselect=bool(config_paths),
|
|
187
|
+
)
|
|
128
188
|
|
|
129
189
|
effective_type = train_type if train_type != "auto" else selection.train_type
|
|
130
190
|
if effective_type not in {"rl", "sft"}:
|
|
131
|
-
effective_type = click.prompt(
|
|
191
|
+
effective_type = click.prompt(
|
|
192
|
+
"Detected config type is ambiguous. Enter type", type=click.Choice(["rl", "sft"])
|
|
193
|
+
)
|
|
132
194
|
|
|
133
195
|
cfg_path = selection.path
|
|
134
196
|
click.echo(f"Using config: {cfg_path} ({effective_type})")
|
|
@@ -199,6 +261,7 @@ def train_command(
|
|
|
199
261
|
task_url_override=task_url,
|
|
200
262
|
model_override=model,
|
|
201
263
|
idempotency=idempotency,
|
|
264
|
+
allow_experimental=allow_experimental,
|
|
202
265
|
dry_run=dry_run,
|
|
203
266
|
poll=poll,
|
|
204
267
|
poll_timeout=poll_timeout,
|
|
@@ -211,6 +274,7 @@ def train_command(
|
|
|
211
274
|
backend_base=backend_base,
|
|
212
275
|
synth_key=synth_key,
|
|
213
276
|
dataset_override=dataset_override_path,
|
|
277
|
+
allow_experimental=allow_experimental,
|
|
214
278
|
dry_run=dry_run,
|
|
215
279
|
poll=poll,
|
|
216
280
|
poll_timeout=poll_timeout,
|
|
@@ -219,11 +283,14 @@ def train_command(
|
|
|
219
283
|
)
|
|
220
284
|
|
|
221
285
|
|
|
222
|
-
def _wait_for_training_file(
|
|
286
|
+
def _wait_for_training_file(
|
|
287
|
+
backend_base: str, api_key: str, file_id: str, *, timeout: float = 120.0
|
|
288
|
+
) -> None:
|
|
223
289
|
url = f"{backend_base}/learning/files/{file_id}"
|
|
224
290
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
225
291
|
elapsed = 0.0
|
|
226
292
|
interval = 2.0
|
|
293
|
+
first_check = True
|
|
227
294
|
while True:
|
|
228
295
|
resp = http_get(url, headers=headers, timeout=30.0)
|
|
229
296
|
if resp.status_code == 200:
|
|
@@ -231,17 +298,55 @@ def _wait_for_training_file(backend_base: str, api_key: str, file_id: str, *, ti
|
|
|
231
298
|
data = resp.json()
|
|
232
299
|
except Exception:
|
|
233
300
|
data = {}
|
|
234
|
-
status = str(
|
|
301
|
+
status = str(
|
|
302
|
+
data.get("status") or data.get("state") or data.get("storage_state") or "ready"
|
|
303
|
+
).lower()
|
|
304
|
+
if first_check:
|
|
305
|
+
click.echo(f"File uploaded successfully (id={file_id}, status={status})")
|
|
306
|
+
first_check = False
|
|
235
307
|
if status in {"ready", "uploaded", "stored", "complete"}:
|
|
308
|
+
click.echo(f"✓ Training file ready (status={status})")
|
|
236
309
|
return
|
|
310
|
+
# Show progress for processing states
|
|
311
|
+
if status in {"processing", "pending", "validating"}:
|
|
312
|
+
click.echo(
|
|
313
|
+
f" Waiting for file processing... (status={status}, {elapsed:.0f}s elapsed)"
|
|
314
|
+
)
|
|
237
315
|
elif resp.status_code == 404:
|
|
238
316
|
# Keep polling; object may not be visible yet
|
|
239
|
-
|
|
317
|
+
if first_check:
|
|
318
|
+
click.echo(f"Waiting for file {file_id} to become visible...")
|
|
319
|
+
first_check = False
|
|
320
|
+
elif resp.status_code in {401, 403}:
|
|
321
|
+
# Auth errors won't resolve by polling - fail immediately
|
|
322
|
+
try:
|
|
323
|
+
error_body = resp.json()
|
|
324
|
+
except Exception:
|
|
325
|
+
error_body = resp.text[:400]
|
|
326
|
+
click.echo("\n[ERROR] Authentication failed when checking training file:")
|
|
327
|
+
click.echo(f" URL: {url}")
|
|
328
|
+
click.echo(f" Status: {resp.status_code}")
|
|
329
|
+
click.echo(f" Response: {error_body}")
|
|
330
|
+
click.echo(f" API key: {mask_value(api_key)}")
|
|
331
|
+
raise click.ClickException(
|
|
332
|
+
f"Authentication error ({resp.status_code}). "
|
|
333
|
+
"Check that your SYNTH_API_KEY is valid and has permission to access this organization's files."
|
|
334
|
+
)
|
|
240
335
|
else:
|
|
241
|
-
|
|
336
|
+
# Other errors - show details but keep polling
|
|
337
|
+
try:
|
|
338
|
+
error_body = resp.json()
|
|
339
|
+
except Exception:
|
|
340
|
+
error_body = resp.text[:400]
|
|
341
|
+
click.echo(f"[WARN] Unexpected response checking file {file_id}:")
|
|
342
|
+
click.echo(f" URL: {url}")
|
|
343
|
+
click.echo(f" Status: {resp.status_code}")
|
|
344
|
+
click.echo(f" Response: {error_body}")
|
|
242
345
|
|
|
243
346
|
if elapsed >= timeout:
|
|
244
|
-
raise click.ClickException(
|
|
347
|
+
raise click.ClickException(
|
|
348
|
+
f"Training file {file_id} not ready after {timeout:.0f}s (last status: {resp.status_code})"
|
|
349
|
+
)
|
|
245
350
|
sleep(interval)
|
|
246
351
|
elapsed += interval
|
|
247
352
|
|
|
@@ -254,31 +359,41 @@ def handle_rl(
|
|
|
254
359
|
task_url_override: str | None,
|
|
255
360
|
model_override: str | None,
|
|
256
361
|
idempotency: str | None,
|
|
362
|
+
allow_experimental: bool | None,
|
|
257
363
|
dry_run: bool,
|
|
258
364
|
poll: bool,
|
|
259
365
|
poll_timeout: float,
|
|
260
366
|
poll_interval: float,
|
|
261
367
|
) -> None:
|
|
262
|
-
overrides:
|
|
368
|
+
overrides: dict[str, Any] = {
|
|
369
|
+
"backend": backend_base,
|
|
370
|
+
"task_url": task_url_override,
|
|
371
|
+
"model": model_override,
|
|
372
|
+
}
|
|
263
373
|
build = build_rl_payload(
|
|
264
374
|
config_path=cfg_path,
|
|
265
375
|
task_url=task_url_override or os.environ.get("TASK_APP_URL", ""),
|
|
266
376
|
overrides=overrides,
|
|
267
377
|
idempotency=idempotency,
|
|
378
|
+
allow_experimental=allow_experimental,
|
|
268
379
|
)
|
|
269
380
|
|
|
270
381
|
# Backend-side verification: try ALL org environment keys against /health and /task_info
|
|
271
382
|
verify_url = f"{backend_base}/rl/verify_task_app"
|
|
272
383
|
verify_headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
|
|
273
384
|
try:
|
|
274
|
-
vresp = http_post(
|
|
385
|
+
vresp = http_post(
|
|
386
|
+
verify_url, headers=verify_headers, json_body={"endpoint_base_url": build.task_url}
|
|
387
|
+
)
|
|
275
388
|
try:
|
|
276
389
|
vjs = vresp.json()
|
|
277
390
|
except Exception:
|
|
278
391
|
vjs = {"status": vresp.status_code, "text": (vresp.text or "")[:400]}
|
|
279
392
|
except Exception as _ve:
|
|
280
|
-
raise click.ClickException(
|
|
281
|
-
|
|
393
|
+
raise click.ClickException(
|
|
394
|
+
f"Task app verification call failed: {type(_ve).__name__}: {_ve}"
|
|
395
|
+
) from _ve
|
|
396
|
+
if vresp.status_code is not None and vresp.status_code >= 400:
|
|
282
397
|
click.echo("Task app verification error:\n" + preview_json(vjs, limit=800))
|
|
283
398
|
raise click.ClickException(f"Verification failed with status {vresp.status_code}")
|
|
284
399
|
if not bool(vjs.get("any_ok")):
|
|
@@ -314,9 +429,6 @@ def handle_rl(
|
|
|
314
429
|
|
|
315
430
|
click.echo(f"POST {create_url}")
|
|
316
431
|
click.echo("Payload preview:\n" + preview_json(build.payload, limit=800))
|
|
317
|
-
if dry_run:
|
|
318
|
-
click.echo("Dry run enabled; skipping submission")
|
|
319
|
-
return
|
|
320
432
|
|
|
321
433
|
resp = http_post(create_url, headers=headers, json_body=build.payload)
|
|
322
434
|
try:
|
|
@@ -346,6 +458,7 @@ def handle_sft(
|
|
|
346
458
|
backend_base: str,
|
|
347
459
|
synth_key: str,
|
|
348
460
|
dataset_override: Path | None,
|
|
461
|
+
allow_experimental: bool | None,
|
|
349
462
|
dry_run: bool,
|
|
350
463
|
poll: bool,
|
|
351
464
|
poll_timeout: float,
|
|
@@ -356,7 +469,11 @@ def handle_sft(
|
|
|
356
469
|
|
|
357
470
|
while True:
|
|
358
471
|
try:
|
|
359
|
-
build = build_sft_payload(
|
|
472
|
+
build = build_sft_payload(
|
|
473
|
+
config_path=cfg_path,
|
|
474
|
+
dataset_override=dataset_path,
|
|
475
|
+
allow_experimental=allow_experimental,
|
|
476
|
+
)
|
|
360
477
|
break
|
|
361
478
|
except TrainError as exc:
|
|
362
479
|
click.echo(str(exc))
|
|
@@ -379,55 +496,94 @@ def handle_sft(
|
|
|
379
496
|
validate_sft_jsonl(build.validation_file)
|
|
380
497
|
|
|
381
498
|
upload_url = f"{backend_base}/learning/files"
|
|
382
|
-
click.echo(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if resp.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
499
|
+
click.echo("\n=== Uploading Training Data ===")
|
|
500
|
+
click.echo(f"Dataset: {build.train_file}")
|
|
501
|
+
click.echo(f"Destination: {upload_url}")
|
|
502
|
+
resp = post_multipart(
|
|
503
|
+
upload_url, api_key=synth_key, file_field="file", file_path=build.train_file
|
|
504
|
+
)
|
|
505
|
+
js = (
|
|
506
|
+
resp.json()
|
|
507
|
+
if resp.headers.get("content-type", "").startswith("application/json")
|
|
508
|
+
else {}
|
|
509
|
+
)
|
|
510
|
+
if resp.status_code is not None and resp.status_code >= 400 or "id" not in js:
|
|
511
|
+
click.echo("\n[ERROR] Training file upload failed:")
|
|
512
|
+
click.echo(f" URL: {upload_url}")
|
|
513
|
+
click.echo(f" Status: {resp.status_code}")
|
|
514
|
+
click.echo(f" Response: {js or resp.text[:400]}")
|
|
515
|
+
click.echo(f" File: {build.train_file}")
|
|
516
|
+
raise click.ClickException(
|
|
517
|
+
f"Training file upload failed with status {resp.status_code}"
|
|
518
|
+
)
|
|
519
|
+
train_file_id = js["id"]
|
|
520
|
+
click.echo(f"✓ Training file uploaded (id={train_file_id})")
|
|
521
|
+
val_file_id = None
|
|
522
|
+
if build.validation_file:
|
|
523
|
+
click.echo(f"Uploading validation dataset: {build.validation_file}")
|
|
524
|
+
vresp = post_multipart(
|
|
525
|
+
upload_url,
|
|
526
|
+
api_key=synth_key,
|
|
527
|
+
file_field="file",
|
|
528
|
+
file_path=build.validation_file,
|
|
529
|
+
)
|
|
530
|
+
vjs = (
|
|
531
|
+
vresp.json()
|
|
532
|
+
if vresp.headers.get("content-type", "").startswith("application/json")
|
|
533
|
+
else {}
|
|
534
|
+
)
|
|
535
|
+
if vresp.status_code is not None and vresp.status_code < 400 and "id" in vjs:
|
|
536
|
+
val_file_id = vjs["id"]
|
|
537
|
+
click.echo(f"✓ Validation file uploaded (id={val_file_id})")
|
|
538
|
+
else:
|
|
539
|
+
click.echo(
|
|
540
|
+
f"[WARN] Validation upload failed ({vresp.status_code}): {vjs or vresp.text[:200]}"
|
|
541
|
+
)
|
|
402
542
|
payload = dict(build.payload)
|
|
403
543
|
payload["training_file_id"] = train_file_id
|
|
404
544
|
if val_file_id:
|
|
405
|
-
payload.setdefault("metadata", {}).setdefault("effective_config", {}).setdefault(
|
|
545
|
+
payload.setdefault("metadata", {}).setdefault("effective_config", {}).setdefault(
|
|
546
|
+
"data", {}
|
|
547
|
+
)["validation_files"] = [val_file_id]
|
|
406
548
|
|
|
549
|
+
click.echo("\n=== Checking File Processing Status ===")
|
|
407
550
|
try:
|
|
408
551
|
_wait_for_training_file(backend_base, synth_key, train_file_id)
|
|
409
552
|
except click.ClickException as exc:
|
|
410
553
|
raise click.ClickException(f"Training file {train_file_id} not ready: {exc}") from exc
|
|
411
554
|
|
|
412
|
-
click.echo("
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return
|
|
555
|
+
click.echo("\n=== Creating Training Job ===")
|
|
556
|
+
click.echo("Job payload preview:")
|
|
557
|
+
click.echo(preview_json(payload, limit=800))
|
|
416
558
|
|
|
417
559
|
create_url = f"{backend_base}/learning/jobs"
|
|
418
560
|
headers = {"Authorization": f"Bearer {synth_key}", "Content-Type": "application/json"}
|
|
561
|
+
click.echo(f"\nPOST {create_url}")
|
|
419
562
|
resp = http_post(create_url, headers=headers, json_body=payload)
|
|
420
|
-
js =
|
|
421
|
-
|
|
563
|
+
js = (
|
|
564
|
+
resp.json()
|
|
565
|
+
if resp.headers.get("content-type", "").startswith("application/json")
|
|
566
|
+
else {}
|
|
567
|
+
)
|
|
422
568
|
if resp.status_code not in (200, 201):
|
|
423
|
-
|
|
569
|
+
click.echo("\n[ERROR] Job creation failed:")
|
|
570
|
+
click.echo(f" URL: {create_url}")
|
|
571
|
+
click.echo(f" Status: {resp.status_code}")
|
|
572
|
+
click.echo(f" Response: {preview_json(js, limit=600)}")
|
|
573
|
+
raise click.ClickException(f"Job creation failed with status {resp.status_code}")
|
|
424
574
|
job_id = js.get("job_id") or js.get("id")
|
|
425
575
|
if not job_id:
|
|
426
576
|
raise click.ClickException("Response missing job id")
|
|
577
|
+
click.echo(f"✓ Job created (id={job_id})")
|
|
427
578
|
|
|
579
|
+
click.echo("\n=== Starting Training Job ===")
|
|
428
580
|
start_url = f"{backend_base}/learning/jobs/{job_id}/start"
|
|
429
|
-
click.echo(f"POST {start_url}
|
|
430
|
-
|
|
581
|
+
click.echo(f"POST {start_url}")
|
|
582
|
+
start_resp = http_post(start_url, headers=headers, json_body={})
|
|
583
|
+
if start_resp.status_code not in (200, 201):
|
|
584
|
+
click.echo(f"[WARN] Job start returned status {start_resp.status_code}")
|
|
585
|
+
else:
|
|
586
|
+
click.echo("✓ Job started")
|
|
431
587
|
|
|
432
588
|
if not poll:
|
|
433
589
|
click.echo(f"Started job {job_id} (polling disabled)")
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Iterable
|
|
3
6
|
from dataclasses import dataclass
|
|
4
7
|
from pathlib import Path
|
|
5
|
-
from typing import Iterable
|
|
6
8
|
|
|
7
9
|
import click
|
|
8
10
|
|
|
9
11
|
from .utils import REPO_ROOT, load_toml, preview_json
|
|
10
12
|
|
|
11
13
|
_SKIP_DIRS = {".git", "__pycache__", ".venv", "node_modules", "dist", "build"}
|
|
14
|
+
_STATE_FILE = os.path.expanduser("~/.synth-ai/demo.json")
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
@dataclass(slots=True)
|
|
@@ -17,9 +20,43 @@ class ConfigCandidate:
|
|
|
17
20
|
train_type: str # "rl", "sft", or "unknown"
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
def _load_last_config() -> Path | None:
|
|
24
|
+
"""Load the last used training config path from state file."""
|
|
25
|
+
try:
|
|
26
|
+
if os.path.isfile(_STATE_FILE):
|
|
27
|
+
with open(_STATE_FILE) as fh:
|
|
28
|
+
data = json.load(fh)
|
|
29
|
+
if isinstance(data, dict):
|
|
30
|
+
last_config = data.get("LAST_CONFIG")
|
|
31
|
+
if last_config:
|
|
32
|
+
path = Path(last_config).resolve()
|
|
33
|
+
if path.exists():
|
|
34
|
+
return path
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _save_last_config(config_path: Path) -> None:
|
|
41
|
+
"""Save the last used training config path to state file."""
|
|
42
|
+
try:
|
|
43
|
+
data = {}
|
|
44
|
+
if os.path.isfile(_STATE_FILE):
|
|
45
|
+
with open(_STATE_FILE) as fh:
|
|
46
|
+
data = json.load(fh) or {}
|
|
47
|
+
if not isinstance(data, dict):
|
|
48
|
+
data = {}
|
|
49
|
+
data["LAST_CONFIG"] = str(config_path.resolve())
|
|
50
|
+
os.makedirs(os.path.dirname(_STATE_FILE), exist_ok=True)
|
|
51
|
+
with open(_STATE_FILE, "w") as fh:
|
|
52
|
+
json.dump(data, fh)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
20
57
|
def _iter_candidate_paths() -> Iterable[Path]:
|
|
21
58
|
seen: set[Path] = set()
|
|
22
|
-
|
|
59
|
+
|
|
23
60
|
# Prioritize current working directory first
|
|
24
61
|
try:
|
|
25
62
|
cwd = Path.cwd().resolve()
|
|
@@ -135,23 +172,46 @@ def discover_configs(explicit: list[str], *, requested_type: str | None) -> list
|
|
|
135
172
|
return candidates
|
|
136
173
|
|
|
137
174
|
|
|
138
|
-
def prompt_for_config(
|
|
175
|
+
def prompt_for_config(
|
|
176
|
+
candidates: list[ConfigCandidate], *, requested_type: str | None, allow_autoselect: bool = False
|
|
177
|
+
) -> ConfigCandidate:
|
|
139
178
|
if not candidates:
|
|
140
179
|
raise click.ClickException("No training configs found. Pass --config explicitly.")
|
|
141
180
|
|
|
181
|
+
# Check for last used config and move it to the top if found
|
|
182
|
+
last_config = _load_last_config()
|
|
183
|
+
default_idx = 1
|
|
184
|
+
|
|
185
|
+
if allow_autoselect and len(candidates) == 1:
|
|
186
|
+
chosen = candidates[0]
|
|
187
|
+
_save_last_config(chosen.path)
|
|
188
|
+
return chosen
|
|
189
|
+
|
|
190
|
+
if last_config:
|
|
191
|
+
for idx, cand in enumerate(candidates):
|
|
192
|
+
if cand.path.resolve() == last_config:
|
|
193
|
+
# Move last used config to the front
|
|
194
|
+
candidates.insert(0, candidates.pop(idx))
|
|
195
|
+
break
|
|
196
|
+
|
|
142
197
|
click.echo("Select a training config:")
|
|
143
198
|
for idx, cand in enumerate(candidates, start=1):
|
|
144
199
|
label = cand.train_type if cand.train_type != "unknown" else "?"
|
|
145
|
-
|
|
200
|
+
last_marker = " (last used)" if last_config and cand.path.resolve() == last_config else ""
|
|
201
|
+
click.echo(f" {idx}) [{label}] {cand.path}{last_marker}")
|
|
146
202
|
click.echo(" 0) Abort")
|
|
147
203
|
|
|
148
|
-
choice = click.prompt("Enter choice", type=int)
|
|
204
|
+
choice = click.prompt("Enter choice", type=int, default=default_idx)
|
|
149
205
|
if choice == 0:
|
|
150
206
|
raise click.ClickException("Aborted by user")
|
|
151
207
|
if choice < 0 or choice > len(candidates):
|
|
152
208
|
raise click.ClickException("Invalid selection")
|
|
153
209
|
|
|
154
210
|
selection = candidates[choice - 1]
|
|
211
|
+
|
|
212
|
+
# Save this config as the last used
|
|
213
|
+
_save_last_config(selection.path)
|
|
214
|
+
|
|
155
215
|
try:
|
|
156
216
|
data = load_toml(selection.path)
|
|
157
217
|
preview = preview_json({k: data.get(k) for k in list(data.keys())[:4]}, limit=320)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from collections.abc import Callable, Iterable, MutableMapping
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Callable, Iterable, MutableMapping
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
@@ -56,12 +56,12 @@ class EnvResolver:
|
|
|
56
56
|
def _collect_default_candidates(config_path: Path | None) -> list[Path]:
|
|
57
57
|
candidates: list[Path] = []
|
|
58
58
|
cwd = Path.cwd()
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
# Prioritize CWD env files
|
|
61
61
|
cwd_env = cwd / ".env"
|
|
62
62
|
if cwd_env.exists():
|
|
63
63
|
candidates.append(cwd_env.resolve())
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
# Search for additional .env files in CWD subdirectories
|
|
66
66
|
for sub in cwd.glob("**/.env"):
|
|
67
67
|
try:
|
|
@@ -76,13 +76,13 @@ def _collect_default_candidates(config_path: Path | None) -> list[Path]:
|
|
|
76
76
|
if len(candidates) >= 20:
|
|
77
77
|
break
|
|
78
78
|
candidates.append(resolved)
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
# Then config path env file
|
|
81
81
|
if config_path:
|
|
82
82
|
cfg_env = config_path.parent / ".env"
|
|
83
83
|
if cfg_env.exists():
|
|
84
84
|
candidates.append(cfg_env.resolve())
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
# Then repo env files
|
|
87
87
|
repo_env = REPO_ROOT / ".env"
|
|
88
88
|
if repo_env.exists():
|
|
@@ -90,7 +90,7 @@ def _collect_default_candidates(config_path: Path | None) -> list[Path]:
|
|
|
90
90
|
examples_env = REPO_ROOT / "examples" / ".env"
|
|
91
91
|
if examples_env.exists():
|
|
92
92
|
candidates.append(examples_env.resolve())
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
# Search shallow depth for additional .env files in examples
|
|
95
95
|
for sub in (REPO_ROOT / "examples").glob("**/.env"):
|
|
96
96
|
try:
|
|
@@ -105,7 +105,7 @@ def _collect_default_candidates(config_path: Path | None) -> list[Path]:
|
|
|
105
105
|
if len(candidates) >= 20:
|
|
106
106
|
break
|
|
107
107
|
candidates.append(resolved)
|
|
108
|
-
|
|
108
|
+
|
|
109
109
|
deduped: list[Path] = []
|
|
110
110
|
for path in candidates:
|
|
111
111
|
if path not in deduped:
|
|
@@ -156,8 +156,27 @@ def resolve_env(
|
|
|
156
156
|
raise click.ClickException(f"Env file not found: {path}")
|
|
157
157
|
resolver = EnvResolver(provided)
|
|
158
158
|
else:
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
# Check for saved .env path from demo command
|
|
160
|
+
try:
|
|
161
|
+
from synth_ai.demos.demo_task_apps.core import load_env_file_path
|
|
162
|
+
|
|
163
|
+
saved_env_path = load_env_file_path()
|
|
164
|
+
if saved_env_path:
|
|
165
|
+
saved_path = Path(saved_env_path)
|
|
166
|
+
if saved_path.exists():
|
|
167
|
+
click.echo(f"Using .env file: {saved_path}")
|
|
168
|
+
resolver = EnvResolver([saved_path])
|
|
169
|
+
else:
|
|
170
|
+
# Saved path no longer exists, fall back to prompt
|
|
171
|
+
resolver = EnvResolver(_collect_default_candidates(config_path))
|
|
172
|
+
resolver.select_new_env()
|
|
173
|
+
else:
|
|
174
|
+
resolver = EnvResolver(_collect_default_candidates(config_path))
|
|
175
|
+
resolver.select_new_env()
|
|
176
|
+
except Exception:
|
|
177
|
+
# If import fails or any error, fall back to original behavior
|
|
178
|
+
resolver = EnvResolver(_collect_default_candidates(config_path))
|
|
179
|
+
resolver.select_new_env()
|
|
161
180
|
|
|
162
181
|
# Preload selected .env keys into process env so downstream lookups succeed
|
|
163
182
|
try:
|
|
@@ -207,10 +226,10 @@ def _resolve_key(resolver: EnvResolver, spec: KeySpec) -> str:
|
|
|
207
226
|
break
|
|
208
227
|
if env_val:
|
|
209
228
|
click.echo(f"Found {spec.name} in current sources: {mask_value(env_val)}")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
229
|
+
# Automatically use and persist the value (no prompt)
|
|
230
|
+
_maybe_persist(resolver, spec, env_val)
|
|
231
|
+
os.environ[spec.name] = env_val
|
|
232
|
+
return env_val
|
|
214
233
|
options: list[tuple[str, Callable[[], str | None]]] = []
|
|
215
234
|
|
|
216
235
|
def _enter_manual() -> str:
|
|
@@ -254,8 +273,7 @@ def _resolve_key(resolver: EnvResolver, spec: KeySpec) -> str:
|
|
|
254
273
|
|
|
255
274
|
|
|
256
275
|
def _maybe_persist(resolver: EnvResolver, spec: KeySpec, value: str) -> None:
|
|
257
|
-
|
|
258
|
-
return
|
|
276
|
+
# Automatically save (no prompt)
|
|
259
277
|
resolver.set_value(spec.name, value)
|
|
260
278
|
click.echo(f"Saved {spec.name} to {resolver.current_path}")
|
|
261
279
|
|