@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,80 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ from sweagent import REPO_ROOT
10
+ from sweagent.utils.log import get_logger
11
+
12
+ logger = get_logger("swea-config", emoji="🔧")
13
+
14
+
15
+ def _convert_path_relative_to_repo_root(path: Path | str, root: Path | None = None) -> Path | str:
16
+ original_type = type(path)
17
+ path = Path(path).resolve()
18
+ root = Path(root or os.getenv("SWE_AGENT_CONFIG_ROOT", REPO_ROOT))
19
+ relative_path = path.relative_to(root) if root in path.parents else path
20
+ return relative_path if original_type is Path else str(relative_path)
21
+
22
+
23
+ def _could_be_a_path(v: Any) -> bool:
24
+ try:
25
+ return Path(v).exists()
26
+ except Exception:
27
+ return False
28
+
29
+
30
+ def _strip_abspath_from_dict(value: dict | list | str, root: Path | None = None) -> dict | list | str:
31
+ root = Path(root or os.getenv("SWE_AGENT_CONFIG_ROOT", REPO_ROOT))
32
+ if isinstance(value, dict):
33
+ return {k: _strip_abspath_from_dict(v, root) for k, v in value.items()}
34
+ elif isinstance(value, list):
35
+ return [_strip_abspath_from_dict(v, root) for v in value]
36
+ elif isinstance(value, str) and _could_be_a_path(value):
37
+ return _convert_path_relative_to_repo_root(value, root)
38
+ else:
39
+ return value
40
+
41
+
42
+ def _convert_path_to_abspath(path: Path | str) -> Path:
43
+ """If path is not absolute, convert it to an absolute path
44
+ using the SWE_AGENT_CONFIG_ROOT environment variable (if set) or
45
+ REPO_ROOT as base.
46
+ """
47
+ path = Path(path)
48
+ root = Path(os.getenv("SWE_AGENT_CONFIG_ROOT", REPO_ROOT))
49
+ assert root.is_dir()
50
+ if not path.is_absolute():
51
+ path = root / path
52
+ assert path.is_absolute()
53
+ return path.resolve()
54
+
55
+
56
+ def _convert_paths_to_abspath(paths: list[Path] | list[str]) -> list[Path]:
57
+ return [_convert_path_to_abspath(p) for p in paths]
58
+
59
+
60
+ def load_environment_variables(path: Path | None = None):
61
+ """Load environment variables from a .env file.
62
+ If path is not provided, we first look for a .env file in the current working
63
+ directory and then in the repository root.
64
+ """
65
+ if path is None:
66
+ cwd_path = Path.cwd() / ".env"
67
+ repo_path = REPO_ROOT / ".env"
68
+ if cwd_path.exists():
69
+ path = cwd_path
70
+ elif repo_path.exists():
71
+ path = REPO_ROOT / ".env"
72
+ else:
73
+ logger.debug("No .env file found")
74
+ return
75
+ if not path.is_file():
76
+ msg = f"No .env file found at {path}"
77
+ raise FileNotFoundError(msg)
78
+ anything_loaded = load_dotenv(dotenv_path=path)
79
+ if anything_loaded:
80
+ logger.info(f"Loaded environment variables from {path}")
@@ -0,0 +1,27 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import yaml
6
+
7
+
8
+ def load_file(path: Path | str | None) -> Any:
9
+ """Load files based on their extension."""
10
+ if path is None:
11
+ return None
12
+ if isinstance(path, str):
13
+ path = Path(path)
14
+ if not path.exists():
15
+ raise FileNotFoundError(path)
16
+ if path.is_dir():
17
+ from datasets import load_from_disk
18
+
19
+ return load_from_disk(path)
20
+ if path.suffix in [".json", ".traj"]:
21
+ return json.loads(path.read_text())
22
+ if path.suffix == ".jsonl":
23
+ return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
24
+ if path.suffix == ".yaml":
25
+ return yaml.safe_load(path.read_text())
26
+ msg = f"Unsupported file extension: {path.suffix}"
27
+ raise NotImplementedError(msg)
@@ -0,0 +1,118 @@
1
+ import re
2
+
3
+ from ghapi.all import GhApi
4
+
5
+ GITHUB_ISSUE_URL_PATTERN = re.compile(r"github\.com\/(.*?)\/(.*?)\/issues\/(\d+)")
6
+
7
+
8
+ class InvalidGithubURL(Exception):
9
+ """Raised when a github URL is invalid"""
10
+
11
+
12
+ GITHUB_REPO_URL_PATTERN = re.compile(r".*[/@]?github\.com\/([^/]+)\/([^/]+)")
13
+
14
+
15
+ def _is_github_repo_url(data_path: str) -> bool:
16
+ """Check if data_path is an URL pointing to a github repository.
17
+ Paths to issues or PRs will also match this pattern.
18
+ """
19
+ return GITHUB_REPO_URL_PATTERN.search(data_path) is not None
20
+
21
+
22
+ def _is_github_issue_url(data_path: str) -> bool:
23
+ """Check if data_path is an URL pointing to a github issue"""
24
+ return GITHUB_ISSUE_URL_PATTERN.search(data_path) is not None
25
+
26
+
27
+ def _get_commit(api: GhApi, owner: str, repo: str, ref: str | None = None):
28
+ """Get commit object from github api
29
+
30
+ Args:
31
+ api (GhApi):
32
+ owner (str): Repo owner, e.g., "SWE-agent"
33
+ repo (str): Repo, e.g., "SWE-agent"
34
+ ref (str, optional): Branch, tag or commit hash
35
+
36
+ Returns:
37
+ _type_: _description_
38
+ """
39
+ if ref:
40
+ return api.repos.get_commit(owner, repo, ref) # type: ignore
41
+ return api.repos.list_commits(owner, repo)[0] # type: ignore
42
+
43
+
44
+ def _parse_gh_issue_url(issue_url: str) -> tuple[str, str, str]:
45
+ """
46
+ Returns:
47
+ owner: Repo owner
48
+ repo: Repo name
49
+ issue number: Issue number as str
50
+
51
+ Raises:
52
+ InvalidGithubURL: If the URL is not a valid github issue URL
53
+ """
54
+ match = GITHUB_ISSUE_URL_PATTERN.search(issue_url)
55
+ if not match:
56
+ msg = f"Invalid GitHub issue URL: {issue_url}"
57
+ raise InvalidGithubURL(msg)
58
+ res = match.groups()
59
+ assert len(res) == 3
60
+ return tuple(res) # type: ignore
61
+
62
+
63
+ def _parse_gh_repo_url(repo_url: str) -> tuple[str, str]:
64
+ """
65
+ Returns:
66
+ owner: Repo owner/org
67
+ repo: Repo name
68
+
69
+ Raises:
70
+ InvalidGithubURL: If the URL is not a valid github repo URL
71
+ """
72
+ match = GITHUB_REPO_URL_PATTERN.search(repo_url)
73
+ if not match:
74
+ msg = f"Invalid GitHub issue URL: {repo_url}"
75
+ raise InvalidGithubURL(msg)
76
+ res = match.groups()
77
+ assert len(res) == 2
78
+ return tuple(res) # type: ignore
79
+
80
+
81
+ def _get_gh_issue_data(issue_url: str, *, token: str = ""):
82
+ """Returns github issue data in the form of a dictionary.
83
+ See https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue
84
+ for return format
85
+ """
86
+ owner, repo, issue_number = _parse_gh_issue_url(issue_url)
87
+ api = GhApi(token=token)
88
+ return api.issues.get(owner, repo, issue_number) # type: ignore
89
+
90
+
91
+ def _get_problem_statement_from_github_issue(
92
+ owner: str, repo: str, issue_number: str, *, token: str | None = ""
93
+ ) -> str:
94
+ """Return problem statement from github issue"""
95
+ api = GhApi(token=token)
96
+ issue = api.issues.get(owner, repo, issue_number) # type: ignore
97
+ title = issue.title if issue.title else ""
98
+ body = issue.body if issue.body else ""
99
+ return f"{title}\n{body}\n"
100
+
101
+
102
+ def _get_associated_commit_urls(org: str, repo: str, issue_number: str, *, token: str = "") -> list[str]:
103
+ """Return the URLs of commits that would close an issue."""
104
+ api = GhApi(token=token)
105
+ # Strangely the "pull_request" field of api.issues.get is often not set
106
+ # so we have to go through the events to check if there's a commit
107
+ events = api.issues.list_events(org, repo, issue_number) # type: ignore
108
+ commit_urls = []
109
+ for event in events:
110
+ if event.event != "referenced":
111
+ continue
112
+ if not event.commit_id:
113
+ continue
114
+ commit = api.repos.get_commit(org, repo, event.commit_id) # type: ignore
115
+ message = commit.commit.message
116
+ if f"fixes #{issue_number}" in message.lower() or f"closes #{issue_number}" in message.lower():
117
+ commit_urls.append(commit.html_url)
118
+ return commit_urls
@@ -0,0 +1,14 @@
1
+ from sweagent.utils.log import get_logger
2
+
3
+
4
+ def _warn_probably_wrong_jinja_syntax(template: str | None) -> None:
5
+ """Warn if the template uses {var} instead of {{var}}."""
6
+ if template is None:
7
+ return
8
+ if "{" not in template:
9
+ return
10
+ for s in ["{%", "{ %", "{{"]:
11
+ if s in template:
12
+ return
13
+ logger = get_logger("swea-config", emoji="🔧")
14
+ logger.warning("Probably wrong Jinja syntax in template: %s. Make sure to use {{var}} instead of {var}.", template)
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import threading
6
+ import uuid
7
+ from collections.abc import Callable
8
+ from pathlib import Path, PurePath
9
+
10
+ from rich.logging import RichHandler
11
+ from rich.text import Text
12
+
13
+ _SET_UP_LOGGERS: set[str] = set()
14
+ _ADDITIONAL_HANDLERS: dict[str, logging.Handler] = {}
15
+ _LOG_LOCK = threading.Lock()
16
+
17
+ logging.TRACE = 5 # type: ignore
18
+ logging.addLevelName(logging.TRACE, "TRACE") # type: ignore
19
+
20
+
21
+ def _interpret_level(level: int | str | None, *, default=logging.DEBUG) -> int:
22
+ if not level:
23
+ return default
24
+ if isinstance(level, int):
25
+ return level
26
+ if level.isnumeric():
27
+ return int(level)
28
+ return getattr(logging, level.upper())
29
+
30
+
31
+ _STREAM_LEVEL = _interpret_level(os.environ.get("SWE_AGENT_LOG_STREAM_LEVEL"))
32
+ _INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER = False
33
+
34
+ _THREAD_NAME_TO_LOG_SUFFIX: dict[str, str] = {}
35
+ """Mapping from thread name to suffix to add to the logger name."""
36
+
37
+
38
+ def register_thread_name(name: str) -> None:
39
+ """Register a suffix to add to the logger name for the current thread."""
40
+ thread_name = threading.current_thread().name
41
+ _THREAD_NAME_TO_LOG_SUFFIX[thread_name] = name
42
+
43
+
44
+ class _RichHandlerWithEmoji(RichHandler):
45
+ def __init__(self, emoji: str, *args, **kwargs):
46
+ """Subclass of RichHandler that adds an emoji to the log message."""
47
+ super().__init__(*args, **kwargs)
48
+ if not emoji.endswith(" "):
49
+ emoji += " "
50
+ self.emoji = emoji
51
+
52
+ def get_level_text(self, record: logging.LogRecord) -> Text:
53
+ level_name = record.levelname.replace("WARNING", "WARN")
54
+ return Text.styled((self.emoji + level_name).ljust(10), f"logging.level.{level_name.lower()}")
55
+
56
+
57
+ def get_logger(name: str, *, emoji: str = "") -> logging.Logger:
58
+ """Get logger. Use this instead of `logging.getLogger` to ensure
59
+ that the logger is set up with the correct handlers.
60
+ """
61
+ thread_name = threading.current_thread().name
62
+ if thread_name != "MainThread":
63
+ name = name + "-" + _THREAD_NAME_TO_LOG_SUFFIX.get(thread_name, thread_name)
64
+ logger = logging.getLogger(name)
65
+ if logger.hasHandlers():
66
+ # Already set up
67
+ return logger
68
+ handler = _RichHandlerWithEmoji(
69
+ emoji=emoji,
70
+ show_time=bool(os.environ.get("SWE_AGENT_LOG_TIME", False)),
71
+ show_path=False,
72
+ )
73
+ handler.setLevel(_STREAM_LEVEL)
74
+ # Set to lowest level and only use stream handlers to adjust levels
75
+ logger.setLevel(logging.TRACE) # type: ignore
76
+ logger.addHandler(handler)
77
+ logger.propagate = False
78
+ _SET_UP_LOGGERS.add(name)
79
+ with _LOG_LOCK:
80
+ for handler in _ADDITIONAL_HANDLERS.values():
81
+ my_filter = getattr(handler, "my_filter", None)
82
+ if my_filter is None:
83
+ logger.addHandler(handler)
84
+ elif isinstance(my_filter, str) and my_filter in name:
85
+ logger.addHandler(handler)
86
+ elif callable(my_filter) and my_filter(name):
87
+ logger.addHandler(handler)
88
+ if _INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER:
89
+ _add_logger_name_to_stream_handler(logger)
90
+ return logger
91
+
92
+
93
+ def add_file_handler(
94
+ path: PurePath | str,
95
+ *,
96
+ filter: str | Callable[[str], bool] | None = None,
97
+ level: int | str = logging.TRACE, # type: ignore[attr-defined]
98
+ id_: str = "",
99
+ ) -> str:
100
+ """Adds a file handler to all loggers that we have set up
101
+ and all future loggers that will be set up with `get_logger`.
102
+
103
+ Args:
104
+ filter: If str: Check that the logger name contains the filter string.
105
+ If callable: Check that the logger name satisfies the condition returned by the callable.
106
+ level: The level of the handler.
107
+ id_: The id of the handler. If not provided, a random id will be generated.
108
+
109
+ Returns:
110
+ The id of the handler. This can be used to remove the handler later.
111
+ """
112
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
113
+ handler = logging.FileHandler(path, encoding="utf-8")
114
+ formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
115
+ handler.setFormatter(formatter)
116
+ handler.setLevel(_interpret_level(level))
117
+ with _LOG_LOCK:
118
+ # Lock because other thread might be modifying the _SET_UP_LOGGERS set
119
+ for name in _SET_UP_LOGGERS:
120
+ if filter is not None:
121
+ if isinstance(filter, str) and filter not in name:
122
+ continue
123
+ if callable(filter) and not filter(name):
124
+ continue
125
+ logger = logging.getLogger(name)
126
+ logger.addHandler(handler)
127
+ handler.my_filter = filter # type: ignore
128
+ if not id_:
129
+ id_ = str(uuid.uuid4())
130
+ _ADDITIONAL_HANDLERS[id_] = handler
131
+ return id_
132
+
133
+
134
+ def remove_file_handler(id_: str) -> None:
135
+ """Remove a file handler by its id."""
136
+ handler = _ADDITIONAL_HANDLERS.pop(id_)
137
+ with _LOG_LOCK:
138
+ # Lock because other thread might be modifying the _SET_UP_LOGGERS set
139
+ for log_name in _SET_UP_LOGGERS:
140
+ logger = logging.getLogger(log_name)
141
+ logger.removeHandler(handler)
142
+
143
+
144
+ def _add_logger_name_to_stream_handler(logger: logging.Logger) -> None:
145
+ for handler in logger.handlers:
146
+ if isinstance(handler, _RichHandlerWithEmoji):
147
+ formatter = logging.Formatter("[%(name)s] %(message)s")
148
+ handler.setFormatter(formatter)
149
+
150
+
151
+ def add_logger_names_to_stream_handlers() -> None:
152
+ """Add the logger name to the stream handler for all loggers that we have set up."""
153
+ global _INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER
154
+ _INCLUDE_LOGGER_NAME_IN_STREAM_HANDLER = True
155
+ with _LOG_LOCK:
156
+ for logger in _SET_UP_LOGGERS:
157
+ _add_logger_name_to_stream_handler(logging.getLogger(logger))
158
+
159
+
160
+ def set_stream_handler_levels(level: int) -> None:
161
+ """Set the default stream level and adjust the levels of all stream handlers
162
+ to be at most the given level.
163
+
164
+ Note: Can only be used to lower the level, not raise it.
165
+ """
166
+ global _STREAM_LEVEL
167
+ _STREAM_LEVEL = level
168
+ with _LOG_LOCK:
169
+ for name in _SET_UP_LOGGERS:
170
+ logger = logging.getLogger(name)
171
+ for handler in logger.handlers:
172
+ if isinstance(handler, _RichHandlerWithEmoji):
173
+ current_level = handler.level
174
+ if current_level < level:
175
+ handler.setLevel(level)
@@ -0,0 +1,152 @@
1
+ from collections.abc import Callable
2
+
3
+ from unidiff import PatchSet
4
+
5
+
6
+ class PatchFormatter:
7
+ def __init__(
8
+ self,
9
+ patch: str,
10
+ read_method: Callable[[str], str],
11
+ ):
12
+ """Given the final patch and access to the container that contains the repository,
13
+ extract relevant lines from the modified file.
14
+
15
+ Args:
16
+ patch: The patch as a string.
17
+ read_method: Callable with path to file (relative to repository root) as argument
18
+ that returns the file content as a string.
19
+ """
20
+ self._patch = PatchSet(patch)
21
+ self._patched_files: dict[str, str] = {}
22
+ self._original_files: dict[str, str] = {}
23
+ self._patch_applied = True
24
+ self._read_file = read_method
25
+ self._read_files(original=False)
26
+
27
+ @staticmethod
28
+ def _merge_intervals(starts: list[int], stops: list[int]) -> tuple[list[int], list[int]]:
29
+ """Given two lists of integers, starts and stops, merges all overlapping intervals.
30
+
31
+ For example `starts=[1, 5, 18]`, `stops=[10, 13, 20]`
32
+ should return `starts=[1, 18]`, `stops=[13, 20]`
33
+ """
34
+ if not starts:
35
+ assert not stops
36
+ return [], []
37
+
38
+ intervals = sorted(zip(starts, stops))
39
+ merged = []
40
+ for start, stop in intervals:
41
+ if not merged or merged[-1][1] < start:
42
+ # No overlap
43
+ merged.append([start, stop])
44
+ else:
45
+ # Overlap
46
+ merged[-1][1] = max(merged[-1][1], stop)
47
+ # Unzip again
48
+ merged_starts, merged_stops = zip(*merged)
49
+ return list(merged_starts), list(merged_stops)
50
+
51
+ def format_file(self, text: str, starts: list[int], stops: list[int], *, linenos: bool = True) -> str:
52
+ """Reads file and returns string representation of the relevant lines.
53
+
54
+ Args:
55
+ path: The path to the file within the repo location
56
+ starts: The starting line numbers of the relevant lines. The first line is line 1.
57
+ stops: The stopping line numbers of the relevant lines. The stop is not inclusive.
58
+ The first line is line 1.
59
+ linenos: Whether to include line numbers
60
+ """
61
+ if not starts:
62
+ assert not stops
63
+ return ""
64
+
65
+ assert len(starts) == len(stops)
66
+ assert all(start >= 1 for start in starts)
67
+ assert all(start < stop for start, stop in zip(starts, stops))
68
+ starts, stops = self._merge_intervals(starts, stops)
69
+ assert all(hunk1_start < hunk2_start for hunk1_start, hunk2_start in zip(starts, starts[1:]))
70
+ out: list[str] = []
71
+ if starts[0] > 1:
72
+ # Count from 1
73
+ out.append(f"[{starts[0] - 1} lines above omitted]")
74
+ last_stop: int | None = None
75
+ lines = text.splitlines()
76
+ for start, stop in zip(starts, stops):
77
+ assert start >= 1
78
+ if last_stop is not None:
79
+ n_omitted = start - last_stop
80
+ # Check that we have non-overlapping hunks
81
+ assert n_omitted >= 0
82
+ if n_omitted:
83
+ out.append(f"\n[{n_omitted} lines omitted]\n")
84
+ # Count from 1
85
+ these_lines = lines[start - 1 : stop - 1]
86
+ if linenos:
87
+ out.append("\n".join([f"{i:6d}: {l}" for i, l in enumerate(these_lines, start=start)]))
88
+ else:
89
+ out.append("\n".join(these_lines))
90
+ last_stop = stop
91
+ if last_stop < len(lines):
92
+ # Stop is not inclusive
93
+ omitted = len(lines) - last_stop
94
+ assert omitted > 0
95
+ out.append(f"[{omitted} lines below omitted]")
96
+ return "\n".join(out)
97
+
98
+ def _get_hunk_lines(self, original: bool, *, context_length: int) -> dict[str, tuple[list[int], list[int]]]:
99
+ """Get the starts and stops for all files in the patch.
100
+
101
+ Args:
102
+ original: Whether to read the original file or the patched file
103
+ context_length: The number of lines to include above and below the hunk
104
+
105
+ Returns:
106
+ A dictionary with the file path as key and a tuple of lists of starts and stops as value.
107
+ """
108
+ out: dict[str, tuple[list[int], list[int]]] = {}
109
+ for patch in self._patch:
110
+ if not patch.is_modified_file:
111
+ continue
112
+ starts: list[int] = []
113
+ stops: list[int] = []
114
+ for hunk in patch:
115
+ if original:
116
+ # 1 is the lowest line number
117
+ start = max(1, hunk.source_start - context_length)
118
+ stop = hunk.source_start + hunk.source_length + context_length
119
+ else:
120
+ start = max(1, hunk.target_start - context_length)
121
+ stop = hunk.target_start + hunk.target_length + context_length
122
+ starts.append(start)
123
+ stops.append(stop)
124
+ out[patch.path] = (starts, stops)
125
+ return out
126
+
127
+ def _read_files(self, original: bool) -> None:
128
+ for patch in self._patch:
129
+ path = patch.path
130
+ if not patch.is_modified_file:
131
+ continue
132
+ if original:
133
+ msg = "Original file reading not implemented"
134
+ raise NotImplementedError(msg)
135
+ else:
136
+ assert self._patch_applied
137
+ self._patched_files[path] = self._read_file(path)
138
+
139
+ @staticmethod
140
+ def concat_files_strings(files: dict[str, str]) -> str:
141
+ """Concatenate multiple `read_files` outputs into a single string."""
142
+ out = []
143
+ for path, content in files.items():
144
+ out.append(f"[File: {path}]\n{content}")
145
+ return "\n\n".join(out)
146
+
147
+ def get_files_str(self, *, original: bool, context_length: int | None = 50, linenos: bool = True) -> str:
148
+ hunk_lines = self._get_hunk_lines(original=original, context_length=context_length)
149
+ sources = self._original_files if original else self._patched_files
150
+ return self.concat_files_strings(
151
+ {path: self.format_file(text, *hunk_lines[path], linenos=linenos) for path, text in sources.items()}
152
+ )
@@ -0,0 +1,45 @@
1
+ import io
2
+ from copy import deepcopy
3
+ from typing import Any
4
+
5
+ from ruamel.yaml import YAML
6
+ from ruamel.yaml.scalarstring import LiteralScalarString as LSS
7
+
8
+
9
+ def _convert_to_yaml_literal_string(d: Any) -> Any:
10
+ """Convert any multi-line strings in nested data object to LiteralScalarString.
11
+ This will then use the `|-` syntax of yaml.
12
+ """
13
+ d = deepcopy(d)
14
+ if isinstance(d, dict):
15
+ for key, value in d.items():
16
+ d[key] = _convert_to_yaml_literal_string(value)
17
+ elif isinstance(d, list):
18
+ for i, item in enumerate(d):
19
+ d[i] = _convert_to_yaml_literal_string(item)
20
+ elif isinstance(d, str) and "\n" in d:
21
+ d = LSS(d.replace("\r\n", "\n").replace("\r", "\n"))
22
+ return d
23
+
24
+
25
+ def _yaml_serialization_with_linebreaks(data: Any) -> str:
26
+ data = _convert_to_yaml_literal_string(data)
27
+ yaml = YAML()
28
+ yaml.indent(mapping=2, sequence=4, offset=2)
29
+ yaml.width = float("inf")
30
+ yaml.default_flow_style = False
31
+ buffer = io.StringIO()
32
+ yaml.dump(data, buffer)
33
+ return buffer.getvalue()
34
+
35
+
36
+ def merge_nested_dicts(d1: dict, d2: dict) -> dict:
37
+ """Merge two nested dictionaries, updating d1 in place.
38
+ If a key exists in both dictionaries, the value from d2 will be used.
39
+ """
40
+ for key, value in d2.items():
41
+ if isinstance(value, dict):
42
+ d1[key] = merge_nested_dicts(d1.get(key, {}), value)
43
+ else:
44
+ d1[key] = value
45
+ return d1
File without changes