Merge branch 'main' into json-coders

This commit is contained in:
Paul Gauthier 2024-08-13 17:03:30 -07:00
commit e1b83ba6b5
81 changed files with 2210 additions and 720 deletions

View file

@ -21,12 +21,12 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine importlib-metadata==7.2.1
pip install build setuptools wheel twine importlib-metadata==7.2.1
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
python setup.py sdist bdist_wheel
python -m build
twine upload dist/*

1
.gitignore vendored
View file

@ -5,6 +5,7 @@ aider.code-workspace
.aider*
aider_chat.egg-info/
build
dist/
Gemfile.lock
_site
.jekyll-cache/

View file

@ -1,6 +1,51 @@
# Release history
### Aider v0.50.0
- Infinite output for DeepSeek Coder, Mistral models in addition to Anthropic's models.
- New `--deepseek` switch to use DeepSeek Coder.
- DeepSeek Coder uses 8k token output.
- New `--chat-mode <mode>` switch to launch in ask/help/code modes.
- New `/code <message>` command request a code edit while in `ask` mode.
- Web scraper is more robust if page never idles.
- Improved token and cost reporting for infinite output.
- Improvements and bug fixes for `/read` only files.
- Switched from `setup.py` to `pyproject.toml`, by @branchvincent.
- Bug fix to persist files added during `/ask`.
- Bug fix for chat history size in `/tokens`.
- Aider wrote 66% of the code in this release.
### Aider v0.49.1
- Bugfix to `/help`.
### Aider v0.49.0
- Add read-only files to the chat context with `/read` and `--read`, including from outside the git repo.
- `/diff` now shows diffs of all changes resulting from your request, including lint and test fixes.
- New `/clipboard` command to paste images or text from the clipboard, replaces `/add-clipboard-image`.
- Now shows the markdown scraped when you add a url with `/web`.
- When [scripting aider](https://aider.chat/docs/scripting.html) messages can now contain in-chat `/` commands.
- Aider in docker image now suggests the correct command to update to latest version.
- Improved retries on API errors (was easy to test during Sonnet outage).
- Added `--mini` for `gpt-4o-mini`.
- Bugfix to keep session cost accurate when using `/ask` and `/help`.
- Performance improvements for repo map calculation.
- `/tokens` now shows the active model.
- Enhanced commit message attribution options:
- New `--attribute-commit-message-author` to prefix commit messages with 'aider: ' if aider authored the changes, replaces `--attribute-commit-message`.
- New `--attribute-commit-message-committer` to prefix all commit messages with 'aider: '.
- Aider wrote 61% of the code in this release.
### Aider v0.48.1
- Added `openai/gpt-4o-2024-08-06`.
- Worked around litellm bug that removes OpenRouter app headers when using `extra_headers`.
- Improved progress indication during repo map processing.
- Corrected instructions for upgrading the docker container to latest aider version.
- Removed obsolete 16k token limit on commit diffs, use per-model limits.
### Aider v0.48.0
- Performance improvements for large/mono repos.

View file

@ -1 +0,0 @@
include requirements.txt

View file

@ -35,18 +35,18 @@ cog.out(open("aider/website/_includes/get-started.md").read())
You can get started quickly like this:
```
$ pip install aider-chat
python -m pip install aider-chat
# Change directory into a git repo
$ cd /to/your/git/repo
cd /to/your/git/repo
# Work with Claude 3.5 Sonnet on your repo
$ export ANTHROPIC_API_KEY=your-key-goes-here
$ aider
export ANTHROPIC_API_KEY=your-key-goes-here
aider
# Work with GPT-4o on your repo
$ export OPENAI_API_KEY=your-key-goes-here
$ aider
export OPENAI_API_KEY=your-key-goes-here
aider
```
<!--[[[end]]]-->

View file

@ -1 +1 @@
__version__ = "0.48.1-dev"
__version__ = "0.50.1-dev"

View file

@ -82,6 +82,14 @@ def get_parser(default_config_files, git_root):
const=gpt_4o_model,
help=f"Use {gpt_4o_model} model for the main chat",
)
gpt_4o_mini_model = "gpt-4o-mini"
group.add_argument(
"--mini",
action="store_const",
dest="model",
const=gpt_4o_mini_model,
help=f"Use {gpt_4o_mini_model} model for the main chat",
)
gpt_4_turbo_model = "gpt-4-1106-preview"
group.add_argument(
"--4-turbo",
@ -101,6 +109,14 @@ def get_parser(default_config_files, git_root):
const=gpt_3_model_name,
help=f"Use {gpt_3_model_name} model for the main chat",
)
deepseek_model = "deepseek/deepseek-coder"
group.add_argument(
"--deepseek",
action="store_const",
dest="model",
const=deepseek_model,
help=f"Use {deepseek_model} model for the main chat",
)
##########
group = parser.add_argument_group("Model Settings")
@ -159,6 +175,7 @@ def get_parser(default_config_files, git_root):
)
group.add_argument(
"--edit-format",
"--chat-mode",
metavar="EDIT_FORMAT",
default=None,
help="Specify what edit format the LLM should use (default depends on model)",
@ -350,10 +367,16 @@ def get_parser(default_config_files, git_root):
help="Attribute aider commits in the git committer name (default: True)",
)
group.add_argument(
"--attribute-commit-message",
"--attribute-commit-message-author",
action=argparse.BooleanOptionalAction,
default=False,
help="Prefix commit messages with 'aider: ' (default: False)",
help="Prefix commit messages with 'aider: ' if aider authored the changes (default: False)",
)
group.add_argument(
"--attribute-commit-message-committer",
action=argparse.BooleanOptionalAction,
default=False,
help="Prefix all commit messages with 'aider: ' (default: False)",
)
group.add_argument(
"--commit",
@ -420,6 +443,12 @@ def get_parser(default_config_files, git_root):
metavar="FILE",
help="specify a file to edit (can be used multiple times)",
)
group.add_argument(
"--read",
action="append",
metavar="FILE",
help="specify a read-only file (can be used multiple times)",
)
group.add_argument(
"--vim",
action="store_true",

View file

@ -30,7 +30,7 @@ from aider.llm import litellm
from aider.mdstream import MarkdownStream
from aider.repo import GitRepo
from aider.repomap import RepoMap
from aider.sendchat import send_with_retries
from aider.sendchat import retry_exceptions, send_completion
from aider.utils import format_content, format_messages, is_image_file
from ..dump import dump # noqa: F401
@ -50,6 +50,7 @@ def wrap_fence(name):
class Coder:
abs_fnames = None
abs_read_only_fnames = None
repo = None
last_aider_commit_hash = None
aider_edited_files = None
@ -70,6 +71,11 @@ class Coder:
lint_outcome = None
test_outcome = None
multi_response_content = ""
partial_response_content = ""
commit_before_message = []
message_cost = 0.0
message_tokens_sent = 0
message_tokens_received = 0
@classmethod
def create(
@ -89,6 +95,8 @@ class Coder:
else:
main_model = models.Model(models.DEFAULT_MODEL_NAME)
if edit_format == "code":
edit_format = None
if edit_format is None:
if from_coder:
edit_format = from_coder.edit_format
@ -112,6 +120,7 @@ class Coder:
# Bring along context from the old Coder
update = dict(
fnames=list(from_coder.abs_fnames),
read_only_fnames=list(from_coder.abs_read_only_fnames), # Copy read-only files
done_messages=done_messages,
cur_messages=from_coder.cur_messages,
aider_commit_hashes=from_coder.aider_commit_hashes,
@ -143,7 +152,10 @@ class Coder:
main_model = self.main_model
weak_model = main_model.weak_model
prefix = "Model:"
output = f" {main_model.name} with {self.edit_format} edit format"
output = f" {main_model.name} with"
if main_model.info.get("supports_assistant_prefill"):
output += " ♾️"
output += f" {self.edit_format} edit format"
if weak_model is not main_model:
prefix = "Models:"
output += f", weak model {weak_model.name}"
@ -193,7 +205,7 @@ class Coder:
io,
repo=None,
fnames=None,
pretty=True,
read_only_fnames=None,
show_diffs=False,
auto_commits=True,
dirty_commits=True,
@ -217,6 +229,7 @@ class Coder:
summarizer=None,
total_cost=0.0,
):
self.commit_before_message = []
self.aider_commit_hashes = set()
self.rejected_urls = set()
self.abs_root_path_cache = {}
@ -240,6 +253,7 @@ class Coder:
self.verbose = verbose
self.abs_fnames = set()
self.abs_read_only_fnames = set()
if cur_messages:
self.cur_messages = cur_messages
@ -263,9 +277,9 @@ class Coder:
self.code_theme = code_theme
self.dry_run = dry_run
self.pretty = pretty
self.pretty = self.io.pretty
if pretty:
if self.pretty:
self.console = Console()
else:
self.console = Console(force_terminal=False, no_color=True)
@ -314,6 +328,15 @@ class Coder:
if not self.repo:
self.find_common_root()
if read_only_fnames:
self.abs_read_only_fnames = set()
for fname in read_only_fnames:
abs_fname = self.abs_root_path(fname)
if os.path.exists(abs_fname):
self.abs_read_only_fnames.add(abs_fname)
else:
self.io.tool_error(f"Error: Read-only file {fname} does not exist. Skipping.")
if map_tokens is None:
use_repo_map = main_model.use_repo_map
map_tokens = 1024
@ -376,8 +399,10 @@ class Coder:
self.linter.set_linter(lang, cmd)
def show_announcements(self):
bold = True
for line in self.get_announcements():
self.io.tool_output(line)
self.io.tool_output(line, bold=bold)
bold = False
def find_common_root(self):
if len(self.abs_fnames) == 1:
@ -444,6 +469,10 @@ class Coder:
all_content = ""
for _fname, content in self.get_abs_fnames_content():
all_content += content + "\n"
for _fname in self.abs_read_only_fnames:
content = self.io.read_text(_fname)
if content is not None:
all_content += content + "\n"
good = False
for fence_open, fence_close in self.fences:
@ -485,6 +514,19 @@ class Coder:
return prompt
def get_read_only_files_content(self):
prompt = ""
for fname in self.abs_read_only_fnames:
content = self.io.read_text(fname)
if content is not None and not is_image_file(fname):
relative_fname = self.get_rel_fname(fname)
prompt += "\n"
prompt += relative_fname
prompt += f"\n{self.fence[0]}\n"
prompt += content
prompt += f"{self.fence[1]}\n"
return prompt
def get_cur_message_text(self):
text = ""
for msg in self.cur_messages:
@ -522,9 +564,13 @@ class Coder:
mentioned_fnames.update(self.get_ident_filename_matches(mentioned_idents))
other_files = set(self.get_all_abs_files()) - set(self.abs_fnames)
all_abs_files = set(self.get_all_abs_files())
repo_abs_read_only_fnames = set(self.abs_read_only_fnames) & all_abs_files
chat_files = set(self.abs_fnames) | repo_abs_read_only_fnames
other_files = all_abs_files - chat_files
repo_content = self.repo_map.get_repo_map(
self.abs_fnames,
chat_files,
other_files,
mentioned_fnames=mentioned_fnames,
mentioned_idents=mentioned_idents,
@ -534,7 +580,7 @@ class Coder:
if not repo_content:
repo_content = self.repo_map.get_repo_map(
set(),
set(self.get_all_abs_files()),
all_abs_files,
mentioned_fnames=mentioned_fnames,
mentioned_idents=mentioned_idents,
)
@ -543,7 +589,7 @@ class Coder:
if not repo_content:
repo_content = self.repo_map.get_repo_map(
set(),
set(self.get_all_abs_files()),
all_abs_files,
)
return repo_content
@ -572,12 +618,6 @@ class Coder:
files_content = self.gpt_prompts.files_no_full_files
files_reply = "Ok."
if files_content:
files_messages += [
dict(role="user", content=files_content),
dict(role="assistant", content=files_reply),
]
images_message = self.get_images_message()
if images_message is not None:
files_messages += [
@ -585,6 +625,24 @@ class Coder:
dict(role="assistant", content="Ok."),
]
read_only_content = self.get_read_only_files_content()
if read_only_content:
files_messages += [
dict(
role="user", content=self.gpt_prompts.read_only_files_prefix + read_only_content
),
dict(
role="assistant",
content="Ok, I will use these files as references.",
),
]
if files_content:
files_messages += [
dict(role="user", content=files_content),
dict(role="assistant", content=files_reply),
]
return files_messages
def get_images_message(self):
@ -597,9 +655,11 @@ class Coder:
mime_type, _ = mimetypes.guess_type(fname)
if mime_type and mime_type.startswith("image/"):
image_url = f"data:{mime_type};base64,{content}"
image_messages.append(
{"type": "image_url", "image_url": {"url": image_url, "detail": "high"}}
)
rel_fname = self.get_rel_fname(fname)
image_messages += [
{"type": "text", "text": f"Image file: {rel_fname}"},
{"type": "image_url", "image_url": {"url": image_url, "detail": "high"}},
]
if not image_messages:
return None
@ -609,7 +669,7 @@ class Coder:
def run_stream(self, user_message):
self.io.user_input(user_message)
self.init_before_message()
yield from self.send_new_user_message(user_message)
yield from self.send_message(user_message)
def init_before_message(self):
self.reflected_message = None
@ -617,48 +677,39 @@ class Coder:
self.lint_outcome = None
self.test_outcome = None
self.edit_outcome = None
if self.repo:
self.commit_before_message.append(self.repo.get_head())
def run(self, with_message=None):
while True:
self.init_before_message()
def run(self, with_message=None, preproc=True):
try:
if with_message:
self.io.user_input(with_message)
self.run_one(with_message, preproc)
return self.partial_response_content
try:
if with_message:
new_user_message = with_message
self.io.user_input(with_message)
else:
new_user_message = self.run_loop()
while True:
try:
user_message = self.get_input()
self.run_one(user_message, preproc)
self.show_undo_hint()
except KeyboardInterrupt:
self.keyboard_interrupt()
except EOFError:
return
while new_user_message:
self.reflected_message = None
list(self.send_new_user_message(new_user_message))
new_user_message = None
if self.reflected_message:
if self.num_reflections < self.max_reflections:
self.num_reflections += 1
new_user_message = self.reflected_message
else:
self.io.tool_error(
f"Only {self.max_reflections} reflections allowed, stopping."
)
if with_message:
return self.partial_response_content
except KeyboardInterrupt:
self.keyboard_interrupt()
except EOFError:
return
def run_loop(self):
inp = self.io.get_input(
def get_input(self):
inchat_files = self.get_inchat_relative_files()
read_only_files = [self.get_rel_fname(fname) for fname in self.abs_read_only_fnames]
all_files = sorted(set(inchat_files + read_only_files))
return self.io.get_input(
self.root,
self.get_inchat_relative_files(),
all_files,
self.get_addable_relative_files(),
self.commands,
self.abs_read_only_fnames,
)
def preproc_user_input(self, inp):
if not inp:
return
@ -670,6 +721,28 @@ class Coder:
return inp
def run_one(self, user_message, preproc):
self.init_before_message()
if preproc:
message = self.preproc_user_input(user_message)
else:
message = user_message
while message:
self.reflected_message = None
list(self.send_message(message))
if not self.reflected_message:
break
if self.num_reflections >= self.max_reflections:
self.io.tool_error(f"Only {self.max_reflections} reflections allowed, stopping.")
return
self.num_reflections += 1
message = self.reflected_message
def check_for_urls(self, inp):
url_pattern = re.compile(r"(https?://[^\s/$.?#].[^\s]*[^\s,.])")
urls = list(set(url_pattern.findall(inp))) # Use set to remove duplicates
@ -678,7 +751,7 @@ class Coder:
if url not in self.rejected_urls:
if self.io.confirm_ask(f"Add {url} to the chat?"):
inp += "\n\n"
inp += self.commands.cmd_web(url)
inp += self.commands.cmd_web(url, paginate=False)
added_urls.append(url)
else:
self.rejected_urls.add(url)
@ -826,6 +899,7 @@ class Coder:
self.summarize_end()
messages += self.done_messages
messages += self.get_files_messages()
if self.gpt_prompts.system_reminder:
@ -852,7 +926,7 @@ class Coder:
final = messages[-1]
max_input_tokens = self.main_model.info.get("max_input_tokens")
max_input_tokens = self.main_model.info.get("max_input_tokens") or 0
# Add the reminder prompt if we still have room to include it.
if (
max_input_tokens is None
@ -872,7 +946,7 @@ class Coder:
return messages
def send_new_user_message(self, inp):
def send_message(self, inp):
self.aider_edited_files = None
self.cur_messages += [
@ -891,6 +965,8 @@ class Coder:
else:
self.mdstream = None
retry_delay = 0.125
self.usage_report = None
exhausted = False
interrupted = False
@ -899,6 +975,14 @@ class Coder:
try:
yield from self.send(messages, functions=self.functions)
break
except retry_exceptions() as err:
self.io.tool_error(str(err))
retry_delay *= 2
if retry_delay > 60:
break
self.io.tool_output(f"Retrying in {retry_delay:.1f} seconds...")
time.sleep(retry_delay)
continue
except KeyboardInterrupt:
interrupted = True
break
@ -911,7 +995,7 @@ class Coder:
return
except FinishReasonLength:
# We hit the output limit!
if not self.main_model.can_prefill:
if not self.main_model.info.get("supports_assistant_prefill"):
exhausted = True
break
@ -920,7 +1004,9 @@ class Coder:
if messages[-1]["role"] == "assistant":
messages[-1]["content"] = self.multi_response_content
else:
messages.append(dict(role="assistant", content=self.multi_response_content))
messages.append(
dict(role="assistant", content=self.multi_response_content, prefix=True)
)
except Exception as err:
self.io.tool_error(f"Unexpected error: {err}")
traceback.print_exc()
@ -935,8 +1021,7 @@ class Coder:
self.io.tool_output()
if self.usage_report:
self.io.tool_output(self.usage_report)
self.show_usage_report()
if exhausted:
self.show_exhausted_error()
@ -1011,10 +1096,10 @@ class Coder:
output_tokens = 0
if self.partial_response_content:
output_tokens = self.main_model.token_count(self.partial_response_content)
max_output_tokens = self.main_model.info.get("max_output_tokens", 0)
max_output_tokens = self.main_model.info.get("max_output_tokens") or 0
input_tokens = self.main_model.token_count(self.format_messages())
max_input_tokens = self.main_model.info.get("max_input_tokens", 0)
max_input_tokens = self.main_model.info.get("max_input_tokens") or 0
total_tokens = input_tokens + output_tokens
@ -1159,9 +1244,8 @@ class Coder:
self.io.log_llm_history("TO LLM", format_messages(messages))
interrupted = False
try:
hash_object, completion = send_with_retries(
hash_object, completion = send_completion(
model.name,
messages,
functions,
@ -1176,9 +1260,9 @@ class Coder:
yield from self.show_send_output_stream(completion)
else:
self.show_send_output(completion)
except KeyboardInterrupt:
except KeyboardInterrupt as kbi:
self.keyboard_interrupt()
interrupted = True
raise kbi
finally:
self.io.log_llm_history(
"LLM RESPONSE",
@ -1193,10 +1277,7 @@ class Coder:
if args:
self.io.ai_output(json.dumps(args, indent=4))
if interrupted:
raise KeyboardInterrupt
self.calculate_and_show_tokens_and_cost(messages, completion)
self.calculate_and_show_tokens_and_cost(messages, completion)
def show_send_output(self, completion):
if self.verbose:
@ -1218,7 +1299,7 @@ class Coder:
show_func_err = func_err
try:
self.partial_response_content = completion.choices[0].message.content
self.partial_response_content = completion.choices[0].message.content or ""
except AttributeError as content_err:
show_content_err = content_err
@ -1312,13 +1393,19 @@ class Coder:
prompt_tokens = self.main_model.token_count(messages)
completion_tokens = self.main_model.token_count(self.partial_response_content)
self.usage_report = f"Tokens: {prompt_tokens:,} sent, {completion_tokens:,} received."
self.message_tokens_sent += prompt_tokens
self.message_tokens_received += completion_tokens
tokens_report = (
f"Tokens: {self.message_tokens_sent:,} sent, {self.message_tokens_received:,} received."
)
if self.main_model.info.get("input_cost_per_token"):
cost += prompt_tokens * self.main_model.info.get("input_cost_per_token")
if self.main_model.info.get("output_cost_per_token"):
cost += completion_tokens * self.main_model.info.get("output_cost_per_token")
self.total_cost += cost
self.message_cost += cost
def format_cost(value):
if value == 0:
@ -1329,13 +1416,24 @@ class Coder:
else:
return f"{value:.{max(2, 2 - int(math.log10(magnitude)))}f}"
self.usage_report += (
f" Cost: ${format_cost(cost)} request, ${format_cost(self.total_cost)} session."
cost_report = (
f" Cost: ${format_cost(self.message_cost)} message,"
f" ${format_cost(self.total_cost)} session."
)
self.usage_report = tokens_report + cost_report
else:
self.usage_report = tokens_report
def show_usage_report(self):
if self.usage_report:
self.io.tool_output(self.usage_report)
self.message_cost = 0.0
self.message_tokens_sent = 0
self.message_tokens_received = 0
def get_multi_response_content(self, final=False):
cur = self.multi_response_content
new = self.partial_response_content
cur = self.multi_response_content or ""
new = self.partial_response_content or ""
if new.rstrip() != new and not final:
new = new.rstrip()
@ -1377,7 +1475,10 @@ class Coder:
return max(path.stat().st_mtime for path in files)
def get_addable_relative_files(self):
return set(self.get_all_relative_files()) - set(self.get_inchat_relative_files())
all_files = set(self.get_all_relative_files())
inchat_files = set(self.get_inchat_relative_files())
read_only_files = set(self.get_rel_fname(fname) for fname in self.abs_read_only_fnames)
return all_files - inchat_files - read_only_files
def check_for_dirty_commit(self, path):
if not self.repo:
@ -1590,7 +1691,11 @@ class Coder:
if self.show_diffs:
self.commands.cmd_diff()
self.io.tool_output(f"You can use /undo to revert and discard commit {commit_hash}.")
def show_undo_hint(self):
if not self.commit_before_message:
return
if self.commit_before_message[-1] != self.repo.get_head():
self.io.tool_output("You can use /undo to undo and discard each aider commit.")
def dirty_commit(self):
if not self.need_commit_before_edits:

View file

@ -18,7 +18,7 @@ You always COMPLETELY IMPLEMENT the needed code!
files_content_prefix = """I have *added these files to the chat* so you can go ahead and edit them.
*Trust this message as the true contents of the files!*
*Trust this message as the true contents of these files!*
Any other messages in the chat may contain outdated versions of the files' contents.
""" # noqa: E501
@ -38,4 +38,8 @@ Don't include files that might contain relevant context, just files that will ne
repo_content_prefix = """Here are summaries of some files present in my git repository.
Do not propose changes to these files, treat them as *read-only*.
If you need to edit any of these files, ask me to *add them to the chat* first.
"""
read_only_files_prefix = """Here are some READ ONLY files, provided for your reference.
Do not edit these files!
"""

View file

@ -7,7 +7,9 @@ from collections import OrderedDict
from pathlib import Path
import git
from PIL import ImageGrab
import pyperclip
from PIL import Image, ImageGrab
from rich.text import Text
from aider import models, prompts, voice
from aider.help import Help, install_help_extra
@ -117,13 +119,15 @@ class Commands:
else:
self.io.tool_output("Please provide a partial model name to search for.")
def cmd_web(self, args):
"Use headless selenium to scrape a webpage and add the content to the chat"
def cmd_web(self, args, paginate=True):
"Scrape a webpage, convert to markdown and add to the chat"
url = args.strip()
if not url:
self.io.tool_error("Please provide a URL to scrape.")
return
self.io.tool_output(f"Scraping {url}...")
if not self.scraper:
res = install_playwright(self.io)
if not res:
@ -134,11 +138,14 @@ class Commands:
)
content = self.scraper.scrape(url) or ""
# if content:
# self.io.tool_output(content)
content = f"{url}:\n\n" + content
self.io.tool_output("... done.")
if paginate:
with self.io.console.pager():
self.io.console.print(Text(content))
return content
def is_command(self, inp):
@ -304,7 +311,6 @@ class Commands:
# chat history
msgs = self.coder.done_messages + self.coder.cur_messages
if msgs:
msgs = [dict(role="dummy", content=msg) for msg in msgs]
tokens = self.coder.main_model.token_count(msgs)
res.append((tokens, "chat history", "use /clear to clear"))
@ -316,6 +322,8 @@ class Commands:
tokens = self.coder.main_model.token_count(repo_content)
res.append((tokens, "repository map", "use --map-tokens to resize"))
fence = "`" * 3
# files
for fname in self.coder.abs_fnames:
relative_fname = self.coder.get_rel_fname(fname)
@ -324,11 +332,23 @@ class Commands:
tokens = self.coder.main_model.token_count_for_image(fname)
else:
# approximate
content = f"{relative_fname}\n```\n" + content + "```\n"
content = f"{relative_fname}\n{fence}\n" + content + "{fence}\n"
tokens = self.coder.main_model.token_count(content)
res.append((tokens, f"{relative_fname}", "use /drop to drop from chat"))
res.append((tokens, f"{relative_fname}", "/drop to remove"))
self.io.tool_output("Approximate context window usage, in tokens:")
# read-only files
for fname in self.coder.abs_read_only_fnames:
relative_fname = self.coder.get_rel_fname(fname)
content = self.io.read_text(fname)
if content is not None and not is_image_file(relative_fname):
# approximate
content = f"{relative_fname}\n{fence}\n" + content + "{fence}\n"
tokens = self.coder.main_model.token_count(content)
res.append((tokens, f"{relative_fname} (read-only)", "/drop to remove"))
self.io.tool_output(
f"Approximate context window usage for {self.coder.main_model.name}, in tokens:"
)
self.io.tool_output()
width = 8
@ -344,7 +364,7 @@ class Commands:
total_cost = 0.0
for tk, msg, tip in res:
total += tk
cost = tk * self.coder.main_model.info.get("input_cost_per_token", 0)
cost = tk * (self.coder.main_model.info.get("input_cost_per_token") or 0)
total_cost += cost
msg = msg.ljust(col_width)
self.io.tool_output(f"${cost:7.4f} {fmt(tk)} {msg} {tip}") # noqa: E231
@ -352,7 +372,7 @@ class Commands:
self.io.tool_output("=" * (width + cost_width + 1))
self.io.tool_output(f"${total_cost:7.4f} {fmt(total)} tokens total") # noqa: E231
limit = self.coder.main_model.info.get("max_input_tokens", 0)
limit = self.coder.main_model.info.get("max_input_tokens") or 0
if not limit:
return
@ -440,27 +460,36 @@ class Commands:
# Get the current HEAD after undo
current_head_hash = self.coder.repo.repo.head.commit.hexsha[:7]
current_head_message = self.coder.repo.repo.head.commit.message.strip()
self.io.tool_output(f"HEAD is: {current_head_hash} {current_head_message}")
self.io.tool_output(f"Now at: {current_head_hash} {current_head_message}")
if self.coder.main_model.send_undo_reply:
return prompts.undo_command_reply
def cmd_diff(self, args=""):
"Display the diff of the last aider commit"
"Display the diff of changes since the last message"
if not self.coder.repo:
self.io.tool_error("No git repository found.")
return
last_commit_hash = self.coder.repo.repo.head.commit.hexsha[:7]
if last_commit_hash not in self.coder.aider_commit_hashes:
self.io.tool_error(f"Last commit {last_commit_hash} was not an aider commit.")
self.io.tool_error("You could try `/git diff` or `/git diff HEAD^`.")
current_head = self.coder.repo.get_head()
if current_head is None:
self.io.tool_error("Unable to get current commit. The repository might be empty.")
return
if len(self.coder.commit_before_message) < 2:
commit_before_message = current_head + "^"
else:
commit_before_message = self.coder.commit_before_message[-2]
if not commit_before_message or commit_before_message == current_head:
self.io.tool_error("No changes to display since the last message.")
return
self.io.tool_output(f"Diff since {commit_before_message[:7]}...")
diff = self.coder.repo.diff_commits(
self.coder.pretty,
"HEAD^",
commit_before_message,
"HEAD",
)
@ -472,6 +501,9 @@ class Commands:
fname = f'"{fname}"'
return fname
def completions_read(self):
return self.completions_add()
def completions_add(self):
files = set(self.coder.get_all_relative_files())
files = files - set(self.coder.get_inchat_relative_files())
@ -558,6 +590,18 @@ class Commands:
if abs_file_path in self.coder.abs_fnames:
self.io.tool_error(f"{matched_file} is already in the chat")
elif abs_file_path in self.coder.abs_read_only_fnames:
if self.coder.repo and self.coder.repo.path_in_repo(matched_file):
self.coder.abs_read_only_fnames.remove(abs_file_path)
self.coder.abs_fnames.add(abs_file_path)
self.io.tool_output(
f"Moved {matched_file} from read-only to editable files in the chat"
)
added_fnames.append(matched_file)
else:
self.io.tool_error(
f"Cannot add {matched_file} as it's not part of the repository"
)
else:
if is_image_file(matched_file) and not self.coder.main_model.accepts_images:
self.io.tool_error(
@ -575,20 +619,12 @@ class Commands:
self.coder.check_added_files()
added_fnames.append(matched_file)
if not added_fnames:
return
# only reply if there's been some chatting since the last edit
if not self.coder.cur_messages:
return
reply = prompts.added_files.format(fnames=", ".join(added_fnames))
return reply
def completions_drop(self):
files = self.coder.get_inchat_relative_files()
files = [self.quote_fname(fn) for fn in files]
return files
read_only_files = [self.coder.get_rel_fname(fn) for fn in self.coder.abs_read_only_fnames]
all_files = files + read_only_files
all_files = [self.quote_fname(fn) for fn in all_files]
return all_files
def cmd_drop(self, args=""):
"Remove files from the chat session to free up context space"
@ -596,9 +632,19 @@ class Commands:
if not args.strip():
self.io.tool_output("Dropping all files from the chat session.")
self.coder.abs_fnames = set()
self.coder.abs_read_only_fnames = set()
return
filenames = parse_quoted_filenames(args)
for word in filenames:
# Handle read-only files separately, without glob_filtered_to_repo
read_only_matched = [f for f in self.coder.abs_read_only_fnames if word in f]
if read_only_matched:
for matched_file in read_only_matched:
self.coder.abs_read_only_fnames.remove(matched_file)
self.io.tool_output(f"Removed read-only file {matched_file} from the chat")
matched_files = self.glob_filtered_to_repo(word)
if not matched_files:
@ -678,7 +724,7 @@ class Commands:
add = result.returncode != 0
else:
response = self.io.prompt_ask(
"Add the output to the chat? (y/n/instructions): ", default="y"
"Add the output to the chat?\n(y/n/instructions)", default=""
).strip()
if response.lower() in ["yes", "y"]:
@ -718,6 +764,7 @@ class Commands:
other_files = []
chat_files = []
read_only_files = []
for file in files:
abs_file_path = self.coder.abs_root_path(file)
if abs_file_path in self.coder.abs_fnames:
@ -725,8 +772,13 @@ class Commands:
else:
other_files.append(file)
if not chat_files and not other_files:
self.io.tool_output("\nNo files in chat or git repo.")
# Add read-only files
for abs_file_path in self.coder.abs_read_only_fnames:
rel_file_path = self.coder.get_rel_fname(abs_file_path)
read_only_files.append(rel_file_path)
if not chat_files and not other_files and not read_only_files:
self.io.tool_output("\nNo files in chat, git repo, or read-only list.")
return
if other_files:
@ -734,6 +786,11 @@ class Commands:
for file in other_files:
self.io.tool_output(f" {file}")
if read_only_files:
self.io.tool_output("\nRead-only files:\n")
for file in read_only_files:
self.io.tool_output(f" {file}")
if chat_files:
self.io.tool_output("\nFiles in chat:\n")
for file in chat_files:
@ -787,13 +844,23 @@ class Commands:
"""
user_msg += "\n".join(self.coder.get_announcements()) + "\n"
assistant_msg = coder.run(user_msg)
coder.run(user_msg, preproc=False)
self.coder.cur_messages += [
dict(role="user", content=user_msg),
dict(role="assistant", content=assistant_msg),
]
self.coder.total_cost += coder.total_cost
if self.coder.repo_map:
map_tokens = self.coder.repo_map.max_map_tokens
map_mul_no_files = self.coder.repo_map.map_mul_no_files
else:
map_tokens = 0
map_mul_no_files = 1
raise SwitchCoder(
edit_format=self.coder.edit_format,
summarize_from_coder=False,
from_coder=coder,
map_tokens=map_tokens,
map_mul_no_files=map_mul_no_files,
show_announcements=False,
)
def clone(self):
return Commands(
@ -805,28 +872,35 @@ class Commands:
def cmd_ask(self, args):
"Ask questions about the code base without editing any files"
return self._generic_chat_command(args, "ask")
def cmd_code(self, args):
"Ask for changes to your code"
return self._generic_chat_command(args, self.coder.main_model.edit_format)
def _generic_chat_command(self, args, edit_format):
if not args.strip():
self.io.tool_error("Please provide a question or topic for the chat.")
self.io.tool_error(f"Please provide a question or topic for the {edit_format} chat.")
return
from aider.coders import Coder
chat_coder = Coder.create(
coder = Coder.create(
io=self.io,
from_coder=self.coder,
edit_format="ask",
edit_format=edit_format,
summarize_from_coder=False,
)
user_msg = args
assistant_msg = chat_coder.run(user_msg)
coder.run(user_msg)
self.coder.cur_messages += [
dict(role="user", content=user_msg),
dict(role="assistant", content=assistant_msg),
]
self.coder.total_cost += chat_coder.total_cost
raise SwitchCoder(
edit_format=self.coder.edit_format,
summarize_from_coder=False,
from_coder=coder,
show_announcements=False,
)
def get_help_md(self):
"Show help about all commands in markdown"
@ -894,27 +968,82 @@ class Commands:
return text
def cmd_add_clipboard_image(self, args):
"Add an image from the clipboard to the chat"
def cmd_clipboard(self, args):
"Add image/text from the clipboard to the chat (optionally provide a name for the image)"
try:
# Check for image first
image = ImageGrab.grabclipboard()
if image is None:
self.io.tool_error("No image found in clipboard.")
if isinstance(image, Image.Image):
if args.strip():
filename = args.strip()
ext = os.path.splitext(filename)[1].lower()
if ext in (".jpg", ".jpeg", ".png"):
basename = filename
else:
basename = f"{filename}.png"
else:
basename = "clipboard_image.png"
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, basename)
image_format = "PNG" if basename.lower().endswith(".png") else "JPEG"
image.save(temp_file_path, image_format)
abs_file_path = Path(temp_file_path).resolve()
# Check if a file with the same name already exists in the chat
existing_file = next(
(f for f in self.coder.abs_fnames if Path(f).name == abs_file_path.name), None
)
if existing_file:
self.coder.abs_fnames.remove(existing_file)
self.io.tool_output(f"Replaced existing image in the chat: {existing_file}")
self.coder.abs_fnames.add(str(abs_file_path))
self.io.tool_output(f"Added clipboard image to the chat: {abs_file_path}")
self.coder.check_added_files()
return
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
image.save(temp_file.name, "PNG")
temp_file_path = temp_file.name
# If not an image, try to get text
text = pyperclip.paste()
if text:
self.io.tool_output(text)
return text
abs_file_path = Path(temp_file_path).resolve()
self.coder.abs_fnames.add(str(abs_file_path))
self.io.tool_output(f"Added clipboard image to the chat: {abs_file_path}")
self.coder.check_added_files()
return prompts.added_files.format(fnames=str(abs_file_path))
self.io.tool_error("No image or text content found in clipboard.")
return
except Exception as e:
self.io.tool_error(f"Error adding clipboard image: {e}")
self.io.tool_error(f"Error processing clipboard content: {e}")
def cmd_read(self, args):
"Add a file to the chat that is for reference, not to be edited"
if not args.strip():
self.io.tool_error("Please provide a filename to read.")
return
filename = args.strip()
abs_path = os.path.abspath(filename)
if not os.path.exists(abs_path):
self.io.tool_error(f"File not found: {abs_path}")
return
if not os.path.isfile(abs_path):
self.io.tool_error(f"Not a file: {abs_path}")
return
self.coder.abs_read_only_fnames.add(abs_path)
self.io.tool_output(f"Added {abs_path} to read-only files.")
def cmd_map(self, args):
"Print out the current repository map"
repo_map = self.coder.get_repo_map()
if repo_map:
self.io.tool_output(repo_map)
else:
self.io.tool_output("No repository map available.")
def expand_subdir(file_path):

View file

@ -15,6 +15,7 @@ from pygments.lexers import MarkdownLexer, guess_lexer_for_filename
from pygments.token import Token
from pygments.util import ClassNotFound
from rich.console import Console
from rich.style import Style as RichStyle
from rich.text import Text
from .dump import dump # noqa: F401
@ -22,10 +23,13 @@ from .utils import is_image_file
class AutoCompleter(Completer):
def __init__(self, root, rel_fnames, addable_rel_fnames, commands, encoding):
def __init__(
self, root, rel_fnames, addable_rel_fnames, commands, encoding, abs_read_only_fnames=None
):
self.addable_rel_fnames = addable_rel_fnames
self.rel_fnames = rel_fnames
self.encoding = encoding
self.abs_read_only_fnames = abs_read_only_fnames or []
fname_to_rel_fnames = defaultdict(list)
for rel_fname in addable_rel_fnames:
@ -47,7 +51,11 @@ class AutoCompleter(Completer):
for rel_fname in rel_fnames:
self.words.add(rel_fname)
fname = Path(root) / rel_fname
all_fnames = [Path(root) / rel_fname for rel_fname in rel_fnames]
if abs_read_only_fnames:
all_fnames.extend(abs_read_only_fnames)
for fname in all_fnames:
try:
with open(fname, "r", encoding=self.encoding) as f:
content = f.read()
@ -217,7 +225,7 @@ class InputOutput:
with open(str(filename), "w", encoding=self.encoding) as f:
f.write(content)
def get_input(self, root, rel_fnames, addable_rel_fnames, commands):
def get_input(self, root, rel_fnames, addable_rel_fnames, commands, abs_read_only_fnames=None):
if self.pretty:
style = dict(style=self.user_input_color) if self.user_input_color else dict()
self.console.rule(**style)
@ -244,7 +252,12 @@ class InputOutput:
style = None
completer_instance = AutoCompleter(
root, rel_fnames, addable_rel_fnames, commands, self.encoding
root,
rel_fnames,
addable_rel_fnames,
commands,
self.encoding,
abs_read_only_fnames=abs_read_only_fnames,
)
while True:
@ -317,7 +330,7 @@ class InputOutput:
def user_input(self, inp, log_only=True):
if not log_only:
style = dict(style=self.user_input_color) if self.user_input_color else dict()
self.console.print(inp, **style)
self.console.print(Text(inp), **style)
prefix = "####"
if inp:
@ -341,18 +354,19 @@ class InputOutput:
self.num_user_asks += 1
if self.yes is True:
res = "yes"
res = "y"
elif self.yes is False:
res = "no"
res = "n"
else:
res = prompt(question + " ", default=default)
hist = f"{question.strip()} {res.strip()}"
res = res.lower().strip()
is_yes = res in ("y", "yes")
hist = f"{question.strip()} {'y' if is_yes else 'n'}"
self.append_chat_history(hist, linebreak=True, blockquote=True)
if not res or not res.strip():
return
return res.strip().lower().startswith("y")
return is_yes
def prompt_ask(self, question, default=None):
self.num_user_asks += 1
@ -389,7 +403,7 @@ class InputOutput:
style = dict(style=self.tool_error_color) if self.tool_error_color else dict()
self.console.print(message, **style)
def tool_output(self, *messages, log_only=False):
def tool_output(self, *messages, log_only=False, bold=False):
if messages:
hist = " ".join(messages)
hist = f"{hist.strip()}"
@ -397,8 +411,10 @@ class InputOutput:
if not log_only:
messages = list(map(Text, messages))
style = dict(style=self.tool_output_color) if self.tool_output_color else dict()
self.console.print(*messages, **style)
style = dict(color=self.tool_output_color) if self.tool_output_color else dict()
style["reverse"] = bold
style = RichStyle(**style)
self.console.print(*messages, style=style)
def append_chat_history(self, text, linebreak=False, blockquote=False, strip=True):
if blockquote:

View file

@ -4,8 +4,11 @@ import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
os.environ["OR_SITE_URL"] = "http://aider.chat"
os.environ["OR_APP_NAME"] = "Aider"
AIDER_SITE_URL = "https://aider.chat"
AIDER_APP_NAME = "Aider"
os.environ["OR_SITE_URL"] = AIDER_SITE_URL
os.environ["OR_APP_NAME"] = AIDER_APP_NAME
# `import litellm` takes 1.5 seconds, defer it!

View file

@ -384,6 +384,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
all_files = args.files + (args.file or [])
fnames = [str(Path(fn).resolve()) for fn in all_files]
read_only_fnames = [str(Path(fn).resolve()) for fn in (args.read or [])]
if len(all_files) > 1:
good = True
for fname in all_files:
@ -415,11 +416,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
return main(argv, input, output, right_repo_root, return_coder=return_coder)
if args.just_check_update:
update_available = check_version(io, just_check=True)
update_available = check_version(io, just_check=True, verbose=args.verbose)
return 0 if not update_available else 1
if args.check_update:
check_version(io)
check_version(io, verbose=args.verbose)
if args.models:
models.print_matching_models(io, args.models)
@ -475,12 +476,13 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
repo = GitRepo(
io,
fnames,
git_dname or ".",
git_dname,
args.aiderignore,
models=main_model.commit_message_models(),
attribute_author=args.attribute_author,
attribute_committer=args.attribute_committer,
attribute_commit_message=args.attribute_commit_message,
attribute_commit_message_author=args.attribute_commit_message_author,
attribute_commit_message_committer=args.attribute_commit_message_committer,
commit_prompt=args.commit_prompt,
subtree_only=args.subtree_only,
)
@ -501,7 +503,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
io=io,
repo=repo,
fnames=fnames,
pretty=args.pretty,
read_only_fnames=read_only_fnames,
show_diffs=args.show_diffs,
auto_commits=args.auto_commits,
dirty_commits=args.dirty_commits,
@ -618,8 +620,15 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
coder.run()
return
except SwitchCoder as switch:
coder = Coder.create(io=io, from_coder=coder, **switch.kwargs)
coder.show_announcements()
kwargs = dict(io=io, from_coder=coder)
kwargs.update(switch.kwargs)
if "show_announcements" in kwargs:
del kwargs["show_announcements"]
coder = Coder.create(**kwargs)
if switch.kwargs.get("show_announcements") is not False:
coder.show_announcements()
def load_slow_imports():

View file

@ -3,6 +3,7 @@ import importlib
import json
import math
import os
import platform
import sys
from dataclasses import dataclass, fields
from pathlib import Path
@ -13,7 +14,7 @@ from PIL import Image
from aider import urls
from aider.dump import dump # noqa: F401
from aider.llm import litellm
from aider.llm import AIDER_APP_NAME, AIDER_SITE_URL, litellm
DEFAULT_MODEL_NAME = "gpt-4o"
@ -70,7 +71,6 @@ class ModelSettings:
lazy: bool = False
reminder_as_sys_msg: bool = False
examples_as_sys_msg: bool = False
can_prefill: bool = False
extra_headers: Optional[dict] = None
max_tokens: Optional[int] = None
@ -152,6 +152,16 @@ MODEL_SETTINGS = [
lazy=True,
reminder_as_sys_msg=True,
),
ModelSettings(
"gpt-4o-2024-08-06",
"diff",
weak_model_name="gpt-4o-mini",
use_repo_map=True,
send_undo_reply=True,
accepts_images=True,
lazy=True,
reminder_as_sys_msg=True,
),
ModelSettings(
"gpt-4o",
"diff",
@ -238,7 +248,6 @@ MODEL_SETTINGS = [
weak_model_name="claude-3-haiku-20240307",
use_repo_map=True,
send_undo_reply=True,
can_prefill=True,
),
ModelSettings(
"openrouter/anthropic/claude-3-opus",
@ -246,13 +255,11 @@ MODEL_SETTINGS = [
weak_model_name="openrouter/anthropic/claude-3-haiku",
use_repo_map=True,
send_undo_reply=True,
can_prefill=True,
),
ModelSettings(
"claude-3-sonnet-20240229",
"whole",
weak_model_name="claude-3-haiku-20240307",
can_prefill=True,
),
ModelSettings(
"claude-3-5-sonnet-20240620",
@ -260,7 +267,6 @@ MODEL_SETTINGS = [
weak_model_name="claude-3-haiku-20240307",
use_repo_map=True,
examples_as_sys_msg=True,
can_prefill=True,
accepts_images=True,
max_tokens=8192,
extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"},
@ -271,9 +277,12 @@ MODEL_SETTINGS = [
weak_model_name="claude-3-haiku-20240307",
use_repo_map=True,
examples_as_sys_msg=True,
can_prefill=True,
max_tokens=8192,
extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"},
extra_headers={
"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15",
"HTTP-Referer": AIDER_SITE_URL,
"X-Title": AIDER_APP_NAME,
},
),
ModelSettings(
"openrouter/anthropic/claude-3.5-sonnet",
@ -281,10 +290,13 @@ MODEL_SETTINGS = [
weak_model_name="openrouter/anthropic/claude-3-haiku-20240307",
use_repo_map=True,
examples_as_sys_msg=True,
can_prefill=True,
accepts_images=True,
max_tokens=8192,
extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"},
extra_headers={
"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15",
"HTTP-Referer": "https://aider.chat",
"X-Title": "Aider",
},
),
# Vertex AI Claude models
# Does not yet support 8k token
@ -294,7 +306,6 @@ MODEL_SETTINGS = [
weak_model_name="vertex_ai/claude-3-haiku@20240307",
use_repo_map=True,
examples_as_sys_msg=True,
can_prefill=True,
accepts_images=True,
),
ModelSettings(
@ -303,13 +314,11 @@ MODEL_SETTINGS = [
weak_model_name="vertex_ai/claude-3-haiku@20240307",
use_repo_map=True,
send_undo_reply=True,
can_prefill=True,
),
ModelSettings(
"vertex_ai/claude-3-sonnet@20240229",
"whole",
weak_model_name="vertex_ai/claude-3-haiku@20240307",
can_prefill=True,
),
# Cohere
ModelSettings(
@ -405,9 +414,7 @@ class Model:
self.missing_keys = res.get("missing_keys")
self.keys_in_environment = res.get("keys_in_environment")
max_input_tokens = self.info.get("max_input_tokens")
if not max_input_tokens:
max_input_tokens = 0
max_input_tokens = self.info.get("max_input_tokens") or 0
if max_input_tokens < 32 * 1024:
self.max_chat_history_tokens = 1024
else:
@ -470,14 +477,10 @@ class Model:
if "gpt-3.5" in model or "gpt-4" in model:
self.reminder_as_sys_msg = True
if "anthropic" in model:
self.can_prefill = True
if "3.5-sonnet" in model or "3-5-sonnet" in model:
self.edit_format = "diff"
self.use_repo_map = True
self.examples_as_sys_msg = True
self.can_prefill = True
# use the defaults
if self.edit_format == "diff":
@ -512,6 +515,9 @@ class Model:
return litellm.encode(model=self.name, text=text)
def token_count(self, messages):
if type(messages) is list:
return litellm.token_counter(model=self.name, messages=messages)
if not self.tokenizer:
return
@ -669,6 +675,13 @@ def sanity_check_model(io, model):
io.tool_error(f"Model {model}: Missing these environment variables:")
for key in model.missing_keys:
io.tool_error(f"- {key}")
if platform.system() == "Windows" or True:
io.tool_output(
"If you just set these environment variables using `setx` you may need to restart"
" your terminal or command prompt for the changes to take effect."
)
elif not model.keys_in_environment:
show = True
io.tool_output(f"Model {model}: Unknown which environment variables are required.")

View file

@ -29,7 +29,8 @@ class GitRepo:
models=None,
attribute_author=True,
attribute_committer=True,
attribute_commit_message=False,
attribute_commit_message_author=False,
attribute_commit_message_committer=False,
commit_prompt=None,
subtree_only=False,
):
@ -41,7 +42,8 @@ class GitRepo:
self.attribute_author = attribute_author
self.attribute_committer = attribute_committer
self.attribute_commit_message = attribute_commit_message
self.attribute_commit_message_author = attribute_commit_message_author
self.attribute_commit_message_committer = attribute_commit_message_committer
self.commit_prompt = commit_prompt
self.subtree_only = subtree_only
self.ignore_file_cache = {}
@ -98,7 +100,9 @@ class GitRepo:
else:
commit_message = self.get_commit_message(diffs, context)
if aider_edits and self.attribute_commit_message:
if aider_edits and self.attribute_commit_message_author:
commit_message = "aider: " + commit_message
elif self.attribute_commit_message_committer:
commit_message = "aider: " + commit_message
if not commit_message:
@ -130,7 +134,7 @@ class GitRepo:
self.repo.git.commit(cmd)
commit_hash = self.repo.head.commit.hexsha[:7]
self.io.tool_output(f"Commit {commit_hash} {commit_message}")
self.io.tool_output(f"Commit {commit_hash} {commit_message}", bold=True)
# Restore the env
@ -155,10 +159,6 @@ class GitRepo:
return self.repo.git_dir
def get_commit_message(self, diffs, context):
if len(diffs) >= 4 * 1024 * 4:
self.io.tool_error("Diff is too large to generate a commit message.")
return
diffs = "# Diffs:\n" + diffs
content = ""
@ -172,7 +172,12 @@ class GitRepo:
dict(role="user", content=content),
]
commit_message = None
for model in self.models:
num_tokens = model.token_count(messages)
max_tokens = model.info.get("max_input_tokens") or 0
if max_tokens and num_tokens > max_tokens:
continue
commit_message = simple_send_with_retries(model.name, messages)
if commit_message:
break
@ -226,6 +231,8 @@ class GitRepo:
args = []
if pretty:
args += ["--color"]
else:
args += ["--color=never"]
args += [from_commit, to_commit]
diffs = self.repo.git.diff(*args)
@ -355,3 +362,9 @@ class GitRepo:
return True
return self.repo.is_dirty(path=path)
def get_head(self):
try:
return self.repo.head.commit.hexsha
except ValueError:
return None

View file

@ -60,6 +60,9 @@ class RepoMap:
self.main_model = main_model
self.tree_cache = {}
self.tree_context_cache = {}
def token_count(self, text):
len_text = len(text)
if len_text < 200:
@ -471,24 +474,28 @@ class RepoMap:
if key in self.tree_cache:
return self.tree_cache[key]
code = self.io.read_text(abs_fname) or ""
if not code.endswith("\n"):
code += "\n"
if rel_fname not in self.tree_context_cache:
code = self.io.read_text(abs_fname) or ""
if not code.endswith("\n"):
code += "\n"
context = TreeContext(
rel_fname,
code,
color=False,
line_number=False,
child_context=False,
last_line=False,
margin=0,
mark_lois=False,
loi_pad=0,
# header_max=30,
show_top_of_file_parent_scope=False,
)
context = TreeContext(
rel_fname,
code,
color=False,
line_number=False,
child_context=False,
last_line=False,
margin=0,
mark_lois=False,
loi_pad=0,
# header_max=30,
show_top_of_file_parent_scope=False,
)
self.tree_context_cache[rel_fname] = context
context = self.tree_context_cache[rel_fname]
context.lines_of_interest = set()
context.add_lines_of_interest(lois)
context.add_context()
res = context.format()

View file

@ -87,26 +87,48 @@ class Scraper:
def scrape(self, url):
"""
Scrape a url and turn it into readable markdown.
Scrape a url and turn it into readable markdown if it's HTML.
If it's plain text or non-HTML, return it as-is.
`url` - the URLto scrape.
`url` - the URL to scrape.
"""
if self.playwright_available:
content = self.scrape_with_playwright(url)
content, mime_type = self.scrape_with_playwright(url)
else:
content = self.scrape_with_httpx(url)
content, mime_type = self.scrape_with_httpx(url)
if not content:
self.print_error(f"Failed to retrieve content from {url}")
return None
self.try_pandoc()
content = self.html_to_markdown(content)
# Check if the content is HTML based on MIME type or content
if (mime_type and mime_type.startswith("text/html")) or (
mime_type is None and self.looks_like_html(content)
):
self.try_pandoc()
content = self.html_to_markdown(content)
return content
def looks_like_html(self, content):
"""
Check if the content looks like HTML.
"""
if isinstance(content, str):
# Check for common HTML tags
html_patterns = [
r"<!DOCTYPE\s+html",
r"<html",
r"<head",
r"<body",
r"<div",
r"<p>",
r"<a\s+href=",
]
return any(re.search(pattern, content, re.IGNORECASE) for pattern in html_patterns)
return False
# Internals...
def scrape_with_playwright(self, url):
import playwright
@ -118,7 +140,7 @@ class Scraper:
except Exception as e:
self.playwright_available = False
self.print_error(str(e))
return
return None, None
try:
context = browser.new_context(ignore_https_errors=not self.verify_ssl)
@ -131,23 +153,28 @@ class Scraper:
page.set_extra_http_headers({"User-Agent": user_agent})
response = None
try:
page.goto(url, wait_until="networkidle", timeout=5000)
response = page.goto(url, wait_until="networkidle", timeout=5000)
except playwright._impl._errors.TimeoutError:
self.print_error(f"Timeout while loading {url}")
except playwright._impl._errors.Error as e:
self.print_error(f"Error navigating to {url}: {str(e)}")
return None
return None, None
try:
content = page.content()
mime_type = (
response.header_value("content-type").split(";")[0] if response else None
)
except playwright._impl._errors.Error as e:
self.print_error(f"Error retrieving page content: {str(e)}")
content = None
mime_type = None
finally:
browser.close()
return content
return content, mime_type
def scrape_with_httpx(self, url):
import httpx
@ -157,12 +184,12 @@ class Scraper:
with httpx.Client(headers=headers, verify=self.verify_ssl) as client:
response = client.get(url)
response.raise_for_status()
return response.text
return response.text, response.headers.get("content-type", "").split(";")[0]
except httpx.HTTPError as http_err:
self.print_error(f"HTTP error occurred: {http_err}")
except Exception as err:
self.print_error(f"An error occurred: {err}")
return None
return None, None
def try_pandoc(self):
if self.pandoc_available:

View file

@ -14,24 +14,28 @@ CACHE = None
# CACHE = Cache(CACHE_PATH)
def retry_exceptions():
import httpx
return (
httpx.ConnectError,
httpx.RemoteProtocolError,
httpx.ReadTimeout,
litellm.exceptions.APIConnectionError,
litellm.exceptions.APIError,
litellm.exceptions.RateLimitError,
litellm.exceptions.ServiceUnavailableError,
litellm.exceptions.Timeout,
litellm.exceptions.InternalServerError,
litellm.llms.anthropic.AnthropicError,
)
def lazy_litellm_retry_decorator(func):
def wrapper(*args, **kwargs):
import httpx
decorated_func = backoff.on_exception(
backoff.expo,
(
httpx.ConnectError,
httpx.RemoteProtocolError,
httpx.ReadTimeout,
litellm.exceptions.APIConnectionError,
litellm.exceptions.APIError,
litellm.exceptions.RateLimitError,
litellm.exceptions.ServiceUnavailableError,
litellm.exceptions.Timeout,
litellm.exceptions.InternalServerError,
litellm.llms.anthropic.AnthropicError,
),
retry_exceptions(),
max_time=60,
on_backoff=lambda details: print(
f"{details.get('exception', 'Exception')}\nRetry in {details['wait']:.1f} seconds."
@ -42,8 +46,7 @@ def lazy_litellm_retry_decorator(func):
return wrapper
@lazy_litellm_retry_decorator
def send_with_retries(
def send_completion(
model_name, messages, functions, stream, temperature=0, extra_headers=None, max_tokens=None
):
from aider.llm import litellm
@ -54,6 +57,7 @@ def send_with_retries(
temperature=temperature,
stream=stream,
)
if functions is not None:
kwargs["tools"] = [dict(type="functions", function=functions[0])]
if extra_headers is not None:
@ -79,9 +83,10 @@ def send_with_retries(
return hash_object, res
@lazy_litellm_retry_decorator
def simple_send_with_retries(model_name, messages):
try:
_hash, response = send_with_retries(
_hash, response = send_completion(
model_name=model_name,
messages=messages,
functions=None,

View file

@ -44,7 +44,7 @@ class ChdirTemporaryDirectory(IgnorantTemporaryDirectory):
def __enter__(self):
res = super().__enter__()
os.chdir(self.temp_dir.name)
os.chdir(Path(self.temp_dir.name).resolve())
return res
def __exit__(self, exc_type, exc_val, exc_tb):
@ -112,13 +112,19 @@ def format_messages(messages, title=None):
content = msg.get("content")
if isinstance(content, list): # Handle list content (e.g., image messages)
for item in content:
if isinstance(item, dict) and "image_url" in item:
output.append(f"{role} Image URL: {item['image_url']['url']}")
if isinstance(item, dict):
for key, value in item.items():
if isinstance(value, dict) and "url" in value:
output.append(f"{role} {key.capitalize()} URL: {value['url']}")
else:
output.append(f"{role} {key}: {value}")
else:
output.append(f"{role} {item}")
elif isinstance(content, str): # Handle string content
output.append(format_content(role, content))
content = msg.get("function_call")
if content:
output.append(f"{role} {content}")
function_call = msg.get("function_call")
if function_call:
output.append(f"{role} Function Call: {function_call}")
return "\n".join(output)

View file

@ -10,12 +10,15 @@ from aider import utils
from aider.dump import dump # noqa: F401
def check_version(io, just_check=False):
def check_version(io, just_check=False, verbose=False):
fname = Path.home() / ".aider" / "caches" / "versioncheck"
if not just_check and fname.exists():
day = 60 * 60 * 24
since = time.time() - fname.stat().st_mtime
if since < day:
if verbose:
hours = since / 60 / 60
io.tool_output(f"Too soon to check version: {hours:.1f} hours")
return
# To keep startup fast, avoid importing this unless needed
@ -27,7 +30,7 @@ def check_version(io, just_check=False):
latest_version = data["info"]["version"]
current_version = aider.__version__
if just_check:
if just_check or verbose:
io.tool_output(f"Current version: {current_version}")
io.tool_output(f"Latest version: {latest_version}")
@ -41,11 +44,13 @@ def check_version(io, just_check=False):
fname.parent.mkdir(parents=True, exist_ok=True)
fname.touch()
if just_check:
if just_check or verbose:
if is_update_available:
io.tool_output("Update available")
else:
io.tool_output("No update available")
if just_check:
return is_update_available
if not is_update_available:

View file

@ -16,6 +16,51 @@ cog.out(text)
# Release history
### Aider v0.50.0
- Infinite output for DeepSeek Coder, Mistral models in addition to Anthropic's models.
- New `--deepseek` switch to use DeepSeek Coder.
- DeepSeek Coder uses 8k token output.
- New `--chat-mode <mode>` switch to launch in ask/help/code modes.
- New `/code <message>` command request a code edit while in `ask` mode.
- Web scraper is more robust if page never idles.
- Improved token and cost reporting for infinite output.
- Improvements and bug fixes for `/read` only files.
- Switched from `setup.py` to `pyproject.toml`, by @branchvincent.
- Bug fix to persist files added during `/ask`.
- Bug fix for chat history size in `/tokens`.
- Aider wrote 66% of the code in this release.
### Aider v0.49.1
- Bugfix to `/help`.
### Aider v0.49.0
- Add read-only files to the chat context with `/read` and `--read`, including from outside the git repo.
- `/diff` now shows diffs of all changes resulting from your request, including lint and test fixes.
- New `/clipboard` command to paste images or text from the clipboard, replaces `/add-clipboard-image`.
- Now shows the markdown scraped when you add a url with `/web`.
- When [scripting aider](https://aider.chat/docs/scripting.html) messages can now contain in-chat `/` commands.
- Aider in docker image now suggests the correct command to update to latest version.
- Improved retries on API errors (was easy to test during Sonnet outage).
- Added `--mini` for `gpt-4o-mini`.
- Bugfix to keep session cost accurate when using `/ask` and `/help`.
- Performance improvements for repo map calculation.
- `/tokens` now shows the active model.
- Enhanced commit message attribution options:
- New `--attribute-commit-message-author` to prefix commit messages with 'aider: ' if aider authored the changes, replaces `--attribute-commit-message`.
- New `--attribute-commit-message-committer` to prefix all commit messages with 'aider: '.
- Aider wrote 61% of the code in this release.
### Aider v0.48.1
- Added `openai/gpt-4o-2024-08-06`.
- Worked around litellm bug that removes OpenRouter app headers when using `extra_headers`.
- Improved progress indication during repo map processing.
- Corrected instructions for upgrading the docker container to latest aider version.
- Removed obsolete 16k token limit on commit diffs, use per-model limits.
### Aider v0.48.0
- Performance improvements for large/mono repos.

File diff suppressed because it is too large Load diff

View file

@ -1,90 +1,126 @@
<canvas id="blameChart" width="800" height="450" style="margin-top: 20px"></canvas>
<canvas id="blameChart" width="800" height="360" style="margin-top: 20px"></canvas>
<canvas id="linesChart" width="800" height="360" style="margin-top: 20px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
var ctx = document.getElementById('blameChart').getContext('2d');
var blameCtx = document.getElementById('blameChart').getContext('2d');
var linesCtx = document.getElementById('linesChart').getContext('2d');
var labels = [{% for row in site.data.blame %}'{{ row.end_tag }}',{% endfor %}];
var blameData = {
labels: labels,
datasets: [{
label: 'Aider\'s Contribution to Each Release',
data: [
{% for row in site.data.blame %}
{
x: '{{ row.end_date }}',
y: {{ row.aider_percentage }},
r: Math.sqrt({{ row.aider_total }}) * 1.5,
label: '{{ row.end_tag }}',
percentage: {{ row.aider_percentage }},
lines: {{ row.aider_total }}
},
{% endfor %}
],
backgroundColor: 'rgba(54, 162, 235, 0.2)',
label: 'Aider\'s percent of new code by release',
data: [{% for row in site.data.blame %}{ x: '{{ row.end_tag }}', y: {{ row.aider_percentage }}, lines: {{ row.aider_total }} },{% endfor %}],
backgroundColor: 'rgba(54, 162, 235, 0.8)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
};
var blameChart = new Chart(ctx, {
type: 'bubble',
var linesData = {
labels: labels,
datasets: [{
label: 'Aider\'s lines of new code',
data: [{% for row in site.data.blame %}{ x: '{{ row.end_tag }}', y: {{ row.aider_total }} },{% endfor %}],
backgroundColor: 'rgba(255, 99, 132, 0.8)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}]
};
var blameChart = new Chart(blameCtx, {
type: 'bar',
data: blameData,
options: {
scales: {
x: {
type: 'time',
time: {
unit: 'month',
displayFormats: {
month: 'MMM YYYY'
}
},
type: 'category',
title: {
display: true,
text: 'Release date'
text: 'Version'
},
ticks: {
maxRotation: 45,
minRotation: 45
},
min: moment('{{ site.data.blame | first | map: "end_date" | first }}').subtract(1, 'month'),
max: moment('{{ site.data.blame | last | map: "end_date" | first }}').add(1, 'month')
}
},
y: {
title: {
display: true,
text: 'Aider Contribution (% of code)'
text: 'Percent of new code'
},
beginAtZero: true
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.raw.label}: ${Math.round(context.raw.percentage)}% (${context.raw.lines} lines)`;
}
}
},
legend: {
display: true,
position: 'top',
labels: {
generateLabels: function(chart) {
return [{
text: 'Bubble size: Lines of code contributed by aider',
fillStyle: 'rgba(54, 162, 235, 0.2)',
strokeStyle: 'rgba(54, 162, 235, 1)',
lineWidth: 1,
hidden: false,
index: 0
}];
var label = 'Aider\'s contribution';
var value = context.parsed.y || 0;
var lines = context.raw.lines || 0;
return `${label}: ${Math.round(value)}% (${lines} lines)`;
}
}
},
title: {
display: true,
text: 'Aider\'s Contribution to Each Release',
text: 'Percent of new code written by aider, by release',
font: {
size: 16
}
}
}
}
});
var linesChart = new Chart(linesCtx, {
type: 'bar',
data: linesData,
options: {
scales: {
x: {
type: 'category',
title: {
display: true,
text: 'Version'
},
ticks: {
maxRotation: 45,
minRotation: 45
}
},
y: {
title: {
display: true,
text: 'Lines of new code'
},
beginAtZero: true
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
var label = 'New lines of code by aider';
var value = context.parsed.y || 0;
return `${label}: ${value}`;
}
}
},
title: {
display: true,
text: 'Lines of new code written by aider, by release',
font: {
size: 16
}

View file

@ -2,16 +2,16 @@
You can get started quickly like this:
```
$ pip install aider-chat
python -m pip install aider-chat
# Change directory into a git repo
$ cd /to/your/git/repo
cd /to/your/git/repo
# Work with Claude 3.5 Sonnet on your repo
$ export ANTHROPIC_API_KEY=your-key-goes-here
$ aider
export ANTHROPIC_API_KEY=your-key-goes-here
aider
# Work with GPT-4o on your repo
$ export OPENAI_API_KEY=your-key-goes-here
$ aider
export OPENAI_API_KEY=your-key-goes-here
aider
```

View file

@ -44,6 +44,10 @@ Model azure/gpt-4-turbo: Missing these environment variables:
- AZURE_API_KEY
```
{: .tip }
On Windows,
if you just set these environment variables using `setx` you may need to restart your terminal or
command prompt for the changes to take effect.
## Unknown which environment variables are required

View file

@ -0,0 +1,5 @@
{: .tip }
In some environments you may get "aider command not found" errors.
You can try `python -m aider` or
[see here for more info](/docs/troubleshooting/aider-not-found.html).

View file

@ -0,0 +1,7 @@
{: .tip }
Using a Python
[virtual environment](https://docs.python.org/3/library/venv.html){:target="_blank"}
is recommended.
Or, you could
[use pipx to install aider](/docs/install/pipx.html)
once for your whole system.

View file

@ -209,7 +209,7 @@ that aider originally used.
Switching from ctags to tree-sitter provides a bunch of benefits:
- The map is richer, showing full function call signatures and other details straight from the source files.
- Thanks to `py-tree-sitter-languages`, we get full support for many programming languages via a python package that's automatically installed as part of the normal `pip install aider-chat`.
- Thanks to `py-tree-sitter-languages`, we get full support for many programming languages via a python package that's automatically installed as part of the normal `python -m pip install aider-chat`.
- We remove the requirement for users to manually install `universal-ctags` via some external tool or package manager (brew, apt, choco, etc).
- Tree-sitter integration is a key enabler for future work and capabilities for aider.

View file

@ -23,7 +23,7 @@ making it the best available model for pair programming with AI.
To use Claude 3 Opus with aider:
```
pip install aider-chat
python -m pip install aider-chat
export ANTHROPIC_API_KEY=sk-...
aider --opus
```

View file

@ -46,7 +46,7 @@ It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.htm
Use the `--browser` switch to launch the browser version of aider:
```
pip install aider-chat
python -m pip install aider-chat
export OPENAI_API_KEY=<key> # Mac/Linux
setx OPENAI_API_KEY <key> # Windows, restart shell after setx

View file

@ -116,7 +116,7 @@ for more details, but
you can get started quickly with aider and Sonnet like this:
```
$ pip install aider-chat
$ python -m pip install aider-chat
$ export ANTHROPIC_API_KEY=<key> # Mac/Linux
$ setx ANTHROPIC_API_KEY <key> # Windows, restart shell after setx

View file

@ -30,7 +30,7 @@ included for scale.
You can code with all of these models using aider like this:
```
$ pip install aider-chat
$ python -m pip install aider-chat
# Change directory into a git repo to work on
$ cd /to/your/git/repo

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Before After
Before After

View file

@ -38,12 +38,18 @@
## Use gpt-4o model for the main chat
#4o: false
## Use gpt-4o-mini model for the main chat
#mini: false
## Use gpt-4-1106-preview model for the main chat
#4-turbo: false
## Use gpt-3.5-turbo model for the main chat
#35turbo: false
## Use deepseek/deepseek-coder model for the main chat
#deepseek: false
#################
# Model Settings:
@ -167,8 +173,11 @@
## Attribute aider commits in the git committer name (default: True)
#attribute-committer: true
## Prefix commit messages with 'aider: ' (default: False)
#attribute-commit-message: false
## Prefix commit messages with 'aider: ' if aider authored the changes (default: False)
#attribute-commit-message-author: false
## Prefix all commit messages with 'aider: ' (default: False)
#attribute-commit-message-committer: false
## Commit all pending changes with a suitable commit message, then exit
#commit: false
@ -206,6 +215,9 @@
## specify a file to edit (can be used multiple times)
#file:
## specify a read-only file (can be used multiple times)
#read:
## Use VI editing mode in the terminal (default: False)
#vim: false

View file

@ -42,12 +42,18 @@
## Use gpt-4o model for the main chat
#AIDER_4O=
## Use gpt-4o-mini model for the main chat
#AIDER_MINI=
## Use gpt-4-1106-preview model for the main chat
#AIDER_4_TURBO=
## Use gpt-3.5-turbo model for the main chat
#AIDER_35TURBO=
## Use deepseek/deepseek-coder model for the main chat
#AIDER_DEEPSEEK=
#################
# Model Settings:
@ -171,8 +177,11 @@
## Attribute aider commits in the git committer name (default: True)
#AIDER_ATTRIBUTE_COMMITTER=true
## Prefix commit messages with 'aider: ' (default: False)
#AIDER_ATTRIBUTE_COMMIT_MESSAGE=false
## Prefix commit messages with 'aider: ' if aider authored the changes (default: False)
#AIDER_ATTRIBUTE_COMMIT_MESSAGE_AUTHOR=false
## Prefix all commit messages with 'aider: ' (default: False)
#AIDER_ATTRIBUTE_COMMIT_MESSAGE_COMMITTER=false
## Commit all pending changes with a suitable commit message, then exit
#AIDER_COMMIT=false
@ -210,6 +219,9 @@
## specify a file to edit (can be used multiple times)
#AIDER_FILE=
## specify a read-only file (can be used multiple times)
#AIDER_READ=
## Use VI editing mode in the terminal (default: False)
#AIDER_VIM=false

View file

@ -77,12 +77,18 @@ cog.outl("```")
## Use gpt-4o model for the main chat
#4o: false
## Use gpt-4o-mini model for the main chat
#mini: false
## Use gpt-4-1106-preview model for the main chat
#4-turbo: false
## Use gpt-3.5-turbo model for the main chat
#35turbo: false
## Use deepseek/deepseek-coder model for the main chat
#deepseek: false
#################
# Model Settings:
@ -206,8 +212,11 @@ cog.outl("```")
## Attribute aider commits in the git committer name (default: True)
#attribute-committer: true
## Prefix commit messages with 'aider: ' (default: False)
#attribute-commit-message: false
## Prefix commit messages with 'aider: ' if aider authored the changes (default: False)
#attribute-commit-message-author: false
## Prefix all commit messages with 'aider: ' (default: False)
#attribute-commit-message-committer: false
## Commit all pending changes with a suitable commit message, then exit
#commit: false
@ -245,6 +254,9 @@ cog.outl("```")
## specify a file to edit (can be used multiple times)
#file:
## specify a read-only file (can be used multiple times)
#read:
## Use VI editing mode in the terminal (default: False)
#vim: false

View file

@ -84,12 +84,18 @@ cog.outl("```")
## Use gpt-4o model for the main chat
#AIDER_4O=
## Use gpt-4o-mini model for the main chat
#AIDER_MINI=
## Use gpt-4-1106-preview model for the main chat
#AIDER_4_TURBO=
## Use gpt-3.5-turbo model for the main chat
#AIDER_35TURBO=
## Use deepseek/deepseek-coder model for the main chat
#AIDER_DEEPSEEK=
#################
# Model Settings:
@ -213,8 +219,11 @@ cog.outl("```")
## Attribute aider commits in the git committer name (default: True)
#AIDER_ATTRIBUTE_COMMITTER=true
## Prefix commit messages with 'aider: ' (default: False)
#AIDER_ATTRIBUTE_COMMIT_MESSAGE=false
## Prefix commit messages with 'aider: ' if aider authored the changes (default: False)
#AIDER_ATTRIBUTE_COMMIT_MESSAGE_AUTHOR=false
## Prefix all commit messages with 'aider: ' (default: False)
#AIDER_ATTRIBUTE_COMMIT_MESSAGE_COMMITTER=false
## Commit all pending changes with a suitable commit message, then exit
#AIDER_COMMIT=false
@ -252,6 +261,9 @@ cog.outl("```")
## specify a file to edit (can be used multiple times)
#AIDER_FILE=
## specify a read-only file (can be used multiple times)
#AIDER_READ=
## Use VI editing mode in the terminal (default: False)
#AIDER_VIM=false

View file

@ -26,8 +26,8 @@ cog.out(get_md_help())
]]]-->
```
usage: aider [-h] [--openai-api-key] [--anthropic-api-key] [--model]
[--opus] [--sonnet] [--4] [--4o] [--4-turbo]
[--35turbo] [--models] [--openai-api-base]
[--opus] [--sonnet] [--4] [--4o] [--mini] [--4-turbo]
[--35turbo] [--deepseek] [--models] [--openai-api-base]
[--openai-api-type] [--openai-api-version]
[--openai-api-deployment-id] [--openai-organization-id]
[--model-settings-file] [--model-metadata-file]
@ -47,12 +47,13 @@ usage: aider [-h] [--openai-api-key] [--anthropic-api-key] [--model]
[--dirty-commits | --no-dirty-commits]
[--attribute-author | --no-attribute-author]
[--attribute-committer | --no-attribute-committer]
[--attribute-commit-message | --no-attribute-commit-message]
[--attribute-commit-message-author | --no-attribute-commit-message-author]
[--attribute-commit-message-committer | --no-attribute-commit-message-committer]
[--commit] [--commit-prompt] [--dry-run | --no-dry-run]
[--lint] [--lint-cmd] [--auto-lint | --no-auto-lint]
[--test-cmd] [--auto-test | --no-auto-test] [--test]
[--file] [--vim] [--voice-language] [--version]
[--just-check-update]
[--file] [--read] [--vim] [--voice-language]
[--version] [--just-check-update]
[--check-update | --no-check-update] [--apply] [--yes]
[-v] [--show-repo-map] [--show-prompts] [--exit]
[--message] [--message-file] [--encoding] [-c] [--gui]
@ -100,6 +101,10 @@ Aliases:
Use gpt-4o model for the main chat
Environment variable: `AIDER_4O`
### `--mini`
Use gpt-4o-mini model for the main chat
Environment variable: `AIDER_MINI`
### `--4-turbo`
Use gpt-4-1106-preview model for the main chat
Environment variable: `AIDER_4_TURBO`
@ -113,6 +118,10 @@ Aliases:
- `--3`
- `-3`
### `--deepseek`
Use deepseek/deepseek-coder model for the main chat
Environment variable: `AIDER_DEEPSEEK`
## Model Settings:
### `--models MODEL`
@ -160,6 +169,9 @@ Aliases:
### `--edit-format EDIT_FORMAT`
Specify what edit format the LLM should use (default depends on model)
Environment variable: `AIDER_EDIT_FORMAT`
Aliases:
- `--edit-format EDIT_FORMAT`
- `--chat-mode EDIT_FORMAT`
### `--weak-model WEAK_MODEL`
Specify the model to use for commit messages and chat history summarization (default depends on --model)
@ -327,13 +339,21 @@ Aliases:
- `--attribute-committer`
- `--no-attribute-committer`
### `--attribute-commit-message`
Prefix commit messages with 'aider: ' (default: False)
### `--attribute-commit-message-author`
Prefix commit messages with 'aider: ' if aider authored the changes (default: False)
Default: False
Environment variable: `AIDER_ATTRIBUTE_COMMIT_MESSAGE`
Environment variable: `AIDER_ATTRIBUTE_COMMIT_MESSAGE_AUTHOR`
Aliases:
- `--attribute-commit-message`
- `--no-attribute-commit-message`
- `--attribute-commit-message-author`
- `--no-attribute-commit-message-author`
### `--attribute-commit-message-committer`
Prefix all commit messages with 'aider: ' (default: False)
Default: False
Environment variable: `AIDER_ATTRIBUTE_COMMIT_MESSAGE_COMMITTER`
Aliases:
- `--attribute-commit-message-committer`
- `--no-attribute-commit-message-committer`
### `--commit`
Commit all pending changes with a suitable commit message, then exit
@ -396,6 +416,10 @@ Environment variable: `AIDER_TEST`
specify a file to edit (can be used multiple times)
Environment variable: `AIDER_FILE`
### `--read FILE`
specify a read-only file (can be used multiple times)
Environment variable: `AIDER_READ`
### `--vim`
Use VI editing mode in the terminal (default: False)
Default: False

View file

@ -73,7 +73,7 @@ cd aider
# It's recommended to make a virtual environment
# Install the dependencies listed in the `requirements.txt` file:
pip install -e .
python -m pip install -e .
# Run the local version of Aider:
python -m aider

View file

@ -55,5 +55,9 @@ Aider marks commits that it either authored or committed.
You can use `--no-attribute-author` and `--no-attribute-committer` to disable
modification of the git author and committer name fields.
Additionally, you can use `--attribute-commit-message` to prefix commit messages with 'aider: '.
This option is disabled by default, but can be useful for easily identifying commits made by aider.
Additionally, you can use the following options to prefix commit messages:
- `--attribute-commit-message-author`: Prefix commit messages with 'aider: ' if aider authored the changes.
- `--attribute-commit-message-committer`: Prefix all commit messages with 'aider: ', regardless of whether aider authored the changes or not.
Both of these options are disabled by default, but can be useful for easily identifying changes made by aider.

View file

@ -15,6 +15,8 @@ for more details,
or the
[usage instructions](https://aider.chat/docs/usage.html) to start coding with aider.
{% include python-m-aider.md %}
<div class="video-container">
<video controls poster="/assets/install.jpg">
<source src="/assets/install.mp4" type="video/mp4">

View file

@ -25,13 +25,7 @@ To work with Anthropic's models like Claude 3.5 Sonnet you need a paid
[Anthropic API key](https://docs.anthropic.com/claude/reference/getting-started-with-the-api).
{: .tip }
Using a Python
[virtual environment](https://docs.python.org/3/library/venv.html){:target="_blank"}
is recommended.
Or, you could
[use pipx to install aider](/docs/install/pipx.html)
once for your whole system.
{% include venv-pipx.md %}
## Mac/Linux install
@ -59,10 +53,7 @@ $ aider --4o --openai-api-key sk-xxx...
$ aider --sonnet --anthropic-api-key sk-xxx...
```
{: .tip }
In some environments the `aider` command may not be available
on your shell path.
You can also run aider like this: `python -m aider`
{% include python-m-aider.md %}
## Working with other LLMs

View file

@ -8,7 +8,7 @@ nav_order: 100
If you are using aider to work on a python project, sometimes your project will require
specific versions of python packages which conflict with the versions that aider
requires.
If this happens, the `pip install` command may return errors like these:
If this happens, the `python -m pip install` command may return errors like these:
```
aider-chat 0.23.0 requires somepackage==X.Y.Z, but you have somepackage U.W.V which is incompatible.

View file

@ -5,7 +5,7 @@ description: Aider supports pretty much all popular coding languages.
---
# Supported languages
Aider supports almost all popular coding languages.
Aider should work well with most popular coding languages.
This is because top LLMs are fluent in most mainstream languages,
and familiar with popular libraries, packages and frameworks.
@ -20,8 +20,6 @@ a [repository map](https://aider.chat/docs/repomap.html).
Aider can currently produce repository maps for many popular
mainstream languages, listed below.
Aider should work quite well for other languages, even those
without repo map or linter support.
<!--[[[cog
from aider.repomap import get_supported_languages_md
@ -82,3 +80,30 @@ cog.out(get_supported_languages_md())
<!--[[[end]]]-->
## How to add support for another language
Aider should work quite well for other languages, even those
without repo map or linter support.
You should really try coding with aider before
assuming it needs better support for your language.
That said, if aider already has support for linting your language,
then it should be possible to add repo map support.
To build a repo map, aider needs the `tags.scm` file
from the given language's tree-sitter grammar.
If you can find and share that file in a
[GitHub issue](https://github.com/paul-gauthier/aider/issues),
then it may be possible to add repo map support.
If aider doesn't support linting, it will be complicated to
add linting and repo map support.
That is because aider relies on
[py-tree-sitter-languages](https://github.com/grantjenks/py-tree-sitter-languages)
to provide pre-packaged versions of tree-sitter
parsers for many languages.
Aider needs to be easy for users to install in many environments,
and it is probably too complex to add dependencies on
additional individual tree-sitter parsers.

View file

@ -136,6 +136,16 @@ The model also has to successfully apply all its changes to the source file with
tr.selected {
color: #0056b3;
}
table {
table-layout: fixed;
}
td, th {
word-wrap: break-word;
overflow-wrap: break-word;
}
td:nth-child(3), td:nth-child(4) {
font-size: 12px;
}
</style>
## Code refactoring leaderboard
@ -291,7 +301,7 @@ Submit results by opening a PR with edits to the
By Paul Gauthier,
last updated
<!--[[[cog
import os
import subprocess
import datetime
files = [
@ -300,11 +310,17 @@ files = [
'aider/website/_data/refactor_leaderboard.yml'
]
mod_times = [os.path.getmtime(file) for file in files]
latest_mod_time = max(mod_times)
mod_date = datetime.datetime.fromtimestamp(latest_mod_time)
cog.out(f"{mod_date.strftime('%B %d, %Y.')}")
def get_last_modified_date(file):
result = subprocess.run(['git', 'log', '-1', '--format=%ct', file], capture_output=True, text=True)
if result.returncode == 0:
timestamp = int(result.stdout.strip())
return datetime.datetime.fromtimestamp(timestamp)
return datetime.datetime.min
mod_dates = [get_last_modified_date(file) for file in files]
latest_mod_date = max(mod_dates)
cog.out(f"{latest_mod_date.strftime('%B %d, %Y.')}")
]]]-->
August 06, 2024.
August 10, 2024.
<!--[[[end]]]-->
</p>

View file

@ -14,7 +14,7 @@ Aider has some built in shortcuts for the most popular Anthropic models and
has been tested and benchmarked to work well with them:
```
pip install aider-chat
python -m pip install aider-chat
export ANTHROPIC_API_KEY=<key> # Mac/Linux
setx ANTHROPIC_API_KEY <key> # Windows, restart shell after setx
@ -33,8 +33,8 @@ aider --models anthropic/
Anthropic has very low rate limits.
You can access all the Anthropic models via
[OpenRouter](openrouter.md)
without rate limits.
For example: `aider --model openrouter/anthropic/claude-3.5-sonnet`
or [Google Vertex AI](vertex.md)
with more generous rate limits.
You can use `aider --model <model-name>` to use any other Anthropic model.
For example, if you want to use a specific version of Opus

View file

@ -8,7 +8,7 @@ nav_order: 500
Aider can connect to the OpenAI models on Azure.
```
pip install aider-chat
python -m pip install aider-chat
# Mac/Linux:
export AZURE_API_KEY=<key>

View file

@ -13,7 +13,7 @@ You'll need a [Cohere API key](https://dashboard.cohere.com/welcome/login).
To use **Command-R+**:
```
pip install aider-chat
python -m pip install aider-chat
export COHERE_API_KEY=<key> # Mac/Linux
setx COHERE_API_KEY <key> # Windows, restart shell after setx

View file

@ -6,19 +6,15 @@ nav_order: 500
# DeepSeek
Aider can connect to the DeepSeek.com API.
The DeepSeek Coder V2 model gets the top score on aider's code editing benchmark.
The DeepSeek Coder V2 model has a top score on aider's code editing benchmark.
```
pip install aider-chat
python -m pip install aider-chat
export DEEPSEEK_API_KEY=<key> # Mac/Linux
setx DEEPSEEK_API_KEY <key> # Windows, restart shell after setx
# Use DeepSeek Coder V2
aider --model deepseek/deepseek-coder
aider --deepseek
```
See the [model warnings](warnings.html)
section for information on warnings which will occur
when working with models that aider is not familiar with.

View file

@ -12,7 +12,7 @@ with code editing capability that's comparable to GPT-3.5.
You'll need a [Gemini API key](https://aistudio.google.com/app/u/2/apikey).
```
pip install aider-chat
python -m pip install aider-chat
export GEMINI_API_KEY=<key> # Mac/Linux
setx GEMINI_API_KEY <key> # Windows, restart shell after setx

View file

@ -13,7 +13,7 @@ You'll need a [Groq API key](https://console.groq.com/keys).
To use **Llama3 70B**:
```
pip install aider-chat
python -m pip install aider-chat
export GROQ_API_KEY=<key> # Mac/Linux
setx GROQ_API_KEY <key> # Windows, restart shell after setx

View file

@ -15,7 +15,7 @@ ollama pull <model>
ollama serve
# In another terminal window...
pip install aider-chat
python -m pip install aider-chat
export OLLAMA_API_BASE=http://127.0.0.1:11434 # Mac/Linux
setx OLLAMA_API_BASE http://127.0.0.1:11434 # Windows, restart shell after setx

View file

@ -8,7 +8,7 @@ nav_order: 500
Aider can connect to any LLM which is accessible via an OpenAI compatible API endpoint.
```
pip install aider-chat
python -m pip install aider-chat
# Mac/Linux:
export OPENAI_API_BASE=<endpoint>

View file

@ -14,7 +14,7 @@ Aider has some built in shortcuts for the most popular OpenAI models and
has been tested and benchmarked to work well with them:
```
pip install aider-chat
python -m pip install aider-chat
export OPENAI_API_KEY=<key> # Mac/Linux
setx OPENAI_API_KEY <key> # Windows, restart shell after setx

View file

@ -9,7 +9,7 @@ Aider can connect to [models provided by OpenRouter](https://openrouter.ai/model
You'll need an [OpenRouter API key](https://openrouter.ai/keys).
```
pip install aider-chat
python -m pip install aider-chat
export OPENROUTER_API_KEY=<key> # Mac/Linux
setx OPENROUTER_API_KEY <key> # Windows, restart shell after setx
@ -21,15 +21,15 @@ aider --model openrouter/<provider>/<model>
aider --models openrouter/
```
In particular, Llama3 70B works well with aider, at low cost:
In particular, many aider users access Sonnet via OpenRouter:
```
pip install aider-chat
python -m pip install aider-chat
export OPENROUTER_API_KEY=<key> # Mac/Linux
setx OPENROUTER_API_KEY <key> # Windows, restart shell after setx
aider --model openrouter/meta-llama/llama-3-70b-instruct
aider --model openrouter/anthropic/claude-3.5-sonnet
```

View file

@ -77,6 +77,7 @@ cog.out(''.join(lines))
- FIREWORKS_API_KEY
- FRIENDLIAI_API_KEY
- GEMINI_API_KEY
- GITHUB_API_KEY
- GROQ_API_KEY
- HUGGINGFACE_API_KEY
- MARITALK_API_KEY

View file

@ -0,0 +1,43 @@
---
parent: Connecting to LLMs
nav_order: 550
---
# Vertex AI
Aider can connect to models provided by Google Vertex AI.
You will need to install the
[gcloud CLI](https://cloud.google.com/sdk/docs/install) and [login](https://cloud.google.com/sdk/docs/initializing) with a GCP account
or service account with permission to use the Vertex AI API.
With your chosen login method, the gcloud CLI should automatically set the
`GOOGLE_APPLICATION_CREDENTIALS` environment variable which points to the credentials file.
To configure Aider to use the Vertex AI API, you need to set `VERTEXAI_PROJECT` (the GCP project ID)
and `VERTEXAI_LOCATION` (the GCP region) [environment variables for Aider](/docs/config/dotenv.html).
Note that Claude on Vertex AI is only available in certain GCP regions,
check [the model card](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet)
for your model to see which regions are supported.
Example `.env` file:
```
VERTEXAI_PROJECT=my-project
VERTEXAI_LOCATION=us-east5
```
Then you can run aider with the `--model` command line switch, like this:
```
aider --model vertex_ai/claude-3-5-sonnet@20240620
```
Or you can use the [yaml config](/docs/config/aider_conf.html) to set the model to any of the
models supported by Vertex AI.
Example `.aider.conf.yml` file:
```yaml
model: vertex_ai/claude-3-5-sonnet@20240620
```

View file

@ -74,6 +74,10 @@ coder.run("make a script that prints hello world")
# Send another instruction
coder.run("make it say goodbye")
# You can run in-chat "/" commands too
coder.run("/tokens")
```
See the

View file

@ -0,0 +1,25 @@
---
parent: Troubleshooting
nav_order: 28
---
# Aider not found
In some environments the `aider` command may not be available
on your shell path.
This can occur because of permissions/security settings in your OS,
and often happens to Windows users.
You may see an error message like this:
> aider: The term 'aider' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Below is the most fail safe way to install and run aider in these situations:
```
python -m pip install aider-chat
python -m aider
```
{% include venv-pipx.md %}

View file

@ -48,7 +48,7 @@ It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.htm
Use the `--browser` switch to launch the browser version of aider:
```
pip install aider-chat
python -m pip install aider-chat
export OPENAI_API_KEY=<key> # Mac/Linux
setx OPENAI_API_KEY <key> # Windows, restart shell after setx

View file

@ -15,27 +15,30 @@ cog.out(get_help_md())
|Command|Description|
|:------|:----------|
| **/add** | Add files to the chat so GPT can edit them or review them in detail |
| **/add-clipboard-image** | Add an image from the clipboard to the chat |
| **/ask** | Ask questions about the code base without editing any files |
| **/chat-mode** | Switch to a new chat mode |
| **/clear** | Clear the chat history |
| **/clipboard** | Add image/text from the clipboard to the chat (optionally provide a name for the image) |
| **/code** | Ask for changes to your code |
| **/commit** | Commit edits to the repo made outside the chat (commit message optional) |
| **/diff** | Display the diff of the last aider commit |
| **/diff** | Display the diff of changes since the last message |
| **/drop** | Remove files from the chat session to free up context space |
| **/exit** | Exit the application |
| **/git** | Run a git command |
| **/help** | Ask questions about aider |
| **/lint** | Lint and fix provided files or in-chat files if none provided |
| **/ls** | List all known files and indicate which are included in the chat session |
| **/map** | Print out the current repository map |
| **/model** | Switch to a new LLM |
| **/models** | Search the list of available models |
| **/quit** | Exit the application |
| **/read** | Add a file to the chat that is for reference, not to be edited |
| **/run** | Run a shell command and optionally add the output to the chat (alias: !) |
| **/test** | Run a shell command and add the output to the chat on non-zero exit code |
| **/tokens** | Report on the number of tokens used by the current chat context |
| **/undo** | Undo the last git commit if it was done by aider |
| **/voice** | Record and transcribe voice input |
| **/web** | Use headless selenium to scrape a webpage and add the content to the chat |
| **/web** | Scrape a webpage, convert to markdown and add to the chat |
<!--[[[end]]]-->

