@elizaos/sweagent-root 2.0.0-alpha
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.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/package.json +71 -0
- package/python/LICENSE +21 -0
- package/python/config/README.md +15 -0
- package/python/config/bash_only.yaml +222 -0
- package/python/config/benchmarks/250212_sweagent_heavy_sbl.yaml +188 -0
- package/python/config/benchmarks/250225_anthropic_filemap_simple_review.yaml +75 -0
- package/python/config/benchmarks/250522_anthropic_filemap_simple_review.yaml +92 -0
- package/python/config/benchmarks/250526_anthropic_filemap_simple_review_sbl.yaml +93 -0
- package/python/config/benchmarks/anthropic_filemap_multilingual.yaml +66 -0
- package/python/config/coding_challenge.yaml +104 -0
- package/python/config/default.yaml +69 -0
- package/python/config/default_backticks.yaml +69 -0
- package/python/config/default_mm_no_images.yaml +82 -0
- package/python/config/default_mm_with_images.yaml +83 -0
- package/python/config/demo/default.yaml +80 -0
- package/python/config/demo/no_instructions.yaml +69 -0
- package/python/config/demo/only_bash.yaml +60 -0
- package/python/config/exotic/default_shell.yaml +52 -0
- package/python/config/exotic/windowed_replace.yaml +125 -0
- package/python/config/exotic/windowed_replace_late_repro.yaml +127 -0
- package/python/config/human/human.yaml +24 -0
- package/python/config/human/human_demo.yaml +52 -0
- package/python/config/sweagent_0_7/07.yaml +101 -0
- package/python/config/sweagent_0_7/07_fcalling.yaml +100 -0
- package/python/config/sweagent_0_7/07_from_url.yaml +114 -0
- package/python/config/sweagent_0_7/07_thought_action.yaml +102 -0
- package/python/config/sweagent_0_7/07_thought_action_xml.yaml +96 -0
- package/python/mlc_config.json +44 -0
- package/python/pyproject.toml +262 -0
- package/python/sweagent/__init__.py +114 -0
- package/python/sweagent/__main__.py +4 -0
- package/python/sweagent/agent/__init__.py +0 -0
- package/python/sweagent/agent/action_sampler.py +317 -0
- package/python/sweagent/agent/agents.py +1294 -0
- package/python/sweagent/agent/extra/shell_agent.py +106 -0
- package/python/sweagent/agent/history_processors.py +399 -0
- package/python/sweagent/agent/hooks/__init__.py +0 -0
- package/python/sweagent/agent/hooks/abstract.py +139 -0
- package/python/sweagent/agent/hooks/status.py +34 -0
- package/python/sweagent/agent/models.py +896 -0
- package/python/sweagent/agent/problem_statement.py +312 -0
- package/python/sweagent/agent/reviewer.py +664 -0
- package/python/sweagent/environment/__init__.py +0 -0
- package/python/sweagent/environment/hooks/__init__.py +0 -0
- package/python/sweagent/environment/hooks/abstract.py +60 -0
- package/python/sweagent/environment/hooks/status.py +28 -0
- package/python/sweagent/environment/repo.py +219 -0
- package/python/sweagent/environment/swe_env.py +276 -0
- package/python/sweagent/exceptions.py +54 -0
- package/python/sweagent/inspector/README.md +6 -0
- package/python/sweagent/inspector/__init__.py +0 -0
- package/python/sweagent/inspector/favicon.ico +0 -0
- package/python/sweagent/inspector/fileViewer.js +354 -0
- package/python/sweagent/inspector/icons/computer.png +0 -0
- package/python/sweagent/inspector/icons/edit_icon.svg +11 -0
- package/python/sweagent/inspector/icons/swe-agent-logo-50.png +0 -0
- package/python/sweagent/inspector/icons/swellama_blue.png +0 -0
- package/python/sweagent/inspector/icons/swellama_brown.png +0 -0
- package/python/sweagent/inspector/icons/swellama_grey.png +0 -0
- package/python/sweagent/inspector/icons/swellama_tan.png +0 -0
- package/python/sweagent/inspector/index.html +25 -0
- package/python/sweagent/inspector/server.py +354 -0
- package/python/sweagent/inspector/static.py +169 -0
- package/python/sweagent/inspector/style.css +454 -0
- package/python/sweagent/run/__init__.py +0 -0
- package/python/sweagent/run/_progress.py +158 -0
- package/python/sweagent/run/batch_instances.py +419 -0
- package/python/sweagent/run/common.py +387 -0
- package/python/sweagent/run/compare_runs.py +123 -0
- package/python/sweagent/run/extract_pred.py +19 -0
- package/python/sweagent/run/hooks/__init__.py +0 -0
- package/python/sweagent/run/hooks/abstract.py +67 -0
- package/python/sweagent/run/hooks/apply_patch.py +106 -0
- package/python/sweagent/run/hooks/open_pr.py +244 -0
- package/python/sweagent/run/hooks/swe_bench_evaluate.py +113 -0
- package/python/sweagent/run/inspector_cli.py +493 -0
- package/python/sweagent/run/merge_predictions.py +64 -0
- package/python/sweagent/run/quick_stats.py +96 -0
- package/python/sweagent/run/remove_unfinished.py +63 -0
- package/python/sweagent/run/rich_test.py +91 -0
- package/python/sweagent/run/run.py +147 -0
- package/python/sweagent/run/run_batch.py +442 -0
- package/python/sweagent/run/run_replay.py +219 -0
- package/python/sweagent/run/run_shell.py +155 -0
- package/python/sweagent/run/run_single.py +225 -0
- package/python/sweagent/run/run_traj_to_demo.py +85 -0
- package/python/sweagent/tools/__init__.py +0 -0
- package/python/sweagent/tools/bundle.py +57 -0
- package/python/sweagent/tools/commands.py +220 -0
- package/python/sweagent/tools/parsing.py +619 -0
- package/python/sweagent/tools/tools.py +430 -0
- package/python/sweagent/tools/utils.py +108 -0
- package/python/sweagent/types.py +102 -0
- package/python/sweagent/utils/__init__.py +0 -0
- package/python/sweagent/utils/config.py +80 -0
- package/python/sweagent/utils/files.py +27 -0
- package/python/sweagent/utils/github.py +118 -0
- package/python/sweagent/utils/jinja_warnings.py +14 -0
- package/python/sweagent/utils/log.py +175 -0
- package/python/sweagent/utils/patch_formatter.py +152 -0
- package/python/sweagent/utils/serialization.py +45 -0
- package/python/tests/__init__.py +0 -0
- package/python/tests/conftest.py +191 -0
- package/python/tests/test_agent.py +258 -0
- package/python/tests/test_batch_instance.py +43 -0
- package/python/tests/test_commands/_interactive_dummy.py +35 -0
- package/python/tests/test_commands/interactive_dummy_wrapper.sh +29 -0
- package/python/tests/test_data/config_files/dummy_interactive.yaml +62 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/Dockerfile +20 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/README.md +13 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/challenge.json +12 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/customrandom.c +50 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/docker-compose.yml +14 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/release +0 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/server +0 -0
- package/python/tests/test_data/data_sources/ctf/crypto/Katy/solver.py +12 -0
- package/python/tests/test_data/data_sources/ctf/forensics/flash/README.md +16 -0
- package/python/tests/test_data/data_sources/ctf/forensics/flash/challenge.json +9 -0
- package/python/tests/test_data/data_sources/ctf/forensics/flash/flash_c8429a430278283c0e571baebca3d139.zip +0 -0
- package/python/tests/test_data/data_sources/ctf/misc/networking_1/README.md +15 -0
- package/python/tests/test_data/data_sources/ctf/misc/networking_1/challenge.json +10 -0
- package/python/tests/test_data/data_sources/ctf/misc/networking_1/networking.pcap +0 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/Dockerfile +28 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/README.md +14 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/challenge.json +14 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/docker-compose.yml +14 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/flag.txt +1 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/warmup +0 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/warmup.c +26 -0
- package/python/tests/test_data/data_sources/ctf/pwn/warmup/warmup.py +9 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/README.md +14 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/challenge.json +8 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/rock +0 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/rock.cpp +167 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/solution.cpp +24 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/test_solver/solution.py +6 -0
- package/python/tests/test_data/data_sources/ctf/rev/rock/test_solver/test.sh +10 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/000-default.conf +18 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/Dockerfile +20 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/cgi/file.pl +38 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/cgi/forms.pl +40 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/cgi/hello.pl +11 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/challenge.json +12 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/docker-compose.yml +14 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/flag +1 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/index.html +11 -0
- package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/solution.txt +1 -0
- package/python/tests/test_data/data_sources/debug_20240322.json +1 -0
- package/python/tests/test_data/data_sources/expert_instances.yaml +16 -0
- package/python/tests/test_data/data_sources/human_eval.json +1 -0
- package/python/tests/test_data/data_sources/simple_instances.yaml +3 -0
- package/python/tests/test_data/data_sources/simple_instances_long.yaml +30 -0
- package/python/tests/test_data/data_sources/swe-bench-dev-easy.json +1 -0
- package/python/tests/test_data/data_sources/swe-bench-dev-easy_first_only.json +1 -0
- package/python/tests/test_data/data_sources/swe-bench-lite-test.json +1 -0
- package/python/tests/test_data/trajectories/gpt4__swe-agent-test-repo__default_from_url__t-0.00__p-0.95__c-3.00__install-1/6e44b9__sweagenttestrepo-1c2844.traj +342 -0
- package/python/tests/test_data/trajectories/gpt4__swe-agent-test-repo__default_from_url__t-0.00__p-0.95__c-3.00__install-1/solution_missing_colon.py +15 -0
- package/python/tests/test_data/trajectories/gpt4__swe-agent__test-repo__default_from_url__t-0.00__p-0.95__c-3.00__install-1/args.yaml +518 -0
- package/python/tests/test_data/trajectories/gpt4__swe-agent__test-repo__default_from_url__t-0.00__p-0.95__c-3.00__install-1/swe-agent__test-repo-i1.traj +124 -0
- package/python/tests/test_data/trajectories/gpt4__swe-bench-dev-easy_first_only__default__t-0.00__p-0.95__c-3.00__install-1/all_preds.jsonl +1 -0
- package/python/tests/test_data/trajectories/gpt4__swe-bench-dev-easy_first_only__default__t-0.00__p-0.95__c-3.00__install-1/args.yaml +520 -0
- package/python/tests/test_data/trajectories/gpt4__swe-bench-dev-easy_first_only__default__t-0.00__p-0.95__c-3.00__install-1/patches/pydicom__pydicom-1458.patch +18 -0
- package/python/tests/test_data/trajectories/gpt4__swe-bench-dev-easy_first_only__default__t-0.00__p-0.95__c-3.00__install-1/pydicom__pydicom-1458.traj +257 -0
- package/python/tests/test_env.py +66 -0
- package/python/tests/test_env_utils.py +129 -0
- package/python/tests/test_history_processors.py +40 -0
- package/python/tests/test_models.py +23 -0
- package/python/tests/test_openai_live.py +164 -0
- package/python/tests/test_packaging.py +7 -0
- package/python/tests/test_parsing.py +131 -0
- package/python/tests/test_problem_statement_multimodal.py +111 -0
- package/python/tests/test_quick_stats.py +42 -0
- package/python/tests/test_run.py +37 -0
- package/python/tests/test_run_batch.py +110 -0
- package/python/tests/test_run_hooks.py +114 -0
- package/python/tests/test_run_replay.py +33 -0
- package/python/tests/test_run_single.py +125 -0
- package/python/tests/test_tools_command_parsing.py +193 -0
- package/python/tests/test_utils.py +15 -0
- package/python/tests/tools/__init__.py +0 -0
- package/python/tests/tools/conftest.py +12 -0
- package/python/tests/tools/test_default_utils.py +153 -0
- package/python/tests/tools/test_edit_replace.py +0 -0
- package/python/tests/tools/test_split_string.py +82 -0
- package/python/tests/utils.py +29 -0
- package/python/tools/diff_state/bin/_state_diff_state +52 -0
- package/python/tools/diff_state/config.yaml +2 -0
- package/python/tools/edit_anthropic/bin/_state_anthropic +21 -0
- package/python/tools/edit_anthropic/bin/str_replace_editor +710 -0
- package/python/tools/edit_anthropic/config.yaml +56 -0
- package/python/tools/edit_anthropic/install.sh +3 -0
- package/python/tools/filemap/bin/filemap +45 -0
- package/python/tools/filemap/config.yaml +9 -0
- package/python/tools/filemap/install.sh +2 -0
- package/python/tools/forfeit/bin/exit_forfeit +5 -0
- package/python/tools/forfeit/config.yaml +5 -0
- package/python/tools/image_tools/bin/view_image +36 -0
- package/python/tools/image_tools/config.yaml +9 -0
- package/python/tools/multilingual_setup/bin/do_nothing +2 -0
- package/python/tools/multilingual_setup/config.yaml +1 -0
- package/python/tools/multilingual_setup/install.sh +45 -0
- package/python/tools/registry/bin/_read_env +10 -0
- package/python/tools/registry/bin/_write_env +10 -0
- package/python/tools/registry/config.yaml +1 -0
- package/python/tools/registry/install.sh +6 -0
- package/python/tools/registry/lib/__init__.py +0 -0
- package/python/tools/registry/lib/registry.py +56 -0
- package/python/tools/review_on_submit_m/README.md +6 -0
- package/python/tools/review_on_submit_m/bin/submit +54 -0
- package/python/tools/review_on_submit_m/config.yaml +6 -0
- package/python/tools/review_on_submit_m/install.sh +0 -0
- package/python/tools/search/bin/find_file +31 -0
- package/python/tools/search/bin/search_dir +39 -0
- package/python/tools/search/bin/search_file +55 -0
- package/python/tools/search/config.yaml +37 -0
- package/python/tools/search/install.sh +3 -0
- package/python/tools/submit/bin/submit +17 -0
- package/python/tools/submit/config.yaml +5 -0
- package/python/tools/web_browser/bin/click_mouse +41 -0
- package/python/tools/web_browser/bin/close_site +28 -0
- package/python/tools/web_browser/bin/double_click_mouse +37 -0
- package/python/tools/web_browser/bin/drag_mouse +46 -0
- package/python/tools/web_browser/bin/execute_script_on_page +39 -0
- package/python/tools/web_browser/bin/get_console_output +48 -0
- package/python/tools/web_browser/bin/move_mouse +35 -0
- package/python/tools/web_browser/bin/navigate_back +33 -0
- package/python/tools/web_browser/bin/navigate_forward +33 -0
- package/python/tools/web_browser/bin/open_site +36 -0
- package/python/tools/web_browser/bin/press_keys_on_page +51 -0
- package/python/tools/web_browser/bin/reload_page +33 -0
- package/python/tools/web_browser/bin/run_web_browser_server +394 -0
- package/python/tools/web_browser/bin/screenshot_site +38 -0
- package/python/tools/web_browser/bin/scroll_on_page +40 -0
- package/python/tools/web_browser/bin/set_browser_window_size +40 -0
- package/python/tools/web_browser/bin/type_text +34 -0
- package/python/tools/web_browser/bin/wait_time +39 -0
- package/python/tools/web_browser/config.yaml +155 -0
- package/python/tools/web_browser/install.sh +22 -0
- package/python/tools/web_browser/lib/browser_manager.py +404 -0
- package/python/tools/web_browser/lib/web_browser_config.py +33 -0
- package/python/tools/web_browser/lib/web_browser_utils.py +126 -0
- package/python/tools/web_browser/test_console.html +1 -0
- package/python/tools/windowed/bin/_state +25 -0
- package/python/tools/windowed/bin/create +29 -0
- package/python/tools/windowed/bin/goto +37 -0
- package/python/tools/windowed/bin/open +49 -0
- package/python/tools/windowed/bin/scroll_down +12 -0
- package/python/tools/windowed/bin/scroll_up +13 -0
- package/python/tools/windowed/config.yaml +38 -0
- package/python/tools/windowed/install.sh +15 -0
- package/python/tools/windowed/lib/__init__.py +0 -0
- package/python/tools/windowed/lib/flake8_utils.py +147 -0
- package/python/tools/windowed/lib/windowed_file.py +312 -0
- package/python/tools/windowed_edit_linting/bin/edit +128 -0
- package/python/tools/windowed_edit_linting/config.yaml +31 -0
- package/python/tools/windowed_edit_linting/install.sh +5 -0
- package/python/tools/windowed_edit_replace/bin/edit +172 -0
- package/python/tools/windowed_edit_replace/bin/insert +77 -0
- package/python/tools/windowed_edit_replace/config.yaml +60 -0
- package/python/tools/windowed_edit_replace/install.sh +5 -0
- package/python/tools/windowed_edit_rewrite/bin/edit +78 -0
- package/python/tools/windowed_edit_rewrite/config.yaml +11 -0
- package/python/tools/windowed_edit_rewrite/install.sh +5 -0
- package/python/trajectories/demonstrations/ctf/crypto/BabyEncryption.traj +318 -0
- package/python/trajectories/demonstrations/ctf/crypto/BabyTimeCapsule.traj +197 -0
- package/python/trajectories/demonstrations/ctf/crypto/eps.traj +289 -0
- package/python/trajectories/demonstrations/ctf/crypto/katy.traj +368 -0
- package/python/trajectories/demonstrations/ctf/forensics/flash.traj +102 -0
- package/python/trajectories/demonstrations/ctf/misc/networking_1.traj +102 -0
- package/python/trajectories/demonstrations/ctf/pwn/warmup.traj +159 -0
- package/python/trajectories/demonstrations/ctf/rev/rock.traj +251 -0
- package/python/trajectories/demonstrations/ctf/web/i_got_id_demo.traj +422 -0
- package/python/trajectories/demonstrations/function_calling_simple.traj +151 -0
- package/python/trajectories/demonstrations/human_thought__swe-bench-HumanEvalFix-python__lcb__t-0.00__p-0.95__c-4.00__install-0/humanevalfix-python-0.traj +129 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__default__t-0.20__p-0.95__c-2.00__install-1___install_from_source/marshmallow-code__marshmallow-1867.traj +318 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__default_sys-env_cursors_window100__t-0.20__p-0.95__c-2.00__install-1/marshmallow-code__marshmallow-1867.traj +251 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__default_sys-env_window100__t-0.20__p-0.95__c-2.00__install-1/marshmallow-code__marshmallow-1867.traj +399 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__function_calling__install-1/marshmallow-code__marshmallow-1867.traj +594 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__function_calling_replace__install-1/marshmallow-code__marshmallow-1867.traj +592 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__function_calling_replace_from_source/marshmallow-code__marshmallow-1867.traj +3316 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__xml_sys-env_cursors_window100__t-0.20__p-0.95__c-2.00__install-1/marshmallow-code__marshmallow-1867.traj +251 -0
- package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__xml_sys-env_window100__t-0.20__p-0.95__c-2.00__install-1/marshmallow-code__marshmallow-1867.traj +399 -0
- package/python/trajectories/demonstrations/str_replace_anthropic_demo.yaml +432 -0
- package/rust/Cargo.toml +100 -0
- package/rust/README.md +49 -0
- package/rust/src/agent/action_sampler.rs +130 -0
- package/rust/src/agent/agents.rs +1029 -0
- package/rust/src/agent/history_processors.rs +277 -0
- package/rust/src/agent/hooks/mod.rs +208 -0
- package/rust/src/agent/mod.rs +24 -0
- package/rust/src/agent/models.rs +837 -0
- package/rust/src/agent/problem_statement.rs +355 -0
- package/rust/src/agent/reviewer.rs +505 -0
- package/rust/src/bin/sweagent.rs +784 -0
- package/rust/src/environment/deployment.rs +631 -0
- package/rust/src/environment/hooks/mod.rs +114 -0
- package/rust/src/environment/mod.rs +16 -0
- package/rust/src/environment/repo.rs +265 -0
- package/rust/src/environment/runtime.rs +237 -0
- package/rust/src/environment/swe_env.rs +248 -0
- package/rust/src/exceptions.rs +228 -0
- package/rust/src/lib.rs +68 -0
- package/rust/src/monitoring.rs +482 -0
- package/rust/src/run/hooks/mod.rs +134 -0
- package/rust/src/run/mod.rs +12 -0
- package/rust/src/run/run_batch.rs +563 -0
- package/rust/src/run/run_single.rs +196 -0
- package/rust/src/tools/bundle.rs +224 -0
- package/rust/src/tools/commands.rs +173 -0
- package/rust/src/tools/mod.rs +295 -0
- package/rust/src/tools/parsing.rs +354 -0
- package/rust/src/tools/registry.rs +143 -0
- package/rust/src/types.rs +554 -0
- package/rust/src/utils/config.rs +105 -0
- package/rust/src/utils/files.rs +137 -0
- package/rust/src/utils/github.rs +171 -0
- package/rust/src/utils/log.rs +65 -0
- package/rust/src/utils/mod.rs +17 -0
- package/rust/src/utils/serialization.rs +181 -0
- package/rust/src/utils/template.rs +173 -0
- package/typescript/README.md +335 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from sweagent import REPO_ROOT
|
|
10
|
+
from sweagent.utils.log import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("swea-config", emoji="🔧")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _convert_path_relative_to_repo_root(path: Path | str, root: Path | None = None) -> Path | str:
|
|
16
|
+
original_type = type(path)
|
|
17
|
+
path = Path(path).resolve()
|
|
18
|
+
root = Path(root or os.getenv("SWE_AGENT_CONFIG_ROOT", REPO_ROOT))
|
|
19
|
+
relative_path = path.relative_to(root) if root in path.parents else path
|
|
20
|
+
return relative_path if original_type is Path else str(relative_path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _could_be_a_path(v: Any) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
return Path(v).exists()
|
|
26
|
+
except Exception:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _strip_abspath_from_dict(value: dict | list | str, root: Path | None = None) -> dict | list | str:
|
|
31
|
+
root = Path(root or os.getenv("SWE_AGENT_CONFIG_ROOT", REPO_ROOT))
|
|
32
|
+
if isinstance(value, dict):
|
|
33
|
+
return {k: _strip_abspath_from_dict(v, root) for k, v in value.items()}
|
|
34
|
+
elif isinstance(value, list):
|
|
35
|
+
return [_strip_abspath_from_dict(v, root) for v in value]
|
|
36
|
+
elif isinstance(value, str) and _could_be_a_path(value):
|
|
37
|
+
return _convert_path_relative_to_repo_root(value, root)
|
|
38
|
+
else:
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _convert_path_to_abspath(path: Path | str) -> Path:
|
|
43
|
+
"""If path is not absolute, convert it to an absolute path
|
|
44
|
+
using the SWE_AGENT_CONFIG_ROOT environment variable (if set) or
|
|
45
|
+
REPO_ROOT as base.
|
|
46
|
+
"""
|
|
47
|
+
path = Path(path)
|
|
48
|
+
root = Path(os.getenv("SWE_AGENT_CONFIG_ROOT", REPO_ROOT))
|
|
49
|
+
assert root.is_dir()
|
|
50
|
+
if not path.is_absolute():
|
|
51
|
+
path = root / path
|
|
52
|
+
assert path.is_absolute()
|
|
53
|
+
return path.resolve()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _convert_paths_to_abspath(paths: list[Path] | list[str]) -> list[Path]:
|
|
57
|
+
return [_convert_path_to_abspath(p) for p in paths]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_environment_variables(path: Path | None = None):
|
|
61
|
+
"""Load environment variables from a .env file.
|
|
62
|
+
If path is not provided, we first look for a .env file in the current working
|
|
63
|
+
directory and then in the repository root.
|
|
64
|
+
"""
|
|
65
|
+
if path is None:
|
|
66
|
+
cwd_path = Path.cwd() / ".env"
|
|
67
|
+
repo_path = REPO_ROOT / ".env"
|
|
68
|
+
if cwd_path.exists():
|
|
69
|
+
path = cwd_path
|
|
70
|
+
elif repo_path.exists():
|
|
71
|
+
path = REPO_ROOT / ".env"
|
|
72
|
+
else:
|
|
73
|
+
logger.debug("No .env file found")
|
|
74
|
+
return
|
|
75
|
+
if not path.is_file():
|
|
76
|
+
msg = f"No .env file found at {path}"
|
|
77
|
+
raise FileNotFoundError(msg)
|
|
78
|
+
anything_loaded = load_dotenv(dotenv_path=path)
|
|
79
|
+
if anything_loaded:
|
|
80
|
+
logger.info(f"Loaded environment variables from {path}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_file(path: Path | str | None) -> Any:
|
|
9
|
+
"""Load files based on their extension."""
|
|
10
|
+
if path is None:
|
|
11
|
+
return None
|
|
12
|
+
if isinstance(path, str):
|
|
13
|
+
path = Path(path)
|
|
14
|
+
if not path.exists():
|
|
15
|
+
raise FileNotFoundError(path)
|
|
16
|
+
if path.is_dir():
|
|
17
|
+
from datasets import load_from_disk
|
|
18
|
+
|
|
19
|
+
return load_from_disk(path)
|
|
20
|
+
if path.suffix in [".json", ".traj"]:
|
|
21
|
+
return json.loads(path.read_text())
|
|
22
|
+
if path.suffix == ".jsonl":
|
|
23
|
+
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
|
|
24
|
+
if path.suffix == ".yaml":
|
|
25
|
+
return yaml.safe_load(path.read_text())
|
|
26
|
+
msg = f"Unsupported file extension: {path.suffix}"
|
|
27
|
+
raise NotImplementedError(msg)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ghapi.all import GhApi
|
|
4
|
+
|
|
5
|
+
GITHUB_ISSUE_URL_PATTERN = re.compile(r"github\.com\/(.*?)\/(.*?)\/issues\/(\d+)")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidGithubURL(Exception):
|
|
9
|
+
"""Raised when a github URL is invalid"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
GITHUB_REPO_URL_PATTERN = re.compile(r".*[/@]?github\.com\/([^/]+)\/([^/]+)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _is_github_repo_url(data_path: str) -> bool:
|
|
16
|
+
"""Check if data_path is an URL pointing to a github repository.
|
|
17
|
+
Paths to issues or PRs will also match this pattern.
|
|
18
|
+
"""
|
|
19
|
+
return GITHUB_REPO_URL_PATTERN.search(data_path) is not None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_github_issue_url(data_path: str) -> bool:
|
|
23
|
+
"""Check if data_path is an URL pointing to a github issue"""
|
|
24
|
+
return GITHUB_ISSUE_URL_PATTERN.search(data_path) is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_commit(api: GhApi, owner: str, repo: str, ref: str | None = None):
|
|
28
|
+
"""Get commit object from github api
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
api (GhApi):
|
|
32
|
+
owner (str): Repo owner, e.g., "SWE-agent"
|
|
33
|
+
repo (str): Repo, e.g., "SWE-agent"
|
|
34
|
+
ref (str, optional): Branch, tag or commit hash
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
_type_: _description_
|
|
38
|
+
"""
|
|
39
|
+
if ref:
|
|
40
|
+
return api.repos.get_commit(owner, repo, ref) # type: ignore
|
|
41
|
+
return api.repos.list_commits(owner, repo)[0] # type: ignore
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_gh_issue_url(issue_url: str) -> tuple[str, str, str]:
|
|
45
|
+
"""
|
|
46
|
+
Returns:
|
|
47
|
+
owner: Repo owner
|
|
48
|
+
repo: Repo name
|
|
49
|
+
issue number: Issue number as str
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
InvalidGithubURL: If the URL is not a valid github issue URL
|
|
53
|
+
"""
|
|
54
|
+
match = GITHUB_ISSUE_URL_PATTERN.search(issue_url)
|
|
55
|
+
if not match:
|
|
56
|
+
msg = f"Invalid GitHub issue URL: {issue_url}"
|
|
57
|
+
raise InvalidGithubURL(msg)
|
|
58
|
+
res = match.groups()
|
|
59
|
+
assert len(res) == 3
|
|
60
|
+
return tuple(res) # type: ignore
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_gh_repo_url(repo_url: str) -> tuple[str, str]:
|
|
64
|
+
"""
|
|
65
|
+
Returns:
|
|
66
|
+
owner: Repo owner/org
|
|
67
|
+
repo: Repo name
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
InvalidGithubURL: If the URL is not a valid github repo URL
|
|
71
|
+
"""
|
|
72
|
+
match = GITHUB_REPO_URL_PATTERN.search(repo_url)
|
|
73
|
+
if not match:
|
|
74
|
+
msg = f"Invalid GitHub issue URL: {repo_url}"
|
|
75
|
+
raise InvalidGithubURL(msg)
|
|
76
|
+
res = match.groups()
|
|
77
|
+
assert len(res) == 2
|
|
78
|
+
return tuple(res) # type: ignore
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_gh_issue_data(issue_url: str, *, token: str = ""):
|
|
82
|
+
"""Returns github issue data in the form of a dictionary.
|
|
83
|
+
See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue
|
|
84
|
+
for return format
|
|
85
|
+
"""
|
|
86
|
+
owner, repo, issue_number = _parse_gh_issue_url(issue_url)
|
|
87
|
+
api = GhApi(token=token)
|
|
88
|
+
return api.issues.get(owner, repo, issue_number) # type: ignore
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _get_problem_statement_from_github_issue(
|
|
92
|
+
owner: str, repo: str, issue_number: str, *, token: str | None = ""
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Return problem statement from github issue"""
|
|
95
|
+
api = GhApi(token=token)
|
|
96
|
+
issue = api.issues.get(owner, repo, issue_number) # type: ignore
|
|
97
|
+
title = issue.title if issue.title else ""
|
|
98
|
+
body = issue.body if issue.body else ""
|
|
99
|
+
return f"{title}\n{body}\n"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _get_associated_commit_urls(org: str, repo: str, issue_number: str, *, token: str = "") -> list[str]:
|
|
103
|
+
"""Return the URLs of commits that would close an issue."""
|
|
104
|
+
api = GhApi(token=token)
|
|
105
|
+
# Strangely the "pull_request" field of api.issues.get is often not set
|
|
106
|
+
# so we have to go through the events to check if there's a commit
|
|
107
|
+
events = api.issues.list_events(org, repo, issue_number) # type: ignore
|
|
108
|
+
commit_urls = []
|
|
109
|
+
for event in events:
|
|
110
|
+
if event.event != "referenced":
|
|
111
|
+
continue
|
|
112
|
+
if not event.commit_id:
|
|
113
|
+
continue
|
|
114
|
+
commit = api.repos.get_commit(org, repo, event.commit_id) # type: ignore
|
|
115
|
+
message = commit.commit.message
|
|
116
|
+
if f"fixes #{issue_number}" in message.lower() or f"closes #{issue_number}" in message.lower():
|
|
117
|
+
commit_urls.append(commit.html_url)
|
|
118
|
+
return commit_urls
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from sweagent.utils.log import get_logger
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _warn_probably_wrong_jinja_syntax(template: str | None) -> None:
|
|
5
|
+
"""Warn if the template uses {var} instead of {{var}}."""
|
|
6
|
+
if template is None:
|
|
7
|
+
return
|
|
8
|
+
if "{" not in template:
|
|
9
|
+
return
|
|
10
|
+
for s in ["{%", "{ %", "{{"]:
|
|
11
|
+
if s in template:
|
|
12
|
+
return
|
|
13
|
+
logger = get_logger("swea-config", emoji="🔧")
|
|
14
|
+
logger.warning("Probably wrong Jinja syntax in template: %s. Make sure to use {{var}} instead of {var}.", template)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from pathlib import Path, PurePath
|
|
9
|
+
|
|
10
|
+
from rich.logging import RichHandler
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
_SET_UP_LOGGERS: set[str] = set()
|
|
14
|
+
_ADDITIONAL_HANDLERS: dict[str, logging.Handler] = {}
|
|
15
|
+
_LOG_LOCK = threading.Lock()
|
|
16
|
+
|
|
17
|
+
logging.TRACE = 5 # type: ignore
|
|
18
|
+
logging.addLevelName(logging.TRACE, "TRACE") # type: ignore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _interpret_level(level: int | str | None, *, default=logging.DEBUG) -> int:
|
|
22
|
+
if not level:
|
|
23
|
+
return default
|
|
24
|
+
if isinstance(level, int):
|
|
25
|
+
return level
|
|
26
|
+
if level.isnumeric():
|
|
27
|
+
return int(level)
|
|
28
|
+
return getattr(logging, level.upper())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_STREAM_LEVEL = _interpret_level(os.environ.get("SWE_AGENT_LOG_STREAM_LEVEL"))
|
|
32
|
+
_INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER = False
|
|
33
|
+
|
|
34
|
+
_THREAD_NAME_TO_LOG_SUFFIX: dict[str, str] = {}
|
|
35
|
+
"""Mapping from thread name to suffix to add to the logger name."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def register_thread_name(name: str) -> None:
|
|
39
|
+
"""Register a suffix to add to the logger name for the current thread."""
|
|
40
|
+
thread_name = threading.current_thread().name
|
|
41
|
+
_THREAD_NAME_TO_LOG_SUFFIX[thread_name] = name
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _RichHandlerWithEmoji(RichHandler):
|
|
45
|
+
def __init__(self, emoji: str, *args, **kwargs):
|
|
46
|
+
"""Subclass of RichHandler that adds an emoji to the log message."""
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
if not emoji.endswith(" "):
|
|
49
|
+
emoji += " "
|
|
50
|
+
self.emoji = emoji
|
|
51
|
+
|
|
52
|
+
def get_level_text(self, record: logging.LogRecord) -> Text:
|
|
53
|
+
level_name = record.levelname.replace("WARNING", "WARN")
|
|
54
|
+
return Text.styled((self.emoji + level_name).ljust(10), f"logging.level.{level_name.lower()}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_logger(name: str, *, emoji: str = "") -> logging.Logger:
|
|
58
|
+
"""Get logger. Use this instead of `logging.getLogger` to ensure
|
|
59
|
+
that the logger is set up with the correct handlers.
|
|
60
|
+
"""
|
|
61
|
+
thread_name = threading.current_thread().name
|
|
62
|
+
if thread_name != "MainThread":
|
|
63
|
+
name = name + "-" + _THREAD_NAME_TO_LOG_SUFFIX.get(thread_name, thread_name)
|
|
64
|
+
logger = logging.getLogger(name)
|
|
65
|
+
if logger.hasHandlers():
|
|
66
|
+
# Already set up
|
|
67
|
+
return logger
|
|
68
|
+
handler = _RichHandlerWithEmoji(
|
|
69
|
+
emoji=emoji,
|
|
70
|
+
show_time=bool(os.environ.get("SWE_AGENT_LOG_TIME", False)),
|
|
71
|
+
show_path=False,
|
|
72
|
+
)
|
|
73
|
+
handler.setLevel(_STREAM_LEVEL)
|
|
74
|
+
# Set to lowest level and only use stream handlers to adjust levels
|
|
75
|
+
logger.setLevel(logging.TRACE) # type: ignore
|
|
76
|
+
logger.addHandler(handler)
|
|
77
|
+
logger.propagate = False
|
|
78
|
+
_SET_UP_LOGGERS.add(name)
|
|
79
|
+
with _LOG_LOCK:
|
|
80
|
+
for handler in _ADDITIONAL_HANDLERS.values():
|
|
81
|
+
my_filter = getattr(handler, "my_filter", None)
|
|
82
|
+
if my_filter is None:
|
|
83
|
+
logger.addHandler(handler)
|
|
84
|
+
elif isinstance(my_filter, str) and my_filter in name:
|
|
85
|
+
logger.addHandler(handler)
|
|
86
|
+
elif callable(my_filter) and my_filter(name):
|
|
87
|
+
logger.addHandler(handler)
|
|
88
|
+
if _INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER:
|
|
89
|
+
_add_logger_name_to_stream_handler(logger)
|
|
90
|
+
return logger
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def add_file_handler(
|
|
94
|
+
path: PurePath | str,
|
|
95
|
+
*,
|
|
96
|
+
filter: str | Callable[[str], bool] | None = None,
|
|
97
|
+
level: int | str = logging.TRACE, # type: ignore[attr-defined]
|
|
98
|
+
id_: str = "",
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Adds a file handler to all loggers that we have set up
|
|
101
|
+
and all future loggers that will be set up with `get_logger`.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
filter: If str: Check that the logger name contains the filter string.
|
|
105
|
+
If callable: Check that the logger name satisfies the condition returned by the callable.
|
|
106
|
+
level: The level of the handler.
|
|
107
|
+
id_: The id of the handler. If not provided, a random id will be generated.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The id of the handler. This can be used to remove the handler later.
|
|
111
|
+
"""
|
|
112
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
handler = logging.FileHandler(path, encoding="utf-8")
|
|
114
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
|
|
115
|
+
handler.setFormatter(formatter)
|
|
116
|
+
handler.setLevel(_interpret_level(level))
|
|
117
|
+
with _LOG_LOCK:
|
|
118
|
+
# Lock because other thread might be modifying the _SET_UP_LOGGERS set
|
|
119
|
+
for name in _SET_UP_LOGGERS:
|
|
120
|
+
if filter is not None:
|
|
121
|
+
if isinstance(filter, str) and filter not in name:
|
|
122
|
+
continue
|
|
123
|
+
if callable(filter) and not filter(name):
|
|
124
|
+
continue
|
|
125
|
+
logger = logging.getLogger(name)
|
|
126
|
+
logger.addHandler(handler)
|
|
127
|
+
handler.my_filter = filter # type: ignore
|
|
128
|
+
if not id_:
|
|
129
|
+
id_ = str(uuid.uuid4())
|
|
130
|
+
_ADDITIONAL_HANDLERS[id_] = handler
|
|
131
|
+
return id_
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def remove_file_handler(id_: str) -> None:
|
|
135
|
+
"""Remove a file handler by its id."""
|
|
136
|
+
handler = _ADDITIONAL_HANDLERS.pop(id_)
|
|
137
|
+
with _LOG_LOCK:
|
|
138
|
+
# Lock because other thread might be modifying the _SET_UP_LOGGERS set
|
|
139
|
+
for log_name in _SET_UP_LOGGERS:
|
|
140
|
+
logger = logging.getLogger(log_name)
|
|
141
|
+
logger.removeHandler(handler)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _add_logger_name_to_stream_handler(logger: logging.Logger) -> None:
|
|
145
|
+
for handler in logger.handlers:
|
|
146
|
+
if isinstance(handler, _RichHandlerWithEmoji):
|
|
147
|
+
formatter = logging.Formatter("[%(name)s] %(message)s")
|
|
148
|
+
handler.setFormatter(formatter)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add_logger_names_to_stream_handlers() -> None:
|
|
152
|
+
"""Add the logger name to the stream handler for all loggers that we have set up."""
|
|
153
|
+
global _INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER
|
|
154
|
+
_INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER = True
|
|
155
|
+
with _LOG_LOCK:
|
|
156
|
+
for logger in _SET_UP_LOGGERS:
|
|
157
|
+
_add_logger_name_to_stream_handler(logging.getLogger(logger))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def set_stream_handler_levels(level: int) -> None:
|
|
161
|
+
"""Set the default stream level and adjust the levels of all stream handlers
|
|
162
|
+
to be at most the given level.
|
|
163
|
+
|
|
164
|
+
Note: Can only be used to lower the level, not raise it.
|
|
165
|
+
"""
|
|
166
|
+
global _STREAM_LEVEL
|
|
167
|
+
_STREAM_LEVEL = level
|
|
168
|
+
with _LOG_LOCK:
|
|
169
|
+
for name in _SET_UP_LOGGERS:
|
|
170
|
+
logger = logging.getLogger(name)
|
|
171
|
+
for handler in logger.handlers:
|
|
172
|
+
if isinstance(handler, _RichHandlerWithEmoji):
|
|
173
|
+
current_level = handler.level
|
|
174
|
+
if current_level < level:
|
|
175
|
+
handler.setLevel(level)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from unidiff import PatchSet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PatchFormatter:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
patch: str,
|
|
10
|
+
read_method: Callable[[str], str],
|
|
11
|
+
):
|
|
12
|
+
"""Given the final patch and access to the container that contains the repository,
|
|
13
|
+
extract relevant lines from the modified file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
patch: The patch as a string.
|
|
17
|
+
read_method: Callable with path to file (relative to repository root) as argument
|
|
18
|
+
that returns the file content as a string.
|
|
19
|
+
"""
|
|
20
|
+
self._patch = PatchSet(patch)
|
|
21
|
+
self._patched_files: dict[str, str] = {}
|
|
22
|
+
self._original_files: dict[str, str] = {}
|
|
23
|
+
self._patch_applied = True
|
|
24
|
+
self._read_file = read_method
|
|
25
|
+
self._read_files(original=False)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _merge_intervals(starts: list[int], stops: list[int]) -> tuple[list[int], list[int]]:
|
|
29
|
+
"""Given two lists of integers, starts and stops, merges all overlapping intervals.
|
|
30
|
+
|
|
31
|
+
For example `starts=[1, 5, 18]`, `stops=[10, 13, 20]`
|
|
32
|
+
should return `starts=[1, 18]`, `stops=[13, 20]`
|
|
33
|
+
"""
|
|
34
|
+
if not starts:
|
|
35
|
+
assert not stops
|
|
36
|
+
return [], []
|
|
37
|
+
|
|
38
|
+
intervals = sorted(zip(starts, stops))
|
|
39
|
+
merged = []
|
|
40
|
+
for start, stop in intervals:
|
|
41
|
+
if not merged or merged[-1][1] < start:
|
|
42
|
+
# No overlap
|
|
43
|
+
merged.append([start, stop])
|
|
44
|
+
else:
|
|
45
|
+
# Overlap
|
|
46
|
+
merged[-1][1] = max(merged[-1][1], stop)
|
|
47
|
+
# Unzip again
|
|
48
|
+
merged_starts, merged_stops = zip(*merged)
|
|
49
|
+
return list(merged_starts), list(merged_stops)
|
|
50
|
+
|
|
51
|
+
def format_file(self, text: str, starts: list[int], stops: list[int], *, linenos: bool = True) -> str:
|
|
52
|
+
"""Reads file and returns string representation of the relevant lines.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
path: The path to the file within the repo location
|
|
56
|
+
starts: The starting line numbers of the relevant lines. The first line is line 1.
|
|
57
|
+
stops: The stopping line numbers of the relevant lines. The stop is not inclusive.
|
|
58
|
+
The first line is line 1.
|
|
59
|
+
linenos: Whether to include line numbers
|
|
60
|
+
"""
|
|
61
|
+
if not starts:
|
|
62
|
+
assert not stops
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
assert len(starts) == len(stops)
|
|
66
|
+
assert all(start >= 1 for start in starts)
|
|
67
|
+
assert all(start < stop for start, stop in zip(starts, stops))
|
|
68
|
+
starts, stops = self._merge_intervals(starts, stops)
|
|
69
|
+
assert all(hunk1_start < hunk2_start for hunk1_start, hunk2_start in zip(starts, starts[1:]))
|
|
70
|
+
out: list[str] = []
|
|
71
|
+
if starts[0] > 1:
|
|
72
|
+
# Count from 1
|
|
73
|
+
out.append(f"[{starts[0] - 1} lines above omitted]")
|
|
74
|
+
last_stop: int | None = None
|
|
75
|
+
lines = text.splitlines()
|
|
76
|
+
for start, stop in zip(starts, stops):
|
|
77
|
+
assert start >= 1
|
|
78
|
+
if last_stop is not None:
|
|
79
|
+
n_omitted = start - last_stop
|
|
80
|
+
# Check that we have non-overlapping hunks
|
|
81
|
+
assert n_omitted >= 0
|
|
82
|
+
if n_omitted:
|
|
83
|
+
out.append(f"\n[{n_omitted} lines omitted]\n")
|
|
84
|
+
# Count from 1
|
|
85
|
+
these_lines = lines[start - 1 : stop - 1]
|
|
86
|
+
if linenos:
|
|
87
|
+
out.append("\n".join([f"{i:6d}: {l}" for i, l in enumerate(these_lines, start=start)]))
|
|
88
|
+
else:
|
|
89
|
+
out.append("\n".join(these_lines))
|
|
90
|
+
last_stop = stop
|
|
91
|
+
if last_stop < len(lines):
|
|
92
|
+
# Stop is not inclusive
|
|
93
|
+
omitted = len(lines) - last_stop
|
|
94
|
+
assert omitted > 0
|
|
95
|
+
out.append(f"[{omitted} lines below omitted]")
|
|
96
|
+
return "\n".join(out)
|
|
97
|
+
|
|
98
|
+
def _get_hunk_lines(self, original: bool, *, context_length: int) -> dict[str, tuple[list[int], list[int]]]:
|
|
99
|
+
"""Get the starts and stops for all files in the patch.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
original: Whether to read the original file or the patched file
|
|
103
|
+
context_length: The number of lines to include above and below the hunk
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A dictionary with the file path as key and a tuple of lists of starts and stops as value.
|
|
107
|
+
"""
|
|
108
|
+
out: dict[str, tuple[list[int], list[int]]] = {}
|
|
109
|
+
for patch in self._patch:
|
|
110
|
+
if not patch.is_modified_file:
|
|
111
|
+
continue
|
|
112
|
+
starts: list[int] = []
|
|
113
|
+
stops: list[int] = []
|
|
114
|
+
for hunk in patch:
|
|
115
|
+
if original:
|
|
116
|
+
# 1 is the lowest line number
|
|
117
|
+
start = max(1, hunk.source_start - context_length)
|
|
118
|
+
stop = hunk.source_start + hunk.source_length + context_length
|
|
119
|
+
else:
|
|
120
|
+
start = max(1, hunk.target_start - context_length)
|
|
121
|
+
stop = hunk.target_start + hunk.target_length + context_length
|
|
122
|
+
starts.append(start)
|
|
123
|
+
stops.append(stop)
|
|
124
|
+
out[patch.path] = (starts, stops)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
def _read_files(self, original: bool) -> None:
|
|
128
|
+
for patch in self._patch:
|
|
129
|
+
path = patch.path
|
|
130
|
+
if not patch.is_modified_file:
|
|
131
|
+
continue
|
|
132
|
+
if original:
|
|
133
|
+
msg = "Original file reading not implemented"
|
|
134
|
+
raise NotImplementedError(msg)
|
|
135
|
+
else:
|
|
136
|
+
assert self._patch_applied
|
|
137
|
+
self._patched_files[path] = self._read_file(path)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def concat_files_strings(files: dict[str, str]) -> str:
|
|
141
|
+
"""Concatenate multiple `read_files` outputs into a single string."""
|
|
142
|
+
out = []
|
|
143
|
+
for path, content in files.items():
|
|
144
|
+
out.append(f"[File: {path}]\n{content}")
|
|
145
|
+
return "\n\n".join(out)
|
|
146
|
+
|
|
147
|
+
def get_files_str(self, *, original: bool, context_length: int | None = 50, linenos: bool = True) -> str:
|
|
148
|
+
hunk_lines = self._get_hunk_lines(original=original, context_length=context_length)
|
|
149
|
+
sources = self._original_files if original else self._patched_files
|
|
150
|
+
return self.concat_files_strings(
|
|
151
|
+
{path: self.format_file(text, *hunk_lines[path], linenos=linenos) for path, text in sources.items()}
|
|
152
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ruamel.yaml import YAML
|
|
6
|
+
from ruamel.yaml.scalarstring import LiteralScalarString as LSS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _convert_to_yaml_literal_string(d: Any) -> Any:
|
|
10
|
+
"""Convert any multi-line strings in nested data object to LiteralScalarString.
|
|
11
|
+
This will then use the `|-` syntax of yaml.
|
|
12
|
+
"""
|
|
13
|
+
d = deepcopy(d)
|
|
14
|
+
if isinstance(d, dict):
|
|
15
|
+
for key, value in d.items():
|
|
16
|
+
d[key] = _convert_to_yaml_literal_string(value)
|
|
17
|
+
elif isinstance(d, list):
|
|
18
|
+
for i, item in enumerate(d):
|
|
19
|
+
d[i] = _convert_to_yaml_literal_string(item)
|
|
20
|
+
elif isinstance(d, str) and "\n" in d:
|
|
21
|
+
d = LSS(d.replace("\r\n", "\n").replace("\r", "\n"))
|
|
22
|
+
return d
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _yaml_serialization_with_linebreaks(data: Any) -> str:
|
|
26
|
+
data = _convert_to_yaml_literal_string(data)
|
|
27
|
+
yaml = YAML()
|
|
28
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
29
|
+
yaml.width = float("inf")
|
|
30
|
+
yaml.default_flow_style = False
|
|
31
|
+
buffer = io.StringIO()
|
|
32
|
+
yaml.dump(data, buffer)
|
|
33
|
+
return buffer.getvalue()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def merge_nested_dicts(d1: dict, d2: dict) -> dict:
|
|
37
|
+
"""Merge two nested dictionaries, updating d1 in place.
|
|
38
|
+
If a key exists in both dictionaries, the value from d2 will be used.
|
|
39
|
+
"""
|
|
40
|
+
for key, value in d2.items():
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
d1[key] = merge_nested_dicts(d1.get(key, {}), value)
|
|
43
|
+
else:
|
|
44
|
+
d1[key] = value
|
|
45
|
+
return d1
|
|
File without changes
|