aider-ce 0.88.20__py3-none-any.whl

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 (279) hide show
  1. aider/__init__.py +20 -0
  2. aider/__main__.py +4 -0
  3. aider/_version.py +34 -0
  4. aider/analytics.py +258 -0
  5. aider/args.py +1056 -0
  6. aider/args_formatter.py +228 -0
  7. aider/change_tracker.py +133 -0
  8. aider/coders/__init__.py +36 -0
  9. aider/coders/agent_coder.py +2166 -0
  10. aider/coders/agent_prompts.py +104 -0
  11. aider/coders/architect_coder.py +48 -0
  12. aider/coders/architect_prompts.py +40 -0
  13. aider/coders/ask_coder.py +9 -0
  14. aider/coders/ask_prompts.py +35 -0
  15. aider/coders/base_coder.py +3613 -0
  16. aider/coders/base_prompts.py +87 -0
  17. aider/coders/chat_chunks.py +64 -0
  18. aider/coders/context_coder.py +53 -0
  19. aider/coders/context_prompts.py +75 -0
  20. aider/coders/editblock_coder.py +657 -0
  21. aider/coders/editblock_fenced_coder.py +10 -0
  22. aider/coders/editblock_fenced_prompts.py +143 -0
  23. aider/coders/editblock_func_coder.py +141 -0
  24. aider/coders/editblock_func_prompts.py +27 -0
  25. aider/coders/editblock_prompts.py +175 -0
  26. aider/coders/editor_diff_fenced_coder.py +9 -0
  27. aider/coders/editor_diff_fenced_prompts.py +11 -0
  28. aider/coders/editor_editblock_coder.py +9 -0
  29. aider/coders/editor_editblock_prompts.py +21 -0
  30. aider/coders/editor_whole_coder.py +9 -0
  31. aider/coders/editor_whole_prompts.py +12 -0
  32. aider/coders/help_coder.py +16 -0
  33. aider/coders/help_prompts.py +46 -0
  34. aider/coders/patch_coder.py +706 -0
  35. aider/coders/patch_prompts.py +159 -0
  36. aider/coders/search_replace.py +757 -0
  37. aider/coders/shell.py +37 -0
  38. aider/coders/single_wholefile_func_coder.py +102 -0
  39. aider/coders/single_wholefile_func_prompts.py +27 -0
  40. aider/coders/udiff_coder.py +429 -0
  41. aider/coders/udiff_prompts.py +115 -0
  42. aider/coders/udiff_simple.py +14 -0
  43. aider/coders/udiff_simple_prompts.py +25 -0
  44. aider/coders/wholefile_coder.py +144 -0
  45. aider/coders/wholefile_func_coder.py +134 -0
  46. aider/coders/wholefile_func_prompts.py +27 -0
  47. aider/coders/wholefile_prompts.py +65 -0
  48. aider/commands.py +2173 -0
  49. aider/copypaste.py +72 -0
  50. aider/deprecated.py +126 -0
  51. aider/diffs.py +128 -0
  52. aider/dump.py +29 -0
  53. aider/editor.py +147 -0
  54. aider/exceptions.py +115 -0
  55. aider/format_settings.py +26 -0
  56. aider/gui.py +545 -0
  57. aider/help.py +163 -0
  58. aider/help_pats.py +19 -0
  59. aider/helpers/__init__.py +9 -0
  60. aider/helpers/similarity.py +98 -0
  61. aider/history.py +180 -0
  62. aider/io.py +1608 -0
  63. aider/linter.py +304 -0
  64. aider/llm.py +55 -0
  65. aider/main.py +1415 -0
  66. aider/mcp/__init__.py +174 -0
  67. aider/mcp/server.py +149 -0
  68. aider/mdstream.py +243 -0
  69. aider/models.py +1313 -0
  70. aider/onboarding.py +429 -0
  71. aider/openrouter.py +129 -0
  72. aider/prompts.py +56 -0
  73. aider/queries/tree-sitter-language-pack/README.md +7 -0
  74. aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  78. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  79. aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  80. aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  81. aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  82. aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  83. aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  84. aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  85. aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  86. aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  87. aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  88. aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  89. aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  90. aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  91. aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  92. aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  93. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  94. aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  95. aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  96. aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  97. aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  98. aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  99. aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  100. aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  101. aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  102. aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  103. aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  104. aider/queries/tree-sitter-languages/README.md +24 -0
  105. aider/queries/tree-sitter-languages/c-tags.scm +9 -0
  106. aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  107. aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  108. aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
  109. aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  110. aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  111. aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
  112. aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  113. aider/queries/tree-sitter-languages/go-tags.scm +30 -0
  114. aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  115. aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  116. aider/queries/tree-sitter-languages/java-tags.scm +20 -0
  117. aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  118. aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
  119. aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  120. aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  121. aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  122. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  123. aider/queries/tree-sitter-languages/php-tags.scm +26 -0
  124. aider/queries/tree-sitter-languages/python-tags.scm +12 -0
  125. aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
  126. aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  127. aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
  128. aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
  129. aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  130. aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
  131. aider/reasoning_tags.py +82 -0
  132. aider/repo.py +621 -0
  133. aider/repomap.py +1174 -0
  134. aider/report.py +260 -0
  135. aider/resources/__init__.py +3 -0
  136. aider/resources/model-metadata.json +776 -0
  137. aider/resources/model-settings.yml +2068 -0
  138. aider/run_cmd.py +133 -0
  139. aider/scrape.py +293 -0
  140. aider/sendchat.py +242 -0
  141. aider/sessions.py +256 -0
  142. aider/special.py +203 -0
  143. aider/tools/__init__.py +72 -0
  144. aider/tools/command.py +105 -0
  145. aider/tools/command_interactive.py +122 -0
  146. aider/tools/delete_block.py +182 -0
  147. aider/tools/delete_line.py +155 -0
  148. aider/tools/delete_lines.py +184 -0
  149. aider/tools/extract_lines.py +341 -0
  150. aider/tools/finished.py +48 -0
  151. aider/tools/git_branch.py +129 -0
  152. aider/tools/git_diff.py +60 -0
  153. aider/tools/git_log.py +57 -0
  154. aider/tools/git_remote.py +53 -0
  155. aider/tools/git_show.py +51 -0
  156. aider/tools/git_status.py +46 -0
  157. aider/tools/grep.py +256 -0
  158. aider/tools/indent_lines.py +221 -0
  159. aider/tools/insert_block.py +288 -0
  160. aider/tools/list_changes.py +86 -0
  161. aider/tools/ls.py +93 -0
  162. aider/tools/make_editable.py +85 -0
  163. aider/tools/make_readonly.py +69 -0
  164. aider/tools/remove.py +91 -0
  165. aider/tools/replace_all.py +126 -0
  166. aider/tools/replace_line.py +173 -0
  167. aider/tools/replace_lines.py +217 -0
  168. aider/tools/replace_text.py +187 -0
  169. aider/tools/show_numbered_context.py +147 -0
  170. aider/tools/tool_utils.py +313 -0
  171. aider/tools/undo_change.py +95 -0
  172. aider/tools/update_todo_list.py +156 -0
  173. aider/tools/view.py +57 -0
  174. aider/tools/view_files_matching.py +141 -0
  175. aider/tools/view_files_with_symbol.py +129 -0
  176. aider/urls.py +17 -0
  177. aider/utils.py +456 -0
  178. aider/versioncheck.py +113 -0
  179. aider/voice.py +205 -0
  180. aider/waiting.py +38 -0
  181. aider/watch.py +318 -0
  182. aider/watch_prompts.py +12 -0
  183. aider/website/Gemfile +8 -0
  184. aider/website/_includes/blame.md +162 -0
  185. aider/website/_includes/get-started.md +22 -0
  186. aider/website/_includes/help-tip.md +5 -0
  187. aider/website/_includes/help.md +24 -0
  188. aider/website/_includes/install.md +5 -0
  189. aider/website/_includes/keys.md +4 -0
  190. aider/website/_includes/model-warnings.md +67 -0
  191. aider/website/_includes/multi-line.md +22 -0
  192. aider/website/_includes/python-m-aider.md +5 -0
  193. aider/website/_includes/recording.css +228 -0
  194. aider/website/_includes/recording.md +34 -0
  195. aider/website/_includes/replit-pipx.md +9 -0
  196. aider/website/_includes/works-best.md +1 -0
  197. aider/website/_sass/custom/custom.scss +103 -0
  198. aider/website/docs/config/adv-model-settings.md +2261 -0
  199. aider/website/docs/config/agent-mode.md +194 -0
  200. aider/website/docs/config/aider_conf.md +548 -0
  201. aider/website/docs/config/api-keys.md +90 -0
  202. aider/website/docs/config/dotenv.md +493 -0
  203. aider/website/docs/config/editor.md +127 -0
  204. aider/website/docs/config/mcp.md +95 -0
  205. aider/website/docs/config/model-aliases.md +104 -0
  206. aider/website/docs/config/options.md +890 -0
  207. aider/website/docs/config/reasoning.md +210 -0
  208. aider/website/docs/config.md +44 -0
  209. aider/website/docs/faq.md +384 -0
  210. aider/website/docs/git.md +76 -0
  211. aider/website/docs/index.md +47 -0
  212. aider/website/docs/install/codespaces.md +39 -0
  213. aider/website/docs/install/docker.md +57 -0
  214. aider/website/docs/install/optional.md +100 -0
  215. aider/website/docs/install/replit.md +8 -0
  216. aider/website/docs/install.md +115 -0
  217. aider/website/docs/languages.md +264 -0
  218. aider/website/docs/legal/contributor-agreement.md +111 -0
  219. aider/website/docs/legal/privacy.md +104 -0
  220. aider/website/docs/llms/anthropic.md +77 -0
  221. aider/website/docs/llms/azure.md +48 -0
  222. aider/website/docs/llms/bedrock.md +132 -0
  223. aider/website/docs/llms/cohere.md +34 -0
  224. aider/website/docs/llms/deepseek.md +32 -0
  225. aider/website/docs/llms/gemini.md +49 -0
  226. aider/website/docs/llms/github.md +111 -0
  227. aider/website/docs/llms/groq.md +36 -0
  228. aider/website/docs/llms/lm-studio.md +39 -0
  229. aider/website/docs/llms/ollama.md +75 -0
  230. aider/website/docs/llms/openai-compat.md +39 -0
  231. aider/website/docs/llms/openai.md +58 -0
  232. aider/website/docs/llms/openrouter.md +78 -0
  233. aider/website/docs/llms/other.md +117 -0
  234. aider/website/docs/llms/vertex.md +50 -0
  235. aider/website/docs/llms/warnings.md +10 -0
  236. aider/website/docs/llms/xai.md +53 -0
  237. aider/website/docs/llms.md +54 -0
  238. aider/website/docs/more/analytics.md +127 -0
  239. aider/website/docs/more/edit-formats.md +116 -0
  240. aider/website/docs/more/infinite-output.md +165 -0
  241. aider/website/docs/more-info.md +8 -0
  242. aider/website/docs/recordings/auto-accept-architect.md +31 -0
  243. aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
  244. aider/website/docs/recordings/index.md +21 -0
  245. aider/website/docs/recordings/model-accepts-settings.md +69 -0
  246. aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
  247. aider/website/docs/repomap.md +112 -0
  248. aider/website/docs/scripting.md +100 -0
  249. aider/website/docs/sessions.md +203 -0
  250. aider/website/docs/troubleshooting/aider-not-found.md +24 -0
  251. aider/website/docs/troubleshooting/edit-errors.md +76 -0
  252. aider/website/docs/troubleshooting/imports.md +62 -0
  253. aider/website/docs/troubleshooting/models-and-keys.md +54 -0
  254. aider/website/docs/troubleshooting/support.md +79 -0
  255. aider/website/docs/troubleshooting/token-limits.md +96 -0
  256. aider/website/docs/troubleshooting/warnings.md +12 -0
  257. aider/website/docs/troubleshooting.md +11 -0
  258. aider/website/docs/usage/browser.md +57 -0
  259. aider/website/docs/usage/caching.md +49 -0
  260. aider/website/docs/usage/commands.md +133 -0
  261. aider/website/docs/usage/conventions.md +119 -0
  262. aider/website/docs/usage/copypaste.md +121 -0
  263. aider/website/docs/usage/images-urls.md +48 -0
  264. aider/website/docs/usage/lint-test.md +118 -0
  265. aider/website/docs/usage/modes.md +211 -0
  266. aider/website/docs/usage/not-code.md +179 -0
  267. aider/website/docs/usage/notifications.md +87 -0
  268. aider/website/docs/usage/tips.md +79 -0
  269. aider/website/docs/usage/tutorials.md +30 -0
  270. aider/website/docs/usage/voice.md +121 -0
  271. aider/website/docs/usage/watch.md +294 -0
  272. aider/website/docs/usage.md +102 -0
  273. aider/website/share/index.md +101 -0
  274. aider_ce-0.88.20.dist-info/METADATA +187 -0
  275. aider_ce-0.88.20.dist-info/RECORD +279 -0
  276. aider_ce-0.88.20.dist-info/WHEEL +5 -0
  277. aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
  278. aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
  279. aider_ce-0.88.20.dist-info/top_level.txt +1 -0