View file

@ -13,7 +13,20 @@ Aider has 3 different chat modes:
- `help` - Aider will answer questions about using aider, configuring, troubleshooting, etc.
By default, aider starts in "code" mode. As you are talking, you can
send messages in other modes using `/ask` and `/help` commands:
send individual messages in a specific mode using `/code`, `/ask`, and `/help` commands:
Using `/code`, `/ask`, and `/help` applies just to that particular message.
Your next message will go back to the active mode (usually "code" mode by default).
You can switch the active mode in a sticky way
with the `/chat-mode <mode>` command:
```
/chat-mode code
/chat-mode ask
/chat-mode help
```
Or you can launch aider in one of the modes with the `--chat-mode <mode>` switch.
<div class="chat-transcript" markdown="1">
@ -33,17 +46,22 @@ This is the source code to the popular django package.
Run `aider --model ollama/<ollama-model>`.
See these docs for more info: https://aider.chat/docs/llms/ollama.html
#### /code Add a function to calculate the factorial of a number
Certainly! I'll add a function to calculate the factorial of a number. Here's the change:
```python
def factorial(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial(n - 1)
# Example usage
print(factorial(5)) # Output: 120
```
This function uses recursion to calculate the factorial. It's been added to your code.
</div>
Using `/ask` and `/help` applies just to that particular message.
Your next message will go back to code mode.
You can switch between the modes in a sticky way
with the `/chat-mode <mode>` command:
```
/chat-mode code
/chat-mode ask
/chat-mode help
```

