@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,312 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from sweagent import TOOLS_DIR
|
|
5
|
+
except ImportError:
|
|
6
|
+
pass
|
|
7
|
+
else:
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
sys.path.append(str(TOOLS_DIR / "registry" / "lib"))
|
|
11
|
+
|
|
12
|
+
from registry import registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileNotOpened(Exception):
|
|
16
|
+
"""Raised when no file is opened."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TextNotFound(Exception):
|
|
20
|
+
"""Raised when the text is not found in the window."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_all(a_str: str, sub: str):
|
|
24
|
+
start = 0
|
|
25
|
+
while True:
|
|
26
|
+
start = a_str.find(sub, start)
|
|
27
|
+
if start == -1:
|
|
28
|
+
return
|
|
29
|
+
yield start
|
|
30
|
+
start += len(sub)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ReplacementInfo:
|
|
34
|
+
def __init__(self, first_replaced_line: int, n_search_lines: int, n_replace_lines: int, n_replacements: int):
|
|
35
|
+
self.first_replaced_line = first_replaced_line
|
|
36
|
+
self.n_search_lines = n_search_lines
|
|
37
|
+
self.n_replace_lines = n_replace_lines
|
|
38
|
+
self.n_replacements = n_replacements
|
|
39
|
+
|
|
40
|
+
def __repr__(self):
|
|
41
|
+
return f"ReplacementInfo(first_replaced_line={self.first_replaced_line}, n_search_lines={self.n_search_lines}, n_replace_lines={self.n_replace_lines}, n_replacements={self.n_replacements})"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InsertInfo:
|
|
45
|
+
def __init__(self, first_inserted_line: int, n_lines_added: int):
|
|
46
|
+
self.first_inserted_line = first_inserted_line
|
|
47
|
+
self.n_lines_added = n_lines_added
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class WindowedFile:
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
path: Path | None = None,
|
|
54
|
+
*,
|
|
55
|
+
first_line: int | None = None,
|
|
56
|
+
window: int | None = None,
|
|
57
|
+
exit_on_exception: bool = True,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
path: Path to the file to open.
|
|
63
|
+
first_line: First line of the display window.
|
|
64
|
+
window: Number of lines to display.
|
|
65
|
+
exit_on_exception: If False, will raise exception.
|
|
66
|
+
If true, will print an error message and exit.
|
|
67
|
+
|
|
68
|
+
Will create file if not found.
|
|
69
|
+
|
|
70
|
+
Internal convention/notes:
|
|
71
|
+
|
|
72
|
+
* All line numbers are 0-indexed.
|
|
73
|
+
* Previously, we used "current_line" for the internal state
|
|
74
|
+
of the window position, pointing to the middle of the window.
|
|
75
|
+
Now, we use `first_line` for this purpose (it's simpler this way).
|
|
76
|
+
"""
|
|
77
|
+
_path = registry.get_if_none(path, "CURRENT_FILE")
|
|
78
|
+
self._exit_on_exception = exit_on_exception
|
|
79
|
+
if not _path:
|
|
80
|
+
if self._exit_on_exception:
|
|
81
|
+
print("No file open. Use the open command first.")
|
|
82
|
+
exit(1)
|
|
83
|
+
raise FileNotOpened
|
|
84
|
+
self.path = Path(_path)
|
|
85
|
+
if self.path.is_dir():
|
|
86
|
+
msg = f"Error: {self.path} is a directory. You can only open files. Use cd or ls to navigate directories."
|
|
87
|
+
if self._exit_on_exception:
|
|
88
|
+
print(msg)
|
|
89
|
+
exit(1)
|
|
90
|
+
raise IsADirectoryError(msg)
|
|
91
|
+
if not self.path.exists():
|
|
92
|
+
msg = f"Error: File {self.path} not found"
|
|
93
|
+
if self._exit_on_exception:
|
|
94
|
+
print(msg)
|
|
95
|
+
exit(1)
|
|
96
|
+
raise FileNotFoundError(msg)
|
|
97
|
+
registry["CURRENT_FILE"] = str(self.path.resolve())
|
|
98
|
+
self.window = int(registry.get_if_none(window, "WINDOW"))
|
|
99
|
+
self.overlap = int(registry.get("OVERLAP", 0))
|
|
100
|
+
# Ensure that we get a valid current line by using the setter
|
|
101
|
+
self._first_line = 0
|
|
102
|
+
self.first_line = int(
|
|
103
|
+
registry.get_if_none(
|
|
104
|
+
first_line,
|
|
105
|
+
"FIRST_LINE",
|
|
106
|
+
0,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
self.offset_multiplier = 1 / 6
|
|
110
|
+
self._original_text = self.text
|
|
111
|
+
self._original_first_line = self.first_line
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def first_line(self) -> int:
|
|
115
|
+
return self._first_line
|
|
116
|
+
|
|
117
|
+
@first_line.setter
|
|
118
|
+
def first_line(self, value: int | float):
|
|
119
|
+
self._original_first_line = self.first_line
|
|
120
|
+
value = int(value)
|
|
121
|
+
self._first_line = max(0, min(value, self.n_lines - self.window))
|
|
122
|
+
registry["FIRST_LINE"] = self.first_line
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def text(self) -> str:
|
|
126
|
+
return self.path.read_text()
|
|
127
|
+
|
|
128
|
+
@text.setter
|
|
129
|
+
def text(self, new_text: str):
|
|
130
|
+
self._original_text = self.text
|
|
131
|
+
self.path.write_text(new_text)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def n_lines(self) -> int:
|
|
135
|
+
return len(self.text.splitlines())
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def line_range(self) -> tuple[int, int]:
|
|
139
|
+
"""Return first and last line (inclusive) of the display window, such
|
|
140
|
+
that exactly `window` many lines are displayed.
|
|
141
|
+
This means `line_range[1] - line_range[0] == window-1` as long as there are
|
|
142
|
+
at least `window` lines in the file. `first_line` does the handling
|
|
143
|
+
of making sure that we don't go out of bounds.
|
|
144
|
+
"""
|
|
145
|
+
return self.first_line, min(self.first_line + self.window - 1, self.n_lines - 1)
|
|
146
|
+
|
|
147
|
+
def get_window_text(
|
|
148
|
+
self, *, line_numbers: bool = False, status_line: bool = False, pre_post_line: bool = False
|
|
149
|
+
) -> str:
|
|
150
|
+
"""Get the text in the current display window with optional status/extra information
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
line_numbers: include line numbers in the output
|
|
154
|
+
status_line: include the status line in the output (file path, total lines)
|
|
155
|
+
pre_post_line: include the pre/post line in the output (number of lines above/below)
|
|
156
|
+
"""
|
|
157
|
+
start_line, end_line = self.line_range
|
|
158
|
+
lines = self.text.split("\n")[start_line : end_line + 1]
|
|
159
|
+
out_lines = []
|
|
160
|
+
if status_line:
|
|
161
|
+
out_lines.append(f"[File: {self.path} ({self.n_lines} lines total)]")
|
|
162
|
+
if pre_post_line:
|
|
163
|
+
if start_line > 0:
|
|
164
|
+
out_lines.append(f"({start_line} more lines above)")
|
|
165
|
+
if line_numbers:
|
|
166
|
+
out_lines.extend(f"{i + start_line + 1}:{line}" for i, line in enumerate(lines))
|
|
167
|
+
else:
|
|
168
|
+
out_lines.extend(lines)
|
|
169
|
+
if pre_post_line:
|
|
170
|
+
if end_line < self.n_lines - 1:
|
|
171
|
+
out_lines.append(f"({self.n_lines - end_line - 1} more lines below)")
|
|
172
|
+
return "\n".join(out_lines)
|
|
173
|
+
|
|
174
|
+
def set_window_text(self, new_text: str, *, line_range: tuple[int, int] | None = None) -> None:
|
|
175
|
+
"""Replace the text in the current display window with a new string."""
|
|
176
|
+
text = self.text.split("\n")
|
|
177
|
+
if line_range is not None:
|
|
178
|
+
start, stop = line_range
|
|
179
|
+
else:
|
|
180
|
+
start, stop = self.line_range
|
|
181
|
+
|
|
182
|
+
# Handle empty replacement text (deletion case)
|
|
183
|
+
new_lines = new_text.split("\n") if new_text else []
|
|
184
|
+
text[start : stop + 1] = new_lines
|
|
185
|
+
self.text = "\n".join(text)
|
|
186
|
+
|
|
187
|
+
def replace_in_window(
|
|
188
|
+
self,
|
|
189
|
+
search: str,
|
|
190
|
+
replace: str,
|
|
191
|
+
*,
|
|
192
|
+
reset_first_line: str = "top",
|
|
193
|
+
) -> "ReplacementInfo":
|
|
194
|
+
"""Search and replace in the window.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
search: The string to search for (can be multi-line).
|
|
198
|
+
replace: The string to replace it with (can be multi-line).
|
|
199
|
+
reset_first_line: If "keep", we keep the current line. Otherwise, we
|
|
200
|
+
`goto` the line where the replacement started with this mode.
|
|
201
|
+
"""
|
|
202
|
+
window_text = self.get_window_text()
|
|
203
|
+
# Update line number
|
|
204
|
+
index = window_text.find(search)
|
|
205
|
+
if index == -1:
|
|
206
|
+
if self._exit_on_exception:
|
|
207
|
+
print(f"Error: Text not found: {search}")
|
|
208
|
+
exit(1)
|
|
209
|
+
raise TextNotFound
|
|
210
|
+
window_start_line, _ = self.line_range
|
|
211
|
+
replace_start_line = window_start_line + len(window_text[:index].split("\n")) - 1
|
|
212
|
+
new_window_text = window_text.replace(search, replace)
|
|
213
|
+
self.set_window_text(new_window_text)
|
|
214
|
+
if reset_first_line == "keep":
|
|
215
|
+
pass
|
|
216
|
+
else:
|
|
217
|
+
self.goto(replace_start_line, mode=reset_first_line)
|
|
218
|
+
return ReplacementInfo(
|
|
219
|
+
first_replaced_line=replace_start_line,
|
|
220
|
+
n_search_lines=len(search.split("\n")),
|
|
221
|
+
n_replace_lines=len(replace.split("\n")),
|
|
222
|
+
n_replacements=1,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def find_all_occurrences(self, search: str, zero_based: bool = True) -> list[int]:
|
|
226
|
+
"""Returns the line numbers of all occurrences of the search string."""
|
|
227
|
+
indices = list(_find_all(self.text, search))
|
|
228
|
+
line_numbers = []
|
|
229
|
+
for index in indices:
|
|
230
|
+
line_no = len(self.text[:index].split("\n"))
|
|
231
|
+
if zero_based:
|
|
232
|
+
line_numbers.append(line_no - 1)
|
|
233
|
+
else:
|
|
234
|
+
line_numbers.append(line_no)
|
|
235
|
+
return line_numbers
|
|
236
|
+
|
|
237
|
+
def replace(self, search: str, replace: str, *, reset_first_line: str = "top") -> "ReplacementInfo":
|
|
238
|
+
indices = list(_find_all(self.text, search))
|
|
239
|
+
if not indices:
|
|
240
|
+
if self._exit_on_exception:
|
|
241
|
+
print(f"Error: Text not found: {search}")
|
|
242
|
+
exit(1)
|
|
243
|
+
raise TextNotFound
|
|
244
|
+
replace_start_line = len(self.text[: indices[0]].split("\n"))
|
|
245
|
+
new_text = self.text.replace(search, replace)
|
|
246
|
+
self.text = new_text
|
|
247
|
+
if reset_first_line == "keep":
|
|
248
|
+
pass
|
|
249
|
+
else:
|
|
250
|
+
self.goto(replace_start_line, mode=reset_first_line)
|
|
251
|
+
return ReplacementInfo(
|
|
252
|
+
first_replaced_line=replace_start_line,
|
|
253
|
+
n_search_lines=len(search.split("\n")),
|
|
254
|
+
n_replace_lines=len(replace.split("\n")),
|
|
255
|
+
n_replacements=len(indices),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def print_window(self, *, line_numbers: bool = True, status_line: bool = True, pre_post_line: bool = True):
|
|
259
|
+
print(self.get_window_text(line_numbers=line_numbers, status_line=status_line, pre_post_line=pre_post_line))
|
|
260
|
+
|
|
261
|
+
def goto(self, line: int, mode: str = "top"):
|
|
262
|
+
if mode == "top":
|
|
263
|
+
self.first_line = line - self.window * self.offset_multiplier
|
|
264
|
+
else:
|
|
265
|
+
raise NotImplementedError
|
|
266
|
+
|
|
267
|
+
def scroll(self, n_lines: int):
|
|
268
|
+
if n_lines > 0:
|
|
269
|
+
self.first_line += n_lines - self.overlap
|
|
270
|
+
elif n_lines < 0:
|
|
271
|
+
self.first_line += n_lines + self.overlap
|
|
272
|
+
|
|
273
|
+
def undo_edit(self):
|
|
274
|
+
self.text = self._original_text
|
|
275
|
+
self.first_line = self._original_first_line
|
|
276
|
+
|
|
277
|
+
def insert(self, text: str, line: int | None = None, *, reset_first_line: str = "top") -> "InsertInfo":
|
|
278
|
+
# Standardize empty text handling
|
|
279
|
+
if not text:
|
|
280
|
+
return InsertInfo(first_inserted_line=(self.n_lines if line is None else line), n_lines_added=0)
|
|
281
|
+
|
|
282
|
+
# Remove single trailing newline if it exists
|
|
283
|
+
text = text[:-1] if text.endswith("\n") else text
|
|
284
|
+
|
|
285
|
+
if line is None:
|
|
286
|
+
# Append to end of file
|
|
287
|
+
if not self.text:
|
|
288
|
+
new_text = text
|
|
289
|
+
else:
|
|
290
|
+
current_text = self.text[:-1] if self.text.endswith("\n") else self.text
|
|
291
|
+
new_text = current_text + "\n" + text
|
|
292
|
+
insert_line = self.n_lines
|
|
293
|
+
elif line < 0:
|
|
294
|
+
# Insert at start of file
|
|
295
|
+
if not self.text:
|
|
296
|
+
new_text = text
|
|
297
|
+
else:
|
|
298
|
+
current_text = self.text[1:] if self.text.startswith("\n") else self.text
|
|
299
|
+
new_text = text + "\n" + current_text
|
|
300
|
+
insert_line = 0
|
|
301
|
+
else:
|
|
302
|
+
# Insert at specific line
|
|
303
|
+
lines = self.text.split("\n")
|
|
304
|
+
lines.insert(line, text)
|
|
305
|
+
new_text = "\n".join(lines)
|
|
306
|
+
insert_line = line
|
|
307
|
+
|
|
308
|
+
self.text = new_text
|
|
309
|
+
if reset_first_line != "keep":
|
|
310
|
+
self.goto(insert_line, mode=reset_first_line)
|
|
311
|
+
|
|
312
|
+
return InsertInfo(first_inserted_line=insert_line, n_lines_added=len(text.split("\n")))
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from sweagent import TOOLS_DIR
|
|
9
|
+
except ImportError:
|
|
10
|
+
pass
|
|
11
|
+
else:
|
|
12
|
+
default_lib = TOOLS_DIR / "windowed" / "lib"
|
|
13
|
+
assert default_lib.is_dir()
|
|
14
|
+
sys.path.append(str(default_lib))
|
|
15
|
+
sys.path.append(str(TOOLS_DIR / "registry" / "lib"))
|
|
16
|
+
|
|
17
|
+
from windowed_file import FileNotOpened, WindowedFile # type: ignore
|
|
18
|
+
from flake8_utils import flake8, format_flake8_output # type: ignore
|
|
19
|
+
|
|
20
|
+
_USAGE_MSG = """Usage: edit <start_line>:<end_line>
|
|
21
|
+
<replacement_text>
|
|
22
|
+
end_of_edit"""
|
|
23
|
+
|
|
24
|
+
_EDIT_SUCCESS_MSG = """File updated. Please review the changes and make sure they are correct
|
|
25
|
+
(correct indentation, no duplicate lines, etc). Edit the file again if necessary."""
|
|
26
|
+
|
|
27
|
+
_LINT_ERROR_TEMPLATE = """Your proposed edit has introduced new syntax error(s). Please read this error message carefully and then retry editing the file.
|
|
28
|
+
|
|
29
|
+
ERRORS:
|
|
30
|
+
{errors}
|
|
31
|
+
|
|
32
|
+
This is how your edit would have looked if applied
|
|
33
|
+
------------------------------------------------
|
|
34
|
+
{window_applied}
|
|
35
|
+
------------------------------------------------
|
|
36
|
+
|
|
37
|
+
This is the original code before your edit
|
|
38
|
+
------------------------------------------------
|
|
39
|
+
{window_original}
|
|
40
|
+
------------------------------------------------
|
|
41
|
+
|
|
42
|
+
Your changes have NOT been applied. Please fix your edit command and try again.
|
|
43
|
+
DO NOT re-run the same failed edit command. Running it again will lead to the same error."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
47
|
+
parser = argparse.ArgumentParser()
|
|
48
|
+
parser.add_argument("line_range", help="Line range in format start:end")
|
|
49
|
+
parser.add_argument("replacement_text", help="Text to insert", nargs="?")
|
|
50
|
+
return parser
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_line_range(line_range: str) -> Tuple[int, int]:
|
|
54
|
+
try:
|
|
55
|
+
start, end = map(int, line_range.split(":"))
|
|
56
|
+
return start - 1, end - 1
|
|
57
|
+
except ValueError:
|
|
58
|
+
print(_USAGE_MSG)
|
|
59
|
+
exit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(line_range: str, replacement_text: Union[str, None] = None):
|
|
63
|
+
# Handle file opening
|
|
64
|
+
try:
|
|
65
|
+
wf = WindowedFile(exit_on_exception=False)
|
|
66
|
+
except FileNotOpened:
|
|
67
|
+
print("No file opened. Use the `open` command first.")
|
|
68
|
+
exit(1)
|
|
69
|
+
|
|
70
|
+
# Parse line range
|
|
71
|
+
start_line, end_line = parse_line_range(line_range)
|
|
72
|
+
|
|
73
|
+
if replacement_text is None:
|
|
74
|
+
# Read replacement text from stdin (e.g., when sent via bash heredoc)
|
|
75
|
+
# if not provided as argument
|
|
76
|
+
replacement_lines = []
|
|
77
|
+
while True:
|
|
78
|
+
try:
|
|
79
|
+
line = input()
|
|
80
|
+
if line == "end_of_edit":
|
|
81
|
+
break
|
|
82
|
+
replacement_lines.append(line)
|
|
83
|
+
except EOFError:
|
|
84
|
+
break
|
|
85
|
+
replacement_text = "\n".join(replacement_lines)
|
|
86
|
+
else:
|
|
87
|
+
if replacement_text.endswith("\n"):
|
|
88
|
+
replacement_text = replacement_text[:-1]
|
|
89
|
+
|
|
90
|
+
if replacement_text is None:
|
|
91
|
+
print(_USAGE_MSG)
|
|
92
|
+
exit(1)
|
|
93
|
+
|
|
94
|
+
# Get pre-edit linting errors
|
|
95
|
+
pre_edit_lint = flake8(wf.path)
|
|
96
|
+
|
|
97
|
+
# Perform the edit
|
|
98
|
+
wf.set_window_text(replacement_text, line_range=(start_line, end_line))
|
|
99
|
+
|
|
100
|
+
# Check for new linting errors
|
|
101
|
+
post_edit_lint = flake8(wf.path)
|
|
102
|
+
new_flake8_output = format_flake8_output(
|
|
103
|
+
post_edit_lint,
|
|
104
|
+
previous_errors_string=pre_edit_lint,
|
|
105
|
+
replacement_window=(start_line, end_line),
|
|
106
|
+
replacement_n_lines=len(replacement_text.splitlines()),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if new_flake8_output:
|
|
110
|
+
# Show error and revert changes
|
|
111
|
+
with_edits = wf.get_window_text(line_numbers=True, status_line=True, pre_post_line=True)
|
|
112
|
+
wf.undo_edit()
|
|
113
|
+
without_edits = wf.get_window_text(line_numbers=True, status_line=True, pre_post_line=True)
|
|
114
|
+
print(
|
|
115
|
+
_LINT_ERROR_TEMPLATE.format(
|
|
116
|
+
errors=new_flake8_output, window_applied=with_edits, window_original=without_edits
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
exit(1)
|
|
120
|
+
|
|
121
|
+
# Success - update window position and show result
|
|
122
|
+
wf.goto(start_line, mode="top")
|
|
123
|
+
print(_EDIT_SUCCESS_MSG)
|
|
124
|
+
wf.print_window()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main(**vars(get_parser().parse_args()))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
tools:
|
|
2
|
+
edit:
|
|
3
|
+
signature: |
|
|
4
|
+
edit <start_line>:<end_line>
|
|
5
|
+
<replacement_text>
|
|
6
|
+
end_of_edit
|
|
7
|
+
# Note: Without function calling we should add back:
|
|
8
|
+
# The replacement text is terminated by a line with only
|
|
9
|
+
# end_of_edit on
|
|
10
|
+
docstring: >
|
|
11
|
+
Replaces lines <start_line> through <end_line> (inclusive) with the given text
|
|
12
|
+
in the open file.
|
|
13
|
+
All of the <replacement text> will be entered, so make
|
|
14
|
+
sure your indentation is formatted properly.
|
|
15
|
+
|
|
16
|
+
Please note that THIS COMMAND REQUIRES PROPER INDENTATION.
|
|
17
|
+
If you'd like to add the line ' print(x)' you must fully write that out, with all those spaces before the code!
|
|
18
|
+
end_name: "end_of_edit"
|
|
19
|
+
arguments:
|
|
20
|
+
- name: start_line
|
|
21
|
+
type: integer
|
|
22
|
+
description: "the line number to start the edit at"
|
|
23
|
+
required: true
|
|
24
|
+
- name: end_line
|
|
25
|
+
type: integer
|
|
26
|
+
description: "the line number to end the edit at (inclusive)"
|
|
27
|
+
required: true
|
|
28
|
+
- name: replacement_text
|
|
29
|
+
type: string
|
|
30
|
+
description: "the text to replace the current selection with"
|
|
31
|
+
required: true
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from sweagent import TOOLS_DIR
|
|
7
|
+
except ImportError:
|
|
8
|
+
pass
|
|
9
|
+
else:
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
default_lib = TOOLS_DIR / "windowed" / "lib"
|
|
13
|
+
assert default_lib.is_dir()
|
|
14
|
+
sys.path.append(str(default_lib))
|
|
15
|
+
sys.path.append(str(TOOLS_DIR / "registry" / "lib"))
|
|
16
|
+
|
|
17
|
+
from windowed_file import FileNotOpened, TextNotFound, WindowedFile # type: ignore
|
|
18
|
+
from flake8_utils import flake8, format_flake8_output # type: ignore
|
|
19
|
+
|
|
20
|
+
RETRY_WITH_OUTPUT_TOKEN = "###SWE-AGENT-RETRY-WITH-OUTPUT###"
|
|
21
|
+
|
|
22
|
+
_NOT_FOUND = """Your edit was not applied (file not modified): Text {search!r} not found in displayed lines (or anywhere in the file).
|
|
23
|
+
Please modify your search string. Did you forget to properly handle whitespace/indentation?
|
|
24
|
+
You can also call `open` again to re-display the file with the correct context.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_NOT_FOUND_IN_WINDOW_MSG = """Your edit was not applied (file not modified): Text {search!r} not found in displayed lines.
|
|
28
|
+
|
|
29
|
+
However, we found the following occurrences of your search string in the file:
|
|
30
|
+
|
|
31
|
+
{occurrences}
|
|
32
|
+
|
|
33
|
+
You can use the `goto` command to navigate to these locations before running the edit command again.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_MULTIPLE_OCCURRENCES_MSG = """Your edit was not applied (file not modified): Found more than one occurrence of {search!r} in the currently displayed lines.
|
|
37
|
+
Please make your search string more specific (for example, by including more lines of context).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_NO_CHANGES_MADE_MSG = """Your search and replace strings are the same. No changes were made. Please modify your search or replace strings."""
|
|
41
|
+
|
|
42
|
+
_SINGLE_EDIT_SUCCESS_MSG = """Text replaced. Please review the changes and make sure they are correct:
|
|
43
|
+
|
|
44
|
+
1. The edited file is correctly indented
|
|
45
|
+
2. The edited file does not contain duplicate lines
|
|
46
|
+
3. The edit does not break existing functionality
|
|
47
|
+
|
|
48
|
+
Edit the file again if necessary."""
|
|
49
|
+
|
|
50
|
+
_MULTIPLE_EDITS_SUCCESS_MSG = """Replaced {n_replacements} occurrences. Please review the changes and make sure they are correct:
|
|
51
|
+
|
|
52
|
+
1. The edited file is correctly indented
|
|
53
|
+
2. The edited file does not contain duplicate lines
|
|
54
|
+
3. The edit does not break existing functionality
|
|
55
|
+
|
|
56
|
+
Edit the file again if necessary."""
|
|
57
|
+
|
|
58
|
+
_LINT_ERROR_TEMPLATE = """Your proposed edit has introduced new syntax error(s). Please read this error message carefully and then retry editing the file.
|
|
59
|
+
|
|
60
|
+
ERRORS:
|
|
61
|
+
|
|
62
|
+
{errors}
|
|
63
|
+
|
|
64
|
+
This is how your edit would have looked if applied
|
|
65
|
+
------------------------------------------------
|
|
66
|
+
{window_applied}
|
|
67
|
+
------------------------------------------------
|
|
68
|
+
|
|
69
|
+
This is the original code before your edit
|
|
70
|
+
------------------------------------------------
|
|
71
|
+
{window_original}
|
|
72
|
+
------------------------------------------------
|
|
73
|
+
|
|
74
|
+
Your changes have NOT been applied. Please fix your edit command and try again.
|
|
75
|
+
DO NOT re-run the same failed edit command. Running it again will lead to the same error.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
80
|
+
parser = argparse.ArgumentParser()
|
|
81
|
+
parser.add_argument("search", type=str)
|
|
82
|
+
parser.add_argument("replace", type=str)
|
|
83
|
+
parser.add_argument("replace_all", type=bool, nargs="?", default=False)
|
|
84
|
+
return parser
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(search: str, replace: str, replace_all: bool):
|
|
88
|
+
try:
|
|
89
|
+
wf = WindowedFile(exit_on_exception=False)
|
|
90
|
+
except FileNotOpened:
|
|
91
|
+
print("No file opened. Either `open` or `create` a file first.")
|
|
92
|
+
exit(1)
|
|
93
|
+
|
|
94
|
+
# Turn \\n into \n etc., i.e., undo the escaping
|
|
95
|
+
# args.replace = args.replace.encode("utf8").decode("unicode_escape")
|
|
96
|
+
|
|
97
|
+
if search == replace:
|
|
98
|
+
print(_NO_CHANGES_MADE_MSG)
|
|
99
|
+
print(RETRY_WITH_OUTPUT_TOKEN)
|
|
100
|
+
exit(2)
|
|
101
|
+
|
|
102
|
+
pre_edit_lint = flake8(wf.path)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if not replace_all:
|
|
106
|
+
window_text = wf.get_window_text()
|
|
107
|
+
if window_text.count(search) > 1:
|
|
108
|
+
print(_MULTIPLE_OCCURRENCES_MSG.format(search=search))
|
|
109
|
+
print(RETRY_WITH_OUTPUT_TOKEN)
|
|
110
|
+
exit(4)
|
|
111
|
+
replacement_info = wf.replace_in_window(search, replace)
|
|
112
|
+
# todo: Should warn if more than one occurrence was found?
|
|
113
|
+
else:
|
|
114
|
+
# todo: Give overview of all replaced occurrences/number of replacements
|
|
115
|
+
replacement_info = wf.replace(search, replace)
|
|
116
|
+
except TextNotFound:
|
|
117
|
+
line_no_founds = wf.find_all_occurrences(search, zero_based=False)
|
|
118
|
+
if line_no_founds:
|
|
119
|
+
print(
|
|
120
|
+
_NOT_FOUND_IN_WINDOW_MSG.format(
|
|
121
|
+
search=search, occurrences="\n".join([f"- line {line_no}" for line_no in line_no_founds])
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
print(_NOT_FOUND.format(search=search))
|
|
126
|
+
print(RETRY_WITH_OUTPUT_TOKEN)
|
|
127
|
+
exit(3)
|
|
128
|
+
|
|
129
|
+
post_edit_lint = flake8(wf.path)
|
|
130
|
+
|
|
131
|
+
if not replace_all:
|
|
132
|
+
# Try to filter out pre-existing errors
|
|
133
|
+
replacement_window = (
|
|
134
|
+
replacement_info.first_replaced_line,
|
|
135
|
+
replacement_info.first_replaced_line + replacement_info.n_search_lines - 1,
|
|
136
|
+
)
|
|
137
|
+
# print(f"{replacement_info=}")
|
|
138
|
+
# print(f"{replacement_window=}")
|
|
139
|
+
# print(f"{pre_edit_lint=}")
|
|
140
|
+
# print(f"{post_edit_lint=}")
|
|
141
|
+
new_flake8_output = format_flake8_output(
|
|
142
|
+
post_edit_lint,
|
|
143
|
+
previous_errors_string=pre_edit_lint,
|
|
144
|
+
replacement_window=replacement_window,
|
|
145
|
+
replacement_n_lines=replacement_info.n_replace_lines,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
# Cannot easily compare the error strings, because line number changes are hard to keep track of
|
|
149
|
+
# So we show all linter errors.
|
|
150
|
+
new_flake8_output = format_flake8_output(post_edit_lint)
|
|
151
|
+
|
|
152
|
+
if new_flake8_output:
|
|
153
|
+
with_edits = wf.get_window_text(line_numbers=True, status_line=True, pre_post_line=True)
|
|
154
|
+
wf.undo_edit()
|
|
155
|
+
without_edits = wf.get_window_text(line_numbers=True, status_line=True, pre_post_line=True)
|
|
156
|
+
print(
|
|
157
|
+
_LINT_ERROR_TEMPLATE.format(
|
|
158
|
+
errors=new_flake8_output, window_applied=with_edits, window_original=without_edits,
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
print(RETRY_WITH_OUTPUT_TOKEN)
|
|
162
|
+
exit(4)
|
|
163
|
+
if not replace_all:
|
|
164
|
+
print(_SINGLE_EDIT_SUCCESS_MSG)
|
|
165
|
+
else:
|
|
166
|
+
print(_MULTIPLE_EDITS_SUCCESS_MSG.format(n_replacements=replacement_info.n_replacements))
|
|
167
|
+
|
|
168
|
+
wf.print_window()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main(**vars(get_parser().parse_args()))
|