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.
- aider/__init__.py +20 -0
- aider/__main__.py +4 -0
- aider/_version.py +34 -0
- aider/analytics.py +258 -0
- aider/args.py +1056 -0
- aider/args_formatter.py +228 -0
- aider/change_tracker.py +133 -0
- aider/coders/__init__.py +36 -0
- aider/coders/agent_coder.py +2166 -0
- aider/coders/agent_prompts.py +104 -0
- aider/coders/architect_coder.py +48 -0
- aider/coders/architect_prompts.py +40 -0
- aider/coders/ask_coder.py +9 -0
- aider/coders/ask_prompts.py +35 -0
- aider/coders/base_coder.py +3613 -0
- aider/coders/base_prompts.py +87 -0
- aider/coders/chat_chunks.py +64 -0
- aider/coders/context_coder.py +53 -0
- aider/coders/context_prompts.py +75 -0
- aider/coders/editblock_coder.py +657 -0
- aider/coders/editblock_fenced_coder.py +10 -0
- aider/coders/editblock_fenced_prompts.py +143 -0
- aider/coders/editblock_func_coder.py +141 -0
- aider/coders/editblock_func_prompts.py +27 -0
- aider/coders/editblock_prompts.py +175 -0
- aider/coders/editor_diff_fenced_coder.py +9 -0
- aider/coders/editor_diff_fenced_prompts.py +11 -0
- aider/coders/editor_editblock_coder.py +9 -0
- aider/coders/editor_editblock_prompts.py +21 -0
- aider/coders/editor_whole_coder.py +9 -0
- aider/coders/editor_whole_prompts.py +12 -0
- aider/coders/help_coder.py +16 -0
- aider/coders/help_prompts.py +46 -0
- aider/coders/patch_coder.py +706 -0
- aider/coders/patch_prompts.py +159 -0
- aider/coders/search_replace.py +757 -0
- aider/coders/shell.py +37 -0
- aider/coders/single_wholefile_func_coder.py +102 -0
- aider/coders/single_wholefile_func_prompts.py +27 -0
- aider/coders/udiff_coder.py +429 -0
- aider/coders/udiff_prompts.py +115 -0
- aider/coders/udiff_simple.py +14 -0
- aider/coders/udiff_simple_prompts.py +25 -0
- aider/coders/wholefile_coder.py +144 -0
- aider/coders/wholefile_func_coder.py +134 -0
- aider/coders/wholefile_func_prompts.py +27 -0
- aider/coders/wholefile_prompts.py +65 -0
- aider/commands.py +2173 -0
- aider/copypaste.py +72 -0
- aider/deprecated.py +126 -0
- aider/diffs.py +128 -0
- aider/dump.py +29 -0
- aider/editor.py +147 -0
- aider/exceptions.py +115 -0
- aider/format_settings.py +26 -0
- aider/gui.py +545 -0
- aider/help.py +163 -0
- aider/help_pats.py +19 -0
- aider/helpers/__init__.py +9 -0
- aider/helpers/similarity.py +98 -0
- aider/history.py +180 -0
- aider/io.py +1608 -0
- aider/linter.py +304 -0
- aider/llm.py +55 -0
- aider/main.py +1415 -0
- aider/mcp/__init__.py +174 -0
- aider/mcp/server.py +149 -0
- aider/mdstream.py +243 -0
- aider/models.py +1313 -0
- aider/onboarding.py +429 -0
- aider/openrouter.py +129 -0
- aider/prompts.py +56 -0
- aider/queries/tree-sitter-language-pack/README.md +7 -0
- aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- aider/queries/tree-sitter-languages/README.md +24 -0
- aider/queries/tree-sitter-languages/c-tags.scm +9 -0
- aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
- aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
- aider/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- aider/queries/tree-sitter-languages/go-tags.scm +30 -0
- aider/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- aider/queries/tree-sitter-languages/java-tags.scm +20 -0
- aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-languages/julia-tags.scm +60 -0
- aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-languages/php-tags.scm +26 -0
- aider/queries/tree-sitter-languages/python-tags.scm +12 -0
- aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
- aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
- aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
- aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- aider/queries/tree-sitter-languages/zig-tags.scm +3 -0
- aider/reasoning_tags.py +82 -0
- aider/repo.py +621 -0
- aider/repomap.py +1174 -0
- aider/report.py +260 -0
- aider/resources/__init__.py +3 -0
- aider/resources/model-metadata.json +776 -0
- aider/resources/model-settings.yml +2068 -0
- aider/run_cmd.py +133 -0
- aider/scrape.py +293 -0
- aider/sendchat.py +242 -0
- aider/sessions.py +256 -0
- aider/special.py +203 -0
- aider/tools/__init__.py +72 -0
- aider/tools/command.py +105 -0
- aider/tools/command_interactive.py +122 -0
- aider/tools/delete_block.py +182 -0
- aider/tools/delete_line.py +155 -0
- aider/tools/delete_lines.py +184 -0
- aider/tools/extract_lines.py +341 -0
- aider/tools/finished.py +48 -0
- aider/tools/git_branch.py +129 -0
- aider/tools/git_diff.py +60 -0
- aider/tools/git_log.py +57 -0
- aider/tools/git_remote.py +53 -0
- aider/tools/git_show.py +51 -0
- aider/tools/git_status.py +46 -0
- aider/tools/grep.py +256 -0
- aider/tools/indent_lines.py +221 -0
- aider/tools/insert_block.py +288 -0
- aider/tools/list_changes.py +86 -0
- aider/tools/ls.py +93 -0
- aider/tools/make_editable.py +85 -0
- aider/tools/make_readonly.py +69 -0
- aider/tools/remove.py +91 -0
- aider/tools/replace_all.py +126 -0
- aider/tools/replace_line.py +173 -0
- aider/tools/replace_lines.py +217 -0
- aider/tools/replace_text.py +187 -0
- aider/tools/show_numbered_context.py +147 -0
- aider/tools/tool_utils.py +313 -0
- aider/tools/undo_change.py +95 -0
- aider/tools/update_todo_list.py +156 -0
- aider/tools/view.py +57 -0
- aider/tools/view_files_matching.py +141 -0
- aider/tools/view_files_with_symbol.py +129 -0
- aider/urls.py +17 -0
- aider/utils.py +456 -0
- aider/versioncheck.py +113 -0
- aider/voice.py +205 -0
- aider/waiting.py +38 -0
- aider/watch.py +318 -0
- aider/watch_prompts.py +12 -0
- aider/website/Gemfile +8 -0
- aider/website/_includes/blame.md +162 -0
- aider/website/_includes/get-started.md +22 -0
- aider/website/_includes/help-tip.md +5 -0
- aider/website/_includes/help.md +24 -0
- aider/website/_includes/install.md +5 -0
- aider/website/_includes/keys.md +4 -0
- aider/website/_includes/model-warnings.md +67 -0
- aider/website/_includes/multi-line.md +22 -0
- aider/website/_includes/python-m-aider.md +5 -0
- aider/website/_includes/recording.css +228 -0
- aider/website/_includes/recording.md +34 -0
- aider/website/_includes/replit-pipx.md +9 -0
- aider/website/_includes/works-best.md +1 -0
- aider/website/_sass/custom/custom.scss +103 -0
- aider/website/docs/config/adv-model-settings.md +2261 -0
- aider/website/docs/config/agent-mode.md +194 -0
- aider/website/docs/config/aider_conf.md +548 -0
- aider/website/docs/config/api-keys.md +90 -0
- aider/website/docs/config/dotenv.md +493 -0
- aider/website/docs/config/editor.md +127 -0
- aider/website/docs/config/mcp.md +95 -0
- aider/website/docs/config/model-aliases.md +104 -0
- aider/website/docs/config/options.md +890 -0
- aider/website/docs/config/reasoning.md +210 -0
- aider/website/docs/config.md +44 -0
- aider/website/docs/faq.md +384 -0
- aider/website/docs/git.md +76 -0
- aider/website/docs/index.md +47 -0
- aider/website/docs/install/codespaces.md +39 -0
- aider/website/docs/install/docker.md +57 -0
- aider/website/docs/install/optional.md +100 -0
- aider/website/docs/install/replit.md +8 -0
- aider/website/docs/install.md +115 -0
- aider/website/docs/languages.md +264 -0
- aider/website/docs/legal/contributor-agreement.md +111 -0
- aider/website/docs/legal/privacy.md +104 -0
- aider/website/docs/llms/anthropic.md +77 -0
- aider/website/docs/llms/azure.md +48 -0
- aider/website/docs/llms/bedrock.md +132 -0
- aider/website/docs/llms/cohere.md +34 -0
- aider/website/docs/llms/deepseek.md +32 -0
- aider/website/docs/llms/gemini.md +49 -0
- aider/website/docs/llms/github.md +111 -0
- aider/website/docs/llms/groq.md +36 -0
- aider/website/docs/llms/lm-studio.md +39 -0
- aider/website/docs/llms/ollama.md +75 -0
- aider/website/docs/llms/openai-compat.md +39 -0
- aider/website/docs/llms/openai.md +58 -0
- aider/website/docs/llms/openrouter.md +78 -0
- aider/website/docs/llms/other.md +117 -0
- aider/website/docs/llms/vertex.md +50 -0
- aider/website/docs/llms/warnings.md +10 -0
- aider/website/docs/llms/xai.md +53 -0
- aider/website/docs/llms.md +54 -0
- aider/website/docs/more/analytics.md +127 -0
- aider/website/docs/more/edit-formats.md +116 -0
- aider/website/docs/more/infinite-output.md +165 -0
- aider/website/docs/more-info.md +8 -0
- aider/website/docs/recordings/auto-accept-architect.md +31 -0
- aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- aider/website/docs/recordings/index.md +21 -0
- aider/website/docs/recordings/model-accepts-settings.md +69 -0
- aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- aider/website/docs/repomap.md +112 -0
- aider/website/docs/scripting.md +100 -0
- aider/website/docs/sessions.md +203 -0
- aider/website/docs/troubleshooting/aider-not-found.md +24 -0
- aider/website/docs/troubleshooting/edit-errors.md +76 -0
- aider/website/docs/troubleshooting/imports.md +62 -0
- aider/website/docs/troubleshooting/models-and-keys.md +54 -0
- aider/website/docs/troubleshooting/support.md +79 -0
- aider/website/docs/troubleshooting/token-limits.md +96 -0
- aider/website/docs/troubleshooting/warnings.md +12 -0
- aider/website/docs/troubleshooting.md +11 -0
- aider/website/docs/usage/browser.md +57 -0
- aider/website/docs/usage/caching.md +49 -0
- aider/website/docs/usage/commands.md +133 -0
- aider/website/docs/usage/conventions.md +119 -0
- aider/website/docs/usage/copypaste.md +121 -0
- aider/website/docs/usage/images-urls.md +48 -0
- aider/website/docs/usage/lint-test.md +118 -0
- aider/website/docs/usage/modes.md +211 -0
- aider/website/docs/usage/not-code.md +179 -0
- aider/website/docs/usage/notifications.md +87 -0
- aider/website/docs/usage/tips.md +79 -0
- aider/website/docs/usage/tutorials.md +30 -0
- aider/website/docs/usage/voice.md +121 -0
- aider/website/docs/usage/watch.md +294 -0
- aider/website/docs/usage.md +102 -0
- aider/website/share/index.md +101 -0
- aider_ce-0.88.20.dist-info/METADATA +187 -0
- aider_ce-0.88.20.dist-info/RECORD +279 -0
- aider_ce-0.88.20.dist-info/WHEEL +5 -0
- aider_ce-0.88.20.dist-info/entry_points.txt +2 -0
- aider_ce-0.88.20.dist-info/licenses/LICENSE.txt +202 -0
- 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
|