View file

@ -49,18 +49,18 @@ cog.out(open("aider/website/_includes/get-started.md").read())
You can get started quickly like this:
```
$ pip install aider-chat
python -m pip install aider-chat
# Change directory into a git repo
$ cd /to/your/git/repo
cd /to/your/git/repo
# Work with Claude 3.5 Sonnet on your repo
$ export ANTHROPIC_API_KEY=your-key-goes-here
$ aider
export ANTHROPIC_API_KEY=your-key-goes-here
aider
# Work with GPT-4o on your repo
$ export OPENAI_API_KEY=your-key-goes-here
$ aider
export OPENAI_API_KEY=your-key-goes-here
aider
```
<!-- NOOP -->

67
pyproject.toml Normal file
View file

@ -0,0 +1,67 @@
# [[[cog
# from aider.help_pats import exclude_website_pats
# ]]]
# [[[end]]]
[project]
name = "aider-chat"
description = "Aider is AI pair programming in your terminal"
readme = "README.md"
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python",
"Topic :: Software Development",
]
requires-python = ">=3.9,<3.13"
dynamic = ["dependencies", "optional-dependencies", "version"]
[project.urls]
Homepage = "https://github.com/paul-gauthier/aider"
[project.scripts]
aider = "aider.main:main"
[tool.setuptools.dynamic]
version = { attr = "aider.__init__.__version__" }
dependencies = { file = "requirements.txt" }
[tool.setuptools.dynamic.optional-dependencies]
dev = { file = "requirements/requirements-dev.txt" }
help = { file = "requirements/requirements-help.txt" }
browser = { file = "requirements/requirements-browser.txt" }
playwright = { file = "requirements/requirements-playwright.txt" }
[tool.setuptools.packages.find]
include = ["aider*", "aider.website"]
[tool.setuptools.package-data]
"aider" = ["queries/*.scm"]
"aider.website" = ["**/*.md"]
[tool.setuptools.exclude-package-data]
"aider.website" = [
# [[[cog
# cog.out("\n".join(f' "{pat}",' for pat in exclude_website_pats))
# ]]]
"examples/**",
"_posts/**",
"HISTORY.md",
"docs/benchmarks*md",
"docs/ctags.md",
"docs/unified-diffs.md",
"docs/leaderboards/index.md",
"assets/**",
# [[[end]]]
]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

