@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,710 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""This is an adaptation of the Anthropic Text Editor tool from
|
|
4
|
+
https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/edit.py
|
|
5
|
+
However, we made it python 3.6 compatible and stateless (all state is saved in a json file)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Optional, Tuple
|
|
16
|
+
import io
|
|
17
|
+
|
|
18
|
+
from registry import registry as REGISTRY
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# There are some super strange "ascii can't decode x" errors,
|
|
22
|
+
# that can be solved with setting the default encoding for stdout
|
|
23
|
+
# (note that python3.6 doesn't have the reconfigure method)
|
|
24
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
TRUNCATED_MESSAGE: str = "<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>"
|
|
27
|
+
MAX_RESPONSE_LEN: int = 16000
|
|
28
|
+
|
|
29
|
+
MAX_WINDOW_EXPANSION_VIEW = int(REGISTRY.get("MAX_WINDOW_EXPANSION_VIEW", 0))
|
|
30
|
+
MAX_WINDOW_EXPANSION_EDIT_CONFIRM = int(REGISTRY.get("MAX_WINDOW_EXPANSION_EDIT_CONFIRM", 0))
|
|
31
|
+
USE_FILEMAP = REGISTRY.get("USE_FILEMAP", "false").lower() == "true"
|
|
32
|
+
USE_LINTER = REGISTRY.get("USE_LINTER", "false").lower() == "true"
|
|
33
|
+
Command = str
|
|
34
|
+
SNIPPET_LINES: int = 4
|
|
35
|
+
LINT_WARNING_TEMPLATE = """
|
|
36
|
+
|
|
37
|
+
<NOTE>Your edits have been applied, but the linter has found syntax errors.</NOTE>
|
|
38
|
+
|
|
39
|
+
<ERRORS>
|
|
40
|
+
{errors}
|
|
41
|
+
</ERRORS>
|
|
42
|
+
|
|
43
|
+
Please review the changes and make sure they are correct.
|
|
44
|
+
In addition to the above errors, please also check the following:
|
|
45
|
+
|
|
46
|
+
1. The edited file is correctly indented
|
|
47
|
+
2. The edited file does not contain duplicate lines
|
|
48
|
+
3. The edit does not break existing functionality
|
|
49
|
+
|
|
50
|
+
<IMPORTANT>In rare cases, the linter errors might not actually be errors or caused by your edit. Please use your own judgement.</IMPORTANT>
|
|
51
|
+
|
|
52
|
+
Edit the file again if necessary.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def maybe_truncate(content: str, truncate_after: Optional[int] = MAX_RESPONSE_LEN):
|
|
57
|
+
"""Truncate content and append a notice if content exceeds the specified length."""
|
|
58
|
+
return (
|
|
59
|
+
content
|
|
60
|
+
if not truncate_after or len(content) <= truncate_after
|
|
61
|
+
else content[:truncate_after] + TRUNCATED_MESSAGE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Flake8Error:
|
|
66
|
+
"""A class to represent a single flake8 error"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, filename: str, line_number: int, col_number: int, problem: str):
|
|
69
|
+
self.filename = filename
|
|
70
|
+
self.line_number = line_number
|
|
71
|
+
self.col_number = col_number
|
|
72
|
+
self.problem = problem
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_line(cls, line: str):
|
|
76
|
+
try:
|
|
77
|
+
prefix, _sep, problem = line.partition(": ")
|
|
78
|
+
filename, line_number, col_number = prefix.split(":")
|
|
79
|
+
except (ValueError, IndexError) as e:
|
|
80
|
+
msg = f"Invalid flake8 error line: {line}"
|
|
81
|
+
raise ValueError(msg) from e
|
|
82
|
+
return cls(filename, int(line_number), int(col_number), problem)
|
|
83
|
+
|
|
84
|
+
def __eq__(self, other):
|
|
85
|
+
if not isinstance(other, Flake8Error):
|
|
86
|
+
return NotImplemented
|
|
87
|
+
return (
|
|
88
|
+
self.filename == other.filename
|
|
89
|
+
and self.line_number == other.line_number
|
|
90
|
+
and self.col_number == other.col_number
|
|
91
|
+
and self.problem == other.problem
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
return f"Flake8Error(filename={self.filename}, line_number={self.line_number}, col_number={self.col_number}, problem={self.problem})"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _update_previous_errors(
|
|
99
|
+
previous_errors: List[Flake8Error], replacement_window: Tuple[int, int], replacement_n_lines: int
|
|
100
|
+
) -> List[Flake8Error]:
|
|
101
|
+
"""Update the line numbers of the previous errors to what they would be after the edit window.
|
|
102
|
+
This is a helper function for `_filter_previous_errors`.
|
|
103
|
+
|
|
104
|
+
All previous errors that are inside of the edit window should not be ignored,
|
|
105
|
+
so they are removed from the previous errors list.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
previous_errors: list of errors with old line numbers
|
|
109
|
+
replacement_window: the window of the edit/lines that will be replaced
|
|
110
|
+
replacement_n_lines: the number of lines that will be used to replace the text
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
list of errors with updated line numbers
|
|
114
|
+
"""
|
|
115
|
+
updated = []
|
|
116
|
+
lines_added = replacement_n_lines - (replacement_window[1] - replacement_window[0] + 1)
|
|
117
|
+
for error in previous_errors:
|
|
118
|
+
if error.line_number < replacement_window[0]:
|
|
119
|
+
# no need to adjust the line number
|
|
120
|
+
updated.append(error)
|
|
121
|
+
continue
|
|
122
|
+
if replacement_window[0] <= error.line_number <= replacement_window[1]:
|
|
123
|
+
# The error is within the edit window, so let's not ignore it
|
|
124
|
+
# either way (we wouldn't know how to adjust the line number anyway)
|
|
125
|
+
continue
|
|
126
|
+
# We're out of the edit window, so we need to adjust the line number
|
|
127
|
+
updated.append(Flake8Error(error.filename, error.line_number + lines_added, error.col_number, error.problem))
|
|
128
|
+
return updated
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def format_flake8_output(
|
|
132
|
+
input_string: str,
|
|
133
|
+
show_line_numbers: bool = False,
|
|
134
|
+
*,
|
|
135
|
+
previous_errors_string: str = "",
|
|
136
|
+
replacement_window: Optional[Tuple[int, int]] = None,
|
|
137
|
+
replacement_n_lines: Optional[int] = None,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Filter flake8 output for previous errors and print it for a given file.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
input_string: The flake8 output as a string
|
|
143
|
+
show_line_numbers: Whether to show line numbers in the output
|
|
144
|
+
previous_errors_string: The previous errors as a string
|
|
145
|
+
replacement_window: The window of the edit (lines that will be replaced)
|
|
146
|
+
replacement_n_lines: The number of lines used to replace the text
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The filtered flake8 output as a string
|
|
150
|
+
"""
|
|
151
|
+
# print(f"Replacement window: {replacement_window}")
|
|
152
|
+
# print("Replacement n lines:", replacement_n_lines)
|
|
153
|
+
# print("Previous errors string:", previous_errors_string)
|
|
154
|
+
# print("Input string:", input_string)
|
|
155
|
+
errors = [Flake8Error.from_line(line.strip()) for line in input_string.split("\n") if line.strip()]
|
|
156
|
+
# print(f"New errors before filtering: {errors=}")
|
|
157
|
+
lines = []
|
|
158
|
+
if previous_errors_string:
|
|
159
|
+
assert replacement_window is not None
|
|
160
|
+
assert replacement_n_lines is not None
|
|
161
|
+
previous_errors = [
|
|
162
|
+
Flake8Error.from_line(line.strip()) for line in previous_errors_string.split("\n") if line.strip()
|
|
163
|
+
]
|
|
164
|
+
# print(f"Previous errors before updating: {previous_errors=}")
|
|
165
|
+
previous_errors = _update_previous_errors(previous_errors, replacement_window, replacement_n_lines)
|
|
166
|
+
# print(f"Previous errors after updating: {previous_errors=}")
|
|
167
|
+
errors = [error for error in errors if error not in previous_errors]
|
|
168
|
+
# Sometimes new errors appear above the replacement window that were 'shadowed' by the previous errors
|
|
169
|
+
# they still clearly aren't caused by the edit.
|
|
170
|
+
errors = [error for error in errors if error.line_number >= replacement_window[0]]
|
|
171
|
+
# print(f"New errors after filtering: {errors=}")
|
|
172
|
+
for error in errors:
|
|
173
|
+
if not show_line_numbers:
|
|
174
|
+
lines.append(f"- {error.problem}")
|
|
175
|
+
else:
|
|
176
|
+
lines.append(f"- line {error.line_number} col {error.col_number}: {error.problem}")
|
|
177
|
+
return "\n".join(lines)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def flake8(file_path: str) -> str:
|
|
181
|
+
"""Run flake8 on a given file and return the output as a string"""
|
|
182
|
+
if Path(file_path).suffix != ".py":
|
|
183
|
+
return ""
|
|
184
|
+
cmd = REGISTRY.get("LINT_COMMAND", "flake8 --isolated --select=F821,F822,F831,E111,E112,E113,E999,E902 {file_path}")
|
|
185
|
+
# don't use capture_output because it's not compatible with python3.6
|
|
186
|
+
out = subprocess.run(cmd.format(file_path=file_path), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
187
|
+
return out.stdout.decode()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Filemap:
|
|
191
|
+
def show_filemap(self, file_contents: str, encoding: str = "utf8"):
|
|
192
|
+
import warnings
|
|
193
|
+
from tree_sitter_languages import get_language, get_parser
|
|
194
|
+
|
|
195
|
+
warnings.simplefilter("ignore", category=FutureWarning)
|
|
196
|
+
|
|
197
|
+
parser = get_parser("python")
|
|
198
|
+
language = get_language("python")
|
|
199
|
+
|
|
200
|
+
tree = parser.parse(bytes(file_contents.encode(encoding, errors="replace")))
|
|
201
|
+
|
|
202
|
+
# See https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries.
|
|
203
|
+
query = language.query("""
|
|
204
|
+
(function_definition
|
|
205
|
+
body: (_) @body)
|
|
206
|
+
""")
|
|
207
|
+
|
|
208
|
+
# TODO: consider special casing docstrings such that they are not elided. This
|
|
209
|
+
# could be accomplished by checking whether `body.text.decode('utf8')` starts
|
|
210
|
+
# with `"""` or `'''`.
|
|
211
|
+
elide_line_ranges = [
|
|
212
|
+
(node.start_point[0], node.end_point[0])
|
|
213
|
+
for node, _ in query.captures(tree.root_node)
|
|
214
|
+
# Only elide if it's sufficiently long
|
|
215
|
+
if node.end_point[0] - node.start_point[0] >= 5
|
|
216
|
+
]
|
|
217
|
+
# Note that tree-sitter line numbers are 0-indexed, but we display 1-indexed.
|
|
218
|
+
elide_lines = {line for start, end in elide_line_ranges for line in range(start, end + 1)}
|
|
219
|
+
elide_messages = [(start, f"... eliding lines {start+1}-{end+1} ...") for start, end in elide_line_ranges]
|
|
220
|
+
out = []
|
|
221
|
+
for i, line in sorted(
|
|
222
|
+
elide_messages + [(i, line) for i, line in enumerate(file_contents.splitlines()) if i not in elide_lines]
|
|
223
|
+
):
|
|
224
|
+
out.append(f"{i+1:6d} {line}")
|
|
225
|
+
return "\n".join(out)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class WindowExpander:
|
|
229
|
+
def __init__(self, suffix: str = ""):
|
|
230
|
+
"""Try to expand viewports to include whole functions, classes, etc. rather than
|
|
231
|
+
using fixed line windows.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
suffix: Filename suffix
|
|
235
|
+
"""
|
|
236
|
+
self.suffix = suffix
|
|
237
|
+
if self.suffix:
|
|
238
|
+
assert self.suffix.startswith(".")
|
|
239
|
+
|
|
240
|
+
def _find_breakpoints(self, lines: List[str], current_line: int, direction=1, max_added_lines: int = 30) -> int:
|
|
241
|
+
"""Returns 1-based line number of breakpoint. This line is meant to still be included in the viewport.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
lines: List of lines of the file
|
|
245
|
+
current_line: 1-based line number of the current viewport
|
|
246
|
+
direction: 1 for down, -1 for up
|
|
247
|
+
max_added_lines: Maximum number of lines to extend
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
1-based line number of breakpoint. This line is meant to still be included in the viewport.
|
|
251
|
+
"""
|
|
252
|
+
assert 1 <= current_line <= len(lines)
|
|
253
|
+
assert 0 <= max_added_lines
|
|
254
|
+
|
|
255
|
+
# 1. Find line range that we want to search for breakpoints in
|
|
256
|
+
|
|
257
|
+
if direction == 1:
|
|
258
|
+
# down
|
|
259
|
+
if current_line == len(lines):
|
|
260
|
+
# already last line, can't extend down
|
|
261
|
+
return current_line
|
|
262
|
+
iter_lines = range(current_line, 1 + min(current_line + max_added_lines, len(lines)))
|
|
263
|
+
elif direction == -1:
|
|
264
|
+
# up
|
|
265
|
+
if current_line == 1:
|
|
266
|
+
# already first line, can't extend up
|
|
267
|
+
return current_line
|
|
268
|
+
iter_lines = range(current_line, -1 + max(current_line - max_added_lines, 1), -1)
|
|
269
|
+
else:
|
|
270
|
+
msg = f"Invalid direction {direction}"
|
|
271
|
+
raise ValueError(msg)
|
|
272
|
+
|
|
273
|
+
# 2. Find the best breakpoint in the line range
|
|
274
|
+
|
|
275
|
+
# Every condition gives a score, the best score is the best breakpoint
|
|
276
|
+
best_score = 0
|
|
277
|
+
best_breakpoint = current_line
|
|
278
|
+
for i_line in iter_lines:
|
|
279
|
+
next_line = None
|
|
280
|
+
line = lines[i_line - 1]
|
|
281
|
+
if i_line + direction in iter_lines:
|
|
282
|
+
next_line = lines[i_line + direction - 1]
|
|
283
|
+
score = 0
|
|
284
|
+
if line == "":
|
|
285
|
+
score = 1
|
|
286
|
+
if next_line == "":
|
|
287
|
+
# Double new blank line:
|
|
288
|
+
score = 2
|
|
289
|
+
if self.suffix == ".py" and any(
|
|
290
|
+
re.match(regex, line) for regex in [r"^\s*def\s+", r"^\s*class\s+", r"^\s*@"]
|
|
291
|
+
):
|
|
292
|
+
# We include decorators here, because they are always on top of the function/class definition
|
|
293
|
+
score = 3
|
|
294
|
+
if score > best_score:
|
|
295
|
+
best_score = score
|
|
296
|
+
best_breakpoint = i_line
|
|
297
|
+
if direction == 1 and i_line != current_line:
|
|
298
|
+
best_breakpoint -= 1
|
|
299
|
+
if i_line == 1 or i_line == len(lines):
|
|
300
|
+
score = 3
|
|
301
|
+
if score > best_score:
|
|
302
|
+
best_score = score
|
|
303
|
+
best_breakpoint = i_line
|
|
304
|
+
# print(f"Score {score} for line {i_line} ({line})")
|
|
305
|
+
|
|
306
|
+
# print(f"Best score {best_score} for line {best_breakpoint} ({lines[best_breakpoint-1]})")
|
|
307
|
+
if direction == 1 and best_breakpoint < current_line or direction == -1 and best_breakpoint > current_line:
|
|
308
|
+
# We don't want to shrink the view port, so we return the current line
|
|
309
|
+
return current_line
|
|
310
|
+
|
|
311
|
+
return best_breakpoint
|
|
312
|
+
|
|
313
|
+
def expand_window(self, lines: List[str], start: int, stop: int, max_added_lines: int) -> Tuple[int, int]:
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
lines: All lines of the file
|
|
318
|
+
start: 1-based line number of the start of the viewport
|
|
319
|
+
stop: 1-based line number of the end of the viewport
|
|
320
|
+
max_added_lines: Maximum number of lines to extend (separately for each side)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Tuple of 1-based line numbers of the start and end of the viewport.
|
|
324
|
+
Both inclusive.
|
|
325
|
+
"""
|
|
326
|
+
# print("Input:", start, stop)
|
|
327
|
+
assert 1 <= start <= stop <= len(lines), (start, stop, len(lines))
|
|
328
|
+
if max_added_lines <= 0:
|
|
329
|
+
# Already at max range, no expansion
|
|
330
|
+
return start, stop
|
|
331
|
+
new_start = self._find_breakpoints(lines, start, direction=-1, max_added_lines=max_added_lines)
|
|
332
|
+
new_stop = self._find_breakpoints(lines, stop, direction=1, max_added_lines=max_added_lines)
|
|
333
|
+
# print(f"Expanded window is {new_start} to {new_stop}")
|
|
334
|
+
assert new_start <= new_stop, (new_start, new_stop)
|
|
335
|
+
assert new_start <= start, (new_start, start)
|
|
336
|
+
assert start - new_start <= max_added_lines, (start, new_start)
|
|
337
|
+
assert new_stop >= stop, (new_stop, stop)
|
|
338
|
+
assert new_stop - stop <= max_added_lines, (new_stop, stop)
|
|
339
|
+
return new_start, new_stop
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class EditTool:
|
|
343
|
+
"""
|
|
344
|
+
An filesystem editor tool that allows the agent to view, create, and edit files.
|
|
345
|
+
The tool parameters are defined by Anthropic and are not editable.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
name = "str_replace_editor"
|
|
349
|
+
|
|
350
|
+
def __init__(self):
|
|
351
|
+
super().__init__()
|
|
352
|
+
self._encoding = None
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def _file_history(self):
|
|
356
|
+
return defaultdict(list, json.loads(REGISTRY.get("file_history", "{}")))
|
|
357
|
+
|
|
358
|
+
@_file_history.setter
|
|
359
|
+
def _file_history(self, value: dict):
|
|
360
|
+
REGISTRY["file_history"] = json.dumps(value)
|
|
361
|
+
|
|
362
|
+
def __call__(
|
|
363
|
+
self,
|
|
364
|
+
*,
|
|
365
|
+
command: Command,
|
|
366
|
+
path: str,
|
|
367
|
+
file_text: Optional[str] = None,
|
|
368
|
+
view_range: Optional[List[int]] = None,
|
|
369
|
+
old_str: Optional[str] = None,
|
|
370
|
+
new_str: Optional[str] = None,
|
|
371
|
+
insert_line: Optional[int] = None,
|
|
372
|
+
**kwargs,
|
|
373
|
+
):
|
|
374
|
+
_path = Path(path)
|
|
375
|
+
self.validate_path(command, _path)
|
|
376
|
+
if command == "view":
|
|
377
|
+
return self.view(_path, view_range)
|
|
378
|
+
elif command == "create":
|
|
379
|
+
if file_text is None:
|
|
380
|
+
print("Parameter `file_text` is required for command: create")
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
self.create_file(_path, file_text)
|
|
383
|
+
return None
|
|
384
|
+
elif command == "str_replace":
|
|
385
|
+
if old_str is None:
|
|
386
|
+
print("Parameter `old_str` is required for command: str_replace")
|
|
387
|
+
sys.exit(2)
|
|
388
|
+
return self.str_replace(_path, old_str, new_str)
|
|
389
|
+
elif command == "insert":
|
|
390
|
+
if insert_line is None:
|
|
391
|
+
print("Parameter `insert_line` is required for command: insert")
|
|
392
|
+
sys.exit(3)
|
|
393
|
+
if new_str is None:
|
|
394
|
+
print("Parameter `new_str` is required for command: insert")
|
|
395
|
+
sys.exit(4)
|
|
396
|
+
return self.insert(_path, insert_line, new_str)
|
|
397
|
+
elif command == "undo_edit":
|
|
398
|
+
return self.undo_edit(_path)
|
|
399
|
+
print(
|
|
400
|
+
f'Unrecognized command {command}. The allowed commands for the {self.name} tool are: "view", "create", "str_replace", "insert", "undo_edit"'
|
|
401
|
+
)
|
|
402
|
+
sys.exit(5)
|
|
403
|
+
|
|
404
|
+
def validate_path(self, command: str, path: Path):
|
|
405
|
+
"""
|
|
406
|
+
Check that the path/command combination is valid.
|
|
407
|
+
"""
|
|
408
|
+
# Check if its an absolute path
|
|
409
|
+
if not path.is_absolute():
|
|
410
|
+
suggested_path = Path.cwd() / path
|
|
411
|
+
print(
|
|
412
|
+
f"The path {path} is not an absolute path, it should start with `/`. Maybe you meant {suggested_path}?"
|
|
413
|
+
)
|
|
414
|
+
sys.exit(6)
|
|
415
|
+
# Check if path exists
|
|
416
|
+
if not path.exists() and command != "create":
|
|
417
|
+
print(f"The path {path} does not exist. Please provide a valid path.")
|
|
418
|
+
sys.exit(7)
|
|
419
|
+
if path.exists() and command == "create":
|
|
420
|
+
print(f"File already exists at: {path}. Cannot overwrite files using command `create`.")
|
|
421
|
+
sys.exit(8)
|
|
422
|
+
# Check if the path points to a directory
|
|
423
|
+
if path.is_dir():
|
|
424
|
+
if command != "view":
|
|
425
|
+
print(f"The path {path} is a directory and only the `view` command can be used on directories")
|
|
426
|
+
sys.exit(9)
|
|
427
|
+
|
|
428
|
+
def create_file(self, path: Path, file_text: str):
|
|
429
|
+
if not path.parent.exists():
|
|
430
|
+
print(f"The parent directory {path.parent} does not exist. Please create it first.")
|
|
431
|
+
sys.exit(21)
|
|
432
|
+
self.write_file(path, file_text)
|
|
433
|
+
self._file_history[path].append(file_text)
|
|
434
|
+
print(f"File created successfully at: {path}")
|
|
435
|
+
|
|
436
|
+
def view(self, path: Path, view_range: Optional[List[int]] = None):
|
|
437
|
+
"""Implement the view command"""
|
|
438
|
+
if path.is_dir():
|
|
439
|
+
if view_range:
|
|
440
|
+
print("The `view_range` parameter is not allowed when `path` points to a directory.")
|
|
441
|
+
sys.exit(10)
|
|
442
|
+
|
|
443
|
+
out = subprocess.run(
|
|
444
|
+
rf"find {path} -maxdepth 2 -not -path '*/\.*'",
|
|
445
|
+
shell=True,
|
|
446
|
+
stdout=subprocess.PIPE,
|
|
447
|
+
stderr=subprocess.PIPE,
|
|
448
|
+
)
|
|
449
|
+
stdout = out.stdout.decode()
|
|
450
|
+
stderr = out.stderr.decode()
|
|
451
|
+
|
|
452
|
+
if not stderr:
|
|
453
|
+
stdout = f"Here's the files and directories up to 2 levels deep in {path}, excluding hidden items:\n{stdout}\n"
|
|
454
|
+
print(stdout)
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
file_content = self.read_file(path)
|
|
458
|
+
if view_range:
|
|
459
|
+
if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range):
|
|
460
|
+
print("Invalid `view_range`. It should be a list of two integers.")
|
|
461
|
+
sys.exit(11)
|
|
462
|
+
file_lines = file_content.split("\n")
|
|
463
|
+
n_lines_file = len(file_lines)
|
|
464
|
+
init_line, final_line = view_range
|
|
465
|
+
if init_line < 1 or init_line > n_lines_file:
|
|
466
|
+
print(
|
|
467
|
+
f"Invalid `view_range`: {view_range}. Its first element `{init_line}` should be within the range of lines of the file: {[1, n_lines_file]}"
|
|
468
|
+
)
|
|
469
|
+
sys.exit(12)
|
|
470
|
+
if final_line > n_lines_file:
|
|
471
|
+
print(
|
|
472
|
+
f"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be smaller than the number of lines in the file: `{n_lines_file}`"
|
|
473
|
+
)
|
|
474
|
+
sys.exit(13)
|
|
475
|
+
if final_line != -1 and final_line < init_line:
|
|
476
|
+
print(
|
|
477
|
+
f"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be larger or equal than its first `{init_line}`"
|
|
478
|
+
)
|
|
479
|
+
sys.exit(14)
|
|
480
|
+
|
|
481
|
+
if final_line == -1:
|
|
482
|
+
final_line = n_lines_file
|
|
483
|
+
|
|
484
|
+
# Expand the viewport to include the whole function or class
|
|
485
|
+
init_line, final_line = WindowExpander(suffix=path.suffix).expand_window(
|
|
486
|
+
file_lines, init_line, final_line, max_added_lines=MAX_WINDOW_EXPANSION_VIEW
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
file_content = "\n".join(file_lines[init_line - 1 : final_line])
|
|
490
|
+
else:
|
|
491
|
+
if path.suffix == ".py" and len(file_content) > MAX_RESPONSE_LEN and USE_FILEMAP:
|
|
492
|
+
try:
|
|
493
|
+
filemap = Filemap().show_filemap(file_content, encoding=self._encoding or "utf-8")
|
|
494
|
+
except Exception:
|
|
495
|
+
# If we fail to show the filemap, just show the truncated file content
|
|
496
|
+
pass
|
|
497
|
+
else:
|
|
498
|
+
print(
|
|
499
|
+
"<NOTE>This file is too large to display entirely. Showing abbreviated version. "
|
|
500
|
+
"Please use `str_replace_editor view` with the `view_range` parameter to show selected lines next.</NOTE>"
|
|
501
|
+
)
|
|
502
|
+
filemap = maybe_truncate(filemap.expandtabs())
|
|
503
|
+
print(filemap)
|
|
504
|
+
print(
|
|
505
|
+
"<IMPORTANT><NOTE>The above file has been abbreviated. Please use `str_replace editor view` with `view_range` to look at relevant files in detail.</NOTE></IMPORTANT>"
|
|
506
|
+
)
|
|
507
|
+
return
|
|
508
|
+
# Else just show
|
|
509
|
+
init_line = 1
|
|
510
|
+
|
|
511
|
+
# init_line is 1-based
|
|
512
|
+
print(self._make_output(file_content, str(path), init_line=init_line))
|
|
513
|
+
|
|
514
|
+
def str_replace(self, path: Path, old_str: str, new_str: Optional[str]):
|
|
515
|
+
"""Implement the str_replace command, which replaces old_str with new_str in the file content"""
|
|
516
|
+
# Read the file content
|
|
517
|
+
file_content = self.read_file(path).expandtabs()
|
|
518
|
+
old_str = old_str.expandtabs()
|
|
519
|
+
new_str = new_str.expandtabs() if new_str is not None else ""
|
|
520
|
+
|
|
521
|
+
# Check if old_str is unique in the file
|
|
522
|
+
occurrences = file_content.count(old_str)
|
|
523
|
+
if occurrences == 0:
|
|
524
|
+
print(f"No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}.")
|
|
525
|
+
sys.exit(15)
|
|
526
|
+
elif occurrences > 1:
|
|
527
|
+
file_content_lines = file_content.split("\n")
|
|
528
|
+
lines = [idx + 1 for idx, line in enumerate(file_content_lines) if old_str in line]
|
|
529
|
+
print(
|
|
530
|
+
f"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {lines}. Please ensure it is unique"
|
|
531
|
+
)
|
|
532
|
+
sys.exit(16)
|
|
533
|
+
|
|
534
|
+
if new_str == old_str:
|
|
535
|
+
print(f"No replacement was performed, old_str `{old_str}` is the same as new_str `{new_str}`.")
|
|
536
|
+
sys.exit(161)
|
|
537
|
+
|
|
538
|
+
pre_edit_lint = ""
|
|
539
|
+
if USE_LINTER:
|
|
540
|
+
try:
|
|
541
|
+
pre_edit_lint = flake8(str(path))
|
|
542
|
+
except Exception as e:
|
|
543
|
+
print(f"Warning: Failed to run pre-edit linter on {path}: {e}")
|
|
544
|
+
|
|
545
|
+
# Replace old_str with new_str
|
|
546
|
+
new_file_content = file_content.replace(old_str, new_str)
|
|
547
|
+
|
|
548
|
+
# Write the new content to the file
|
|
549
|
+
self.write_file(path, new_file_content)
|
|
550
|
+
|
|
551
|
+
post_edit_lint = ""
|
|
552
|
+
if USE_LINTER:
|
|
553
|
+
try:
|
|
554
|
+
post_edit_lint = flake8(str(path))
|
|
555
|
+
except Exception as e:
|
|
556
|
+
print(f"Warning: Failed to run post-edit linter on {path}: {e}")
|
|
557
|
+
|
|
558
|
+
epilogue = ""
|
|
559
|
+
if post_edit_lint:
|
|
560
|
+
...
|
|
561
|
+
replacement_window_start_line = file_content.split(old_str)[0].count("\n") + 1
|
|
562
|
+
replacement_lines = len(new_str.split("\n"))
|
|
563
|
+
replacement_window_end_line = replacement_window_start_line + replacement_lines - 1
|
|
564
|
+
replacement_window = (replacement_window_start_line, replacement_window_end_line)
|
|
565
|
+
errors = format_flake8_output(
|
|
566
|
+
post_edit_lint,
|
|
567
|
+
previous_errors_string=pre_edit_lint,
|
|
568
|
+
replacement_window=replacement_window,
|
|
569
|
+
replacement_n_lines=replacement_lines,
|
|
570
|
+
)
|
|
571
|
+
if errors.strip():
|
|
572
|
+
epilogue = LINT_WARNING_TEMPLATE.format(errors=errors)
|
|
573
|
+
|
|
574
|
+
# Save the content to history
|
|
575
|
+
self._file_history[path].append(file_content)
|
|
576
|
+
|
|
577
|
+
# Create a snippet of the edited section
|
|
578
|
+
replacement_line = file_content.split(old_str)[0].count("\n")
|
|
579
|
+
start_line = max(1, replacement_line - SNIPPET_LINES)
|
|
580
|
+
end_line = min(replacement_line + SNIPPET_LINES + new_str.count("\n"), len(new_file_content.splitlines()))
|
|
581
|
+
start_line, end_line = WindowExpander(suffix=path.suffix).expand_window(
|
|
582
|
+
new_file_content.split("\n"), start_line, end_line, max_added_lines=MAX_WINDOW_EXPANSION_EDIT_CONFIRM
|
|
583
|
+
)
|
|
584
|
+
snippet = "\n".join(new_file_content.split("\n")[start_line - 1 : end_line])
|
|
585
|
+
|
|
586
|
+
# Prepare the success message
|
|
587
|
+
success_msg = f"The file {path} has been edited. "
|
|
588
|
+
success_msg += self._make_output(snippet, f"a snippet of {path}", start_line)
|
|
589
|
+
success_msg += "Review the changes and make sure they are as expected. Edit the file again if necessary."
|
|
590
|
+
success_msg += epilogue
|
|
591
|
+
|
|
592
|
+
print(success_msg)
|
|
593
|
+
|
|
594
|
+
def insert(self, path: Path, insert_line: int, new_str: str):
|
|
595
|
+
"""Implement the insert command, which inserts new_str at the specified line in the file content."""
|
|
596
|
+
file_text = self.read_file(path).expandtabs()
|
|
597
|
+
new_str = new_str.expandtabs()
|
|
598
|
+
file_text_lines = file_text.split("\n")
|
|
599
|
+
n_lines_file = len(file_text_lines)
|
|
600
|
+
|
|
601
|
+
if insert_line < 0 or insert_line > n_lines_file:
|
|
602
|
+
print(
|
|
603
|
+
f"Invalid `insert_line` parameter: {insert_line}. It should be within the range of lines of the file: {[0, n_lines_file]}"
|
|
604
|
+
)
|
|
605
|
+
sys.exit(17)
|
|
606
|
+
|
|
607
|
+
new_str_lines = new_str.split("\n")
|
|
608
|
+
new_file_text_lines = file_text_lines[:insert_line] + new_str_lines + file_text_lines[insert_line:]
|
|
609
|
+
snippet_lines = (
|
|
610
|
+
file_text_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
|
|
611
|
+
+ new_str_lines
|
|
612
|
+
+ file_text_lines[insert_line : insert_line + SNIPPET_LINES]
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
new_file_text = "\n".join(new_file_text_lines)
|
|
616
|
+
snippet = "\n".join(snippet_lines)
|
|
617
|
+
|
|
618
|
+
self.write_file(path, new_file_text)
|
|
619
|
+
self._file_history[path].append(file_text)
|
|
620
|
+
|
|
621
|
+
# todo: Also expand these windows
|
|
622
|
+
|
|
623
|
+
success_msg = f"The file {path} has been edited. "
|
|
624
|
+
success_msg += self._make_output(
|
|
625
|
+
snippet,
|
|
626
|
+
"a snippet of the edited file",
|
|
627
|
+
max(1, insert_line - SNIPPET_LINES + 1),
|
|
628
|
+
)
|
|
629
|
+
success_msg += "Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary."
|
|
630
|
+
print(success_msg)
|
|
631
|
+
|
|
632
|
+
def undo_edit(self, path: Path):
|
|
633
|
+
"""Implement the undo_edit command."""
|
|
634
|
+
if not self._file_history[path]:
|
|
635
|
+
print(f"No edit history found for {path}.")
|
|
636
|
+
sys.exit(18)
|
|
637
|
+
|
|
638
|
+
old_text = self._file_history[path].pop()
|
|
639
|
+
self.write_file(path, old_text)
|
|
640
|
+
|
|
641
|
+
print(f"Last edit to {path} undone successfully. {self._make_output(old_text, str(path))}")
|
|
642
|
+
|
|
643
|
+
def read_file(self, path: Path):
|
|
644
|
+
"""Read the content of a file from a given path; raise a ToolError if an error occurs."""
|
|
645
|
+
encodings = [
|
|
646
|
+
(None, None),
|
|
647
|
+
("utf-8", None),
|
|
648
|
+
("latin-1", None),
|
|
649
|
+
("utf-8", "replace"),
|
|
650
|
+
]
|
|
651
|
+
exception = None
|
|
652
|
+
for self._encoding, errors in encodings:
|
|
653
|
+
try:
|
|
654
|
+
text = path.read_text(encoding=self._encoding, errors=errors)
|
|
655
|
+
except UnicodeDecodeError as e:
|
|
656
|
+
exception = e
|
|
657
|
+
else:
|
|
658
|
+
break
|
|
659
|
+
else:
|
|
660
|
+
print(f"Ran into UnicodeDecodeError {exception} while trying to read {path}")
|
|
661
|
+
sys.exit(19)
|
|
662
|
+
return text
|
|
663
|
+
|
|
664
|
+
def write_file(self, path: Path, file: str):
|
|
665
|
+
"""Write the content of a file to a given path; raise a ToolError if an error occurs."""
|
|
666
|
+
try:
|
|
667
|
+
path.write_text(file, encoding=self._encoding or "utf-8")
|
|
668
|
+
except Exception as e:
|
|
669
|
+
print(f"Ran into {e} while trying to write to {path}")
|
|
670
|
+
sys.exit(20)
|
|
671
|
+
|
|
672
|
+
def _make_output(
|
|
673
|
+
self,
|
|
674
|
+
file_content: str,
|
|
675
|
+
file_descriptor: str,
|
|
676
|
+
init_line: int = 1,
|
|
677
|
+
expand_tabs: bool = True,
|
|
678
|
+
):
|
|
679
|
+
"""Generate output for the CLI based on the content of a file."""
|
|
680
|
+
file_content = maybe_truncate(file_content)
|
|
681
|
+
if expand_tabs:
|
|
682
|
+
file_content = file_content.expandtabs()
|
|
683
|
+
file_content = "\n".join([f"{i + init_line:6}\t{line}" for i, line in enumerate(file_content.split("\n"))])
|
|
684
|
+
return f"Here's the result of running `cat -n` on {file_descriptor}:\n" + file_content + "\n"
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def main():
|
|
688
|
+
parser = argparse.ArgumentParser()
|
|
689
|
+
parser.add_argument("command", type=str)
|
|
690
|
+
parser.add_argument("path", type=str)
|
|
691
|
+
parser.add_argument("--file_text", type=str)
|
|
692
|
+
parser.add_argument("--view_range", type=int, nargs=2)
|
|
693
|
+
parser.add_argument("--old_str", type=str)
|
|
694
|
+
parser.add_argument("--new_str", type=str)
|
|
695
|
+
parser.add_argument("--insert_line", type=int)
|
|
696
|
+
args = parser.parse_args()
|
|
697
|
+
tool = EditTool()
|
|
698
|
+
tool(
|
|
699
|
+
command=args.command,
|
|
700
|
+
path=args.path,
|
|
701
|
+
file_text=args.file_text,
|
|
702
|
+
view_range=args.view_range,
|
|
703
|
+
old_str=args.old_str,
|
|
704
|
+
new_str=args.new_str,
|
|
705
|
+
insert_line=args.insert_line,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
if __name__ == "__main__":
|
|
710
|
+
main()
|