aider/run_cmd.py ADDED
@@ -0,0 +1,133 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+ import sys
5
+ from io import BytesIO
6
+
7
+ import pexpect
8
+ import psutil
9
+
10
+
11
+ def run_cmd(command, verbose=False, error_print=None, cwd=None):
12
+ try:
13
+ if sys.stdin.isatty() and hasattr(pexpect, "spawn") and platform.system() != "Windows":
14
+ return run_cmd_pexpect(command, verbose, cwd)
15
+
16
+ return run_cmd_subprocess(command, verbose, cwd)
17
+ except OSError as e:
18
+ error_message = f"Error occurred while running command '{command}': {str(e)}"
19
+ if error_print is None:
20
+ print(error_message)
21
+ else:
22
+ error_print(error_message)
23
+ return 1, error_message
24
+
25
+
26
+ def get_windows_parent_process_name():
27
+ try:
28
+ current_process = psutil.Process()
29
+ while True:
30
+ parent = current_process.parent()
31
+ if parent is None:
32
+ break
33
+ parent_name = parent.name().lower()
34
+ if parent_name in ["powershell.exe", "cmd.exe"]:
35
+ return parent_name
36
+ current_process = parent
37
+ return None
38
+ except Exception:
39
+ return None
40
+
41
+
42
+ def run_cmd_subprocess(command, verbose=False, cwd=None, encoding=sys.stdout.encoding):
43
+ if verbose:
44
+ print("Using run_cmd_subprocess:", command)
45
+
46
+ try:
47
+ shell = os.environ.get("SHELL", "/bin/sh")
48
+ parent_process = None
49
+
50
+ # Determine the appropriate shell
51
+ if platform.system() == "Windows":
52
+ parent_process = get_windows_parent_process_name()
53
+ if parent_process == "powershell.exe":
54
+ command = f"powershell -Command {command}"
55
+
56
+ if verbose:
57
+ print("Running command:", command)
58
+ print("SHELL:", shell)
59
+ if platform.system() == "Windows":
60
+ print("Parent process:", parent_process)
61
+
62
+ process = subprocess.Popen(
63
+ command,
64
+ stdout=subprocess.PIPE,
65
+ stderr=subprocess.STDOUT,
66
+ text=True,
67
+ shell=True,
68
+ executable=shell if platform.system() != "Windows" else None,
69
+ encoding=encoding,
70
+ errors="replace",
71
+ bufsize=0, # Set bufsize to 0 for unbuffered output
72
+ universal_newlines=True,
73
+ cwd=cwd,
74
+ )
75
+
76
+ output = []
77
+ while True:
78
+ chunk = process.stdout.read(1)
79
+ if not chunk:
80
+ break
81
+ print(chunk, end="", flush=True) # Print the chunk in real-time
82
+ output.append(chunk) # Store the chunk for later use
83
+
84
+ process.wait()
85
+ return process.returncode, "".join(output)
86
+ except Exception as e:
87
+ return 1, str(e)
88
+
89
+
90
+ def run_cmd_pexpect(command, verbose=False, cwd=None):
91
+ """
92
+ Run a shell command interactively using pexpect, capturing all output.
93
+
94
+ :param command: The command to run as a string.
95
+ :param verbose: If True, print output in real-time.
96
+ :return: A tuple containing (exit_status, output)
97
+ """
98
+ if verbose:
99
+ print("Using run_cmd_pexpect:", command)
100
+
101
+ output = BytesIO()
102
+
103
+ def output_callback(b):
104
+ output.write(b)
105
+ return b
106
+
107
+ try:
108
+ # Use the SHELL environment variable, falling back to /bin/sh if not set
109
+ shell = os.environ.get("SHELL", "/bin/sh")
110
+ if verbose:
111
+ print("With shell:", shell)
112
+
113
+ if os.path.exists(shell):
114
+ # Use the shell from SHELL environment variable
115
+ if verbose:
116
+ print("Running pexpect.spawn with shell:", shell)
117
+ child = pexpect.spawn(shell, args=["-i", "-c", command], encoding="utf-8", cwd=cwd)
118
+ else:
119
+ # Fall back to spawning the command directly
120
+ if verbose:
121
+ print("Running pexpect.spawn without shell.")
122
+ child = pexpect.spawn(command, encoding="utf-8", cwd=cwd)
123
+
124
+ # Transfer control to the user, capturing output
125
+ child.interact(output_filter=output_callback)
126
+
127
+ # Wait for the command to finish and get the exit status
128
+ child.close()
129
+ return child.exitstatus, output.getvalue().decode("utf-8", errors="replace")
130
+
131
+ except (pexpect.ExceptionPexpect, TypeError, ValueError) as e:
132
+ error_msg = f"Error running command {command}: {e}"
133
+ return 1, error_msg
aider/scrape.py ADDED
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env python
2
+
3
+ import re
4
+ import sys
5
+
6
+ import pypandoc
7
+
8
+ from aider import __version__, urls, utils
9
+ from aider.dump import dump # noqa: F401
10
+
11
+ aider_user_agent = f"Aider/{__version__} +{urls.website}"
12
+
13
+ # Playwright is nice because it has a simple way to install dependencies on most
14
+ # platforms.
15
+
16
+
17
+ def check_playwright():
18
+ try:
19
+ from playwright.async_api import async_playwright # noqa: F401
20
+ from playwright.sync_api import sync_playwright # noqa: F401
21
+
22
+ has_playwright = True
23
+ except ImportError:
24
+ has_playwright = False
25
+
26
+ return has_playwright
27
+
28
+
29
+ async def check_chromium():
30
+ has_chromium = False
31
+
32
+ if check_playwright():
33
+ from playwright.async_api import async_playwright
34
+
35
+ try:
36
+ async with async_playwright() as p:
37
+ browser = await p.chromium.launch()
38
+ await browser.close()
39
+ has_chromium = True
40
+ except Exception as e:
41
+ has_chromium = False
42
+ print(f"chromium errors {e}")
43
+
44
+ return has_chromium
45
+
46
+
47
+ async def install_playwright(io):
48
+ has_playwright = check_playwright()
49
+ has_chromium = await check_chromium()
50
+
51
+ if has_playwright and has_chromium:
52
+ return True
53
+
54
+ pip_cmd = utils.get_pip_install(["aider-ce[playwright]"])
55
+ chromium_cmd = "-m playwright install --with-deps chromium"
56
+ chromium_cmd = [sys.executable] + chromium_cmd.split()
57
+
58
+ cmds = ""
59
+ if not has_playwright:
60
+ cmds += " ".join(pip_cmd) + "\n"
61
+ if not has_chromium:
62
+ cmds += " ".join(chromium_cmd) + "\n"
63
+
64
+ text = f"""For the best web scraping, install Playwright:
65
+
66
+ {cmds}
67
+ See {urls.enable_playwright} for more info.
68
+ """
69
+
70
+ io.tool_output(text)
71
+ if not await io.confirm_ask("Install playwright?", default="y"):
72
+ return
73
+
74
+ if not has_playwright:
75
+ success, output = utils.run_install(pip_cmd)
76
+ if not success:
77
+ io.tool_error(output)
78
+ return
79
+
80
+ success, output = utils.run_install(chromium_cmd)
81
+ if not success:
82
+ io.tool_error(output)
83
+ return
84
+
85
+ return True
86
+
87
+
88
+ class Scraper:
89
+ pandoc_available = None
90
+ playwright_available = None
91
+ playwright_instructions_shown = False
92
+
93
+ # Public API...
94
+ def __init__(self, print_error=None, playwright_available=None, verify_ssl=True):
95
+ """
96
+ `print_error` - a function to call to print error/debug info.
97
+ `verify_ssl` - if False, disable SSL certificate verification when scraping.
98
+ """
99
+ if print_error:
100
+ self.print_error = print_error
101
+ else:
102
+ self.print_error = print
103
+
104
+ self.playwright_available = playwright_available
105
+ self.verify_ssl = verify_ssl
106
+
107
+ async def scrape(self, url):
108
+ """
109
+ Scrape a url and turn it into readable markdown if it's HTML.
110
+ If it's plain text or non-HTML, return it as-is.
111
+
112
+ `url` - the URL to scrape.
113
+ """
114
+
115
+ if self.playwright_available:
116
+ content, mime_type = await self.scrape_with_playwright(url)
117
+ else:
118
+ content, mime_type = self.scrape_with_httpx(url)
119
+
120
+ if not content:
121
+ self.print_error(f"Failed to retrieve content from {url}")
122
+ return None
123
+
124
+ # Check if the content is HTML based on MIME type or content
125
+ if (mime_type and mime_type.startswith("text/html")) or (
126
+ mime_type is None and self.looks_like_html(content)
127
+ ):
128
+ self.try_pandoc()
129
+ content = self.html_to_markdown(content)
130
+
131
+ return content
132
+
133
+ def looks_like_html(self, content):
134
+ """
135
+ Check if the content looks like HTML.
136
+ """
137
+ if isinstance(content, str):
138
+ # Check for common HTML tags
139
+ html_patterns = [
140
+ r"<!DOCTYPE\s+html",
141
+ r"<html",
142
+ r"<head",
143
+ r"<body",
144
+ r"<div",
145
+ r"<p>",
146
+ r"<a\s+href=",
147
+ ]
148
+ return any(re.search(pattern, content, re.IGNORECASE) for pattern in html_patterns)
149
+ return False
150
+
151
+ # Internals...
152
+ async def scrape_with_playwright(self, url):
153
+ import playwright # noqa: F401
154
+ from playwright.async_api import Error as PlaywrightError
155
+ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
156
+ from playwright.async_api import async_playwright
157
+
158
+ async with async_playwright() as p:
159
+ try:
160
+ browser = await p.chromium.launch()
161
+ except Exception as e:
162
+ self.playwright_available = False
163
+ self.print_error(str(e))
164
+ return None, None
165
+
166
+ try:
167
+ context = await browser.new_context(ignore_https_errors=not self.verify_ssl)
168
+ page = await context.new_page()
169
+
170
+ user_agent = await page.evaluate("navigator.userAgent")
171
+ user_agent = user_agent.replace("Headless", "")
172
+ user_agent = user_agent.replace("headless", "")
173
+ user_agent += " " + aider_user_agent
174
+
175
+ await page.set_extra_http_headers({"User-Agent": user_agent})
176
+
177
+ response = None
178
+ try:
179
+ response = await page.goto(url, wait_until="networkidle", timeout=5000)
180
+ except PlaywrightTimeoutError:
181
+ self.print_error(f"Page didn't quiesce, scraping content anyway: {url}")
182
+ response = None
183
+ except PlaywrightError as e:
184
+ self.print_error(f"Error navigating to {url}: {str(e)}")
185
+ return None, None
186
+
187
+ try:
188
+ content = await page.content()
189
+ mime_type = None
190
+ if response:
191
+ content_type = await response.header_value("content-type")
192
+ if content_type:
193
+ mime_type = content_type.split(";")[0]
194
+ except PlaywrightError as e:
195
+ self.print_error(f"Error retrieving page content: {str(e)}")
196
+ content = None
197
+ mime_type = None
198
+ finally:
199
+ await browser.close()
200
+
201
+ return content, mime_type
202
+
203
+ def scrape_with_httpx(self, url):
204
+ import httpx
205
+
206
+ headers = {"User-Agent": f"Mozilla./5.0 ({aider_user_agent})"}
207
+ try:
208
+ with httpx.Client(
209
+ headers=headers, verify=self.verify_ssl, follow_redirects=True
210
+ ) as client:
211
+ response = client.get(url)
212
+ response.raise_for_status()
213
+ return response.text, response.headers.get("content-type", "").split(";")[0]
214
+ except httpx.HTTPError as http_err:
215
+ self.print_error(f"HTTP error occurred: {http_err}")
216
+ except Exception as err:
217
+ self.print_error(f"An error occurred: {err}")
218
+ return None, None
219
+
220
+ def try_pandoc(self):
221
+ if self.pandoc_available:
222
+ return
223
+
224
+ try:
225
+ pypandoc.get_pandoc_version()
226
+ self.pandoc_available = True
227
+ return
228
+ except OSError:
229
+ pass
230
+
231
+ try:
232
+ pypandoc.download_pandoc(delete_installer=True)
233
+ except Exception as err:
234
+ self.print_error(f"Unable to install pandoc: {err}")
235
+ return
236
+
237
+ self.pandoc_available = True
238
+
239
+ def html_to_markdown(self, page_source):
240
+ from bs4 import BeautifulSoup
241
+
242
+ soup = BeautifulSoup(page_source, "html.parser")
243
+ soup = slimdown_html(soup)
244
+ page_source = str(soup)
245
+
246
+ if not self.pandoc_available:
247
+ return page_source
248
+
249
+ try:
250
+ md = pypandoc.convert_text(page_source, "markdown", format="html")
251
+ except OSError:
252
+ return page_source
253
+
254
+ md = re.sub(r"</div>", " ", md)
255
+ md = re.sub(r"<div>", " ", md)
256
+
257
+ md = re.sub(r"\n\s*\n", "\n\n", md)
258
+
259
+ return md
260
+
261
+
262
+ def slimdown_html(soup):
263
+ for svg in soup.find_all("svg"):
264
+ svg.decompose()
265
+
266
+ if soup.img:
267
+ soup.img.decompose()
268
+
269
+ for tag in soup.find_all(href=lambda x: x and x.startswith("data:")):
270
+ tag.decompose()
271
+
272
+ for tag in soup.find_all(src=lambda x: x and x.startswith("data:")):
273
+ tag.decompose()
274
+
275
+ for tag in soup.find_all(True):
276
+ for attr in list(tag.attrs):
277
+ if attr != "href":
278
+ tag.attrs.pop(attr, None)
279
+
280
+ return soup
281
+
282
+
283
+ async def main(url):
284
+ scraper = Scraper(playwright_available=check_playwright())
285
+ content = await scraper.scrape(url)
286
+ print(content)
287
+
288
+
289
+ if __name__ == "__main__":
290
+ if len(sys.argv) < 2:
291
+ print("Usage: python playw.py <URL>")
292
+ sys.exit(1)
293
+ main(sys.argv[1])
aider/sendchat.py ADDED
@@ -0,0 +1,242 @@
1
+ from aider.dump import dump # noqa: F401
2
+ from aider.utils import format_messages
3
+
4
+
5
+ def sanity_check_messages(messages):
6
+ """Check if messages alternate between user and assistant roles.
7
+ System messages can be interspersed anywhere.
8
+ Also verifies the last non-system message is from the user.
9
+ Validates tool message sequences.
10
+ Returns True if valid, False otherwise."""
11
+ last_role = None
12
+ last_non_system_role = None
13
+ i = 0
14
+ n = len(messages)
15
+
16
+ while i < n:
17
+ msg = messages[i]
18
+ role = msg.get("role")
19
+
20
+ # Handle tool sequences atomically
21
+ if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
22
+ # Validate tool sequence
23
+ expected_ids = {call["id"] for call in msg["tool_calls"]}
24
+ i += 1
25
+
26
+ # Check for tool responses
27
+ while i < n and expected_ids:
28
+ next_msg = messages[i]
29
+ if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in expected_ids:
30
+ expected_ids.discard(next_msg.get("tool_call_id"))
31
+ i += 1
32
+ else:
33
+ break
34
+
35
+ # If we still have expected IDs, the tool sequence is incomplete
36
+ if expected_ids:
37
+ turns = format_messages(messages)
38
+ raise ValueError(
39
+ "Incomplete tool sequence - missing responses for tool calls:\n\n" + turns
40
+ )
41
+
42
+ # Continue to next message after tool sequence
43
+ continue
44
+
45
+ elif role == "tool":
46
+ # Orphaned tool message without preceding assistant tool_calls
47
+ turns = format_messages(messages)
48
+ raise ValueError(
49
+ "Orphaned tool message without preceding assistant tool_calls:\n\n" + turns
50
+ )
51
+
52
+ # Handle normal role alternation
53
+ if role == "system":
54
+ i += 1
55
+ continue
56
+
57
+ if last_role and role == last_role:
58
+ turns = format_messages(messages)
59
+ raise ValueError("Messages don't properly alternate user/assistant:\n\n" + turns)
60
+
61
+ last_role = role
62
+ last_non_system_role = role
63
+ i += 1
64
+
65
+ # Ensure last non-system message is from user
66
+ return last_non_system_role == "user"
67
+
68
+
69
+ def clean_orphaned_tool_messages(messages):
70
+ """Remove orphaned tool messages and incomplete tool sequences.
71
+
72
+ This function removes:
73
+ - Tool messages without a preceding assistant message containing tool_calls
74
+ - Assistant messages with tool_calls that don't have complete tool responses
75
+
76
+ Args:
77
+ messages: List of message dictionaries
78
+
79
+ Returns:
80
+ Cleaned list of messages with orphaned tool sequences removed
81
+ """
82
+ if not messages:
83
+ return messages
84
+
85
+ cleaned = []
86
+ i = 0
87
+ n = len(messages)
88
+
89
+ while i < n:
90
+ msg = messages[i]
91
+ role = msg.get("role")
92
+
93
+ # If it's an assistant message with tool_calls, check if we have complete responses
94
+ if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
95
+ # Start of potential tool sequence
96
+ tool_sequence = [msg]
97
+ expected_ids = {call["id"] for call in msg["tool_calls"]}
98
+ j = i + 1
99
+
100
+ # Collect tool responses
101
+ while j < n and expected_ids:
102
+ next_msg = messages[j]
103
+ if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in expected_ids:
104
+ tool_sequence.append(next_msg)
105
+ expected_ids.discard(next_msg.get("tool_call_id"))
106
+ j += 1
107
+ else:
108
+ break
109
+
110
+ # If we have all tool responses, keep the sequence
111
+ if not expected_ids:
112
+ cleaned.extend(tool_sequence)
113
+ i = j
114
+ else:
115
+ # Incomplete sequence - skip the entire tool sequence
116
+ i = j
117
+ # Don't add anything to cleaned
118
+ continue
119
+
120
+ elif role == "tool":
121
+ # Orphaned tool message without preceding assistant tool_calls - skip it
122
+ i += 1
123
+ continue
124
+ else:
125
+ # Regular message - add it
126
+ cleaned.append(msg)
127
+ i += 1
128
+
129
+ return cleaned
130
+
131
+
132
+ def ensure_alternating_roles(messages):
133
+ """Ensure messages alternate between 'assistant' and 'user' roles.
134
+
135
+ Inserts empty messages of the opposite role when consecutive messages
136
+ of the same 'user' or 'assistant' role are found. Messages with other
137
+ roles (e.g. 'system', 'tool') are ignored by the alternation logic.
138
+
139
+ Also handles tool call sequences properly - when an assistant message
140
+ contains tool_calls, processes the complete tool sequence atomically.
141
+
142
+ Args:
143
+ messages: List of message dictionaries with 'role' and 'content' keys.
144
+
145
+ Returns:
146
+ List of messages with alternating roles.
147
+ """
148
+ if not messages:
149
+ return messages
150
+
151
+ # First clean orphaned tool messages
152
+ messages = clean_orphaned_tool_messages(messages)
153
+
154
+ result = []
155
+ i = 0
156
+ n = len(messages)
157
+ prev_role = None
158
+
159
+ while i < n:
160
+ msg = messages[i]
161
+ role = msg.get("role")
162
+
163
+ # Handle tool call sequences atomically
164
+ if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
165
+ # Start of tool sequence - collect all related messages
166
+ tool_sequence = [msg]
167
+ expected_ids = {call["id"] for call in msg["tool_calls"]}
168
+ i += 1
169
+
170
+ # Collect tool responses
171
+ while i < n and expected_ids:
172
+ next_msg = messages[i]
173
+ if next_msg.get("role") == "tool" and next_msg.get("tool_call_id") in expected_ids:
174
+ tool_sequence.append(next_msg)
175
+ expected_ids.discard(next_msg.get("tool_call_id"))
176
+ i += 1
177
+ else:
178
+ break
179
+
180
+ # Add missing tool responses as empty
181
+ for tool_id in expected_ids:
182
+ tool_sequence.append(
183
+ {"role": "tool", "tool_call_id": tool_id, "content": "(empty response)"}
184
+ )
185
+
186
+ # Add the complete tool sequence to result
187
+ for tool_msg in tool_sequence:
188
+ result.append(tool_msg)
189
+
190
+ # Update prev_role to assistant after processing tool sequence
191
+ prev_role = "assistant"
192
+ continue
193
+
194
+ # Handle normal message alternation
195
+ if role in ("user", "assistant"):
196
+ if role == prev_role:
197
+ # Insert empty message of opposite role
198
+ opposite_role = "user" if role == "assistant" else "assistant"
199
+ result.append(
200
+ {
201
+ "role": opposite_role,
202
+ "content": (
203
+ "(empty response)"
204
+ if opposite_role == "assistant"
205
+ else "(empty request)"
206
+ ),
207
+ }
208
+ )
209
+ prev_role = opposite_role
210
+
211
+ result.append(msg)
212
+ prev_role = role
213
+ else:
214
+ # For non-user/assistant roles, just add them directly
215
+ result.append(msg)
216
+
217
+ i += 1
218
+
219
+ # Consolidate consecutive empty messages in a single pass
220
+ consolidated = []
221
+ for msg in result:
222
+ if not consolidated:
223
+ consolidated.append(msg)
224
+ continue
225
+
226
+ last_msg = consolidated[-1]
227
+ current_role = msg.get("role")
228
+ last_role = last_msg.get("role")
229
+
230
+ # Skip consecutive empty messages with the same role
231
+ if (
232
+ current_role in ("user", "assistant")
233
+ and last_role in ("user", "assistant")
234
+ and current_role == last_role
235
+ and msg.get("content") in ["", "(empty response)", "(empty request)"]
236
+ and last_msg.get("content") in ["", "(empty response)", "(empty request)"]
237
+ ):
238
+ continue
239
+
240
+ consolidated.append(msg)
241
+
242
+ return consolidated