View file

@ -4,9 +4,9 @@
#
# pip-compile --output-file=requirements.txt requirements/requirements.in
#
aiohappyeyeballs==2.3.4
aiohappyeyeballs==2.3.5
# via aiohttp
aiohttp==3.10.0
aiohttp==3.10.3
# via litellm
aiosignal==1.3.1
# via aiohttp
@ -16,7 +16,7 @@ anyio==4.4.0
# via
# httpx
# openai
attrs==23.2.0
attrs==24.2.0
# via
# aiohttp
# jsonschema
@ -30,7 +30,7 @@ certifi==2024.7.4
# httpcore
# httpx
# requests
cffi==1.16.0
cffi==1.17.0
# via
# sounddevice
# soundfile
@ -48,7 +48,7 @@ distro==1.9.0
# via openai
filelock==3.15.4
# via huggingface-hub
flake8==7.1.0
flake8==7.1.1
# via -r requirements/requirements.in
frozenlist==1.4.1
# via
@ -84,13 +84,15 @@ importlib-resources==6.4.0
# via -r requirements/requirements.in
jinja2==3.1.4
# via litellm
jiter==0.5.0
# via openai
jsonschema==4.23.0
# via
# -r requirements/requirements.in
# litellm
jsonschema-specifications==2023.12.1
# via jsonschema
litellm==1.42.9
litellm==1.43.9
# via -r requirements/requirements.in
markdown-it-py==3.0.0
# via rich
@ -110,7 +112,7 @@ numpy==1.26.4
# via
# -r requirements/requirements.in
# scipy
openai==1.37.2
openai==1.40.6
# via litellm
packaging==24.1
# via
@ -123,8 +125,10 @@ pathspec==0.12.1
pillow==10.4.0
# via -r requirements/requirements.in
prompt-toolkit==3.0.47
# via -r requirements/requirements.in
pycodestyle==2.12.0
# via
# -r requirements/requirements.in
# pypager
pycodestyle==2.12.1
# via flake8
pycparser==2.22
# via cffi
@ -137,12 +141,18 @@ pydantic-core==2.20.1
pyflakes==3.2.0
# via flake8
pygments==2.18.0
# via rich
# via
# pypager
# rich
pypager==3.0.1
# via -r requirements/requirements.in
pypandoc==1.13
# via -r requirements/requirements.in
pyperclip==1.9.0
# via -r requirements/requirements.in
python-dotenv==1.0.1
# via litellm
pyyaml==6.0.1
pyyaml==6.0.2
# via
# -r requirements/requirements.in
# huggingface-hub
@ -159,7 +169,7 @@ requests==2.32.3
# tiktoken
rich==13.7.1
# via -r requirements/requirements.in
rpds-py==0.19.1
rpds-py==0.20.0
# via
# jsonschema
# referencing
@ -172,7 +182,7 @@ sniffio==1.3.1
# anyio
# httpx
# openai
sounddevice==0.4.7
sounddevice==0.5.0
# via -r requirements/requirements.in
soundfile==0.12.1
# via -r requirements/requirements.in
@ -181,8 +191,10 @@ soupsieve==2.5
tiktoken==0.7.0
# via litellm
tokenizers==0.19.1
# via litellm
tqdm==4.66.4
# via
# -r requirements/requirements.in
# litellm
tqdm==4.66.5
# via
# huggingface-hub
# openai
@ -204,5 +216,5 @@ wcwidth==0.2.13
# via prompt-toolkit
yarl==1.9.4
# via aiohttp
zipp==3.19.2
zipp==3.20.0
# via importlib-metadata

