synth-ai 0.2.9.dev5__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 +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.dev6.dist-info/METADATA +191 -0
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.9.dev6.dist-info}/RECORD +291 -262
- {synth_ai-0.2.9.dev5.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
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
- synth_ai/experimental/synth_oss.py +0 -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.dev5.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.dev5.dist-info → synth_ai-0.2.9.dev6.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.9.dev6.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.9.dev6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from minisweagent.environments import get_environment
|
|
19
|
+
from synth_ai.environments.environment.tools import EnvToolCall
|
|
20
|
+
|
|
21
|
+
from .shared import summarise_history
|
|
22
|
+
from .tools import TOOLS_SCHEMA
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _environment_type_from_config(config: dict[str, Any]) -> str:
|
|
28
|
+
value = (config or {}).get("environment_class") or os.getenv(
|
|
29
|
+
"SWE_MINI_ENVIRONMENT_CLASS", "local"
|
|
30
|
+
)
|
|
31
|
+
return str(value).strip() or "local"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _environment_kwargs_from_config(config: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
kwargs = dict(config or {}).get("environment_kwargs") or {}
|
|
36
|
+
if not kwargs and (raw := os.getenv("SWE_MINI_ENVIRONMENT_KWARGS")):
|
|
37
|
+
try:
|
|
38
|
+
kwargs = json.loads(raw)
|
|
39
|
+
except Exception: # pragma: no cover - environment var malformed
|
|
40
|
+
logger.warning("Failed to parse SWE_MINI_ENVIRONMENT_KWARGS; ignoring")
|
|
41
|
+
kwargs = {}
|
|
42
|
+
if not isinstance(kwargs, dict):
|
|
43
|
+
logger.warning("environment_kwargs must be a mapping, got %r", type(kwargs))
|
|
44
|
+
kwargs = {}
|
|
45
|
+
return kwargs
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _default_submit_command() -> str:
|
|
49
|
+
return "echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT && git add -A && git diff --cached"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class MiniSweEnvironmentState:
|
|
54
|
+
"""Serializable environment state used for snapshots."""
|
|
55
|
+
|
|
56
|
+
task: dict[str, Any]
|
|
57
|
+
history: list[dict[str, Any]] = field(default_factory=list)
|
|
58
|
+
step_idx: int = 0
|
|
59
|
+
submitted: bool = False
|
|
60
|
+
submission_success: bool | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MiniSweEnvironmentWrapper:
|
|
64
|
+
"""Wrapper around mini-swe-agent environments exposing Synth task-app semantics."""
|
|
65
|
+
|
|
66
|
+
name = "swe-mini"
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
task: dict[str, Any],
|
|
72
|
+
env_config: dict[str, Any] | None = None,
|
|
73
|
+
submit_command: str | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.task = dict(task)
|
|
76
|
+
self.env_config = dict(env_config or {})
|
|
77
|
+
self.submit_command = submit_command or _default_submit_command()
|
|
78
|
+
self.environment_type = _environment_type_from_config(self.env_config)
|
|
79
|
+
kwargs = _environment_kwargs_from_config(self.env_config)
|
|
80
|
+
|
|
81
|
+
self.instance_id = str(
|
|
82
|
+
self.task.get("instance_id") or f"swe-mini-{uuid.uuid4().hex[:8]}"
|
|
83
|
+
)
|
|
84
|
+
self.metadata = dict(self.task.get("metadata") or {})
|
|
85
|
+
self.repo_url = self._resolve_repo_url(self.metadata)
|
|
86
|
+
self.base_commit = (
|
|
87
|
+
self.metadata.get("base_commit")
|
|
88
|
+
or self.metadata.get("environment_setup_commit")
|
|
89
|
+
or None
|
|
90
|
+
)
|
|
91
|
+
self._local_workspace_dir: Path | None = None
|
|
92
|
+
self._remote_workspace: str | None = None
|
|
93
|
+
self._cleanup_workspace = False
|
|
94
|
+
|
|
95
|
+
if self.environment_type == "local":
|
|
96
|
+
workspace = self._prepare_local_workspace(kwargs)
|
|
97
|
+
kwargs.setdefault("cwd", str(workspace))
|
|
98
|
+
kwargs.setdefault("timeout", int(self.env_config.get("timeout", 60)))
|
|
99
|
+
# Merge custom env vars with defaults expected by mini-swe
|
|
100
|
+
merged_env = dict(kwargs.get("env") or {})
|
|
101
|
+
merged_env.setdefault("PAGER", "cat")
|
|
102
|
+
merged_env.setdefault("MANPAGER", "cat")
|
|
103
|
+
merged_env.setdefault("LESS", "-R")
|
|
104
|
+
merged_env.setdefault("PIP_PROGRESS_BAR", "off")
|
|
105
|
+
merged_env.setdefault("TQDM_DISABLE", "1")
|
|
106
|
+
merged_env.setdefault("GIT_TERMINAL_PROMPT", "0")
|
|
107
|
+
kwargs["env"] = merged_env
|
|
108
|
+
self._local_workspace_dir = workspace
|
|
109
|
+
self._cleanup_workspace = True
|
|
110
|
+
else:
|
|
111
|
+
remote_cwd = kwargs.get("cwd")
|
|
112
|
+
if not remote_cwd:
|
|
113
|
+
base_remote = os.getenv("SWE_MINI_REMOTE_WORKSPACE_BASE", "/workspace")
|
|
114
|
+
remote_cwd = f"{base_remote.rstrip('/')}/{self.instance_id}"
|
|
115
|
+
kwargs["cwd"] = remote_cwd
|
|
116
|
+
self._remote_workspace = kwargs["cwd"]
|
|
117
|
+
timeout = self.env_config.get("timeout")
|
|
118
|
+
if timeout and "timeout" not in kwargs:
|
|
119
|
+
kwargs["timeout"] = int(timeout)
|
|
120
|
+
if self.repo_url and "image" not in kwargs:
|
|
121
|
+
image = self.metadata.get("image_name") or os.getenv("SWE_MINI_DOCKER_IMAGE")
|
|
122
|
+
if image:
|
|
123
|
+
kwargs["image"] = image
|
|
124
|
+
if self.environment_type in {"docker", "bubblewrap"}:
|
|
125
|
+
remote_env = dict(kwargs.get("env") or {})
|
|
126
|
+
remote_env.setdefault("GIT_TERMINAL_PROMPT", "0")
|
|
127
|
+
kwargs["env"] = remote_env
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
"Initialising mini-swe environment: type=%s kwargs=%s",
|
|
131
|
+
self.environment_type,
|
|
132
|
+
kwargs,
|
|
133
|
+
)
|
|
134
|
+
self.env = get_environment(
|
|
135
|
+
{
|
|
136
|
+
"environment_class": self.environment_type,
|
|
137
|
+
**kwargs,
|
|
138
|
+
},
|
|
139
|
+
default_type="local",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if self.environment_type != "local":
|
|
143
|
+
self._bootstrap_remote_workspace()
|
|
144
|
+
|
|
145
|
+
self.state = MiniSweEnvironmentState(task=self.task)
|
|
146
|
+
self.last_result: dict[str, Any] | None = None
|
|
147
|
+
self.last_submission: dict[str, Any] | None = None
|
|
148
|
+
|
|
149
|
+
async def initialize(self) -> dict[str, Any]:
|
|
150
|
+
"""Return initial observation."""
|
|
151
|
+
logger.info(
|
|
152
|
+
"Mini-swe task initialised: instance=%s",
|
|
153
|
+
self.task.get("instance_id"),
|
|
154
|
+
)
|
|
155
|
+
return self._build_response(observation=self._build_observation(None), step_idx=0)
|
|
156
|
+
|
|
157
|
+
async def terminate(self) -> dict[str, Any]:
|
|
158
|
+
"""Terminate the environment, returning the final observation."""
|
|
159
|
+
logger.info(
|
|
160
|
+
"Terminating mini-swe environment instance=%s submitted=%s",
|
|
161
|
+
self.task.get("instance_id"),
|
|
162
|
+
self.state.submitted,
|
|
163
|
+
)
|
|
164
|
+
response = self._build_response(
|
|
165
|
+
observation=self._build_observation(self.last_result),
|
|
166
|
+
step_idx=self.state.step_idx,
|
|
167
|
+
)
|
|
168
|
+
self._cleanup_workspaces()
|
|
169
|
+
return response
|
|
170
|
+
|
|
171
|
+
def _cleanup_workspaces(self) -> None:
|
|
172
|
+
if self._cleanup_workspace and self._local_workspace_dir:
|
|
173
|
+
with contextlib.suppress(Exception):
|
|
174
|
+
shutil.rmtree(self._local_workspace_dir)
|
|
175
|
+
self._local_workspace_dir = None
|
|
176
|
+
self._cleanup_workspace = False
|
|
177
|
+
if (
|
|
178
|
+
self._remote_workspace
|
|
179
|
+
and os.getenv("SWE_MINI_CLEANUP_REMOTE_WORKSPACE", "1") not in {"0", "false", "False"}
|
|
180
|
+
):
|
|
181
|
+
with contextlib.suppress(Exception):
|
|
182
|
+
self.env.execute(f"rm -rf {shlex.quote(self._remote_workspace)}")
|
|
183
|
+
self._remote_workspace = None
|
|
184
|
+
|
|
185
|
+
def _resolve_repo_url(self, metadata: dict[str, Any]) -> str | None:
|
|
186
|
+
candidates = [
|
|
187
|
+
metadata.get("repo_url"),
|
|
188
|
+
metadata.get("repo"),
|
|
189
|
+
metadata.get("repository"),
|
|
190
|
+
]
|
|
191
|
+
for value in candidates:
|
|
192
|
+
if not value:
|
|
193
|
+
continue
|
|
194
|
+
repo = str(value).strip()
|
|
195
|
+
if not repo:
|
|
196
|
+
continue
|
|
197
|
+
if repo.startswith("http://") or repo.startswith("https://"):
|
|
198
|
+
url = repo
|
|
199
|
+
else:
|
|
200
|
+
repo = repo.removesuffix(".git")
|
|
201
|
+
url = f"https://github.com/{repo}.git"
|
|
202
|
+
if not url.endswith(".git"):
|
|
203
|
+
url = f"{url}.git"
|
|
204
|
+
return url
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def _prepare_local_workspace(self, kwargs: dict[str, Any]) -> Path:
|
|
208
|
+
if not self.repo_url:
|
|
209
|
+
fallback = Path(kwargs.get("cwd") or self.env_config.get("cwd") or os.getcwd())
|
|
210
|
+
fallback.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
logger.warning(
|
|
212
|
+
"No repo URL provided for swe-mini instance %s; using cwd=%s",
|
|
213
|
+
self.instance_id,
|
|
214
|
+
fallback,
|
|
215
|
+
)
|
|
216
|
+
return fallback
|
|
217
|
+
|
|
218
|
+
root = Path(
|
|
219
|
+
os.getenv("SWE_MINI_LOCAL_WORKSPACE_ROOT")
|
|
220
|
+
or Path.home() / ".cache" / "synth-ai" / "swe-mini" / "workspaces"
|
|
221
|
+
)
|
|
222
|
+
workspace = root / self.instance_id
|
|
223
|
+
if workspace.exists():
|
|
224
|
+
shutil.rmtree(workspace, ignore_errors=True)
|
|
225
|
+
workspace.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
|
|
227
|
+
self._run_local_cmd(
|
|
228
|
+
[
|
|
229
|
+
"git",
|
|
230
|
+
"clone",
|
|
231
|
+
"--filter=blob:none",
|
|
232
|
+
"--no-tags",
|
|
233
|
+
self.repo_url,
|
|
234
|
+
str(workspace),
|
|
235
|
+
],
|
|
236
|
+
description="clone repository",
|
|
237
|
+
)
|
|
238
|
+
if self.base_commit:
|
|
239
|
+
self._run_local_cmd(
|
|
240
|
+
["git", "-C", str(workspace), "checkout", self.base_commit],
|
|
241
|
+
description="checkout base commit",
|
|
242
|
+
)
|
|
243
|
+
self._run_local_cmd(
|
|
244
|
+
["git", "-C", str(workspace), "reset", "--hard"],
|
|
245
|
+
description="reset working tree",
|
|
246
|
+
)
|
|
247
|
+
self._run_local_cmd(
|
|
248
|
+
["git", "-C", str(workspace), "clean", "-ffd"],
|
|
249
|
+
description="clean working tree",
|
|
250
|
+
)
|
|
251
|
+
logger.info(
|
|
252
|
+
"Prepared local workspace for %s at %s (repo=%s, commit=%s)",
|
|
253
|
+
self.instance_id,
|
|
254
|
+
workspace,
|
|
255
|
+
self.repo_url,
|
|
256
|
+
self.base_commit,
|
|
257
|
+
)
|
|
258
|
+
return workspace
|
|
259
|
+
|
|
260
|
+
def _bootstrap_remote_workspace(self) -> None:
|
|
261
|
+
if not self.repo_url or not self._remote_workspace:
|
|
262
|
+
logger.warning(
|
|
263
|
+
"Skipping remote workspace bootstrap for instance %s (repo=%s workspace=%s)",
|
|
264
|
+
self.instance_id,
|
|
265
|
+
self.repo_url,
|
|
266
|
+
self._remote_workspace,
|
|
267
|
+
)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
workspace = self._remote_workspace.rstrip("/")
|
|
271
|
+
base_dir = os.path.dirname(workspace) or "/"
|
|
272
|
+
self._execute_bootstrap_command(f"mkdir -p {shlex.quote(base_dir)}")
|
|
273
|
+
self._execute_bootstrap_command(f"rm -rf {shlex.quote(workspace)}")
|
|
274
|
+
clone_cmd = (
|
|
275
|
+
f"git clone --filter=blob:none --no-tags {shlex.quote(self.repo_url)} {shlex.quote(workspace)}"
|
|
276
|
+
)
|
|
277
|
+
self._execute_bootstrap_command(clone_cmd, timeout=900, description="clone repository")
|
|
278
|
+
if self.base_commit:
|
|
279
|
+
checkout_cmd = (
|
|
280
|
+
f"cd {shlex.quote(workspace)} && git checkout {shlex.quote(self.base_commit)}"
|
|
281
|
+
)
|
|
282
|
+
self._execute_bootstrap_command(checkout_cmd, timeout=300, description="checkout commit")
|
|
283
|
+
self._execute_bootstrap_command(
|
|
284
|
+
f"cd {shlex.quote(workspace)} && git reset --hard",
|
|
285
|
+
description="reset working tree",
|
|
286
|
+
)
|
|
287
|
+
self._execute_bootstrap_command(
|
|
288
|
+
f"cd {shlex.quote(workspace)} && git clean -ffd",
|
|
289
|
+
description="clean working tree",
|
|
290
|
+
)
|
|
291
|
+
logger.info(
|
|
292
|
+
"Prepared remote workspace for %s at %s (repo=%s, commit=%s)",
|
|
293
|
+
self.instance_id,
|
|
294
|
+
workspace,
|
|
295
|
+
self.repo_url,
|
|
296
|
+
self.base_commit,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def _run_local_cmd(
|
|
300
|
+
self, args: list[str], *, cwd: Path | None = None, description: str | None = None
|
|
301
|
+
) -> None:
|
|
302
|
+
logger.debug(
|
|
303
|
+
"Preparing workspace %s: running local command %s",
|
|
304
|
+
self.instance_id,
|
|
305
|
+
" ".join(args),
|
|
306
|
+
)
|
|
307
|
+
proc = subprocess.run(
|
|
308
|
+
args,
|
|
309
|
+
cwd=str(cwd) if cwd else None,
|
|
310
|
+
text=True,
|
|
311
|
+
capture_output=True,
|
|
312
|
+
)
|
|
313
|
+
if proc.returncode != 0:
|
|
314
|
+
desc = description or "command"
|
|
315
|
+
raise RuntimeError(
|
|
316
|
+
f"Failed to {desc} (cmd={' '.join(args)}): {proc.stdout or ''}{proc.stderr or ''}"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def _execute_bootstrap_command(
|
|
320
|
+
self, command: str, *, timeout: int | None = None, description: str | None = None
|
|
321
|
+
) -> None:
|
|
322
|
+
logger.debug(
|
|
323
|
+
"Preparing workspace %s: running remote command %s",
|
|
324
|
+
self.instance_id,
|
|
325
|
+
command,
|
|
326
|
+
)
|
|
327
|
+
result = self.env.execute(command, timeout=timeout)
|
|
328
|
+
if result.get("returncode"):
|
|
329
|
+
desc = description or command
|
|
330
|
+
raise RuntimeError(
|
|
331
|
+
f"Failed to {desc}: rc={result.get('returncode')} output={result.get('output')}"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def _normalize_tool_call(self, tool_call: EnvToolCall | dict[str, Any]) -> EnvToolCall:
|
|
335
|
+
if isinstance(tool_call, EnvToolCall):
|
|
336
|
+
return tool_call
|
|
337
|
+
tool = tool_call.get("tool") or tool_call.get("tool_name")
|
|
338
|
+
if not tool:
|
|
339
|
+
raise ValueError(f"Tool call missing tool name: {tool_call}")
|
|
340
|
+
args = tool_call.get("args") or tool_call.get("arguments") or {}
|
|
341
|
+
if isinstance(args, str):
|
|
342
|
+
try:
|
|
343
|
+
args = json.loads(args)
|
|
344
|
+
except Exception:
|
|
345
|
+
args = {}
|
|
346
|
+
return EnvToolCall(tool=str(tool), args=dict(args))
|
|
347
|
+
|
|
348
|
+
async def step(self, tool_calls: list[EnvToolCall] | list[dict[str, Any]]) -> dict[str, Any]:
|
|
349
|
+
"""Execute run_command or submit_patch tool calls."""
|
|
350
|
+
if not tool_calls:
|
|
351
|
+
raise ValueError("MiniSweEnvironmentWrapper.step requires at least one tool call")
|
|
352
|
+
|
|
353
|
+
responses: list[dict[str, Any]] = []
|
|
354
|
+
for raw_call in tool_calls:
|
|
355
|
+
call = self._normalize_tool_call(raw_call)
|
|
356
|
+
tool = call.tool
|
|
357
|
+
if tool == "run_command":
|
|
358
|
+
responses.append(self._run_command(call))
|
|
359
|
+
elif tool == "submit_patch":
|
|
360
|
+
responses.append(self._submit(call))
|
|
361
|
+
else:
|
|
362
|
+
raise ValueError(f"Unsupported tool '{tool}' for swe-mini environment")
|
|
363
|
+
|
|
364
|
+
last_result = responses[-1] if responses else None
|
|
365
|
+
self.last_result = last_result
|
|
366
|
+
observation = self._build_observation(last_result)
|
|
367
|
+
done = bool(self.state.submitted)
|
|
368
|
+
reward = 0.0
|
|
369
|
+
if done:
|
|
370
|
+
reward = 1.0 if self.state.submission_success else 0.0
|
|
371
|
+
return self._build_response(
|
|
372
|
+
observation=observation,
|
|
373
|
+
step_idx=self.state.step_idx,
|
|
374
|
+
done=done,
|
|
375
|
+
reward=reward,
|
|
376
|
+
info={"responses": responses},
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def _run_command(self, call: EnvToolCall) -> dict[str, Any]:
|
|
380
|
+
command = str(call.args.get("command") or "").strip()
|
|
381
|
+
if not command:
|
|
382
|
+
raise ValueError("run_command requires a non-empty 'command' argument")
|
|
383
|
+
timeout = call.args.get("timeout")
|
|
384
|
+
timeout = int(timeout) if timeout is not None else None
|
|
385
|
+
|
|
386
|
+
started_at = time.time()
|
|
387
|
+
result = self.env.execute(command, timeout=timeout)
|
|
388
|
+
duration = time.time() - started_at
|
|
389
|
+
|
|
390
|
+
record = {
|
|
391
|
+
"command": command,
|
|
392
|
+
"returncode": result.get("returncode"),
|
|
393
|
+
"stdout": result.get("output") or "",
|
|
394
|
+
"duration": duration,
|
|
395
|
+
"timestamp": started_at,
|
|
396
|
+
}
|
|
397
|
+
self.state.history.append(record)
|
|
398
|
+
self.state.step_idx += 1
|
|
399
|
+
logger.info(
|
|
400
|
+
"Executed command step=%s rc=%s",
|
|
401
|
+
self.state.step_idx,
|
|
402
|
+
record["returncode"],
|
|
403
|
+
)
|
|
404
|
+
return record
|
|
405
|
+
|
|
406
|
+
def _submit(self, call: EnvToolCall) -> dict[str, Any]:
|
|
407
|
+
if self.state.submitted:
|
|
408
|
+
logger.info("Submit called again; ignoring additional submission.")
|
|
409
|
+
return {
|
|
410
|
+
"submitted": True,
|
|
411
|
+
"command": None,
|
|
412
|
+
"returncode": 0,
|
|
413
|
+
"stdout": "",
|
|
414
|
+
"submission_success": self.state.submission_success,
|
|
415
|
+
"evaluation": self.last_submission,
|
|
416
|
+
}
|
|
417
|
+
command = str(call.args.get("command") or self.submit_command)
|
|
418
|
+
result = self.env.execute(command)
|
|
419
|
+
record = {
|
|
420
|
+
"command": command,
|
|
421
|
+
"returncode": result.get("returncode"),
|
|
422
|
+
"stdout": result.get("output") or "",
|
|
423
|
+
"duration": 0.0,
|
|
424
|
+
"timestamp": time.time(),
|
|
425
|
+
}
|
|
426
|
+
self.state.history.append(record)
|
|
427
|
+
self.state.step_idx += 1
|
|
428
|
+
diff = self._extract_submission_diff(record["stdout"])
|
|
429
|
+
|
|
430
|
+
evaluation: dict[str, Any] | None = None
|
|
431
|
+
submission_success = False
|
|
432
|
+
if record["returncode"] == 0 and diff is not None:
|
|
433
|
+
evaluation = self._evaluate_submission(diff)
|
|
434
|
+
submission_success = bool(evaluation.get("resolved")) if evaluation else False
|
|
435
|
+
else:
|
|
436
|
+
evaluation = {
|
|
437
|
+
"completed": False,
|
|
438
|
+
"resolved": False,
|
|
439
|
+
"error": "submit command failed or diff unavailable",
|
|
440
|
+
"returncode": record["returncode"],
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
self.state.submitted = True
|
|
444
|
+
self.state.submission_success = submission_success
|
|
445
|
+
self.last_submission = evaluation
|
|
446
|
+
|
|
447
|
+
logger.info(
|
|
448
|
+
"Submission command executed rc=%s resolved=%s",
|
|
449
|
+
record["returncode"],
|
|
450
|
+
submission_success,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
**record,
|
|
455
|
+
"submitted": True,
|
|
456
|
+
"submission_success": submission_success,
|
|
457
|
+
"diff": diff,
|
|
458
|
+
"evaluation": evaluation,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
def _extract_submission_diff(self, stdout: str) -> str | None:
|
|
462
|
+
if stdout is None:
|
|
463
|
+
return None
|
|
464
|
+
lines = stdout.splitlines()
|
|
465
|
+
if not lines:
|
|
466
|
+
return ""
|
|
467
|
+
first = lines[0].strip()
|
|
468
|
+
sentinel = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT"
|
|
469
|
+
if first.startswith(sentinel):
|
|
470
|
+
lines = lines[1:]
|
|
471
|
+
diff = "\n".join(lines).strip("\n")
|
|
472
|
+
return diff
|
|
473
|
+
|
|
474
|
+
def _evaluate_submission(self, diff: str) -> dict[str, Any]:
|
|
475
|
+
metadata = dict(self.task.get("metadata") or {})
|
|
476
|
+
instance = dict(metadata.get("raw_instance") or {})
|
|
477
|
+
instance_id = instance.setdefault("instance_id", self.task.get("instance_id"))
|
|
478
|
+
|
|
479
|
+
required_fields = ["repo", "base_commit", "test_patch", "version"]
|
|
480
|
+
missing = [field for field in required_fields if not instance.get(field)]
|
|
481
|
+
if missing:
|
|
482
|
+
msg = (
|
|
483
|
+
"Cannot run SWE-bench evaluation; task metadata missing required fields "
|
|
484
|
+
f"{missing}. Ensure the dataset preserves full SWE-bench records."
|
|
485
|
+
)
|
|
486
|
+
logger.error(msg)
|
|
487
|
+
return {"completed": False, "resolved": False, "error": msg}
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
from swebench.harness.constants import (
|
|
491
|
+
KEY_INSTANCE_ID,
|
|
492
|
+
KEY_MODEL,
|
|
493
|
+
KEY_PREDICTION,
|
|
494
|
+
)
|
|
495
|
+
except Exception as exc: # pragma: no cover - dependency missing
|
|
496
|
+
msg = (
|
|
497
|
+
"SWE-bench harness is required for official scoring. "
|
|
498
|
+
"Install swebench with evaluation extras."
|
|
499
|
+
)
|
|
500
|
+
logger.exception("Failed to import swebench harness constants: %s", exc)
|
|
501
|
+
return {"completed": False, "resolved": False, "error": f"{msg} ({exc})"}
|
|
502
|
+
|
|
503
|
+
backend = self._resolve_evaluation_backend(metadata)
|
|
504
|
+
|
|
505
|
+
image_name = str(metadata.get("image_name") or "")
|
|
506
|
+
namespace = metadata.get("namespace") or self._namespace_from_image(image_name) or "swebench"
|
|
507
|
+
instance_image_tag = metadata.get("instance_image_tag") or self._image_tag_from_name(image_name) or "latest"
|
|
508
|
+
env_image_tag = metadata.get("env_image_tag") or "latest"
|
|
509
|
+
|
|
510
|
+
model_name = metadata.get("submission_model_name") or metadata.get("model_name") or "synth-ai-agent"
|
|
511
|
+
run_id = f"swe_mini_eval_{uuid.uuid4().hex[:12]}"
|
|
512
|
+
eval_timeout = self._resolve_eval_timeout(metadata)
|
|
513
|
+
rm_image = self._to_bool(metadata.get("eval_rm_image") or os.getenv("SWE_MINI_EVAL_RM_IMAGE", "false"))
|
|
514
|
+
force_rebuild = self._to_bool(metadata.get("eval_force_rebuild") or os.getenv("SWE_MINI_EVAL_FORCE_REBUILD", "false"))
|
|
515
|
+
|
|
516
|
+
prediction = {
|
|
517
|
+
KEY_INSTANCE_ID: instance_id,
|
|
518
|
+
KEY_MODEL: model_name,
|
|
519
|
+
KEY_PREDICTION: diff or "",
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Ensure log root exists so downstream collection succeeds.
|
|
523
|
+
with contextlib.suppress(Exception):
|
|
524
|
+
from swebench.harness.constants import RUN_EVALUATION_LOG_DIR
|
|
525
|
+
|
|
526
|
+
Path(RUN_EVALUATION_LOG_DIR).mkdir(parents=True, exist_ok=True)
|
|
527
|
+
|
|
528
|
+
if backend == "modal_harness":
|
|
529
|
+
evaluation_payload = self._run_modal_harness(
|
|
530
|
+
instance=instance,
|
|
531
|
+
prediction=prediction,
|
|
532
|
+
run_id=run_id,
|
|
533
|
+
eval_timeout=eval_timeout,
|
|
534
|
+
model_name=model_name,
|
|
535
|
+
)
|
|
536
|
+
elif backend == "swe_rex":
|
|
537
|
+
evaluation_payload = self._run_swe_rex(
|
|
538
|
+
instance=instance,
|
|
539
|
+
prediction=prediction,
|
|
540
|
+
run_id=run_id,
|
|
541
|
+
eval_timeout=eval_timeout,
|
|
542
|
+
namespace=namespace,
|
|
543
|
+
instance_image_tag=instance_image_tag,
|
|
544
|
+
env_image_tag=env_image_tag,
|
|
545
|
+
model_name=model_name,
|
|
546
|
+
)
|
|
547
|
+
else:
|
|
548
|
+
evaluation_payload = self._run_local_harness(
|
|
549
|
+
instance=instance,
|
|
550
|
+
prediction=prediction,
|
|
551
|
+
run_id=run_id,
|
|
552
|
+
eval_timeout=eval_timeout,
|
|
553
|
+
namespace=namespace,
|
|
554
|
+
instance_image_tag=instance_image_tag,
|
|
555
|
+
env_image_tag=env_image_tag,
|
|
556
|
+
rm_image=rm_image,
|
|
557
|
+
force_rebuild=force_rebuild,
|
|
558
|
+
model_name=model_name,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
evaluation_payload = dict(evaluation_payload or {})
|
|
562
|
+
evaluation_payload.setdefault("backend", backend)
|
|
563
|
+
evaluation_payload.setdefault("run_id", run_id)
|
|
564
|
+
evaluation_payload.setdefault("model_name", model_name)
|
|
565
|
+
evaluation_payload.setdefault("instance_id", instance_id)
|
|
566
|
+
|
|
567
|
+
artifacts = self._collect_evaluation_artifacts(
|
|
568
|
+
run_id=run_id,
|
|
569
|
+
model_name=model_name,
|
|
570
|
+
instance_id=instance_id,
|
|
571
|
+
)
|
|
572
|
+
# Merge artifact data without clobbering explicit error/resolution flags.
|
|
573
|
+
merged = {**artifacts, **evaluation_payload}
|
|
574
|
+
if artifacts.get("completed"):
|
|
575
|
+
merged["completed"] = True
|
|
576
|
+
else:
|
|
577
|
+
merged.setdefault("completed", False)
|
|
578
|
+
if artifacts.get("resolved"):
|
|
579
|
+
merged["resolved"] = True
|
|
580
|
+
else:
|
|
581
|
+
merged.setdefault("resolved", False)
|
|
582
|
+
merged.setdefault("log_dir", artifacts.get("log_dir"))
|
|
583
|
+
merged.setdefault("report_path", artifacts.get("report_path"))
|
|
584
|
+
merged.setdefault("test_output_path", artifacts.get("test_output_path"))
|
|
585
|
+
if artifacts.get("report") and not merged.get("report"):
|
|
586
|
+
merged["report"] = artifacts["report"]
|
|
587
|
+
if artifacts.get("error") and not merged.get("error"):
|
|
588
|
+
merged["error"] = artifacts["error"]
|
|
589
|
+
return merged
|
|
590
|
+
|
|
591
|
+
def _resolve_evaluation_backend(self, metadata: dict[str, Any]) -> str:
|
|
592
|
+
raw = (
|
|
593
|
+
metadata.get("evaluation_backend")
|
|
594
|
+
or self.env_config.get("evaluation_backend")
|
|
595
|
+
or os.getenv("SWE_MINI_EVALUATION_BACKEND")
|
|
596
|
+
or "local"
|
|
597
|
+
)
|
|
598
|
+
backend = str(raw).strip().lower()
|
|
599
|
+
mapping = {
|
|
600
|
+
"": "local",
|
|
601
|
+
"local": "local",
|
|
602
|
+
"docker": "local",
|
|
603
|
+
"modal": "modal_harness",
|
|
604
|
+
"modal_harness": "modal_harness",
|
|
605
|
+
"modal-harness": "modal_harness",
|
|
606
|
+
"modal-harnesses": "modal_harness",
|
|
607
|
+
"swe_rex": "swe_rex",
|
|
608
|
+
"swe-rex": "swe_rex",
|
|
609
|
+
"swerex": "swe_rex",
|
|
610
|
+
}
|
|
611
|
+
return mapping.get(backend, "local")
|
|
612
|
+
|
|
613
|
+
def _resolve_eval_timeout(self, metadata: dict[str, Any]) -> int:
|
|
614
|
+
raw = (
|
|
615
|
+
metadata.get("evaluation_timeout")
|
|
616
|
+
or self.env_config.get("evaluation_timeout")
|
|
617
|
+
or os.getenv("SWE_MINI_EVALUATION_TIMEOUT")
|
|
618
|
+
or 3600
|
|
619
|
+
)
|
|
620
|
+
try:
|
|
621
|
+
value = int(raw)
|
|
622
|
+
except (TypeError, ValueError):
|
|
623
|
+
return 3600
|
|
624
|
+
return max(1, value)
|
|
625
|
+
|
|
626
|
+
def _run_local_harness(
|
|
627
|
+
self,
|
|
628
|
+
*,
|
|
629
|
+
instance: dict[str, Any],
|
|
630
|
+
prediction: dict[str, Any],
|
|
631
|
+
run_id: str,
|
|
632
|
+
eval_timeout: int,
|
|
633
|
+
namespace: str,
|
|
634
|
+
instance_image_tag: str,
|
|
635
|
+
env_image_tag: str,
|
|
636
|
+
rm_image: bool,
|
|
637
|
+
force_rebuild: bool,
|
|
638
|
+
model_name: str,
|
|
639
|
+
) -> dict[str, Any]:
|
|
640
|
+
try:
|
|
641
|
+
from swebench.harness.run_evaluation import run_instance
|
|
642
|
+
from swebench.harness.test_spec.test_spec import make_test_spec
|
|
643
|
+
except Exception as exc: # pragma: no cover - dependency missing
|
|
644
|
+
msg = (
|
|
645
|
+
"SWE-bench harness is required for official scoring. "
|
|
646
|
+
"Install swebench with evaluation extras."
|
|
647
|
+
)
|
|
648
|
+
logger.exception("Failed to import swebench harness: %s", exc)
|
|
649
|
+
return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "local"}
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
import docker
|
|
653
|
+
except Exception as exc: # pragma: no cover - dependency missing
|
|
654
|
+
msg = "Docker SDK for Python is required to run local SWE-bench evaluation."
|
|
655
|
+
logger.exception("Failed to import docker SDK: %s", exc)
|
|
656
|
+
return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "local"}
|
|
657
|
+
|
|
658
|
+
instance_id = str(instance["instance_id"])
|
|
659
|
+
try:
|
|
660
|
+
test_spec = make_test_spec(
|
|
661
|
+
instance,
|
|
662
|
+
namespace=namespace,
|
|
663
|
+
instance_image_tag=instance_image_tag,
|
|
664
|
+
env_image_tag=env_image_tag,
|
|
665
|
+
)
|
|
666
|
+
except Exception as exc:
|
|
667
|
+
logger.exception("Failed to build SWE-bench test spec for %s: %s", instance_id, exc)
|
|
668
|
+
return {"completed": False, "resolved": False, "error": f"Failed to build test spec: {exc}", "backend": "local"}
|
|
669
|
+
|
|
670
|
+
client = None
|
|
671
|
+
result: dict[str, Any] = {}
|
|
672
|
+
try:
|
|
673
|
+
client = docker.from_env()
|
|
674
|
+
result = run_instance(
|
|
675
|
+
test_spec,
|
|
676
|
+
prediction,
|
|
677
|
+
rm_image,
|
|
678
|
+
force_rebuild,
|
|
679
|
+
client,
|
|
680
|
+
run_id,
|
|
681
|
+
int(eval_timeout),
|
|
682
|
+
rewrite_reports=False,
|
|
683
|
+
)
|
|
684
|
+
except Exception as exc:
|
|
685
|
+
logger.exception("Error while running SWE-bench evaluation for %s: %s", instance_id, exc)
|
|
686
|
+
return {"completed": False, "resolved": False, "error": f"Evaluation failed: {exc}", "backend": "local"}
|
|
687
|
+
finally:
|
|
688
|
+
with contextlib.suppress(Exception):
|
|
689
|
+
if client is not None:
|
|
690
|
+
client.close()
|
|
691
|
+
|
|
692
|
+
payload = {
|
|
693
|
+
"completed": bool(result.get("completed")),
|
|
694
|
+
"resolved": bool(result.get("resolved")),
|
|
695
|
+
"backend": "local",
|
|
696
|
+
}
|
|
697
|
+
return payload
|
|
698
|
+
|
|
699
|
+
def _run_modal_harness(
|
|
700
|
+
self,
|
|
701
|
+
*,
|
|
702
|
+
instance: dict[str, Any],
|
|
703
|
+
prediction: dict[str, Any],
|
|
704
|
+
run_id: str,
|
|
705
|
+
eval_timeout: int,
|
|
706
|
+
model_name: str,
|
|
707
|
+
) -> dict[str, Any]:
|
|
708
|
+
try:
|
|
709
|
+
from swebench.harness.modal_eval import run_instances_modal
|
|
710
|
+
except Exception as exc: # pragma: no cover - dependency missing
|
|
711
|
+
msg = (
|
|
712
|
+
"SWE-bench modal extras are required for the modal_harness backend. "
|
|
713
|
+
"Install swebench[modal] inside the Modal deployment."
|
|
714
|
+
)
|
|
715
|
+
logger.exception("Failed to import swebench modal harness: %s", exc)
|
|
716
|
+
return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "modal_harness"}
|
|
717
|
+
|
|
718
|
+
instance_id = str(instance["instance_id"])
|
|
719
|
+
predictions = {instance_id: dict(prediction)}
|
|
720
|
+
dataset = [instance]
|
|
721
|
+
try:
|
|
722
|
+
run_instances_modal(
|
|
723
|
+
predictions,
|
|
724
|
+
dataset,
|
|
725
|
+
dataset,
|
|
726
|
+
run_id,
|
|
727
|
+
int(eval_timeout),
|
|
728
|
+
)
|
|
729
|
+
except Exception as exc:
|
|
730
|
+
logger.exception("Modal SWE-bench evaluation failed for %s: %s", instance_id, exc)
|
|
731
|
+
return {"completed": False, "resolved": False, "error": f"Modal evaluation failed: {exc}", "backend": "modal_harness"}
|
|
732
|
+
|
|
733
|
+
# run_instances_modal writes reports to RUN_EVALUATION_LOG_DIR; we rely on artifact collection.
|
|
734
|
+
return {"backend": "modal_harness"}
|
|
735
|
+
|
|
736
|
+
def _run_swe_rex(
|
|
737
|
+
self,
|
|
738
|
+
*,
|
|
739
|
+
instance: dict[str, Any],
|
|
740
|
+
prediction: dict[str, Any],
|
|
741
|
+
run_id: str,
|
|
742
|
+
eval_timeout: int,
|
|
743
|
+
namespace: str,
|
|
744
|
+
instance_image_tag: str,
|
|
745
|
+
env_image_tag: str,
|
|
746
|
+
model_name: str,
|
|
747
|
+
) -> dict[str, Any]:
|
|
748
|
+
try:
|
|
749
|
+
from swerex.deployment.config import ModalDeploymentConfig
|
|
750
|
+
from swerex.runtime.abstract import Command, ReadFileRequest, WriteFileRequest
|
|
751
|
+
except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
|
|
752
|
+
msg = (
|
|
753
|
+
"SWE-ReX backend requires the swe-rex package. "
|
|
754
|
+
"Install swe-rex (pip install swe-rex[modal]) to enable this backend."
|
|
755
|
+
)
|
|
756
|
+
logger.exception("Failed to import swe-rex: %s", exc)
|
|
757
|
+
return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "swe_rex"}
|
|
758
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
759
|
+
logger.exception("Unexpected swe-rex import failure: %s", exc)
|
|
760
|
+
return {"completed": False, "resolved": False, "error": f"swe-rex import failed: {exc}", "backend": "swe_rex"}
|
|
761
|
+
|
|
762
|
+
image_spec = (
|
|
763
|
+
instance.get("swe_rex_image")
|
|
764
|
+
or self.env_config.get("swe_rex_image")
|
|
765
|
+
or os.getenv("SWE_REX_MODAL_IMAGE")
|
|
766
|
+
or "ghcr.io/swe-agent/swe-rex-modal:latest"
|
|
767
|
+
)
|
|
768
|
+
install_pipx = self._to_bool(
|
|
769
|
+
instance.get("swe_rex_install_pipx")
|
|
770
|
+
or self.env_config.get("swe_rex_install_pipx")
|
|
771
|
+
or os.getenv("SWE_REX_INSTALL_PIPX", "true")
|
|
772
|
+
)
|
|
773
|
+
modal_kwargs_raw = (
|
|
774
|
+
instance.get("swe_rex_modal_kwargs")
|
|
775
|
+
or self.env_config.get("swe_rex_modal_kwargs")
|
|
776
|
+
or os.getenv("SWE_REX_MODAL_SANDBOX_KWARGS")
|
|
777
|
+
)
|
|
778
|
+
modal_kwargs: dict[str, Any] = {}
|
|
779
|
+
if isinstance(modal_kwargs_raw, (dict, list)):
|
|
780
|
+
modal_kwargs = dict(modal_kwargs_raw or {})
|
|
781
|
+
elif isinstance(modal_kwargs_raw, str) and modal_kwargs_raw.strip():
|
|
782
|
+
try:
|
|
783
|
+
modal_kwargs = dict(json.loads(modal_kwargs_raw))
|
|
784
|
+
except Exception as exc: # pragma: no cover - user input parsing
|
|
785
|
+
logger.warning("Failed to parse SWE_REX_MODAL_SANDBOX_KWARGS=%s: %s", modal_kwargs_raw, exc)
|
|
786
|
+
|
|
787
|
+
deployment_config = ModalDeploymentConfig(
|
|
788
|
+
image=image_spec,
|
|
789
|
+
runtime_timeout=float(
|
|
790
|
+
instance.get("swe_rex_runtime_timeout")
|
|
791
|
+
or self.env_config.get("swe_rex_runtime_timeout")
|
|
792
|
+
or os.getenv("SWE_REX_RUNTIME_TIMEOUT", 900)
|
|
793
|
+
),
|
|
794
|
+
deployment_timeout=float(
|
|
795
|
+
instance.get("swe_rex_deployment_timeout")
|
|
796
|
+
or self.env_config.get("swe_rex_deployment_timeout")
|
|
797
|
+
or os.getenv("SWE_REX_DEPLOYMENT_TIMEOUT", 3600)
|
|
798
|
+
),
|
|
799
|
+
modal_sandbox_kwargs=modal_kwargs,
|
|
800
|
+
install_pipx=bool(install_pipx),
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
remote_root = (
|
|
804
|
+
instance.get("swe_rex_workdir")
|
|
805
|
+
or self.env_config.get("swe_rex_workdir")
|
|
806
|
+
or os.getenv("SWE_REX_REMOTE_WORKDIR")
|
|
807
|
+
or "/root/swebench_eval"
|
|
808
|
+
)
|
|
809
|
+
remote_root = str(remote_root).rstrip("/")
|
|
810
|
+
dataset_remote_path = f"{remote_root}/dataset.json"
|
|
811
|
+
predictions_remote_path = f"{remote_root}/predictions.json"
|
|
812
|
+
|
|
813
|
+
environment_forward_raw = (
|
|
814
|
+
instance.get("swe_rex_forward_env")
|
|
815
|
+
or self.env_config.get("swe_rex_forward_env")
|
|
816
|
+
or os.getenv("SWE_REX_FORWARD_ENV")
|
|
817
|
+
)
|
|
818
|
+
forward_env: dict[str, str] | None = None
|
|
819
|
+
if isinstance(environment_forward_raw, dict):
|
|
820
|
+
forward_env = {str(k): str(v) for k, v in environment_forward_raw.items()}
|
|
821
|
+
elif isinstance(environment_forward_raw, str) and environment_forward_raw.strip():
|
|
822
|
+
try:
|
|
823
|
+
parsed = json.loads(environment_forward_raw)
|
|
824
|
+
if isinstance(parsed, dict):
|
|
825
|
+
forward_env = {str(k): str(v) for k, v in parsed.items()}
|
|
826
|
+
except Exception as exc: # pragma: no cover - parsing failure
|
|
827
|
+
logger.warning("Failed to parse SWE_REX_FORWARD_ENV=%s: %s", environment_forward_raw, exc)
|
|
828
|
+
|
|
829
|
+
# Build coroutine for the async swe-rex flow.
|
|
830
|
+
coro = self._run_swe_rex_async(
|
|
831
|
+
deployment_config=deployment_config,
|
|
832
|
+
remote_root=remote_root,
|
|
833
|
+
dataset_remote_path=dataset_remote_path,
|
|
834
|
+
predictions_remote_path=predictions_remote_path,
|
|
835
|
+
forward_env=forward_env,
|
|
836
|
+
instance=instance,
|
|
837
|
+
prediction=prediction,
|
|
838
|
+
run_id=run_id,
|
|
839
|
+
eval_timeout=eval_timeout,
|
|
840
|
+
namespace=namespace,
|
|
841
|
+
instance_image_tag=instance_image_tag,
|
|
842
|
+
env_image_tag=env_image_tag,
|
|
843
|
+
model_name=model_name,
|
|
844
|
+
Command=Command,
|
|
845
|
+
WriteFileRequest=WriteFileRequest,
|
|
846
|
+
ReadFileRequest=ReadFileRequest,
|
|
847
|
+
)
|
|
848
|
+
try:
|
|
849
|
+
return self._run_coroutine_blocking(coro)
|
|
850
|
+
except Exception as exc: # pragma: no cover - remote execution failure
|
|
851
|
+
logger.exception("SWE-ReX evaluation failed for %s: %s", instance.get("instance_id"), exc)
|
|
852
|
+
return {"completed": False, "resolved": False, "error": f"SWE-ReX evaluation failed: {exc}", "backend": "swe_rex"}
|
|
853
|
+
|
|
854
|
+
async def _run_swe_rex_async(
|
|
855
|
+
self,
|
|
856
|
+
*,
|
|
857
|
+
deployment_config,
|
|
858
|
+
remote_root: str,
|
|
859
|
+
dataset_remote_path: str,
|
|
860
|
+
predictions_remote_path: str,
|
|
861
|
+
forward_env: dict[str, str] | None,
|
|
862
|
+
instance: dict[str, Any],
|
|
863
|
+
prediction: dict[str, Any],
|
|
864
|
+
run_id: str,
|
|
865
|
+
eval_timeout: int,
|
|
866
|
+
namespace: str,
|
|
867
|
+
instance_image_tag: str,
|
|
868
|
+
env_image_tag: str,
|
|
869
|
+
model_name: str,
|
|
870
|
+
Command,
|
|
871
|
+
WriteFileRequest,
|
|
872
|
+
ReadFileRequest,
|
|
873
|
+
) -> dict[str, Any]:
|
|
874
|
+
deployment = deployment_config.get_deployment()
|
|
875
|
+
await deployment.start()
|
|
876
|
+
try:
|
|
877
|
+
runtime = deployment.runtime
|
|
878
|
+
instance_id = str(instance["instance_id"])
|
|
879
|
+
safe_model = prediction["model_name_or_path"].replace("/", "__")
|
|
880
|
+
|
|
881
|
+
# Ensure working directory exists.
|
|
882
|
+
mkdir_resp = await runtime.execute(
|
|
883
|
+
Command(command=["mkdir", "-p", remote_root], timeout=60, shell=False)
|
|
884
|
+
)
|
|
885
|
+
if mkdir_resp.exit_code not in (0, None):
|
|
886
|
+
logger.warning("Failed to ensure remote directory %s (exit=%s)", remote_root, mkdir_resp.exit_code)
|
|
887
|
+
|
|
888
|
+
# Upload dataset & predictions.
|
|
889
|
+
dataset_blob = json.dumps([instance], ensure_ascii=False)
|
|
890
|
+
predictions_blob = json.dumps({instance_id: prediction}, ensure_ascii=False)
|
|
891
|
+
await runtime.write_file(WriteFileRequest(path=dataset_remote_path, content=dataset_blob))
|
|
892
|
+
await runtime.write_file(WriteFileRequest(path=predictions_remote_path, content=predictions_blob))
|
|
893
|
+
|
|
894
|
+
eval_cmd = [
|
|
895
|
+
"python",
|
|
896
|
+
"-m",
|
|
897
|
+
"swebench.harness.run_evaluation",
|
|
898
|
+
"--dataset_name",
|
|
899
|
+
dataset_remote_path,
|
|
900
|
+
"--split",
|
|
901
|
+
"test",
|
|
902
|
+
"--instance_ids",
|
|
903
|
+
instance_id,
|
|
904
|
+
"--predictions_path",
|
|
905
|
+
predictions_remote_path,
|
|
906
|
+
"-id",
|
|
907
|
+
run_id,
|
|
908
|
+
"--modal",
|
|
909
|
+
"true",
|
|
910
|
+
"--timeout",
|
|
911
|
+
str(eval_timeout),
|
|
912
|
+
"--namespace",
|
|
913
|
+
namespace,
|
|
914
|
+
"--instance_image_tag",
|
|
915
|
+
instance_image_tag,
|
|
916
|
+
"--env_image_tag",
|
|
917
|
+
env_image_tag,
|
|
918
|
+
"--max_workers",
|
|
919
|
+
"1",
|
|
920
|
+
]
|
|
921
|
+
|
|
922
|
+
command_timeout = max(eval_timeout + 900, 1200)
|
|
923
|
+
response = await runtime.execute(
|
|
924
|
+
Command(
|
|
925
|
+
command=eval_cmd,
|
|
926
|
+
timeout=command_timeout,
|
|
927
|
+
cwd=remote_root,
|
|
928
|
+
env=forward_env,
|
|
929
|
+
shell=False,
|
|
930
|
+
merge_output_streams=True,
|
|
931
|
+
)
|
|
932
|
+
)
|
|
933
|
+
command_output = (response.stdout or "") + (response.stderr or "")
|
|
934
|
+
exit_code = response.exit_code if response.exit_code is not None else -1
|
|
935
|
+
|
|
936
|
+
# Retrieve artifacts back to local disk.
|
|
937
|
+
artifacts = {}
|
|
938
|
+
try:
|
|
939
|
+
from swebench.harness.constants import RUN_EVALUATION_LOG_DIR
|
|
940
|
+
|
|
941
|
+
local_log_dir = Path(RUN_EVALUATION_LOG_DIR) / run_id / safe_model / instance_id
|
|
942
|
+
local_log_dir.mkdir(parents=True, exist_ok=True)
|
|
943
|
+
|
|
944
|
+
remote_log_dir = f"{remote_root}/logs/run_evaluation/{run_id}/{safe_model}/{instance_id}"
|
|
945
|
+
for filename in ("report.json", "test_output.txt", "run_instance.log", "patch.diff"):
|
|
946
|
+
remote_path = f"{remote_log_dir}/{filename}"
|
|
947
|
+
try:
|
|
948
|
+
content = await runtime.read_file(ReadFileRequest(path=remote_path))
|
|
949
|
+
except Exception:
|
|
950
|
+
continue
|
|
951
|
+
if getattr(content, "content", None):
|
|
952
|
+
(local_log_dir / filename).write_text(content.content)
|
|
953
|
+
|
|
954
|
+
artifacts = {
|
|
955
|
+
"log_dir": str(local_log_dir),
|
|
956
|
+
}
|
|
957
|
+
except Exception as exc: # pragma: no cover - best effort artifact copy
|
|
958
|
+
logger.warning("Failed to copy SWE-ReX artifacts locally: %s", exc)
|
|
959
|
+
|
|
960
|
+
payload = {
|
|
961
|
+
"backend": "swe_rex",
|
|
962
|
+
"command_exit_code": exit_code,
|
|
963
|
+
"command_output": command_output[-4000:] if command_output else "",
|
|
964
|
+
"artifacts": artifacts,
|
|
965
|
+
}
|
|
966
|
+
if exit_code == 0:
|
|
967
|
+
payload.setdefault("completed", True)
|
|
968
|
+
return payload
|
|
969
|
+
finally:
|
|
970
|
+
with contextlib.suppress(Exception):
|
|
971
|
+
await deployment.stop()
|
|
972
|
+
|
|
973
|
+
def _collect_evaluation_artifacts(
|
|
974
|
+
self,
|
|
975
|
+
*,
|
|
976
|
+
run_id: str,
|
|
977
|
+
model_name: str,
|
|
978
|
+
instance_id: str,
|
|
979
|
+
) -> dict[str, Any]:
|
|
980
|
+
try:
|
|
981
|
+
from swebench.harness.constants import (
|
|
982
|
+
LOG_REPORT,
|
|
983
|
+
LOG_TEST_OUTPUT,
|
|
984
|
+
RUN_EVALUATION_LOG_DIR,
|
|
985
|
+
)
|
|
986
|
+
except Exception: # pragma: no cover - dependency missing
|
|
987
|
+
return {
|
|
988
|
+
"completed": False,
|
|
989
|
+
"resolved": False,
|
|
990
|
+
"log_dir": None,
|
|
991
|
+
"report_path": None,
|
|
992
|
+
"test_output_path": None,
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
log_model = model_name.replace("/", "__")
|
|
996
|
+
log_dir = Path(RUN_EVALUATION_LOG_DIR) / run_id / log_model / instance_id
|
|
997
|
+
payload: dict[str, Any] = {
|
|
998
|
+
"log_dir": str(log_dir),
|
|
999
|
+
"report_path": None,
|
|
1000
|
+
"test_output_path": None,
|
|
1001
|
+
"report": None,
|
|
1002
|
+
"completed": False,
|
|
1003
|
+
"resolved": False,
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if not log_dir.exists():
|
|
1007
|
+
return payload
|
|
1008
|
+
|
|
1009
|
+
report_path = log_dir / LOG_REPORT
|
|
1010
|
+
if report_path.exists():
|
|
1011
|
+
payload["report_path"] = str(report_path)
|
|
1012
|
+
try:
|
|
1013
|
+
report_blob = json.loads(report_path.read_text())
|
|
1014
|
+
per_instance = report_blob.get(instance_id)
|
|
1015
|
+
if per_instance is not None:
|
|
1016
|
+
payload["report"] = per_instance
|
|
1017
|
+
payload["completed"] = True
|
|
1018
|
+
payload["resolved"] = bool(per_instance.get("resolved"))
|
|
1019
|
+
except Exception as exc: # pragma: no cover - log parsing failure
|
|
1020
|
+
logger.exception("Failed to parse SWE-bench report for %s: %s", instance_id, exc)
|
|
1021
|
+
payload["error"] = f"Failed to parse report.json: {exc}"
|
|
1022
|
+
|
|
1023
|
+
test_output_path = log_dir / LOG_TEST_OUTPUT
|
|
1024
|
+
if test_output_path.exists():
|
|
1025
|
+
payload["test_output_path"] = str(test_output_path)
|
|
1026
|
+
|
|
1027
|
+
return payload
|
|
1028
|
+
|
|
1029
|
+
@staticmethod
|
|
1030
|
+
def _run_coroutine_blocking(coro):
|
|
1031
|
+
try:
|
|
1032
|
+
loop = asyncio.get_running_loop()
|
|
1033
|
+
except RuntimeError:
|
|
1034
|
+
loop = None
|
|
1035
|
+
|
|
1036
|
+
if loop and loop.is_running():
|
|
1037
|
+
result: dict[str, Any] = {}
|
|
1038
|
+
error: dict[str, Exception] = {}
|
|
1039
|
+
|
|
1040
|
+
def runner():
|
|
1041
|
+
try:
|
|
1042
|
+
result["value"] = asyncio.run(coro)
|
|
1043
|
+
except Exception as exc: # pragma: no cover - propagate to caller
|
|
1044
|
+
error["exc"] = exc
|
|
1045
|
+
|
|
1046
|
+
thread = threading.Thread(target=runner, daemon=True)
|
|
1047
|
+
thread.start()
|
|
1048
|
+
thread.join()
|
|
1049
|
+
if error:
|
|
1050
|
+
raise error["exc"]
|
|
1051
|
+
return result.get("value")
|
|
1052
|
+
|
|
1053
|
+
return asyncio.run(coro)
|
|
1054
|
+
|
|
1055
|
+
@staticmethod
|
|
1056
|
+
def _namespace_from_image(image_name: str) -> str | None:
|
|
1057
|
+
if not image_name:
|
|
1058
|
+
return None
|
|
1059
|
+
parts = image_name.split("/")
|
|
1060
|
+
if len(parts) >= 2:
|
|
1061
|
+
return parts[-2] if parts[0].endswith(".io") else parts[0]
|
|
1062
|
+
return None
|
|
1063
|
+
|
|
1064
|
+
@staticmethod
|
|
1065
|
+
def _image_tag_from_name(image_name: str) -> str | None:
|
|
1066
|
+
if not image_name or ":" not in image_name:
|
|
1067
|
+
return None
|
|
1068
|
+
return image_name.rsplit(":", 1)[-1] or None
|
|
1069
|
+
|
|
1070
|
+
@staticmethod
|
|
1071
|
+
def _to_bool(value: Any) -> bool:
|
|
1072
|
+
if isinstance(value, bool):
|
|
1073
|
+
return value
|
|
1074
|
+
if isinstance(value, str):
|
|
1075
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
1076
|
+
if isinstance(value, (int, float)):
|
|
1077
|
+
return bool(value)
|
|
1078
|
+
return False # pragma: no cover - defensive default
|
|
1079
|
+
|
|
1080
|
+
def _build_observation(self, last_result: dict[str, Any] | None) -> dict[str, Any]:
|
|
1081
|
+
trimmed_history = summarise_history(self.state.history)
|
|
1082
|
+
observation = {
|
|
1083
|
+
"task": self.task,
|
|
1084
|
+
"step_idx": self.state.step_idx,
|
|
1085
|
+
"history": trimmed_history,
|
|
1086
|
+
"submitted": self.state.submitted,
|
|
1087
|
+
"submission_success": self.state.submission_success,
|
|
1088
|
+
"tools": TOOLS_SCHEMA,
|
|
1089
|
+
}
|
|
1090
|
+
if last_result is not None:
|
|
1091
|
+
observation["last"] = last_result
|
|
1092
|
+
if self.last_submission is not None:
|
|
1093
|
+
observation["submission_result"] = self.last_submission
|
|
1094
|
+
return observation
|
|
1095
|
+
|
|
1096
|
+
def _build_response(
|
|
1097
|
+
self,
|
|
1098
|
+
*,
|
|
1099
|
+
observation: dict[str, Any],
|
|
1100
|
+
step_idx: int,
|
|
1101
|
+
done: bool = False,
|
|
1102
|
+
reward: float | None = None,
|
|
1103
|
+
info: dict[str, Any] | None = None,
|
|
1104
|
+
) -> dict[str, Any]:
|
|
1105
|
+
response = {
|
|
1106
|
+
"observation": observation,
|
|
1107
|
+
"step_idx": step_idx,
|
|
1108
|
+
"done": bool(done),
|
|
1109
|
+
}
|
|
1110
|
+
if reward is not None:
|
|
1111
|
+
response["reward"] = reward
|
|
1112
|
+
if info is not None:
|
|
1113
|
+
response["info"] = info
|
|
1114
|
+
return response
|
|
1115
|
+
|
|
1116
|
+
def state_dict(self) -> dict[str, Any]:
|
|
1117
|
+
return {
|
|
1118
|
+
"task": self.state.task,
|
|
1119
|
+
"history": self.state.history,
|
|
1120
|
+
"step_idx": self.state.step_idx,
|
|
1121
|
+
"submitted": self.state.submitted,
|
|
1122
|
+
"submission_success": self.state.submission_success,
|
|
1123
|
+
"last_result": self.last_result,
|
|
1124
|
+
"last_submission": self.last_submission,
|
|
1125
|
+
"environment_type": self.environment_type,
|
|
1126
|
+
"env_config": self.env_config,
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
def load_state_dict(self, payload: dict[str, Any]) -> None:
|
|
1130
|
+
self.state = MiniSweEnvironmentState(
|
|
1131
|
+
task=payload["task"],
|
|
1132
|
+
history=payload.get("history", []),
|
|
1133
|
+
step_idx=int(payload.get("step_idx", 0)),
|
|
1134
|
+
submitted=bool(payload.get("submitted", False)),
|
|
1135
|
+
submission_success=payload.get("submission_success"),
|
|
1136
|
+
)
|
|
1137
|
+
self.last_result = payload.get("last_result")
|
|
1138
|
+
self.last_submission = payload.get("last_submission")
|
|
1139
|
+
self.environment_type = payload.get("environment_type", self.environment_type)
|
|
1140
|
+
self.env_config = payload.get("env_config", self.env_config)
|
|
1141
|
+
|
|
1142
|
+
async def serialize(self) -> dict[str, Any]:
|
|
1143
|
+
return {
|
|
1144
|
+
"name": self.name,
|
|
1145
|
+
"config": {
|
|
1146
|
+
"env_config": self.env_config,
|
|
1147
|
+
"submit_command": self.submit_command,
|
|
1148
|
+
},
|
|
1149
|
+
"state": self.state_dict(),
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
@classmethod
|
|
1153
|
+
async def deserialize(cls, payload: dict[str, Any]) -> MiniSweEnvironmentWrapper:
|
|
1154
|
+
config = payload.get("config", {}) or {}
|
|
1155
|
+
wrapper = cls(
|
|
1156
|
+
task=payload["state"]["task"],
|
|
1157
|
+
env_config=config.get("env_config"),
|
|
1158
|
+
submit_command=config.get("submit_command"),
|
|
1159
|
+
)
|
|
1160
|
+
wrapper.load_state_dict(payload["state"])
|
|
1161
|
+
return wrapper
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
__all__ = ["MiniSweEnvironmentWrapper"]
|