Merge branch 'main' into sitter-map

This commit is contained in:
Paul Gauthier 2023-10-18 15:04:55 -07:00
commit 2c98ea4448
25 changed files with 611 additions and 61 deletions

View file

@ -7,3 +7,4 @@ OLD*
.env .env
.venv .venv
.aider.* .aider.*
build

View file

@ -1,6 +1,7 @@
name: Release name: Release
on: on:
workflow_dispatch:
push: push:
tags: tags:
- 'v*' - 'v*'
@ -29,3 +30,19 @@ jobs:
run: | run: |
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
twine upload dist/* twine upload dist/*
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/aider:${{ github.ref_name }}
${{ secrets.DOCKERHUB_USERNAME }}/aider:latest

View file

@ -1,5 +1,33 @@
# Release history # Release history
### Next release
- Added support for `.aiderignore` file, which instructs aider to ignore parts of the git repo.
- `/run` and `/git` now accept full shell commands, like: `/run (cd subdir; ls)`
- New `--commit` cmd line arg, which just commits all pending changes with a sensible commit message geneated by gpt-3.5.
- Added universal ctags to the [aider docker image](https://aider.chat/docs/docker.html)
### v0.14.2
- Easily [run aider from a docker image](https://aider.chat/docs/docker.html)
- Fixed bug with chat history summarization.
- Fixed bug if `soundfile` package not available.
### v0.14.1
- /add and /drop handle absolute filenames and quoted filenames
- /add checks to be sure files are within the git repo (or root)
- If needed, warn users that in-chat file paths are all relative to the git repo
- Fixed /add bug in when aider launched in repo subdir
- Show models supported by api/key if requested model isn't available
### v0.14.0
- [Support for Claude2 and other LLMs via OpenRouter](https://aider.chat/docs/faq.html#accessing-other-llms-with-openrouter) by @joshuavial
- Documentation for [running the aider benchmarking suite](https://github.com/paul-gauthier/aider/tree/main/benchmark)
- Aider now requires Python >= 3.9
### v0.13.0 ### v0.13.0
- [Only git commit dirty files that GPT tries to edit](https://aider.chat/docs/faq.html#how-did-v0130-change-git-usage) - [Only git commit dirty files that GPT tries to edit](https://aider.chat/docs/faq.html#how-did-v0130-change-git-usage)

View file

@ -134,7 +134,6 @@ Aider has some ability to help GPT figure out which files to edit all by itself,
* Use Meta-ENTER (Esc+ENTER in some environments) to enter multiline chat messages. Or enter `{` alone on the first line to start a multiline message and `}` alone on the last line to end it. * Use Meta-ENTER (Esc+ENTER in some environments) to enter multiline chat messages. Or enter `{` alone on the first line to start a multiline message and `}` alone on the last line to end it.
* If your code is throwing an error, share the error output with GPT using `/run` or by pasting it into the chat. Let GPT figure out and fix the bug. * If your code is throwing an error, share the error output with GPT using `/run` or by pasting it into the chat. Let GPT figure out and fix the bug.
* GPT knows about a lot of standard tools and libraries, but may get some of the fine details wrong about APIs and function arguments. You can paste doc snippets into the chat to resolve these issues. * GPT knows about a lot of standard tools and libraries, but may get some of the fine details wrong about APIs and function arguments. You can paste doc snippets into the chat to resolve these issues.
* [Aider will notice if you launch it on a git repo with uncommitted changes and offer to commit them before proceeding](https://aider.chat/docs/faq.html#how-does-aider-use-git).
* GPT can only see the content of the files you specifically "add to the chat". Aider also sends GPT-4 a [map of your entire git repo](https://aider.chat/docs/ctags.html). So GPT may ask to see additional files if it feels that's needed for your requests. * GPT can only see the content of the files you specifically "add to the chat". Aider also sends GPT-4 a [map of your entire git repo](https://aider.chat/docs/ctags.html). So GPT may ask to see additional files if it feels that's needed for your requests.
* I also shared some general [GPT coding tips on Hacker News](https://news.ycombinator.com/item?id=36211879). * I also shared some general [GPT coding tips on Hacker News](https://news.ycombinator.com/item?id=36211879).
@ -170,4 +169,4 @@ For more information, see the [FAQ](https://aider.chat/docs/faq.html).
* *This project is stellar.* -- [funkytaco](https://github.com/paul-gauthier/aider/issues/112#issuecomment-1637429008) * *This project is stellar.* -- [funkytaco](https://github.com/paul-gauthier/aider/issues/112#issuecomment-1637429008)
* *Amazing project, definitely the best AI coding assistant I've used.* -- [joshuavial](https://github.com/paul-gauthier/aider/issues/84) * *Amazing project, definitely the best AI coding assistant I've used.* -- [joshuavial](https://github.com/paul-gauthier/aider/issues/84)
* *I am an aider addict. I'm getting so much more work done, but in less time.* -- [dandandan](https://discord.com/channels/1131200896827654144/1131200896827654149/1135913253483069470) * *I am an aider addict. I'm getting so much more work done, but in less time.* -- [dandandan](https://discord.com/channels/1131200896827654144/1131200896827654149/1135913253483069470)
* *Best agent for actual dev work in existing codebases.* -- [Nick Dobos](https://twitter.com/NickADobos/status/1690408967963652097?s=20) * *Best agent for actual dev work in existing codebases.* -- [Nick Dobos](https://twitter.com/NickADobos/status/1690408967963652097?s=20)

View file

@ -1 +1 @@
__version__ = "0.13.1-dev" __version__ = "0.14.3-dev"

View file

@ -55,6 +55,7 @@ class Coder:
main_model, main_model,
edit_format, edit_format,
io, io,
skip_model_availabily_check=False,
**kwargs, **kwargs,
): ):
from . import EditBlockCoder, WholeFileCoder from . import EditBlockCoder, WholeFileCoder
@ -62,8 +63,8 @@ class Coder:
if not main_model: if not main_model:
main_model = models.GPT35_16k main_model = models.GPT35_16k
if not main_model.always_available: if not skip_model_availabily_check and not main_model.always_available:
if not check_model_availability(main_model): if not check_model_availability(io, main_model):
if main_model != models.GPT4: if main_model != models.GPT4:
io.tool_error( io.tool_error(
f"API key does not support {main_model.name}, falling back to" f"API key does not support {main_model.name}, falling back to"
@ -99,6 +100,7 @@ class Coder:
stream=True, stream=True,
use_git=True, use_git=True,
voice_language=None, voice_language=None,
aider_ignore_file=None,
): ):
if not fnames: if not fnames:
fnames = [] fnames = []
@ -153,7 +155,7 @@ class Coder:
if use_git: if use_git:
try: try:
self.repo = GitRepo(self.io, fnames, git_dname) self.repo = GitRepo(self.io, fnames, git_dname, aider_ignore_file)
self.root = self.repo.root self.root = self.repo.root
except FileNotFoundError: except FileNotFoundError:
self.repo = None self.repo = None
@ -185,7 +187,7 @@ class Coder:
self.summarizer = ChatSummary(models.Model.weak_model()) self.summarizer = ChatSummary(models.Model.weak_model())
self.summarizer_thread = None self.summarizer_thread = None
self.summarized_done_messages = None self.summarized_done_messages = []
# validate the functions jsonschema # validate the functions jsonschema
if self.functions: if self.functions:
@ -352,7 +354,11 @@ class Coder:
self.summarizer_thread.start() self.summarizer_thread.start()
def summarize_worker(self): def summarize_worker(self):
self.summarized_done_messages = self.summarizer.summarize(self.done_messages) try:
self.summarized_done_messages = self.summarizer.summarize(self.done_messages)
except ValueError as err:
self.io.tool_error(err.args[0])
if self.verbose: if self.verbose:
self.io.tool_output("Finished summarizing chat history.") self.io.tool_output("Finished summarizing chat history.")
@ -364,7 +370,7 @@ class Coder:
self.summarizer_thread = None self.summarizer_thread = None
self.done_messages = self.summarized_done_messages self.done_messages = self.summarized_done_messages
self.summarized_done_messages = None self.summarized_done_messages = []
def move_back_cur_messages(self, message): def move_back_cur_messages(self, message):
self.done_messages += self.cur_messages self.done_messages += self.cur_messages
@ -595,15 +601,17 @@ class Coder:
self.io.tool_error(show_content_err) self.io.tool_error(show_content_err)
raise Exception("No data found in openai response!") raise Exception("No data found in openai response!")
prompt_tokens = completion.usage.prompt_tokens tokens = None
completion_tokens = completion.usage.completion_tokens if hasattr(completion, "usage"):
prompt_tokens = completion.usage.prompt_tokens
completion_tokens = completion.usage.completion_tokens
tokens = f"{prompt_tokens} prompt tokens, {completion_tokens} completion tokens" tokens = f"{prompt_tokens} prompt tokens, {completion_tokens} completion tokens"
if self.main_model.prompt_price: if self.main_model.prompt_price:
cost = prompt_tokens * self.main_model.prompt_price / 1000 cost = prompt_tokens * self.main_model.prompt_price / 1000
cost += completion_tokens * self.main_model.completion_price / 1000 cost += completion_tokens * self.main_model.completion_price / 1000
tokens += f", ${cost:.6f} cost" tokens += f", ${cost:.6f} cost"
self.total_cost += cost self.total_cost += cost
show_resp = self.render_incremental_response(True) show_resp = self.render_incremental_response(True)
if self.pretty: if self.pretty:
@ -614,7 +622,9 @@ class Coder:
show_resp = Text(show_resp or "<no response>") show_resp = Text(show_resp or "<no response>")
self.io.console.print(show_resp) self.io.console.print(show_resp)
self.io.tool_output(tokens)
if tokens is not None:
self.io.tool_output(tokens)
def show_send_output_stream(self, completion): def show_send_output_stream(self, completion):
live = None live = None
@ -895,7 +905,12 @@ class Coder:
return True return True
def check_model_availability(main_model): def check_model_availability(io, main_model):
available_models = openai.Model.list() available_models = openai.Model.list()
model_ids = [model.id for model in available_models["data"]] model_ids = [model.id for model in available_models["data"]]
return main_model.name in model_ids if main_model.name in model_ids:
return True
available_models = ", ".join(model_ids)
io.tool_error(f"API key supports: {available_models}")
return False

View file

@ -1,5 +1,5 @@
import json import json
import shlex import re
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@ -209,6 +209,10 @@ class Commands:
or last_commit.hexsha[:7] != self.coder.last_aider_commit_hash or last_commit.hexsha[:7] != self.coder.last_aider_commit_hash
): ):
self.io.tool_error("The last commit was not made by aider in this chat session.") self.io.tool_error("The last commit was not made by aider in this chat session.")
self.io.tool_error(
"You could try `/git reset --hard HEAD^` but be aware that this is a destructive"
" command!"
)
return return
self.coder.repo.repo.git.reset("--hard", "HEAD~1") self.coder.repo.repo.git.reset("--hard", "HEAD~1")
self.io.tool_output( self.io.tool_output(
@ -228,6 +232,7 @@ class Commands:
if not self.coder.last_aider_commit_hash: if not self.coder.last_aider_commit_hash:
self.io.tool_error("No previous aider commit found.") self.io.tool_error("No previous aider commit found.")
self.io.tool_error("You could try `/git diff` or `/git diff HEAD^`.")
return return
commits = f"{self.coder.last_aider_commit_hash}~1" commits = f"{self.coder.last_aider_commit_hash}~1"
@ -272,29 +277,37 @@ class Commands:
git_files = self.coder.repo.get_tracked_files() if self.coder.repo else [] git_files = self.coder.repo.get_tracked_files() if self.coder.repo else []
all_matched_files = set() all_matched_files = set()
for word in args.split():
filenames = parse_quoted_filenames(args)
for word in filenames:
if Path(word).is_absolute():
fname = Path(word)
else:
fname = Path(self.coder.root) / word
if fname.exists() and fname.is_file():
all_matched_files.add(str(fname))
continue
# an existing dir will fall through and get recursed by glob
matched_files = self.glob_filtered_to_repo(word) matched_files = self.glob_filtered_to_repo(word)
if matched_files:
all_matched_files.update(matched_files)
continue
if not matched_files: if self.io.confirm_ask(f"No files matched '{word}'. Do you want to create {fname}?"):
if any(char in word for char in "*?[]"): fname.touch()
self.io.tool_error(f"No files to add matching pattern: {word}") all_matched_files.add(str(fname))
else:
if Path(word).exists():
if Path(word).is_file():
matched_files = [word]
else:
self.io.tool_error(f"Unable to add: {word}")
elif self.io.confirm_ask(
f"No files matched '{word}'. Do you want to create the file?"
):
(Path(self.coder.root) / word).touch()
matched_files = [word]
all_matched_files.update(matched_files)
for matched_file in all_matched_files: for matched_file in all_matched_files:
abs_file_path = self.coder.abs_root_path(matched_file) abs_file_path = self.coder.abs_root_path(matched_file)
if not abs_file_path.startswith(self.coder.root):
self.io.tool_error(
f"Can not add {abs_file_path}, which is not within {self.coder.root}"
)
continue
if self.coder.repo and matched_file not in git_files: if self.coder.repo and matched_file not in git_files:
self.coder.repo.repo.git.add(abs_file_path) self.coder.repo.repo.git.add(abs_file_path)
git_added.append(matched_file) git_added.append(matched_file)
@ -339,7 +352,8 @@ class Commands:
self.io.tool_output("Dropping all files from the chat session.") self.io.tool_output("Dropping all files from the chat session.")
self.coder.abs_fnames = set() self.coder.abs_fnames = set()
for word in args.split(): filenames = parse_quoted_filenames(args)
for word in filenames:
matched_files = self.glob_filtered_to_repo(word) matched_files = self.glob_filtered_to_repo(word)
if not matched_files: if not matched_files:
@ -355,10 +369,15 @@ class Commands:
"Run a git command" "Run a git command"
combined_output = None combined_output = None
try: try:
parsed_args = shlex.split("git " + args) args = "git " + args
env = dict(GIT_EDITOR="true", **subprocess.os.environ) env = dict(GIT_EDITOR="true", **subprocess.os.environ)
result = subprocess.run( result = subprocess.run(
parsed_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=env,
shell=True,
) )
combined_output = result.stdout combined_output = result.stdout
except Exception as e: except Exception as e:
@ -373,9 +392,8 @@ class Commands:
"Run a shell command and optionally add the output to the chat" "Run a shell command and optionally add the output to the chat"
combined_output = None combined_output = None
try: try:
parsed_args = shlex.split(args)
result = subprocess.run( result = subprocess.run(
parsed_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, shell=True
) )
combined_output = result.stdout combined_output = result.stdout
except Exception as e: except Exception as e:
@ -447,7 +465,9 @@ class Commands:
try: try:
self.voice = voice.Voice() self.voice = voice.Voice()
except voice.SoundDeviceError: except voice.SoundDeviceError:
self.io.tool_error("Unable to import `sounddevice`, is portaudio installed?") self.io.tool_error(
"Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?"
)
return return
history_iter = self.io.get_input_history() history_iter = self.io.get_input_history()
@ -487,3 +507,9 @@ def expand_subdir(file_path):
for file in file_path.rglob("*"): for file in file_path.rglob("*"):
if file.is_file(): if file.is_file():
yield str(file) yield str(file)
def parse_quoted_filenames(args):
filenames = re.findall(r"\"(.+?)\"|(\S+)", args)
filenames = [name for sublist in filenames for name in sublist if name]
return filenames

View file

@ -85,6 +85,8 @@ class ChatSummary:
] ]
summary = simple_send_with_retries(self.model.name, messages) summary = simple_send_with_retries(self.model.name, messages)
if summary is None:
raise ValueError(f"summarizer unexpectedly failed for {self.model.name}")
summary = prompts.summary_prefix + summary summary = prompts.summary_prefix + summary
return [dict(role="user", content=summary)] return [dict(role="user", content=summary)]

View file

@ -60,10 +60,10 @@ def setup_git(git_root, io):
with repo.config_writer() as git_config: with repo.config_writer() as git_config:
if not global_git_config.has_option("user", "name"): if not global_git_config.has_option("user", "name"):
git_config.set_value("user", "name", "Your Name") git_config.set_value("user", "name", "Your Name")
io.tool_error('Update git name with: git config --global user.name "Your Name"') io.tool_error('Update git name with: git config user.name "Your Name"')
if not global_git_config.has_option("user", "email"): if not global_git_config.has_option("user", "email"):
git_config.set_value("user", "email", "you@example.com") git_config.set_value("user", "email", "you@example.com")
io.tool_error('Update git email with: git config --global user.email "you@example.com"') io.tool_error('Update git email with: git config user.email "you@example.com"')
io.tool_output("Git repository created in the current working directory.") io.tool_output("Git repository created in the current working directory.")
@ -74,6 +74,13 @@ def check_gitignore(git_root, io, ask=True):
if not git_root: if not git_root:
return return
try:
repo = git.Repo(git_root)
if repo.ignored(".aider"):
return
except git.exc.InvalidGitRepositoryError:
pass
pat = ".aider*" pat = ".aider*"
gitignore_file = Path(git_root) / ".gitignore" gitignore_file = Path(git_root) / ".gitignore"
@ -142,6 +149,12 @@ def main(argv=None, input=None, output=None, force_git_root=None):
default=models.GPT4.name, default=models.GPT4.name,
help=f"Specify the model to use for the main chat (default: {models.GPT4.name})", help=f"Specify the model to use for the main chat (default: {models.GPT4.name})",
) )
core_group.add_argument(
"--skip-model-availability-check",
metavar="SKIP_MODEL_AVAILABILITY_CHECK",
default=False,
help="Override to skip model availability check (default: False)",
)
core_group.add_argument( core_group.add_argument(
"-3", "-3",
action="store_const", action="store_const",
@ -286,6 +299,21 @@ def main(argv=None, input=None, output=None, force_git_root=None):
default=True, default=True,
help="Enable/disable looking for a git repo (default: True)", help="Enable/disable looking for a git repo (default: True)",
) )
git_group.add_argument(
"--gitignore",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable adding .aider* to .gitignore (default: True)",
)
default_aiderignore_file = (
os.path.join(git_root, ".aiderignore") if git_root else ".aiderignore"
)
git_group.add_argument(
"--aiderignore",
metavar="AIDERIGNORE",
default=default_aiderignore_file,
help="Specify the aider ignore file (default: .aiderignore in git root)",
)
git_group.add_argument( git_group.add_argument(
"--auto-commits", "--auto-commits",
action=argparse.BooleanOptionalAction, action=argparse.BooleanOptionalAction,
@ -302,7 +330,13 @@ def main(argv=None, input=None, output=None, force_git_root=None):
"--dry-run", "--dry-run",
action=argparse.BooleanOptionalAction, action=argparse.BooleanOptionalAction,
default=False, default=False,
help="Enable/disable performing a dry run without modifying files (default: False)", help="Perform a dry run without modifying files (default: False)",
)
git_group.add_argument(
"--commit",
action="store_true",
help="Commit all pending changes with a suitable commit message, then exit",
default=False,
) )
########## ##########
@ -423,7 +457,8 @@ def main(argv=None, input=None, output=None, force_git_root=None):
if args.git: if args.git:
git_root = setup_git(git_root, io) git_root = setup_git(git_root, io)
check_gitignore(git_root, io) if args.gitignore:
check_gitignore(git_root, io)
def scrub_sensitive_info(text): def scrub_sensitive_info(text):
# Replace sensitive information with placeholder # Replace sensitive information with placeholder
@ -465,6 +500,7 @@ def main(argv=None, input=None, output=None, force_git_root=None):
main_model, main_model,
args.edit_format, args.edit_format,
io, io,
args.skip_model_availability_check,
## ##
fnames=fnames, fnames=fnames,
git_dname=git_dname, git_dname=git_dname,
@ -480,11 +516,16 @@ def main(argv=None, input=None, output=None, force_git_root=None):
stream=args.stream, stream=args.stream,
use_git=args.git, use_git=args.git,
voice_language=args.voice_language, voice_language=args.voice_language,
aider_ignore_file=args.aiderignore,
) )
except ValueError as err: except ValueError as err:
io.tool_error(str(err)) io.tool_error(str(err))
return 1 return 1
if args.commit:
coder.commands.cmd_commit("")
return
if args.show_repo_map: if args.show_repo_map:
repo_map = coder.get_repo_map() repo_map = coder.get_repo_map()
if repo_map: if repo_map:
@ -501,6 +542,15 @@ def main(argv=None, input=None, output=None, force_git_root=None):
io.tool_output("Use /help to see in-chat commands, run with --help to see cmd line args") io.tool_output("Use /help to see in-chat commands, run with --help to see cmd line args")
if git_root and Path.cwd().resolve() != Path(git_root).resolve():
io.tool_error(
"Note: in-chat filenames are always relative to the git working dir, not the current"
" working dir."
)
io.tool_error(f"Cur working dir: {Path.cwd()}")
io.tool_error(f"Git working dir: {git_root}")
if args.message: if args.message:
io.tool_output() io.tool_output()
coder.run(with_message=args.message) coder.run(with_message=args.message)

View file

@ -1,5 +1,15 @@
from .model import Model from .model import Model
from .openai import OpenAIModel
from .openrouter import OpenRouterModel
GPT4 = Model.create("gpt-4") GPT4 = Model.create("gpt-4")
GPT35 = Model.create("gpt-3.5-turbo") GPT35 = Model.create("gpt-3.5-turbo")
GPT35_16k = Model.create("gpt-3.5-turbo-16k") GPT35_16k = Model.create("gpt-3.5-turbo-16k")
__all__ = [
OpenAIModel,
OpenRouterModel,
GPT4,
GPT35,
GPT35_16k,
]

View file

@ -2,6 +2,7 @@ import os
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
import git import git
import pathspec
from aider import models, prompts, utils from aider import models, prompts, utils
from aider.sendchat import simple_send_with_retries from aider.sendchat import simple_send_with_retries
@ -11,8 +12,11 @@ from .dump import dump # noqa: F401
class GitRepo: class GitRepo:
repo = None repo = None
aider_ignore_file = None
aider_ignore_spec = None
aider_ignore_ts = 0
def __init__(self, io, fnames, git_dname): def __init__(self, io, fnames, git_dname, aider_ignore_file=None):
self.io = io self.io = io
if git_dname: if git_dname:
@ -49,14 +53,20 @@ class GitRepo:
self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB) self.repo = git.Repo(repo_paths.pop(), odbt=git.GitDB)
self.root = utils.safe_abs_path(self.repo.working_tree_dir) self.root = utils.safe_abs_path(self.repo.working_tree_dir)
if aider_ignore_file:
self.aider_ignore_file = Path(aider_ignore_file)
def commit(self, fnames=None, context=None, prefix=None, message=None): def commit(self, fnames=None, context=None, prefix=None, message=None):
if not fnames and not self.repo.is_dirty(): if not fnames and not self.repo.is_dirty():
return return
diffs = self.get_diffs(fnames)
if not diffs:
return
if message: if message:
commit_message = message commit_message = message
else: else:
diffs = self.get_diffs(fnames)
commit_message = self.get_commit_message(diffs, context) commit_message = self.get_commit_message(diffs, context)
if not commit_message: if not commit_message:
@ -190,7 +200,22 @@ class GitRepo:
for path in files for path in files
) )
return res return self.filter_ignored_files(res)
def filter_ignored_files(self, fnames):
if not self.aider_ignore_file or not self.aider_ignore_file.is_file():
return fnames
mtime = self.aider_ignore_file.stat().st_mtime
if mtime != self.aider_ignore_ts:
self.aider_ignore_ts = mtime
lines = self.aider_ignore_file.read_text().splitlines()
self.aider_ignore_spec = pathspec.PathSpec.from_lines(
pathspec.patterns.GitWildMatchPattern,
lines,
)
return [fname for fname in fnames if not self.aider_ignore_spec.match_file(fname)]
def path_in_repo(self, path): def path_in_repo(self, path):
if not self.repo: if not self.repo:

View file

@ -83,6 +83,8 @@ class RepoMap:
cache_missing = False cache_missing = False
warned_files = set()
def __init__( def __init__(
self, self,
map_tokens=1024, map_tokens=1024,
@ -235,7 +237,10 @@ class RepoMap:
for fname in fnames: for fname in fnames:
if not Path(fname).is_file(): if not Path(fname).is_file():
self.io.tool_error(f"Repo-map can't include {fname}") if fname not in self.warned_files:
self.io.tool_error(f"Repo-map can't include {fname}")
self.warned_files.add(fname)
continue continue
# dump(fname) # dump(fname)

View file

@ -5,7 +5,12 @@ import time
import numpy as np import numpy as np
import openai import openai
import soundfile as sf
try:
import soundfile as sf
except (OSError, ModuleNotFoundError):
sf = None
from prompt_toolkit.shortcuts import prompt from prompt_toolkit.shortcuts import prompt
from .dump import dump # noqa: F401 from .dump import dump # noqa: F401
@ -23,12 +28,14 @@ class Voice:
threshold = 0.15 threshold = 0.15
def __init__(self): def __init__(self):
if sf is None:
raise SoundDeviceError
try: try:
print("Initializing sound device...") print("Initializing sound device...")
import sounddevice as sd import sounddevice as sd
self.sd = sd self.sd = sd
except OSError: except (OSError, ModuleNotFoundError):
raise SoundDeviceError raise SoundDeviceError
def callback(self, indata, frames, time, status): def callback(self, indata, frames, time, status):

View file

@ -1,7 +1,8 @@
FROM python:3.8-slim FROM python:3.10-slim
RUN apt-get update && apt-get install -y less git RUN apt-get update && apt-get install -y less git
COPY requirements.txt /aider/requirements.txt COPY requirements.txt /aider/requirements.txt
RUN pip install lox typer pandas matplotlib imgcat aider-chat RUN pip install lox typer pandas matplotlib imgcat aider-chat
RUN pip install --upgrade pip && pip install -r /aider/requirements.txt RUN pip install --upgrade pip && pip install -r /aider/requirements.txt
RUN git config --global --add safe.directory /aider
WORKDIR /aider WORKDIR /aider

107
benchmark/README.md Normal file
View file

@ -0,0 +1,107 @@
# Aider code editing benchmark harness
Aider uses a "code editing" benchmark to quantitatively measure how well it works
with the GPT-3.5 and GPT-4 models.
This directory holds the harness and tools needed to run the benchmarking suite.
## Background
The benchmark is based on the [Exercism
python](https://github.com/exercism/python) coding exercises.
This
benchmark evaluates how effectively aider and GPT can translate a
natural language coding request into executable code saved into
files that pass unit tests.
It provides an end-to-end evaluation of not just
GPT's coding ability, but also its capacity to *edit existing code*
and *format those code edits* so that aider can save the
edits to the local source files.
See [this writeup for a longer discussion about the benchmark and how to interpret the results](https://aider.chat/docs/benchmarks.html).
The benchmark is intended to be run *inside a docker container*.
This is because the benchmarking harness will be
taking code written by an LLM
and executing it without any human review or supervision!
The LLM could generate dangerous python that harms your system, like this: `import os; os.system("sudo rm -rf /")`.
Running inside a docker container helps limit the damage that could be done.
## Usage
There are 3 main tasks involved in benchmarking aider:
1. Install and setup for benchmarking.
2. Run the benchmark to measure performance across the 133 exercises.
3. Generate a summary report of how many of the exercises succeeded or failed.
### Setup for benchmarking
First, prepare all the groundwork for running the benchmarks.
These steps only need to be done once.
```
# Clone the aider repo
git clone git@github.com:paul-gauthier/aider.git
# Create the scratch dir to hold benchmarking results inside the main aider dir:
cd aider
mkdir tmp.benchmarks
# Clone the exercism repo
git clone git@github.com:exercism/python.git
# Copy the practice exercises into the benchmark scratch dir
cp -rp python/exercises/practice tmp.benchmarks/practice
# Build the docker container
./benchmark/docker_build.sh
```
### Running the benchmark
Launch the docker container and run the benchmark inside it:
```
# Launch the docker container
./benchmark/docker.sh
# Inside the container, install aider as a development build.
# This way you're running the code that you cloned above, including any local changes.
pip install -e .
# Run the benchmark:
./benchmark/benchmark.py a-helpful-name-for-this-run --model gpt-3.5-turbo --edit-format whole --threads 10
```
The above will create a folder `tmp.benchmarks/YYYY-MM-DD-HH-MM-SS--a-helpful-name-for-this-run` with benchmarking results.
Run like this, the script will run all 133 exercises in a random order.
You can run `./benchmark/benchmark.py --help` for a list of all the arguments, but here are the most useful to keep in mind:
- `--model` is the name of the model, same as you would pass directly to `aider`.
- `--edit-format` is the name of the edit format, same as you would pass directly to `aider`. When working with an experimental LLM, I recommend starting with `whole`
- `--threads` specifies how many exercises to benchmark in parallel. Start with a single thread if you are working out the kinks on your benchmarking setup or working with a new model, etc. Once you are getting reliable results, you can speed up the process by running with more threads. 10 works well against the OpenAI APIs.
- `--num-tests` specifies how many of the 133 tests to run before stopping. This is another way to start gently as you debug your benchmarking setup.
- `--keywords` filters the tests to run to only the ones whose name match the supplied argument (similar to `pytest -k xxxx`).
### Generating a benchmark report
You can generate stats about any benchmark, including ones which are still running.
You don't need to run this inside the docker container, as it is just
collecting stats not executing unsafe python.
```
# Generate stats for a specific benchmarking directory
./benchmark/benchmark.py --stats tmp.benchmarks/YYYY-MM-DD-HH-MM-SS--a-helpful-name-for-this-run
```
## Limitations, notes
- If you're experimenting with non-OpenAI models, the benchmarking harness may not provide enough switches/control to specify the integration to such models. You probably need to edit `benchmark.py` to instantiate `Coder()` appropriately. You can just hack this in or add new switches/config.
- Benchmarking all 133 exercises against GPT-4 will cost about $10-20.
- Benchmarking aider is intended for folks who are actively developing aider or doing experimental work adapting it for use with [new LLM models](https://github.com/paul-gauthier/aider/issues/172).
- These scripts are not intended for use by typical aider users.
- Some of the tools are written as `bash` scripts, so it will be hard to use them on Windows.

View file

@ -547,7 +547,7 @@ def run_test(
chat_history_file=history_fname, chat_history_file=history_fname,
) )
main_model = models.Model(model_name) main_model = models.Model.create(model_name)
edit_format = edit_format or main_model.edit_format edit_format = edit_format or main_model.edit_format
dump(main_model) dump(main_model)

9
docker/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
FROM python:3.10-slim
COPY . /aider
RUN apt-get update && \
apt-get install --no-install-recommends -y git universal-ctags libportaudio2 && \
rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir /aider && \
rm -rf /aider
WORKDIR /app
ENTRYPOINT ["aider"]

32
docs/docker.md Normal file
View file

@ -0,0 +1,32 @@
# Run aider with docker (experimental)
You can run aider via docker without doing any local installation, like this:
```
docker pull paulgauthier/aider
docker run -it --volume $(pwd):/app paulgauthier/aider --openai-api-key $OPENAI_API_KEY [...other aider args...]
```
You should run the above commands from the root of your git repo,
since the `--volume` arg maps your current directory into the
docker container.
Given that, you need to be in the root of your git repo for aider to be able to
see the repo and all its files.
You should be sure your that
git repo config contains your user name and email, since the
docker container won't have your global git config.
Run these commands while in your git repo, before
you do the `docker run` command:
```
git config user.email "you@example.com"
git config user.name "Your Name"
```
## Limitations
- When you use the in-chat `/run` command, it will be running shell commands *inside the docker container*. So those commands won't be running in your local environment, which may make it tricky to `/run` tests, etc for your project.
- The `/voice` command won't work unless you can figure out how to give the docker container access to your host audio device. The container has libportaudio2 installed, so it should work if you can do that.

View file

@ -8,6 +8,7 @@
- [Can I use aider with other LLMs, local LLMs, etc?](#can-i-use-aider-with-other-llms-local-llms-etc) - [Can I use aider with other LLMs, local LLMs, etc?](#can-i-use-aider-with-other-llms-local-llms-etc)
- [Can I change the system prompts that aider uses?](#can-i-change-the-system-prompts-that-aider-uses) - [Can I change the system prompts that aider uses?](#can-i-change-the-system-prompts-that-aider-uses)
- [Can I run aider in Google Colab?](#can-i-run-aider-in-google-colab) - [Can I run aider in Google Colab?](#can-i-run-aider-in-google-colab)
- [How can I run aider locally from source code?](#how-can-i-run-aider-locally-from-source-code)
## How does aider use git? ## How does aider use git?
@ -224,3 +225,21 @@ all the raw information being sent to/from GPT in the conversation.
User [imabutahersiddik](https://github.com/imabutahersiddik) User [imabutahersiddik](https://github.com/imabutahersiddik)
has provided this has provided this
[Colab notebook](https://colab.research.google.com/drive/1J9XynhrCqekPL5PR6olHP6eE--rnnjS9?usp=sharing). [Colab notebook](https://colab.research.google.com/drive/1J9XynhrCqekPL5PR6olHP6eE--rnnjS9?usp=sharing).
## How can I run aider locally from source code?
To run the project locally, follow these steps:
```
# Clone the repository:
git clone git@github.com:paul-gauthier/aider.git
# Navigate to the project directory:
cd aider
# Install the dependencies listed in the `requirements.txt` file:
pip install -r requirements.txt
# Run the local version of Aider:
python -m aider.main
```

View file

@ -3,7 +3,9 @@
Speak with GPT about your code! Request new features, test cases or bug fixes using your voice and let GPT do the work of editing the files in your local git repo. As with all of aider's capabilities, you can use voice-to-code with an existing repo or to start a new project. Speak with GPT about your code! Request new features, test cases or bug fixes using your voice and let GPT do the work of editing the files in your local git repo. As with all of aider's capabilities, you can use voice-to-code with an existing repo or to start a new project.
Voice support fits quite naturally into aider's AI pair programming chat interface. Now you can fluidly switch between voice and text chat where you ask GPT to edit the code and your own editor when it makes more sense for you to drive. Voice support fits quite naturally into aider's AI pair programming
chat interface. Now you can fluidly switch between voice and text chat
when you ask GPT to edit your code.
## How to use voice-to-code ## How to use voice-to-code

View file

View file

@ -33,3 +33,4 @@ scipy==1.10.1
jsonschema==4.17.3 jsonschema==4.17.3
sounddevice==0.4.6 sounddevice==0.4.6
soundfile==0.12.1 soundfile==0.12.1
pathspec==0.11.2

2
scripts/versionbump.py Normal file → Executable file
View file

@ -1,3 +1,5 @@
#!/usr/bin/env python
import argparse import argparse
import re import re
import subprocess import subprocess

View file

@ -14,7 +14,7 @@ from aider.coders import Coder
from aider.commands import Commands from aider.commands import Commands
from aider.dump import dump # noqa: F401 from aider.dump import dump # noqa: F401
from aider.io import InputOutput from aider.io import InputOutput
from tests.utils import GitTemporaryDirectory from tests.utils import ChdirTemporaryDirectory, GitTemporaryDirectory, make_repo
class TestCommands(TestCase): class TestCommands(TestCase):
@ -69,8 +69,8 @@ class TestCommands(TestCase):
self.assertNotIn(str(Path("test.txt").resolve()), coder.abs_fnames) self.assertNotIn(str(Path("test.txt").resolve()), coder.abs_fnames)
def test_cmd_add_no_match(self): def test_cmd_add_no_match(self):
# Initialize the Commands and InputOutput objects # yes=False means we will *not* create the file when it is not found
io = InputOutput(pretty=False, yes=True) io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io) coder = Coder.create(models.GPT35, None, io)
@ -82,6 +82,23 @@ class TestCommands(TestCase):
# Check if no files have been added to the chat session # Check if no files have been added to the chat session
self.assertEqual(len(coder.abs_fnames), 0) self.assertEqual(len(coder.abs_fnames), 0)
def test_cmd_add_no_match_but_make_it(self):
# yes=True means we *will* create the file when it is not found
io = InputOutput(pretty=False, yes=True)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
fname = Path("[abc].nonexistent")
# Call the cmd_add method with a non-existent file pattern
commands.cmd_add(str(fname))
# Check if no files have been added to the chat session
self.assertEqual(len(coder.abs_fnames), 1)
self.assertTrue(fname.exists())
def test_cmd_add_drop_directory(self): def test_cmd_add_drop_directory(self):
# Initialize the Commands and InputOutput objects # Initialize the Commands and InputOutput objects
io = InputOutput(pretty=False, yes=False) io = InputOutput(pretty=False, yes=False)
@ -255,6 +272,25 @@ class TestCommands(TestCase):
self.assertNotIn(filenames[1], coder.abs_fnames) self.assertNotIn(filenames[1], coder.abs_fnames)
self.assertIn(filenames[2], coder.abs_fnames) self.assertIn(filenames[2], coder.abs_fnames)
def test_cmd_add_from_subdir_again(self):
with GitTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
Path("side_dir").mkdir()
os.chdir("side_dir")
# add a file that is in the side_dir
with open("temp.txt", "w"):
pass
# this was blowing up with GitCommandError, per:
# https://github.com/paul-gauthier/aider/issues/201
commands.cmd_add("temp.txt")
def test_cmd_commit(self): def test_cmd_commit(self):
with GitTemporaryDirectory(): with GitTemporaryDirectory():
fname = "test.txt" fname = "test.txt"
@ -276,3 +312,93 @@ class TestCommands(TestCase):
commit_message = "Test commit message" commit_message = "Test commit message"
commands.cmd_commit(commit_message) commands.cmd_commit(commit_message)
self.assertFalse(repo.is_dirty()) self.assertFalse(repo.is_dirty())
def test_cmd_add_from_outside_root(self):
with ChdirTemporaryDirectory() as tmp_dname:
root = Path("root")
root.mkdir()
os.chdir(str(root))
io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
outside_file = Path(tmp_dname) / "outside.txt"
outside_file.touch()
# This should not be allowed!
# https://github.com/paul-gauthier/aider/issues/178
commands.cmd_add("../outside.txt")
self.assertEqual(len(coder.abs_fnames), 0)
def test_cmd_add_from_outside_git(self):
with ChdirTemporaryDirectory() as tmp_dname:
root = Path("root")
root.mkdir()
os.chdir(str(root))
make_repo()
io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
outside_file = Path(tmp_dname) / "outside.txt"
outside_file.touch()
# This should not be allowed!
# It was blowing up with GitCommandError, per:
# https://github.com/paul-gauthier/aider/issues/178
commands.cmd_add("../outside.txt")
self.assertEqual(len(coder.abs_fnames), 0)
def test_cmd_add_filename_with_special_chars(self):
with ChdirTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
fname = Path("with[brackets].txt")
fname.touch()
commands.cmd_add(str(fname))
self.assertIn(str(fname.resolve()), coder.abs_fnames)
def test_cmd_add_abs_filename(self):
with ChdirTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
fname = Path("file.txt")
fname.touch()
commands.cmd_add(str(fname.resolve()))
self.assertIn(str(fname.resolve()), coder.abs_fnames)
def test_cmd_add_quoted_filename(self):
with ChdirTemporaryDirectory():
io = InputOutput(pretty=False, yes=False)
from aider.coders import Coder
coder = Coder.create(models.GPT35, None, io)
commands = Commands(io, coder)
fname = Path("file with spaces.txt")
fname.touch()
commands.cmd_add(f'"{fname}"')
self.assertIn(str(fname.resolve()), coder.abs_fnames)

View file

@ -68,7 +68,6 @@ class TestRepo(unittest.TestCase):
git_repo = GitRepo(InputOutput(), None, ".") git_repo = GitRepo(InputOutput(), None, ".")
diffs = git_repo.diff_commits(False, "HEAD~1", "HEAD") diffs = git_repo.diff_commits(False, "HEAD~1", "HEAD")
dump(diffs)
self.assertIn("two", diffs) self.assertIn("two", diffs)
@patch("aider.repo.simple_send_with_retries") @patch("aider.repo.simple_send_with_retries")
@ -171,6 +170,55 @@ class TestRepo(unittest.TestCase):
self.assertIn(str(fname), fnames) self.assertIn(str(fname), fnames)
self.assertIn(str(fname2), fnames) self.assertIn(str(fname2), fnames)
def test_get_tracked_files_with_aiderignore(self):
with GitTemporaryDirectory():
# new repo
raw_repo = git.Repo()
# add it, but no commits at all in the raw_repo yet
fname = Path("new.txt")
fname.touch()
raw_repo.git.add(str(fname))
aiderignore = Path(".aiderignore")
git_repo = GitRepo(InputOutput(), None, None, str(aiderignore))
# better be there
fnames = git_repo.get_tracked_files()
self.assertIn(str(fname), fnames)
# commit it, better still be there
raw_repo.git.commit("-m", "new")
fnames = git_repo.get_tracked_files()
self.assertIn(str(fname), fnames)
# new file, added but not committed
fname2 = Path("new2.txt")
fname2.touch()
raw_repo.git.add(str(fname2))
# both should be there
fnames = git_repo.get_tracked_files()
self.assertIn(str(fname), fnames)
self.assertIn(str(fname2), fnames)
aiderignore.write_text("new.txt\n")
# new.txt should be gone!
fnames = git_repo.get_tracked_files()
self.assertNotIn(str(fname), fnames)
self.assertIn(str(fname2), fnames)
# This does not work in github actions?!
# The mtime doesn't change, even if I time.sleep(1)
# Before doing this write_text()!?
#
# aiderignore.write_text("new2.txt\n")
# new2.txt should be gone!
# fnames = git_repo.get_tracked_files()
# self.assertIn(str(fname), fnames)
# self.assertNotIn(str(fname2), fnames)
def test_get_tracked_files_from_subdir(self): def test_get_tracked_files_from_subdir(self):
with GitTemporaryDirectory(): with GitTemporaryDirectory():
# new repo # new repo
@ -194,3 +242,21 @@ class TestRepo(unittest.TestCase):
raw_repo.git.commit("-m", "new") raw_repo.git.commit("-m", "new")
fnames = git_repo.get_tracked_files() fnames = git_repo.get_tracked_files()
self.assertIn(str(fname), fnames) self.assertIn(str(fname), fnames)
@patch("aider.repo.simple_send_with_retries")
def test_noop_commit(self, mock_send):
mock_send.return_value = '"a good commit message"'
with GitTemporaryDirectory():
# new repo
raw_repo = git.Repo()
# add it, but no commits at all in the raw_repo yet
fname = Path("file.txt")
fname.touch()
raw_repo.git.add(str(fname))
raw_repo.git.commit("-m", "new")
git_repo = GitRepo(InputOutput(), None, None)
git_repo.commit(fnames=[str(fname)])