View file

@ -4,9 +4,9 @@
#
# pip-compile --output-file=requirements/requirements-browser.txt requirements/requirements-browser.in
#
altair==5.3.0
altair==5.4.0
# via streamlit
attrs==23.2.0
attrs==24.2.0
# via
# -c requirements/../requirements.txt
# jsonschema
@ -64,10 +64,11 @@ mdurl==0.1.2
# via
# -c requirements/../requirements.txt
# markdown-it-py
narwhals==1.3.0
# via altair
numpy==1.26.4
# via
# -c requirements/../requirements.txt
# altair
# pandas
# pyarrow
# pydeck
@ -78,9 +79,7 @@ packaging==24.1
# altair
# streamlit
pandas==2.2.2
# via
# altair
# streamlit
# via streamlit
pillow==10.4.0
# via
# -c requirements/../requirements.txt
@ -112,7 +111,7 @@ rich==13.7.1
# via
# -c requirements/../requirements.txt
# streamlit
rpds-py==0.19.1
rpds-py==0.20.0
# via
# -c requirements/../requirements.txt
# jsonschema
@ -123,19 +122,18 @@ smmap==5.0.1
# via
# -c requirements/../requirements.txt
# gitdb
streamlit==1.37.0
streamlit==1.37.1
# via -r requirements/requirements-browser.in
tenacity==8.5.0
# via streamlit
toml==0.10.2
# via streamlit
toolz==0.12.1
# via altair
tornado==6.4.1
# via streamlit
typing-extensions==4.12.2
# via
# -c requirements/../requirements.txt
# altair
# streamlit
tzdata==2024.1
# via pandas
@ -143,5 +141,5 @@ urllib3==2.2.2
# via
# -c requirements/../requirements.txt
# requests
watchdog==4.0.1
watchdog==4.0.2
# via -r requirements/requirements-browser.in

