@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.
Files changed (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +270 -0
  3. package/package.json +71 -0
  4. package/python/LICENSE +21 -0
  5. package/python/config/README.md +15 -0
  6. package/python/config/bash_only.yaml +222 -0
  7. package/python/config/benchmarks/250212_sweagent_heavy_sbl.yaml +188 -0
  8. package/python/config/benchmarks/250225_anthropic_filemap_simple_review.yaml +75 -0
  9. package/python/config/benchmarks/250522_anthropic_filemap_simple_review.yaml +92 -0
  10. package/python/config/benchmarks/250526_anthropic_filemap_simple_review_sbl.yaml +93 -0
  11. package/python/config/benchmarks/anthropic_filemap_multilingual.yaml +66 -0
  12. package/python/config/coding_challenge.yaml +104 -0
  13. package/python/config/default.yaml +69 -0
  14. package/python/config/default_backticks.yaml +69 -0
  15. package/python/config/default_mm_no_images.yaml +82 -0
  16. package/python/config/default_mm_with_images.yaml +83 -0
  17. package/python/config/demo/default.yaml +80 -0
  18. package/python/config/demo/no_instructions.yaml +69 -0
  19. package/python/config/demo/only_bash.yaml +60 -0
  20. package/python/config/exotic/default_shell.yaml +52 -0
  21. package/python/config/exotic/windowed_replace.yaml +125 -0
  22. package/python/config/exotic/windowed_replace_late_repro.yaml +127 -0
  23. package/python/config/human/human.yaml +24 -0
  24. package/python/config/human/human_demo.yaml +52 -0
  25. package/python/config/sweagent_0_7/07.yaml +101 -0
  26. package/python/config/sweagent_0_7/07_fcalling.yaml +100 -0
  27. package/python/config/sweagent_0_7/07_from_url.yaml +114 -0
  28. package/python/config/sweagent_0_7/07_thought_action.yaml +102 -0
  29. package/python/config/sweagent_0_7/07_thought_action_xml.yaml +96 -0
  30. package/python/mlc_config.json +44 -0
  31. package/python/pyproject.toml +262 -0
  32. package/python/sweagent/__init__.py +114 -0
  33. package/python/sweagent/__main__.py +4 -0
  34. package/python/sweagent/agent/__init__.py +0 -0
  35. package/python/sweagent/agent/action_sampler.py +317 -0
  36. package/python/sweagent/agent/agents.py +1294 -0
  37. package/python/sweagent/agent/extra/shell_agent.py +106 -0
  38. package/python/sweagent/agent/history_processors.py +399 -0
  39. package/python/sweagent/agent/hooks/__init__.py +0 -0
  40. package/python/sweagent/agent/hooks/abstract.py +139 -0
  41. package/python/sweagent/agent/hooks/status.py +34 -0
  42. package/python/sweagent/agent/models.py +896 -0
  43. package/python/sweagent/agent/problem_statement.py +312 -0
  44. package/python/sweagent/agent/reviewer.py +664 -0
  45. package/python/sweagent/environment/__init__.py +0 -0
  46. package/python/sweagent/environment/hooks/__init__.py +0 -0
  47. package/python/sweagent/environment/hooks/abstract.py +60 -0
  48. package/python/sweagent/environment/hooks/status.py +28 -0
  49. package/python/sweagent/environment/repo.py +219 -0
  50. package/python/sweagent/environment/swe_env.py +276 -0
  51. package/python/sweagent/exceptions.py +54 -0
  52. package/python/sweagent/inspector/README.md +6 -0
  53. package/python/sweagent/inspector/__init__.py +0 -0
  54. package/python/sweagent/inspector/favicon.ico +0 -0
  55. package/python/sweagent/inspector/fileViewer.js +354 -0
  56. package/python/sweagent/inspector/icons/computer.png +0 -0
  57. package/python/sweagent/inspector/icons/edit_icon.svg +11 -0
  58. package/python/sweagent/inspector/icons/swe-agent-logo-50.png +0 -0
  59. package/python/sweagent/inspector/icons/swellama_blue.png +0 -0
  60. package/python/sweagent/inspector/icons/swellama_brown.png +0 -0
  61. package/python/sweagent/inspector/icons/swellama_grey.png +0 -0
  62. package/python/sweagent/inspector/icons/swellama_tan.png +0 -0
  63. package/python/sweagent/inspector/index.html +25 -0
  64. package/python/sweagent/inspector/server.py +354 -0
  65. package/python/sweagent/inspector/static.py +169 -0
  66. package/python/sweagent/inspector/style.css +454 -0
  67. package/python/sweagent/run/__init__.py +0 -0
  68. package/python/sweagent/run/_progress.py +158 -0
  69. package/python/sweagent/run/batch_instances.py +419 -0
  70. package/python/sweagent/run/common.py +387 -0
  71. package/python/sweagent/run/compare_runs.py +123 -0
  72. package/python/sweagent/run/extract_pred.py +19 -0
  73. package/python/sweagent/run/hooks/__init__.py +0 -0
  74. package/python/sweagent/run/hooks/abstract.py +67 -0
  75. package/python/sweagent/run/hooks/apply_patch.py +106 -0
  76. package/python/sweagent/run/hooks/open_pr.py +244 -0
  77. package/python/sweagent/run/hooks/swe_bench_evaluate.py +113 -0
  78. package/python/sweagent/run/inspector_cli.py +493 -0
  79. package/python/sweagent/run/merge_predictions.py +64 -0
  80. package/python/sweagent/run/quick_stats.py +96 -0
  81. package/python/sweagent/run/remove_unfinished.py +63 -0
  82. package/python/sweagent/run/rich_test.py +91 -0
  83. package/python/sweagent/run/run.py +147 -0
  84. package/python/sweagent/run/run_batch.py +442 -0
  85. package/python/sweagent/run/run_replay.py +219 -0
  86. package/python/sweagent/run/run_shell.py +155 -0
  87. package/python/sweagent/run/run_single.py +225 -0
  88. package/python/sweagent/run/run_traj_to_demo.py +85 -0
  89. package/python/sweagent/tools/__init__.py +0 -0
  90. package/python/sweagent/tools/bundle.py +57 -0
  91. package/python/sweagent/tools/commands.py +220 -0
  92. package/python/sweagent/tools/parsing.py +619 -0
  93. package/python/sweagent/tools/tools.py +430 -0
  94. package/python/sweagent/tools/utils.py +108 -0
  95. package/python/sweagent/types.py +102 -0
  96. package/python/sweagent/utils/__init__.py +0 -0
  97. package/python/sweagent/utils/config.py +80 -0
  98. package/python/sweagent/utils/files.py +27 -0
  99. package/python/sweagent/utils/github.py +118 -0
  100. package/python/sweagent/utils/jinja_warnings.py +14 -0
  101. package/python/sweagent/utils/log.py +175 -0
  102. package/python/sweagent/utils/patch_formatter.py +152 -0
  103. package/python/sweagent/utils/serialization.py +45 -0
  104. package/python/tests/__init__.py +0 -0
  105. package/python/tests/conftest.py +191 -0
  106. package/python/tests/test_agent.py +258 -0
  107. package/python/tests/test_batch_instance.py +43 -0
  108. package/python/tests/test_commands/_interactive_dummy.py +35 -0
  109. package/python/tests/test_commands/interactive_dummy_wrapper.sh +29 -0
  110. package/python/tests/test_data/config_files/dummy_interactive.yaml +62 -0
  111. package/python/tests/test_data/data_sources/ctf/crypto/Katy/Dockerfile +20 -0
  112. package/python/tests/test_data/data_sources/ctf/crypto/Katy/README.md +13 -0
  113. package/python/tests/test_data/data_sources/ctf/crypto/Katy/challenge.json +12 -0
  114. package/python/tests/test_data/data_sources/ctf/crypto/Katy/customrandom.c +50 -0
  115. package/python/tests/test_data/data_sources/ctf/crypto/Katy/docker-compose.yml +14 -0
  116. package/python/tests/test_data/data_sources/ctf/crypto/Katy/release +0 -0
  117. package/python/tests/test_data/data_sources/ctf/crypto/Katy/server +0 -0
  118. package/python/tests/test_data/data_sources/ctf/crypto/Katy/solver.py +12 -0
  119. package/python/tests/test_data/data_sources/ctf/forensics/flash/README.md +16 -0
  120. package/python/tests/test_data/data_sources/ctf/forensics/flash/challenge.json +9 -0
  121. package/python/tests/test_data/data_sources/ctf/forensics/flash/flash_c8429a430278283c0e571baebca3d139.zip +0 -0
  122. package/python/tests/test_data/data_sources/ctf/misc/networking_1/README.md +15 -0
  123. package/python/tests/test_data/data_sources/ctf/misc/networking_1/challenge.json +10 -0
  124. package/python/tests/test_data/data_sources/ctf/misc/networking_1/networking.pcap +0 -0
  125. package/python/tests/test_data/data_sources/ctf/pwn/warmup/Dockerfile +28 -0
  126. package/python/tests/test_data/data_sources/ctf/pwn/warmup/README.md +14 -0
  127. package/python/tests/test_data/data_sources/ctf/pwn/warmup/challenge.json +14 -0
  128. package/python/tests/test_data/data_sources/ctf/pwn/warmup/docker-compose.yml +14 -0
  129. package/python/tests/test_data/data_sources/ctf/pwn/warmup/flag.txt +1 -0
  130. package/python/tests/test_data/data_sources/ctf/pwn/warmup/warmup +0 -0
  131. package/python/tests/test_data/data_sources/ctf/pwn/warmup/warmup.c +26 -0
  132. package/python/tests/test_data/data_sources/ctf/pwn/warmup/warmup.py +9 -0
  133. package/python/tests/test_data/data_sources/ctf/rev/rock/README.md +14 -0
  134. package/python/tests/test_data/data_sources/ctf/rev/rock/challenge.json +8 -0
  135. package/python/tests/test_data/data_sources/ctf/rev/rock/rock +0 -0
  136. package/python/tests/test_data/data_sources/ctf/rev/rock/rock.cpp +167 -0
  137. package/python/tests/test_data/data_sources/ctf/rev/rock/solution.cpp +24 -0
  138. package/python/tests/test_data/data_sources/ctf/rev/rock/test_solver/solution.py +6 -0
  139. package/python/tests/test_data/data_sources/ctf/rev/rock/test_solver/test.sh +10 -0
  140. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/000-default.conf +18 -0
  141. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/Dockerfile +20 -0
  142. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/cgi/file.pl +38 -0
  143. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/cgi/forms.pl +40 -0
  144. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/cgi/hello.pl +11 -0
  145. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/challenge.json +12 -0
  146. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/docker-compose.yml +14 -0
  147. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/flag +1 -0
  148. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/index.html +11 -0
  149. package/python/tests/test_data/data_sources/ctf/web/i_got_id_demo/solution.txt +1 -0
  150. package/python/tests/test_data/data_sources/debug_20240322.json +1 -0
  151. package/python/tests/test_data/data_sources/expert_instances.yaml +16 -0
  152. package/python/tests/test_data/data_sources/human_eval.json +1 -0
  153. package/python/tests/test_data/data_sources/simple_instances.yaml +3 -0
  154. package/python/tests/test_data/data_sources/simple_instances_long.yaml +30 -0
  155. package/python/tests/test_data/data_sources/swe-bench-dev-easy.json +1 -0
  156. package/python/tests/test_data/data_sources/swe-bench-dev-easy_first_only.json +1 -0
  157. package/python/tests/test_data/data_sources/swe-bench-lite-test.json +1 -0
  158. 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
  159. 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
  160. 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
  161. 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
  162. 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
  163. 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
  164. 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
  165. 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
  166. package/python/tests/test_env.py +66 -0
  167. package/python/tests/test_env_utils.py +129 -0
  168. package/python/tests/test_history_processors.py +40 -0
  169. package/python/tests/test_models.py +23 -0
  170. package/python/tests/test_openai_live.py +164 -0
  171. package/python/tests/test_packaging.py +7 -0
  172. package/python/tests/test_parsing.py +131 -0
  173. package/python/tests/test_problem_statement_multimodal.py +111 -0
  174. package/python/tests/test_quick_stats.py +42 -0
  175. package/python/tests/test_run.py +37 -0
  176. package/python/tests/test_run_batch.py +110 -0
  177. package/python/tests/test_run_hooks.py +114 -0
  178. package/python/tests/test_run_replay.py +33 -0
  179. package/python/tests/test_run_single.py +125 -0
  180. package/python/tests/test_tools_command_parsing.py +193 -0
  181. package/python/tests/test_utils.py +15 -0
  182. package/python/tests/tools/__init__.py +0 -0
  183. package/python/tests/tools/conftest.py +12 -0
  184. package/python/tests/tools/test_default_utils.py +153 -0
  185. package/python/tests/tools/test_edit_replace.py +0 -0
  186. package/python/tests/tools/test_split_string.py +82 -0
  187. package/python/tests/utils.py +29 -0
  188. package/python/tools/diff_state/bin/_state_diff_state +52 -0
  189. package/python/tools/diff_state/config.yaml +2 -0
  190. package/python/tools/edit_anthropic/bin/_state_anthropic +21 -0
  191. package/python/tools/edit_anthropic/bin/str_replace_editor +710 -0
  192. package/python/tools/edit_anthropic/config.yaml +56 -0
  193. package/python/tools/edit_anthropic/install.sh +3 -0
  194. package/python/tools/filemap/bin/filemap +45 -0
  195. package/python/tools/filemap/config.yaml +9 -0
  196. package/python/tools/filemap/install.sh +2 -0
  197. package/python/tools/forfeit/bin/exit_forfeit +5 -0
  198. package/python/tools/forfeit/config.yaml +5 -0
  199. package/python/tools/image_tools/bin/view_image +36 -0
  200. package/python/tools/image_tools/config.yaml +9 -0
  201. package/python/tools/multilingual_setup/bin/do_nothing +2 -0
  202. package/python/tools/multilingual_setup/config.yaml +1 -0
  203. package/python/tools/multilingual_setup/install.sh +45 -0
  204. package/python/tools/registry/bin/_read_env +10 -0
  205. package/python/tools/registry/bin/_write_env +10 -0
  206. package/python/tools/registry/config.yaml +1 -0
  207. package/python/tools/registry/install.sh +6 -0
  208. package/python/tools/registry/lib/__init__.py +0 -0
  209. package/python/tools/registry/lib/registry.py +56 -0
  210. package/python/tools/review_on_submit_m/README.md +6 -0
  211. package/python/tools/review_on_submit_m/bin/submit +54 -0
  212. package/python/tools/review_on_submit_m/config.yaml +6 -0
  213. package/python/tools/review_on_submit_m/install.sh +0 -0
  214. package/python/tools/search/bin/find_file +31 -0
  215. package/python/tools/search/bin/search_dir +39 -0
  216. package/python/tools/search/bin/search_file +55 -0
  217. package/python/tools/search/config.yaml +37 -0
  218. package/python/tools/search/install.sh +3 -0
  219. package/python/tools/submit/bin/submit +17 -0
  220. package/python/tools/submit/config.yaml +5 -0
  221. package/python/tools/web_browser/bin/click_mouse +41 -0
  222. package/python/tools/web_browser/bin/close_site +28 -0
  223. package/python/tools/web_browser/bin/double_click_mouse +37 -0
  224. package/python/tools/web_browser/bin/drag_mouse +46 -0
  225. package/python/tools/web_browser/bin/execute_script_on_page +39 -0
  226. package/python/tools/web_browser/bin/get_console_output +48 -0
  227. package/python/tools/web_browser/bin/move_mouse +35 -0
  228. package/python/tools/web_browser/bin/navigate_back +33 -0
  229. package/python/tools/web_browser/bin/navigate_forward +33 -0
  230. package/python/tools/web_browser/bin/open_site +36 -0
  231. package/python/tools/web_browser/bin/press_keys_on_page +51 -0
  232. package/python/tools/web_browser/bin/reload_page +33 -0
  233. package/python/tools/web_browser/bin/run_web_browser_server +394 -0
  234. package/python/tools/web_browser/bin/screenshot_site +38 -0
  235. package/python/tools/web_browser/bin/scroll_on_page +40 -0
  236. package/python/tools/web_browser/bin/set_browser_window_size +40 -0
  237. package/python/tools/web_browser/bin/type_text +34 -0
  238. package/python/tools/web_browser/bin/wait_time +39 -0
  239. package/python/tools/web_browser/config.yaml +155 -0
  240. package/python/tools/web_browser/install.sh +22 -0
  241. package/python/tools/web_browser/lib/browser_manager.py +404 -0
  242. package/python/tools/web_browser/lib/web_browser_config.py +33 -0
  243. package/python/tools/web_browser/lib/web_browser_utils.py +126 -0
  244. package/python/tools/web_browser/test_console.html +1 -0
  245. package/python/tools/windowed/bin/_state +25 -0
  246. package/python/tools/windowed/bin/create +29 -0
  247. package/python/tools/windowed/bin/goto +37 -0
  248. package/python/tools/windowed/bin/open +49 -0
  249. package/python/tools/windowed/bin/scroll_down +12 -0
  250. package/python/tools/windowed/bin/scroll_up +13 -0
  251. package/python/tools/windowed/config.yaml +38 -0
  252. package/python/tools/windowed/install.sh +15 -0
  253. package/python/tools/windowed/lib/__init__.py +0 -0
  254. package/python/tools/windowed/lib/flake8_utils.py +147 -0
  255. package/python/tools/windowed/lib/windowed_file.py +312 -0
  256. package/python/tools/windowed_edit_linting/bin/edit +128 -0
  257. package/python/tools/windowed_edit_linting/config.yaml +31 -0
  258. package/python/tools/windowed_edit_linting/install.sh +5 -0
  259. package/python/tools/windowed_edit_replace/bin/edit +172 -0
  260. package/python/tools/windowed_edit_replace/bin/insert +77 -0
  261. package/python/tools/windowed_edit_replace/config.yaml +60 -0
  262. package/python/tools/windowed_edit_replace/install.sh +5 -0
  263. package/python/tools/windowed_edit_rewrite/bin/edit +78 -0
  264. package/python/tools/windowed_edit_rewrite/config.yaml +11 -0
  265. package/python/tools/windowed_edit_rewrite/install.sh +5 -0
  266. package/python/trajectories/demonstrations/ctf/crypto/BabyEncryption.traj +318 -0
  267. package/python/trajectories/demonstrations/ctf/crypto/BabyTimeCapsule.traj +197 -0
  268. package/python/trajectories/demonstrations/ctf/crypto/eps.traj +289 -0
  269. package/python/trajectories/demonstrations/ctf/crypto/katy.traj +368 -0
  270. package/python/trajectories/demonstrations/ctf/forensics/flash.traj +102 -0
  271. package/python/trajectories/demonstrations/ctf/misc/networking_1.traj +102 -0
  272. package/python/trajectories/demonstrations/ctf/pwn/warmup.traj +159 -0
  273. package/python/trajectories/demonstrations/ctf/rev/rock.traj +251 -0
  274. package/python/trajectories/demonstrations/ctf/web/i_got_id_demo.traj +422 -0
  275. package/python/trajectories/demonstrations/function_calling_simple.traj +151 -0
  276. 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
  277. 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
  278. 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
  279. 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
  280. package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__function_calling__install-1/marshmallow-code__marshmallow-1867.traj +594 -0
  281. package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__function_calling_replace__install-1/marshmallow-code__marshmallow-1867.traj +592 -0
  282. package/python/trajectories/demonstrations/replay__marshmallow-code__marshmallow-1867__function_calling_replace_from_source/marshmallow-code__marshmallow-1867.traj +3316 -0
  283. 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
  284. 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
  285. package/python/trajectories/demonstrations/str_replace_anthropic_demo.yaml +432 -0
  286. package/rust/Cargo.toml +100 -0
  287. package/rust/README.md +49 -0
  288. package/rust/src/agent/action_sampler.rs +130 -0
  289. package/rust/src/agent/agents.rs +1029 -0
  290. package/rust/src/agent/history_processors.rs +277 -0
  291. package/rust/src/agent/hooks/mod.rs +208 -0
  292. package/rust/src/agent/mod.rs +24 -0
  293. package/rust/src/agent/models.rs +837 -0
  294. package/rust/src/agent/problem_statement.rs +355 -0
  295. package/rust/src/agent/reviewer.rs +505 -0
  296. package/rust/src/bin/sweagent.rs +784 -0
  297. package/rust/src/environment/deployment.rs +631 -0
  298. package/rust/src/environment/hooks/mod.rs +114 -0
  299. package/rust/src/environment/mod.rs +16 -0
  300. package/rust/src/environment/repo.rs +265 -0
  301. package/rust/src/environment/runtime.rs +237 -0
  302. package/rust/src/environment/swe_env.rs +248 -0
  303. package/rust/src/exceptions.rs +228 -0
  304. package/rust/src/lib.rs +68 -0
  305. package/rust/src/monitoring.rs +482 -0
  306. package/rust/src/run/hooks/mod.rs +134 -0
  307. package/rust/src/run/mod.rs +12 -0
  308. package/rust/src/run/run_batch.rs +563 -0
  309. package/rust/src/run/run_single.rs +196 -0
  310. package/rust/src/tools/bundle.rs +224 -0
  311. package/rust/src/tools/commands.rs +173 -0
  312. package/rust/src/tools/mod.rs +295 -0
  313. package/rust/src/tools/parsing.rs +354 -0
  314. package/rust/src/tools/registry.rs +143 -0
  315. package/rust/src/types.rs +554 -0
  316. package/rust/src/utils/config.rs +105 -0
  317. package/rust/src/utils/files.rs +137 -0
  318. package/rust/src/utils/github.rs +171 -0
  319. package/rust/src/utils/log.rs +65 -0
  320. package/rust/src/utils/mod.rs +17 -0
  321. package/rust/src/utils/serialization.rs +181 -0
  322. package/rust/src/utils/template.rs +173 -0
  323. 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()