View file

@ -6,7 +6,7 @@
#
alabaster==0.7.16
# via sphinx
babel==2.15.0
babel==2.16.0
# via sphinx
build==1.2.1
# via pip-tools
@ -75,7 +75,7 @@ markupsafe==2.1.5
# via
# -c requirements/../requirements.txt
# jinja2
matplotlib==3.9.1
matplotlib==3.9.2
# via -r requirements/requirements-dev.in
mdurl==0.1.2
# via
@ -137,7 +137,7 @@ python-dateutil==2.9.0.post0
# pandas
pytz==2024.1
# via pandas
pyyaml==6.0.1
pyyaml==6.0.2
# via
# -c requirements/../requirements.txt
# pre-commit
@ -191,7 +191,7 @@ urllib3==2.2.2
# requests
virtualenv==20.26.3
# via pre-commit
wheel==0.43.0
wheel==0.44.0
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:

View file

@ -4,11 +4,11 @@
#
# pip-compile --output-file=requirements/requirements-help.txt requirements/requirements-help.in
#
aiohappyeyeballs==2.3.4
aiohappyeyeballs==2.3.5
# via
# -c requirements/../requirements.txt
# aiohttp
aiohttp==3.10.0
aiohttp==3.10.3
# via
# -c requirements/../requirements.txt
# huggingface-hub
@ -26,7 +26,7 @@ anyio==4.4.0
# -c requirements/../requirements.txt
# httpx
# openai
attrs==23.2.0
attrs==24.2.0
# via
# -c requirements/../requirements.txt
# aiohttp
@ -104,15 +104,19 @@ jinja2==3.1.4
# via
# -c requirements/../requirements.txt
# torch
jiter==0.5.0
# via
# -c requirements/../requirements.txt
# openai
joblib==1.4.2
# via
# nltk
# scikit-learn
llama-index-core==0.10.59
llama-index-core==0.10.65
# via
# -r requirements/requirements-help.in
# llama-index-embeddings-huggingface
llama-index-embeddings-huggingface==0.2.2
llama-index-embeddings-huggingface==0.2.3
# via -r requirements/requirements-help.in
markupsafe==2.1.5
# via
@ -138,7 +142,7 @@ networkx==3.2.1
# -c requirements/../requirements.txt
# llama-index-core
# torch
nltk==3.8.1
nltk==3.8.2
# via llama-index-core
numpy==1.26.4
# via
@ -149,7 +153,7 @@ numpy==1.26.4
# scipy
# sentence-transformers
# transformers
openai==1.37.2
openai==1.40.6
# via
# -c requirements/../requirements.txt
# llama-index-core
@ -178,7 +182,7 @@ python-dateutil==2.9.0.post0
# via pandas
pytz==2024.1
# via pandas
pyyaml==6.0.1
pyyaml==6.0.2
# via
# -c requirements/../requirements.txt
# huggingface-hub
@ -197,7 +201,7 @@ requests==2.32.3
# llama-index-core
# tiktoken
# transformers
safetensors==0.4.3
safetensors==0.4.4
# via transformers
scikit-learn==1.5.1
# via sentence-transformers
@ -216,11 +220,11 @@ sniffio==1.3.1
# anyio
# httpx
# openai
sqlalchemy[asyncio]==2.0.31
sqlalchemy[asyncio]==2.0.32
# via
# llama-index-core
# sqlalchemy
sympy==1.13.1
sympy==1.13.2
# via torch
tenacity==8.5.0
# via llama-index-core
@ -236,7 +240,7 @@ tokenizers==0.19.1
# transformers
torch==2.2.2
# via sentence-transformers
tqdm==4.66.4
tqdm==4.66.5
# via
# -c requirements/../requirements.txt
# huggingface-hub
@ -245,7 +249,7 @@ tqdm==4.66.4
# openai
# sentence-transformers
# transformers
transformers==4.43.3
transformers==4.44.0
# via sentence-transformers
typing-extensions==4.12.2
# via

View file

@ -6,7 +6,7 @@
#
greenlet==3.0.3
# via playwright
playwright==1.45.1
playwright==1.46.0
# via -r requirements/requirements-playwright.in
pyee==11.1.0
# via playwright

View file

@ -22,6 +22,8 @@ pypandoc
litellm
flake8
importlib_resources
pyperclip
pypager
# The proper depdendency is networkx[default], but this brings
# in matplotlib and a bunch of other deps
@ -46,3 +48,7 @@ importlib-metadata<8.0.0
# Because sentence-transformers doesn't like >=2
numpy<2
# Going past this makes dependencies unresolvable
# Seems to be caused by sentence-transformers
tokenizers==0.19.1

View file

@ -2,6 +2,7 @@
import argparse
import subprocess
import sys
from collections import defaultdict
from datetime import datetime
from operator import itemgetter
@ -17,10 +18,14 @@ def blame(start_tag, end_tag=None):
authors = get_commit_authors(commits)
pats = "*.py *.scm *.sh **Dockerfile **Gemfile .github/workflows/*.yml".split()
files = []
for pat in pats:
files += run(["git", "ls-files", pat]).strip().split("\n")
revision = end_tag if end_tag else "HEAD"
files = run(["git", "ls-tree", "-r", "--name-only", revision]).strip().split("\n")
files = [
f
for f in files
if f.endswith((".py", ".scm", ".sh", "Dockerfile", "Gemfile"))
or (f.startswith(".github/workflows/") and f.endswith(".yml"))
]
all_file_counts = {}
grand_total = defaultdict(int)
@ -186,10 +191,14 @@ def get_counts_for_file(start_tag, end_tag, authors, fname):
line_counts[author] += 1
return dict(line_counts)
except subprocess.CalledProcessError:
# print(f"Warning: Unable to blame file {fname}. It may have been added after {start_tag} "
# f"or removed before {end_tag or 'HEAD'}.", file=sys.stderr)
return None
except subprocess.CalledProcessError as e:
if "no such path" in str(e).lower():
# File doesn't exist in this revision range, which is okay
return None
else:
# Some other error occurred
print(f"Warning: Unable to blame file {fname}. Error: {e}", file=sys.stderr)
return None
def get_all_tags_since(start_tag):

View file

@ -9,22 +9,36 @@ import sys
from packaging import version
def check_cog_pyproject():
result = subprocess.run(["cog", "--check", "pyproject.toml"], capture_output=True, text=True)
if result.returncode != 0:
print("Error: cog --check pyproject.toml failed, updating.")
subprocess.run(["cog", "-r", "pyproject.toml"])
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Bump version")
parser.add_argument("new_version", help="New version in x.y.z format")
parser.add_argument(
"--dry-run", action="store_true", help="Print each step without actually executing them"
)
# Function to check if we are on the main branch
def check_branch():
branch = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True).stdout.strip()
branch = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True
).stdout.strip()
if branch != "main":
print("Error: Not on the main branch.")
sys.exit(1)
# Function to check if the working directory is clean
def check_working_directory_clean():
status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True).stdout
status = subprocess.run(
["git", "status", "--porcelain"], capture_output=True, text=True
).stdout
if status:
print("Error: Working directory is not clean.")
sys.exit(1)
@ -32,19 +46,33 @@ def main():
# Function to fetch the latest changes and check if the main branch is up to date
def check_main_branch_up_to_date():
subprocess.run(["git", "fetch", "origin"], check=True)
local_main = subprocess.run(["git", "rev-parse", "main"], capture_output=True, text=True).stdout.strip()
local_main = subprocess.run(
["git", "rev-parse", "main"], capture_output=True, text=True
).stdout.strip()
print(f"Local main commit hash: {local_main}")
origin_main = subprocess.run(["git", "rev-parse", "origin/main"], capture_output=True, text=True).stdout.strip()
origin_main = subprocess.run(
["git", "rev-parse", "origin/main"], capture_output=True, text=True
).stdout.strip()
print(f"Origin main commit hash: {origin_main}")
if local_main != origin_main:
local_date = subprocess.run(["git", "show", "-s", "--format=%ci", "main"], capture_output=True, text=True).stdout.strip()
origin_date = subprocess.run(["git", "show", "-s", "--format=%ci", "origin/main"], capture_output=True, text=True).stdout.strip()
local_date = subprocess.run(
["git", "show", "-s", "--format=%ci", "main"], capture_output=True, text=True
).stdout.strip()
origin_date = subprocess.run(
["git", "show", "-s", "--format=%ci", "origin/main"], capture_output=True, text=True
).stdout.strip()
local_date = datetime.datetime.strptime(local_date, "%Y-%m-%d %H:%M:%S %z")
origin_date = datetime.datetime.strptime(origin_date, "%Y-%m-%d %H:%M:%S %z")
if local_date < origin_date:
print("Error: The local main branch is behind origin/main. Please pull the latest changes.")
print(
"Error: The local main branch is behind origin/main. Please pull the latest"
" changes."
)
elif local_date > origin_date:
print("Error: The origin/main branch is behind the local main branch. Please push your changes.")
print(
"Error: The origin/main branch is behind the local main branch. Please push"
" your changes."
)
else:
print("Error: The main branch and origin/main have diverged.")
sys.exit(1)
@ -53,6 +81,7 @@ def main():
dry_run = args.dry_run
# Perform checks before proceeding
check_cog_pyproject()
check_branch()
check_working_directory_clean()
check_main_branch_up_to_date()

View file

@ -1,73 +0,0 @@
import re
from pathlib import Path
from setuptools import find_packages, setup
from aider import __version__
from aider.help_pats import exclude_website_pats
def get_requirements(suffix=""):
if suffix:
fname = "requirements-" + suffix + ".txt"
fname = Path("requirements") / fname
else:
fname = Path("requirements.txt")
requirements = fname.read_text().splitlines()
return requirements
requirements = get_requirements()
# README
with open("README.md", "r", encoding="utf-8") as f:
long_description = f.read()
long_description = re.sub(r"\n!\[.*\]\(.*\)", "", long_description)
# long_description = re.sub(r"\n- \[.*\]\(.*\)", "", long_description)
# Discover packages, plus the website
packages = find_packages(exclude=["benchmark", "tests"])
packages += ["aider.website"]
print("Packages:", packages)
extras = "dev help browser playwright".split()
setup(
name="aider-chat",
version=__version__,
packages=packages,
include_package_data=True,
package_data={
"aider": ["queries/*.scm"],
"aider.website": ["**/*.md"],
},
exclude_package_data={"aider.website": exclude_website_pats},
install_requires=requirements,
extras_require={extra: get_requirements(extra) for extra in extras},
python_requires=">=3.9,<3.13",
entry_points={
"console_scripts": [
"aider = aider.main:main",
],
},
description="Aider is AI pair programming in your terminal",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/paul-gauthier/aider",
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python",
"Topic :: Software Development",
],
)

View file

@ -189,6 +189,33 @@ class TestCoder(unittest.TestCase):
self.assertEqual(coder.abs_fnames, set([str(fname.resolve())]))
def test_check_for_file_mentions_read_only(self):
with GitTemporaryDirectory():
io = InputOutput(
pretty=False,
yes=True,
)
coder = Coder.create(self.GPT35, None, io)
fname = Path("readonly_file.txt")
fname.touch()
coder.abs_read_only_fnames.add(str(fname.resolve()))
# Mock the get_tracked_files method
mock = MagicMock()
mock.return_value = set([str(fname)])
coder.repo.get_tracked_files = mock
# Call the check_for_file_mentions method
result = coder.check_for_file_mentions(f"Please check {fname}!")
# Assert that the method returns None (user not asked to add the file)
self.assertIsNone(result)
# Assert that abs_fnames is still empty (file not added)
self.assertEqual(coder.abs_fnames, set())
def test_check_for_subdir_mention(self):
with GitTemporaryDirectory():
io = InputOutput(pretty=False, yes=True)
@ -259,7 +286,7 @@ class TestCoder(unittest.TestCase):
files = [file1, file2]
# Initialize the Coder object with the mocked IO and mocked repo
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files, pretty=False)
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)
def mock_send(*args, **kwargs):
coder.partial_response_content = "ok"
@ -286,7 +313,7 @@ class TestCoder(unittest.TestCase):
files = [file1, file2]
# Initialize the Coder object with the mocked IO and mocked repo
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files, pretty=False)
coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files)
def mock_send(*args, **kwargs):
coder.partial_response_content = "ok"
@ -377,7 +404,7 @@ class TestCoder(unittest.TestCase):
fname = Path("file.txt")
io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)], pretty=False)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
self.assertTrue(fname.exists())
@ -434,9 +461,7 @@ new
fname1.write_text("ONE\n")
io = InputOutput(yes=True)
coder = Coder.create(
self.GPT35, "diff", io=io, fnames=[str(fname1), str(fname2)], pretty=False
)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname1), str(fname2)])
def mock_send(*args, **kwargs):
coder.partial_response_content = f"""
@ -489,7 +514,7 @@ TWO
fname2.write_text("OTHER\n")
io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)], pretty=False)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
def mock_send(*args, **kwargs):
coder.partial_response_content = f"""
@ -567,7 +592,7 @@ three
repo.git.commit("-m", "initial")
io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)], pretty=False)
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
def mock_send(*args, **kwargs):
coder.partial_response_content = f"""
@ -640,7 +665,7 @@ two
def test_check_for_urls(self):
io = InputOutput(yes=True)
coder = Coder.create(self.GPT35, None, io=io, pretty=False)
coder = Coder.create(self.GPT35, None, io=io)
coder.commands.scraper = MagicMock()
coder.commands.scraper.scrape = MagicMock(return_value="some content")

View file

@ -10,7 +10,7 @@ from unittest import TestCase, mock
import git
from aider.coders import Coder
from aider.commands import Commands
from aider.commands import Commands, SwitchCoder
from aider.dump import dump # noqa: F401
from aider.io import InputOutput
from aider.models import Model
@ -537,6 +537,62 @@ class TestCommands(TestCase):
commands.cmd_add("file.txt")
self.assertEqual(coder.abs_fnames, set())
def test_cmd_add_read_only_file(self):
with GitTemporaryDirectory():
# Initialize the Commands and InputOutput objects
io = InputOutput(pretty=False, yes=True)
from aider.coders import Coder
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)
# Create a test file
test_file = Path("test_read_only.txt")
test_file.write_text("Test content")
# Add the file as read-only
commands.cmd_read(str(test_file))
# Verify it's in abs_read_only_fnames
self.assertTrue(
any(
os.path.samefile(str(test_file.resolve()), fname)
for fname in coder.abs_read_only_fnames
)
)
# Try to add the read-only file
commands.cmd_add(str(test_file))
# It's not in the repo, should not do anything
self.assertFalse(
any(os.path.samefile(str(test_file.resolve()), fname) for fname in coder.abs_fnames)
)
self.assertTrue(
any(
os.path.samefile(str(test_file.resolve()), fname)
for fname in coder.abs_read_only_fnames
)
)
repo = git.Repo()
repo.git.add(str(test_file))
repo.git.commit("-m", "initial")
# Try to add the read-only file
commands.cmd_add(str(test_file))
# Verify it's now in abs_fnames and not in abs_read_only_fnames
self.assertTrue(
any(os.path.samefile(str(test_file.resolve()), fname) for fname in coder.abs_fnames)
)
self.assertFalse(
any(
os.path.samefile(str(test_file.resolve()), fname)
for fname in coder.abs_read_only_fnames
)
)
def test_cmd_test_unbound_local_error(self):
with ChdirTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
@ -731,6 +787,140 @@ class TestCommands(TestCase):
self.assertNotIn(fname2, str(coder.abs_fnames))
self.assertNotIn(fname3, str(coder.abs_fnames))
def test_cmd_read(self):
with GitTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)
# Create a test file
test_file = Path("test_read.txt")
test_file.write_text("Test content")
# Test the /read command
commands.cmd_read(str(test_file))
# Check if the file was added to abs_read_only_fnames
self.assertTrue(
any(
os.path.samefile(str(test_file.resolve()), fname)
for fname in coder.abs_read_only_fnames
)
)
# Test dropping the read-only file
commands.cmd_drop(str(test_file))
# Check if the file was removed from abs_read_only_fnames
self.assertFalse(
any(
os.path.samefile(str(test_file.resolve()), fname)
for fname in coder.abs_read_only_fnames
)
)
def test_cmd_read_with_external_file(self):
with tempfile.NamedTemporaryFile(mode="w", delete=False) as external_file:
external_file.write("External file content")
external_file_path = external_file.name
try:
with GitTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)
# Test the /read command with an external file
commands.cmd_read(external_file_path)
# Check if the external file was added to abs_read_only_fnames
real_external_file_path = os.path.realpath(external_file_path)
self.assertTrue(
any(
os.path.samefile(real_external_file_path, fname)
for fname in coder.abs_read_only_fnames
)
)
# Test dropping the external read-only file
commands.cmd_drop(Path(external_file_path).name)
# Check if the file was removed from abs_read_only_fnames
self.assertFalse(
any(
os.path.samefile(real_external_file_path, fname)
for fname in coder.abs_read_only_fnames
)
)
finally:
os.unlink(external_file_path)
def test_cmd_diff(self):
with GitTemporaryDirectory() as repo_dir:
repo = git.Repo(repo_dir)
io = InputOutput(pretty=False, yes=True)
coder = Coder.create(self.GPT35, None, io)
commands = Commands(io, coder)
# Create and commit a file
filename = "test_file.txt"
file_path = Path(repo_dir) / filename
file_path.write_text("Initial content\n")
repo.git.add(filename)
repo.git.commit("-m", "Initial commit\n")
# Modify the file to make it dirty
file_path.write_text("Modified content")
# Mock repo.get_commit_message to return a canned commit message
with mock.patch.object(
coder.repo, "get_commit_message", return_value="Canned commit message"
):
# Run cmd_commit
commands.cmd_commit()
# Capture the output of cmd_diff
with mock.patch("builtins.print") as mock_print:
commands.cmd_diff("")
# Check if the diff output is correct
mock_print.assert_called_with(mock.ANY)
diff_output = mock_print.call_args[0][0]
self.assertIn("-Initial content", diff_output)
self.assertIn("+Modified content", diff_output)
# Modify the file again
file_path.write_text("Further modified content")
# Run cmd_commit again
commands.cmd_commit()
# Capture the output of cmd_diff
with mock.patch("builtins.print") as mock_print:
commands.cmd_diff("")
# Check if the diff output is correct
mock_print.assert_called_with(mock.ANY)
diff_output = mock_print.call_args[0][0]
self.assertIn("-Modified content", diff_output)
self.assertIn("+Further modified content", diff_output)
# Modify the file a third time
file_path.write_text("Final modified content")
# Run cmd_commit again
commands.cmd_commit()
# Capture the output of cmd_diff
with mock.patch("builtins.print") as mock_print:
commands.cmd_diff("")
# Check if the diff output is correct
mock_print.assert_called_with(mock.ANY)
diff_output = mock_print.call_args[0][0]
self.assertIn("-Further modified content", diff_output)
self.assertIn("+Final modified content", diff_output)
def test_cmd_ask(self):
io = InputOutput(pretty=False, yes=True)
coder = Coder.create(self.GPT35, None, io)
@ -742,17 +932,12 @@ class TestCommands(TestCase):
with mock.patch("aider.coders.Coder.run") as mock_run:
mock_run.return_value = canned_reply
commands.cmd_ask(question)
with self.assertRaises(SwitchCoder):
commands.cmd_ask(question)
mock_run.assert_called_once()
mock_run.assert_called_once_with(question)
self.assertEqual(len(coder.cur_messages), 2)
self.assertEqual(coder.cur_messages[0]["role"], "user")
self.assertEqual(coder.cur_messages[0]["content"], question)
self.assertEqual(coder.cur_messages[1]["role"], "assistant")
self.assertEqual(coder.cur_messages[1]["content"], canned_reply)
def test_cmd_lint_with_dirty_file(self):
with GitTemporaryDirectory() as repo_dir:
repo = git.Repo(repo_dir)

View file

@ -297,7 +297,7 @@ These changes replace the `subprocess.run` patches with `subprocess.check_output
files = [file1]
# Initialize the Coder object with the mocked IO and mocked repo
coder = Coder.create(self.GPT35, "diff", io=InputOutput(), fnames=files, pretty=False)
coder = Coder.create(self.GPT35, "diff", io=InputOutput(), fnames=files)
def mock_send(*args, **kwargs):
coder.partial_response_content = f"""
@ -340,7 +340,6 @@ new
io=InputOutput(dry_run=True),
fnames=files,
dry_run=True,
pretty=False,
)
def mock_send(*args, **kwargs):

View file

@ -149,17 +149,6 @@ class TestMain(TestCase):
_, kwargs = MockCoder.call_args
assert kwargs["dirty_commits"] is True
assert kwargs["auto_commits"] is True
assert kwargs["pretty"] is True
with patch("aider.coders.Coder.create") as MockCoder:
main(["--no-pretty"], input=DummyInput())
_, kwargs = MockCoder.call_args
assert kwargs["pretty"] is False
with patch("aider.coders.Coder.create") as MockCoder:
main(["--pretty"], input=DummyInput())
_, kwargs = MockCoder.call_args
assert kwargs["pretty"] is True
with patch("aider.coders.Coder.create") as MockCoder:
main(["--no-dirty-commits"], input=DummyInput())
@ -235,6 +224,15 @@ class TestMain(TestCase):
main(["--yes", fname, "--encoding", "iso-8859-15"])
def test_main_exit_calls_version_check(self):
with GitTemporaryDirectory():
with patch("aider.main.check_version") as mock_check_version, patch(
"aider.main.InputOutput"
) as mock_input_output:
main(["--exit"], input=DummyInput(), output=DummyOutput())
mock_check_version.assert_called_once()
mock_input_output.assert_called_once()
@patch("aider.main.InputOutput")
@patch("aider.coders.base_coder.Coder.run")
def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput):
@ -396,3 +394,36 @@ class TestMain(TestCase):
output=DummyOutput(),
)
MockRepoMap.assert_called_once()
def test_read_option(self):
with GitTemporaryDirectory():
test_file = "test_file.txt"
Path(test_file).touch()
coder = main(
["--read", test_file, "--exit", "--yes"],
input=DummyInput(),
output=DummyOutput(),
return_coder=True,
)
self.assertIn(str(Path(test_file).resolve()), coder.abs_read_only_fnames)
def test_read_option_with_external_file(self):
with tempfile.NamedTemporaryFile(mode="w", delete=False) as external_file:
external_file.write("External file content")
external_file_path = external_file.name
try:
with GitTemporaryDirectory():
coder = main(
["--read", external_file_path, "--exit", "--yes"],
input=DummyInput(),
output=DummyOutput(),
return_coder=True,
)
real_external_file_path = os.path.realpath(external_file_path)
self.assertIn(real_external_file_path, coder.abs_read_only_fnames)
finally:
os.unlink(external_file_path)

View file

@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import httpx
from aider.llm import litellm
from aider.sendchat import send_with_retries
from aider.sendchat import simple_send_with_retries
class PrintCalled(Exception):
@ -14,7 +14,7 @@ class PrintCalled(Exception):
class TestSendChat(unittest.TestCase):
@patch("litellm.completion")
@patch("builtins.print")
def test_send_with_retries_rate_limit_error(self, mock_print, mock_completion):
def test_simple_send_with_retries_rate_limit_error(self, mock_print, mock_completion):
mock = MagicMock()
mock.status_code = 500
@ -29,19 +29,19 @@ class TestSendChat(unittest.TestCase):
None,
]
# Call the send_with_retries method
send_with_retries("model", ["message"], None, False)
# Call the simple_send_with_retries method
simple_send_with_retries("model", ["message"])
mock_print.assert_called_once()
@patch("litellm.completion")
@patch("builtins.print")
def test_send_with_retries_connection_error(self, mock_print, mock_completion):
def test_simple_send_with_retries_connection_error(self, mock_print, mock_completion):
# Set up the mock to raise
mock_completion.side_effect = [
httpx.ConnectError("Connection error"),
None,
]
# Call the send_with_retries method
send_with_retries("model", ["message"], None, False)
# Call the simple_send_with_retries method
simple_send_with_retries("model", ["message"])
mock_print.assert_called_once()

View file

@ -288,9 +288,7 @@ after b
files = [file1]
# Initialize the Coder object with the mocked IO and mocked repo
coder = Coder.create(
self.GPT35, "whole", io=InputOutput(), fnames=files, stream=False, pretty=False
)
coder = Coder.create(self.GPT35, "whole", io=InputOutput(), fnames=files, stream=False)
# no trailing newline so the response content below doesn't add ANOTHER newline
new_content = "new\ntwo\nthree"

View file

@ -22,7 +22,13 @@ class TestHelp(unittest.TestCase):
help_coder_run = MagicMock(return_value="")
aider.coders.HelpCoder.run = help_coder_run
commands.cmd_help("hi")
try:
commands.cmd_help("hi")
except aider.commands.SwitchCoder:
pass
else:
# If no exception was raised, fail the test
assert False, "SwitchCoder exception was not raised"
help_coder_run.assert_called_once()

View file

@ -35,7 +35,7 @@ class TestScrape(unittest.TestCase):
self.commands.io.tool_error = mock_print_error
# Run the cmd_web command
result = self.commands.cmd_web("https://example.com")
result = self.commands.cmd_web("https://example.com", paginate=False)
# Assert that the result contains some content
self.assertIsNotNone(result)
@ -100,7 +100,7 @@ class TestScrape(unittest.TestCase):
# Mock the necessary objects and methods
scraper.scrape_with_playwright = MagicMock()
scraper.scrape_with_playwright.return_value = None
scraper.scrape_with_playwright.return_value = (None, None)
# Call the scrape method
result = scraper.scrape("https://example.com")
@ -113,6 +113,54 @@ class TestScrape(unittest.TestCase):
"Failed to retrieve content from https://example.com"
)
# Reset the mock
mock_print_error.reset_mock()
# Test with a different return value
scraper.scrape_with_playwright.return_value = ("Some content", "text/html")
result = scraper.scrape("https://example.com")
# Assert that the result is not None
self.assertIsNotNone(result)
# Assert that print_error was not called
mock_print_error.assert_not_called()
def test_scrape_text_plain(self):
# Create a Scraper instance
scraper = Scraper(print_error=MagicMock(), playwright_available=True)
# Mock the scrape_with_playwright method
plain_text = "This is plain text content."
scraper.scrape_with_playwright = MagicMock(return_value=(plain_text, "text/plain"))
# Call the scrape method
result = scraper.scrape("https://example.com")
# Assert that the result is the same as the input plain text
self.assertEqual(result, plain_text)
def test_scrape_text_html(self):
# Create a Scraper instance
scraper = Scraper(print_error=MagicMock(), playwright_available=True)
# Mock the scrape_with_playwright method
html_content = "<html><body><h1>Test</h1><p>This is HTML content.</p></body></html>"
scraper.scrape_with_playwright = MagicMock(return_value=(html_content, "text/html"))
# Mock the html_to_markdown method
expected_markdown = "# Test\n\nThis is HTML content."
scraper.html_to_markdown = MagicMock(return_value=expected_markdown)
# Call the scrape method
result = scraper.scrape("https://example.com")
# Assert that the result is the expected markdown
self.assertEqual(result, expected_markdown)
# Assert that html_to_markdown was called with the HTML content
scraper.html_to_markdown.assert_called_once_with(html_content)
if __name__ == "__main__":
unittest.main()