Merge branch 'main' into gemini-editblock

This commit is contained in:
Paul Gauthier 2024-04-29 20:42:52 -07:00
commit 65dccb6205
29 changed files with 1478 additions and 529 deletions

View file

@ -1 +1 @@
__version__ = "0.29.3-dev"
__version__ = "0.30.2-dev"

375
aider/args.py Normal file
View file

@ -0,0 +1,375 @@
import argparse
import os
import configargparse
from aider import __version__, models
def get_parser(default_config_files, git_root):
parser = configargparse.ArgumentParser(
description="aider is GPT powered coding in your terminal",
add_config_file_help=True,
default_config_files=default_config_files,
config_file_parser_class=configargparse.YAMLConfigFileParser,
auto_env_var_prefix="AIDER_",
)
##########
group = parser.add_argument_group("Main")
group.add_argument(
"files",
metavar="FILE",
nargs="*",
help="files to edit with an LLM (optional)",
)
group.add_argument(
"--openai-api-key",
metavar="OPENAI_API_KEY",
env_var="OPENAI_API_KEY",
help="Specify the OpenAI API key",
)
group.add_argument(
"--anthropic-api-key",
metavar="ANTHROPIC_API_KEY",
env_var="ANTHROPIC_API_KEY",
help="Specify the OpenAI API key",
)
default_model = models.DEFAULT_MODEL_NAME
group.add_argument(
"--model",
metavar="MODEL",
default=default_model,
help=f"Specify the model to use for the main chat (default: {default_model})",
)
opus_model = "claude-3-opus-20240229"
group.add_argument(
"--opus",
action="store_const",
dest="model",
const=opus_model,
help=f"Use {opus_model} model for the main chat",
)
sonnet_model = "claude-3-sonnet-20240229"
group.add_argument(
"--sonnet",
action="store_const",
dest="model",
const=sonnet_model,
help=f"Use {sonnet_model} model for the main chat",
)
gpt_4_model = "gpt-4-0613"
group.add_argument(
"--4",
"-4",
action="store_const",
dest="model",
const=gpt_4_model,
help=f"Use {gpt_4_model} model for the main chat",
)
gpt_4_turbo_model = "gpt-4-turbo"
group.add_argument(
"--4-turbo-vision",
action="store_const",
dest="model",
const=gpt_4_turbo_model,
help=f"Use {gpt_4_turbo_model} model for the main chat",
)
gpt_3_model_name = "gpt-3.5-turbo"
group.add_argument(
"--35turbo",
"--35-turbo",
"--3",
"-3",
action="store_const",
dest="model",
const=gpt_3_model_name,
help=f"Use {gpt_3_model_name} model for the main chat",
)
##########
group = parser.add_argument_group("Model Settings")
group.add_argument(
"--models",
metavar="MODEL",
help="List known models which match the (partial) MODEL name",
)
group.add_argument(
"--openai-api-base",
metavar="OPENAI_API_BASE",
env_var="OPENAI_API_BASE",
help="Specify the api base url",
)
group.add_argument(
"--openai-api-type",
metavar="OPENAI_API_TYPE",
env_var="OPENAI_API_TYPE",
help="Specify the api_type",
)
group.add_argument(
"--openai-api-version",
metavar="OPENAI_API_VERSION",
env_var="OPENAI_API_VERSION",
help="Specify the api_version",
)
group.add_argument(
"--openai-api-deployment-id",
metavar="OPENAI_API_DEPLOYMENT_ID",
env_var="OPENAI_API_DEPLOYMENT_ID",
help="Specify the deployment_id",
)
group.add_argument(
"--openai-organization-id",
metavar="OPENAI_ORGANIZATION_ID",
env_var="OPENAI_ORGANIZATION_ID",
help="Specify the OpenAI organization ID",
)
group.add_argument(
"--edit-format",
metavar="EDIT_FORMAT",
default=None,
help="Specify what edit format the LLM should use (default depends on model)",
)
group.add_argument(
"--weak-model",
metavar="WEAK_MODEL",
default=None,
help=(
"Specify the model to use for commit messages and chat history summarization (default"
" depends on --model)"
),
)
group.add_argument(
"--show-model-warnings",
action=argparse.BooleanOptionalAction,
default=True,
help="Only work with models that have meta-data available (default: True)",
)
group.add_argument(
"--map-tokens",
type=int,
default=1024,
help="Max number of tokens to use for repo map, use 0 to disable (default: 1024)",
)
##########
group = parser.add_argument_group("History Files")
default_input_history_file = (
os.path.join(git_root, ".aider.input.history") if git_root else ".aider.input.history"
)
default_chat_history_file = (
os.path.join(git_root, ".aider.chat.history.md") if git_root else ".aider.chat.history.md"
)
group.add_argument(
"--input-history-file",
metavar="INPUT_HISTORY_FILE",
default=default_input_history_file,
help=f"Specify the chat input history file (default: {default_input_history_file})",
)
group.add_argument(
"--chat-history-file",
metavar="CHAT_HISTORY_FILE",
default=default_chat_history_file,
help=f"Specify the chat history file (default: {default_chat_history_file})",
)
##########
group = parser.add_argument_group("Output Settings")
group.add_argument(
"--dark-mode",
action="store_true",
help="Use colors suitable for a dark terminal background (default: False)",
default=False,
)
group.add_argument(
"--light-mode",
action="store_true",
help="Use colors suitable for a light terminal background (default: False)",
default=False,
)
group.add_argument(
"--pretty",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable pretty, colorized output (default: True)",
)
group.add_argument(
"--stream",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable streaming responses (default: True)",
)
group.add_argument(
"--user-input-color",
default="#00cc00",
help="Set the color for user input (default: #00cc00)",
)
group.add_argument(
"--tool-output-color",
default=None,
help="Set the color for tool output (default: None)",
)
group.add_argument(
"--tool-error-color",
default="#FF2222",
help="Set the color for tool error messages (default: red)",
)
group.add_argument(
"--assistant-output-color",
default="#0088ff",
help="Set the color for assistant output (default: #0088ff)",
)
group.add_argument(
"--code-theme",
default="default",
help=(
"Set the markdown code theme (default: default, other options include monokai,"
" solarized-dark, solarized-light)"
),
)
group.add_argument(
"--show-diffs",
action="store_true",
help="Show diffs when committing changes (default: False)",
default=False,
)
##########
group = parser.add_argument_group("Git Settings")
group.add_argument(
"--git",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable looking for a git repo (default: True)",
)
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"
)
group.add_argument(
"--aiderignore",
metavar="AIDERIGNORE",
default=default_aiderignore_file,
help="Specify the aider ignore file (default: .aiderignore in git root)",
)
group.add_argument(
"--auto-commits",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable auto commit of LLM changes (default: True)",
)
group.add_argument(
"--dirty-commits",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable commits when repo is found dirty (default: True)",
)
group.add_argument(
"--dry-run",
action=argparse.BooleanOptionalAction,
default=False,
help="Perform a dry run without modifying files (default: False)",
)
group.add_argument(
"--commit",
action="store_true",
help="Commit all pending changes with a suitable commit message, then exit",
default=False,
)
##########
group = parser.add_argument_group("Other Settings")
group.add_argument(
"--voice-language",
metavar="VOICE_LANGUAGE",
default="en",
help="Specify the language for voice using ISO 639-1 code (default: auto)",
)
group.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}",
help="Show the version number and exit",
)
group.add_argument(
"--check-update",
action="store_true",
help="Check for updates and return status in the exit code",
default=False,
)
group.add_argument(
"--skip-check-update",
action="store_true",
help="Skips checking for the update when the program runs",
)
group.add_argument(
"--apply",
metavar="FILE",
help="Apply the changes from the given file instead of running the chat (debug)",
)
group.add_argument(
"--yes",
action="store_true",
help="Always say yes to every confirmation",
default=None,
)
group.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
default=False,
)
group.add_argument(
"--show-repo-map",
action="store_true",
help="Print the repo map and exit (debug)",
default=False,
)
group.add_argument(
"--message",
"--msg",
"-m",
metavar="COMMAND",
help=(
"Specify a single message to send the LLM, process reply then exit (disables chat mode)"
),
)
group.add_argument(
"--message-file",
"-f",
metavar="MESSAGE_FILE",
help=(
"Specify a file containing the message to send the LLM, process reply, then exit"
" (disables chat mode)"
),
)
group.add_argument(
"--encoding",
default="utf-8",
help="Specify the encoding for input and output (default: utf-8)",
)
group.add_argument(
"-c",
"--config",
is_config_file=True,
metavar="CONFIG_FILE",
help=(
"Specify the config file (default: search for .aider.conf.yml in git root, cwd"
" or home directory)"
),
)
group.add_argument(
"--gui",
"--browser",
action="store_true",
help="Run aider in your browser",
default=False,
)
return parser

View file

@ -16,7 +16,7 @@ from jsonschema import Draft7Validator
from rich.console import Console, Text
from rich.markdown import Markdown
from aider import models, prompts, utils
from aider import __version__, models, prompts, utils
from aider.commands import Commands
from aider.history import ChatSummary
from aider.io import InputOutput
@ -45,6 +45,7 @@ class Coder:
abs_fnames = None
repo = None
last_aider_commit_hash = None
aider_edited_files = None
last_asked_for_commit_time = 0
repo_map = None
functions = None
@ -54,6 +55,7 @@ class Coder:
last_keyboard_interrupt = None
max_apply_update_errors = 3
edit_format = None
yield_stream = False
@classmethod
def create(
@ -80,6 +82,55 @@ class Coder:
else:
raise ValueError(f"Unknown edit format {edit_format}")
def get_announcements(self):
lines = []
lines.append(f"Aider v{__version__}")
# Model
main_model = self.main_model
weak_model = main_model.weak_model
prefix = "Model:"
output = f" {main_model.name} with {self.edit_format} edit format"
if weak_model is not main_model:
prefix = "Models:"
output += f", weak model {weak_model.name}"
lines.append(prefix + output)
# Repo
if self.repo:
rel_repo_dir = self.repo.get_rel_repo_dir()
num_files = len(self.repo.get_tracked_files())
lines.append(f"Git repo: {rel_repo_dir} with {num_files:,} files")
if num_files > 1000:
lines.append(
"Warning: For large repos, consider using an .aiderignore file to ignore"
" irrelevant files/dirs."
)
else:
lines.append("Git repo: none")
# Repo-map
if self.repo_map:
map_tokens = self.repo_map.max_map_tokens
if map_tokens > 0:
lines.append(f"Repo-map: using {map_tokens} tokens")
max_map_tokens = 2048
if map_tokens > max_map_tokens:
lines.append(
f"Warning: map-tokens > {max_map_tokens} is not recommended as too much"
" irrelevant code can confuse GPT."
)
else:
lines.append("Repo-map: disabled because map_tokens == 0")
else:
lines.append("Repo-map: disabled")
# Files
for fname in self.get_inchat_relative_files():
lines.append(f"Added {fname} to the chat.")
return lines
def __init__(
self,
main_model,
@ -136,15 +187,6 @@ class Coder:
self.main_model = main_model
weak_model = main_model.weak_model
prefix = "Model:"
output = f" {main_model.name} with {self.edit_format} edit format"
if weak_model is not main_model:
prefix = "Models:"
output += f", weak model {weak_model.name}"
self.io.tool_output(prefix + output)
self.show_diffs = show_diffs
self.commands = Commands(self.io, self, voice_language)
@ -181,17 +223,7 @@ class Coder:
self.abs_fnames.add(fname)
self.check_added_files()
if self.repo:
rel_repo_dir = self.repo.get_rel_repo_dir()
num_files = len(self.repo.get_tracked_files())
self.io.tool_output(f"Git repo: {rel_repo_dir} with {num_files:,} files")
if num_files > 1000:
self.io.tool_error(
"Warning: For large repos, consider using an .aiderignore file to ignore"
" irrelevant files/dirs."
)
else:
self.io.tool_output("Git repo: none")
if not self.repo:
self.find_common_root()
if main_model.use_repo_map and self.repo and self.gpt_prompts.repo_content_prefix:
@ -204,22 +236,6 @@ class Coder:
self.verbose,
)
if map_tokens > 0 and self.repo_map:
self.io.tool_output(f"Repo-map: using {map_tokens} tokens")
max_map_tokens = 2048
if map_tokens > max_map_tokens:
self.io.tool_error(
f"Warning: map-tokens > {max_map_tokens} is not recommended as too much"
" irrelevant code can confuse GPT."
)
elif not map_tokens:
self.io.tool_output("Repo-map: disabled because map_tokens == 0")
else:
self.io.tool_output("Repo-map: disabled")
for fname in self.get_inchat_relative_files():
self.io.tool_output(f"Added {fname} to the chat.")
self.summarizer = ChatSummary(
self.main_model.weak_model,
self.main_model.max_chat_history_tokens,
@ -237,6 +253,10 @@ class Coder:
self.io.tool_output("JSON Schema:")
self.io.tool_output(json.dumps(self.functions, indent=4))
def show_announcements(self):
for line in self.get_announcements():
self.io.tool_output(line)
def find_common_root(self):
if len(self.abs_fnames) == 1:
self.root = os.path.dirname(list(self.abs_fnames)[0])
@ -251,6 +271,12 @@ class Coder:
self.abs_fnames.add(self.abs_root_path(rel_fname))
self.check_added_files()
def drop_rel_fname(self, fname):
abs_fname = self.abs_root_path(fname)
if abs_fname in self.abs_fnames:
self.abs_fnames.remove(abs_fname)
return True
def abs_root_path(self, path):
res = Path(self.root) / path
return utils.safe_abs_path(res)
@ -387,6 +413,11 @@ class Coder:
return {"role": "user", "content": image_messages}
def run_stream(self, user_message):
self.io.user_input(user_message)
self.reflected_message = None
yield from self.send_new_user_message(user_message)
def run(self, with_message=None):
while True:
try:
@ -397,7 +428,9 @@ class Coder:
new_user_message = self.run_loop()
while new_user_message:
new_user_message = self.send_new_user_message(new_user_message)
self.reflected_message = None
list(self.send_new_user_message(new_user_message))
new_user_message = self.reflected_message
if with_message:
return self.partial_response_content
@ -407,6 +440,23 @@ class Coder:
except EOFError:
return
def run_loop(self):
inp = self.io.get_input(
self.root,
self.get_inchat_relative_files(),
self.get_addable_relative_files(),
self.commands,
)
if not inp:
return
if self.commands.is_command(inp):
return self.commands.run(inp)
self.check_for_file_mentions(inp)
return inp
def keyboard_interrupt(self):
now = time.time()
@ -462,24 +512,6 @@ class Coder:
]
self.cur_messages = []
def run_loop(self):
inp = self.io.get_input(
self.root,
self.get_inchat_relative_files(),
self.get_addable_relative_files(),
self.commands,
)
if not inp:
return
if self.commands.is_command(inp):
return self.commands.run(inp)
self.check_for_file_mentions(inp)
return self.send_new_user_message(inp)
def fmt_system_prompt(self, prompt):
prompt = prompt.format(fence=self.fence)
return prompt
@ -522,6 +554,8 @@ class Coder:
return messages
def send_new_user_message(self, inp):
self.aider_edited_files = None
self.cur_messages += [
dict(role="user", content=inp),
]
@ -534,7 +568,9 @@ class Coder:
exhausted = False
interrupted = False
try:
interrupted = self.send(messages, functions=self.functions)
yield from self.send(messages, functions=self.functions)
except KeyboardInterrupt:
interrupted = True
except ExhaustedContextWindow:
exhausted = True
except openai.BadRequestError as err:
@ -563,22 +599,22 @@ class Coder:
else:
content = ""
self.io.tool_output()
if interrupted:
content += "\n^C KeyboardInterrupt"
self.io.tool_output()
if interrupted:
self.cur_messages += [dict(role="assistant", content=content)]
return
edited, edit_error = self.apply_updates()
if edit_error:
self.update_cur_messages(set())
return edit_error
self.reflected_message = edit_error
self.update_cur_messages(edited)
if edited:
self.aider_edited_files = edited
if self.repo and self.auto_commits and not self.dry_run:
saved_message = self.auto_commit(edited)
elif hasattr(self.gpt_prompts, "files_content_gpt_edits_no_repo"):
@ -590,7 +626,7 @@ class Coder:
add_rel_files_message = self.check_for_file_mentions(content)
if add_rel_files_message:
return add_rel_files_message
self.reflected_message = add_rel_files_message
def update_cur_messages(self, edited):
if self.partial_response_content:
@ -658,7 +694,7 @@ class Coder:
self.chat_completion_call_hashes.append(hash_object.hexdigest())
if self.stream:
self.show_send_output_stream(completion)
yield from self.show_send_output_stream(completion)
else:
self.show_send_output(completion)
except KeyboardInterrupt:
@ -673,7 +709,8 @@ class Coder:
if args:
self.io.ai_output(json.dumps(args, indent=4))
return interrupted
if interrupted:
raise KeyboardInterrupt
def show_send_output(self, completion):
if self.verbose:
@ -774,6 +811,7 @@ class Coder:
elif text:
sys.stdout.write(text)
sys.stdout.flush()
yield text
finally:
if mdstream:
self.live_incremental_response(mdstream, True)
@ -1026,6 +1064,7 @@ class Coder:
if res:
commit_hash, commit_message = res
self.last_aider_commit_hash = commit_hash
self.last_aider_commit_message = commit_message
return self.gpt_prompts.files_content_gpt_edits.format(
hash=commit_hash,

View file

@ -18,14 +18,14 @@ Once you understand the request you MUST:
You MUST use this *file listing* format:
path/to/filename.js
{fence[0]}
{fence[0]}javascript
// entire file content ...
// ... goes in between
{fence[1]}
Every *file listing* MUST use this format:
- First line: the filename with any originally provided path
- Second line: opening {fence[0]}
- Second line: opening {fence[0]} including the code language
- ... entire content of the file ...
- Final line: closing {fence[1]}

View file

@ -42,7 +42,9 @@ class Commands:
if content:
self.io.tool_output(content)
self.scraper.show_playwright_instructions()
instructions = self.scraper.get_playwright_instructions()
if instructions:
self.io.tool_error(instructions)
content = f"{url}:\n\n" + content
@ -269,9 +271,7 @@ class Commands:
self.coder.repo.repo.git.reset("--soft", "HEAD~1")
self.io.tool_output(
f"{last_commit.message.strip()}\n"
f"The above commit {self.coder.last_aider_commit_hash} "
"was reset and removed from git.\n"
f"Commit `{self.coder.last_aider_commit_hash}` was reset and removed from git.\n"
)
if self.coder.main_model.send_undo_reply:

533
aider/gui.py Executable file
View file

@ -0,0 +1,533 @@
#!/usr/bin/env python
import os
import random
import sys
import streamlit as st
from aider.coders import Coder
from aider.dump import dump # noqa: F401
from aider.io import InputOutput
from aider.main import main as cli_main
from aider.scrape import Scraper
class CaptureIO(InputOutput):
lines = []
def tool_output(self, msg):
self.lines.append(msg)
def tool_error(self, msg):
self.lines.append(msg)
def get_captured_lines(self):
lines = self.lines
self.lines = []
return lines
def search(text=None):
results = []
for root, _, files in os.walk("aider"):
for file in files:
path = os.path.join(root, file)
if not text or text in path:
results.append(path)
# dump(results)
return results
# Keep state as a resource, which survives browser reloads (since Coder does too)
class State:
keys = set()
def init(self, key, val=None):
if key in self.keys:
return
self.keys.add(key)
setattr(self, key, val)
return True
@st.cache_resource
def get_state():
return State()
@st.cache_resource
def get_coder():
coder = cli_main(return_coder=True)
if not isinstance(coder, Coder):
raise ValueError(coder)
if not coder.repo:
raise ValueError("GUI can currently only be used inside a git repo")
io = CaptureIO(
pretty=False,
yes=True,
dry_run=coder.io.dry_run,
encoding=coder.io.encoding,
)
# coder.io = io # this breaks the input_history
coder.commands.io = io
return coder
class GUI:
prompt = None
prompt_as = "user"
last_undo_empty = None
recent_msgs_empty = None
web_content_empty = None
def announce(self):
lines = self.coder.get_announcements()
lines = " \n".join(lines)
return lines
def show_edit_info(self, edit):
commit_hash = edit.get("commit_hash")
commit_message = edit.get("commit_message")
diff = edit.get("diff")
fnames = edit.get("fnames")
if fnames:
fnames = sorted(fnames)
if not commit_hash and not fnames:
return
show_undo = False
res = ""
if commit_hash:
prefix = "aider: "
if commit_message.startswith(prefix):
commit_message = commit_message[len(prefix) :]
res += f"Commit `{commit_hash}`: {commit_message} \n"
if commit_hash == self.coder.last_aider_commit_hash:
show_undo = True
if fnames:
fnames = [f"`{fname}`" for fname in fnames]
fnames = ", ".join(fnames)
res += f"Applied edits to {fnames}."
if diff:
with st.expander(res):
st.code(diff, language="diff")
if show_undo:
self.add_undo(commit_hash)
else:
with st.container(border=True):
st.write(res)
if show_undo:
self.add_undo(commit_hash)
def add_undo(self, commit_hash):
if self.last_undo_empty:
self.last_undo_empty.empty()
self.last_undo_empty = st.empty()
undone = self.state.last_undone_commit_hash == commit_hash
if not undone:
with self.last_undo_empty:
if self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}"):
self.do_undo(commit_hash)
def do_sidebar(self):
with st.sidebar:
st.title("Aider")
# self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"])
# self.do_recommended_actions()
self.do_add_to_chat()
self.do_recent_msgs()
self.do_clear_chat_history()
# st.container(height=150, border=False)
# st.write("### Experimental")
st.warning(
"This browser version of aider is experimental. Please share feedback in [GitHub"
" issues](https://github.com/paul-gauthier/aider/issues)."
)
def do_settings_tab(self):
pass
def do_recommended_actions(self):
with st.expander("Recommended actions", expanded=True):
with st.popover("Create a git repo to track changes"):
st.write(
"Aider works best when your code is stored in a git repo. \n[See the FAQ"
" for more info](https://aider.chat/docs/faq.html#how-does-aider-use-git)"
)
self.button("Create git repo", key=random.random(), help="?")
with st.popover("Update your `.gitignore` file"):
st.write("It's best to keep aider's internal files out of your git repo.")
self.button("Add `.aider*` to `.gitignore`", key=random.random(), help="?")
def do_add_to_chat(self):
# with st.expander("Add to the chat", expanded=True):
self.do_add_files()
self.do_add_web_page()
def do_add_files(self):
fnames = st.multiselect(
"Add files to the chat",
self.coder.get_all_relative_files(),
default=self.state.initial_inchat_files,
placeholder="Files to edit",
disabled=self.prompt_pending(),
help=(
"Only add the files that need to be *edited* for the task you are working"
" on. Aider will pull in other relevant code to provide context to the LLM."
),
)
for fname in fnames:
if fname not in self.coder.get_inchat_relative_files():
self.coder.add_rel_fname(fname)
self.info(f"Added {fname} to the chat")
for fname in self.coder.get_inchat_relative_files():
if fname not in fnames:
self.coder.drop_rel_fname(fname)
self.info(f"Removed {fname} from the chat")
def do_add_web_page(self):
with st.popover("Add a web page to the chat"):
self.do_web()
def do_add_image(self):
with st.popover("Add image"):
st.markdown("Hello World 👋")
st.file_uploader("Image file", disabled=self.prompt_pending())
def do_run_shell(self):
with st.popover("Run shell commands, tests, etc"):
st.markdown(
"Run a shell command and optionally share the output with the LLM. This is"
" a great way to run your program or run tests and have the LLM fix bugs."
)
st.text_input("Command:")
st.radio(
"Share the command output with the LLM?",
[
"Review the output and decide whether to share",
"Automatically share the output on non-zero exit code (ie, if any tests fail)",
],
)
st.selectbox(
"Recent commands",
[
"my_app.py --doit",
"my_app.py --cleanup",
],
disabled=self.prompt_pending(),
)
def do_tokens_and_cost(self):
with st.expander("Tokens and costs", expanded=True):
pass
def do_show_token_usage(self):
with st.popover("Show token usage"):
st.write("hi")
def do_clear_chat_history(self):
text = "Saves tokens, reduces confusion"
if self.button("Clear chat history", help=text):
self.coder.done_messages = []
self.coder.cur_messages = []
self.info("Cleared chat history. Now the LLM can't see anything before this line.")
def do_show_metrics(self):
st.metric("Cost of last message send & reply", "$0.0019", help="foo")
st.metric("Cost to send next message", "$0.0013", help="foo")
st.metric("Total cost this session", "$0.22")
def do_git(self):
with st.expander("Git", expanded=False):
# st.button("Show last diff")
# st.button("Undo last commit")
self.button("Commit any pending changes")
with st.popover("Run git command"):
st.markdown("## Run git command")
st.text_input("git", value="git ")
self.button("Run")
st.selectbox(
"Recent git commands",
[
"git checkout -b experiment",
"git stash",
],
disabled=self.prompt_pending(),
)
def do_recent_msgs(self):
if not self.recent_msgs_empty:
self.recent_msgs_empty = st.empty()
if self.prompt_pending():
self.recent_msgs_empty.empty()
self.state.recent_msgs_num += 1
with self.recent_msgs_empty:
self.old_prompt = st.selectbox(
"Resend a recent chat message",
self.state.input_history,
placeholder="Choose a recent chat message",
# label_visibility="collapsed",
index=None,
key=f"recent_msgs_{self.state.recent_msgs_num}",
disabled=self.prompt_pending(),
)
if self.old_prompt:
self.prompt = self.old_prompt
def do_messages_container(self):
self.messages = st.container()
# stuff a bunch of vertical whitespace at the top
# to get all the chat text to the bottom
self.messages.container(height=300, border=False)
with self.messages:
for msg in self.state.messages:
role = msg["role"]
if role == "edit":
self.show_edit_info(msg)
elif role == "info":
st.info(msg["content"])
elif role == "text":
text = msg["content"]
line = text.splitlines()[0]
with self.messages.expander(line):
st.text(text)
elif role in ("user", "assistant"):
with st.chat_message(role):
st.write(msg["content"])
# self.cost()
else:
st.dict(msg)
def initialize_state(self):
messages = [
dict(role="info", content=self.announce()),
dict(role="assistant", content="How can I help you?"),
]
self.state.init("messages", messages)
self.state.init("last_aider_commit_hash", self.coder.last_aider_commit_hash)
self.state.init("last_undone_commit_hash")
self.state.init("recent_msgs_num", 0)
self.state.init("web_content_num", 0)
self.state.init("prompt")
self.state.init("scraper")
self.state.init("initial_inchat_files", self.coder.get_inchat_relative_files())
if "input_history" not in self.state.keys:
input_history = list(self.coder.io.get_input_history())
seen = set()
input_history = [x for x in input_history if not (x in seen or seen.add(x))]
self.state.input_history = input_history
self.state.keys.add("input_history")
def button(self, args, **kwargs):
"Create a button, disabled if prompt pending"
# Force everything to be disabled if there is a prompt pending
if self.prompt_pending():
kwargs["disabled"] = True
return st.button(args, **kwargs)
def __init__(self):
self.coder = get_coder()
self.state = get_state()
# Force the coder to cooperate, regardless of cmd line args
self.coder.yield_stream = True
self.coder.stream = True
self.coder.pretty = False
self.initialize_state()
self.do_messages_container()
self.do_sidebar()
user_inp = st.chat_input("Say something")
if user_inp:
self.prompt = user_inp
if self.prompt_pending():
self.process_chat()
if not self.prompt:
return
self.state.prompt = self.prompt
if self.prompt_as == "user":
self.coder.io.add_to_input_history(self.prompt)
self.state.input_history.append(self.prompt)
if self.prompt_as:
self.state.messages.append({"role": self.prompt_as, "content": self.prompt})
if self.prompt_as == "user":
with self.messages.chat_message("user"):
st.write(self.prompt)
elif self.prompt_as == "text":
line = self.prompt.splitlines()[0]
line += "??"
with self.messages.expander(line):
st.text(self.prompt)
# re-render the UI for the prompt_pending state
st.rerun()
def prompt_pending(self):
return self.state.prompt is not None
def cost(self):
cost = random.random() * 0.003 + 0.001
st.caption(f"${cost:0.4f}")
def process_chat(self):
prompt = self.state.prompt
self.state.prompt = None
while prompt:
with self.messages.chat_message("assistant"):
res = st.write_stream(self.coder.run_stream(prompt))
self.state.messages.append({"role": "assistant", "content": res})
# self.cost()
if self.coder.reflected_message:
self.info(self.coder.reflected_message)
prompt = self.coder.reflected_message
with self.messages:
edit = dict(
role="edit",
fnames=self.coder.aider_edited_files,
)
if self.state.last_aider_commit_hash != self.coder.last_aider_commit_hash:
edit["commit_hash"] = self.coder.last_aider_commit_hash
edit["commit_message"] = self.coder.last_aider_commit_message
commits = f"{self.coder.last_aider_commit_hash}~1"
diff = self.coder.repo.diff_commits(
self.coder.pretty,
commits,
self.coder.last_aider_commit_hash,
)
edit["diff"] = diff
self.state.last_aider_commit_hash = self.coder.last_aider_commit_hash
self.state.messages.append(edit)
self.show_edit_info(edit)
# re-render the UI for the non-prompt_pending state
st.rerun()
def info(self, message, echo=True):
info = dict(role="info", content=message)
self.state.messages.append(info)
# We will render the tail of the messages array after this call
if echo:
self.messages.info(message)
def do_web(self):
st.markdown("Add the text content of a web page to the chat")
if not self.web_content_empty:
self.web_content_empty = st.empty()
if self.prompt_pending():
self.web_content_empty.empty()
self.state.web_content_num += 1
with self.web_content_empty:
self.web_content = st.text_input(
"URL",
placeholder="https://...",
key=f"web_content_{self.state.web_content_num}",
)
if not self.web_content:
return
url = self.web_content
if not self.state.scraper:
self.scraper = Scraper(print_error=self.info)
instructions = self.scraper.get_playwright_instructions()
if instructions:
self.info(instructions)
content = self.scraper.scrape(url) or ""
if content.strip():
content = f"{url}\n\n" + content
self.prompt = content
self.prompt_as = "text"
else:
self.info(f"No web content found for `{url}`.")
self.web_content = None
def do_undo(self, commit_hash):
self.last_undo_empty.empty()
if (
self.state.last_aider_commit_hash != commit_hash
or self.coder.last_aider_commit_hash != commit_hash
):
self.info(f"Commit `{commit_hash}` is not the latest commit.")
return
self.coder.commands.io.get_captured_lines()
reply = self.coder.commands.cmd_undo(None)
lines = self.coder.commands.io.get_captured_lines()
lines = "\n".join(lines)
lines = lines.splitlines()
lines = " \n".join(lines)
self.info(lines, echo=False)
self.state.last_undone_commit_hash = commit_hash
if reply:
self.prompt_as = None
self.prompt = reply
def gui_main():
st.set_page_config(
layout="wide",
page_title="Aider",
page_icon="https://aider.chat/assets/favicon-32x32.png",
menu_items={
"Get Help": "https://aider.chat/docs/faq.html",
"Report a bug": "https://github.com/paul-gauthier/aider/issues",
"About": "# Aider\nAI pair programming in your browser.",
},
)
# config_options = st.config._config_options
# for key, value in config_options.items():
# print(f"{key}: {value.value}")
GUI()
if __name__ == "__main__":
status = gui_main()
sys.exit(status)

View file

@ -1,14 +1,14 @@
import argparse
import configparser
import os
import sys
from pathlib import Path
import configargparse
import git
import litellm
from streamlit.web import cli
from aider import __version__, models
from aider.args import get_parser
from aider.coders import Coder
from aider.io import InputOutput
from aider.repo import GitRepo
@ -122,7 +122,64 @@ def check_gitignore(git_root, io, ask=True):
io.tool_output(f"Added {pat} to .gitignore")
def main(argv=None, input=None, output=None, force_git_root=None):
def format_settings(parser, args):
show = scrub_sensitive_info(args, parser.format_values())
show += "\n"
show += "Option settings:\n"
for arg, val in sorted(vars(args).items()):
if val:
val = scrub_sensitive_info(args, str(val))
show += f" - {arg}: {val}\n"
return show
def scrub_sensitive_info(args, text):
# Replace sensitive information with placeholder
if text and args.openai_api_key:
text = text.replace(args.openai_api_key, "***")
if text and args.anthropic_api_key:
text = text.replace(args.anthropic_api_key, "***")
return text
def launch_gui(args):
from aider import gui
print()
print("CONTROL-C to exit...")
target = gui.__file__
st_args = ["run", target]
st_args += [
"--browser.gatherUsageStats=false",
"--runner.magicEnabled=false",
"--server.runOnSave=false",
]
if "-dev" in __version__:
print("Watching for file changes.")
else:
st_args += [
"--global.developmentMode=false",
"--server.fileWatcherType=none",
"--client.toolbarMode=viewer", # minimal?
]
st_args += ["--"] + args
cli.main(st_args)
# from click.testing import CliRunner
# runner = CliRunner()
# from streamlit.web import bootstrap
# bootstrap.load_config_options(flag_options={})
# cli.main_run(target, args)
# sys.argv = ['streamlit', 'run', '--'] + args
def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False):
if argv is None:
argv = sys.argv[1:]
@ -141,364 +198,13 @@ def main(argv=None, input=None, output=None, force_git_root=None):
default_config_files.append(Path.home() / conf_fname) # homedir
default_config_files = list(map(str, default_config_files))
parser = configargparse.ArgumentParser(
description="aider is GPT powered coding in your terminal",
add_config_file_help=True,
default_config_files=default_config_files,
config_file_parser_class=configargparse.YAMLConfigFileParser,
auto_env_var_prefix="AIDER_",
)
##########
core_group = parser.add_argument_group("Main")
core_group.add_argument(
"files",
metavar="FILE",
nargs="*",
help="the directory of a git repo, or a list of files to edit with GPT (optional)",
)
core_group.add_argument(
"--openai-api-key",
metavar="OPENAI_API_KEY",
env_var="OPENAI_API_KEY",
help="Specify the OpenAI API key",
)
core_group.add_argument(
"--anthropic-api-key",
metavar="ANTHROPIC_API_KEY",
env_var="ANTHROPIC_API_KEY",
help="Specify the OpenAI API key",
)
default_model = models.DEFAULT_MODEL_NAME
core_group.add_argument(
"--model",
metavar="MODEL",
default=default_model,
help=f"Specify the model to use for the main chat (default: {default_model})",
)
core_group.add_argument(
"--models",
metavar="MODEL",
help="List known models which match the (partial) MODEL name",
)
opus_model = "claude-3-opus-20240229"
core_group.add_argument(
"--opus",
action="store_const",
dest="model",
const=opus_model,
help=f"Use {opus_model} model for the main chat",
)
sonnet_model = "claude-3-sonnet-20240229"
core_group.add_argument(
"--sonnet",
action="store_const",
dest="model",
const=sonnet_model,
help=f"Use {sonnet_model} model for the main chat",
)
gpt_4_model = "gpt-4-0613"
core_group.add_argument(
"--4",
"-4",
action="store_const",
dest="model",
const=gpt_4_model,
help=f"Use {gpt_4_model} model for the main chat",
)
gpt_4_turbo_model = "gpt-4-turbo"
core_group.add_argument(
"--4-turbo-vision",
action="store_const",
dest="model",
const=gpt_4_turbo_model,
help=f"Use {gpt_4_turbo_model} model for the main chat",
)
gpt_3_model_name = "gpt-3.5-turbo"
core_group.add_argument(
"--35turbo",
"--35-turbo",
"--3",
"-3",
action="store_const",
dest="model",
const=gpt_3_model_name,
help=f"Use {gpt_3_model_name} model for the main chat",
)
core_group.add_argument(
"--voice-language",
metavar="VOICE_LANGUAGE",
default="en",
help="Specify the language for voice using ISO 639-1 code (default: auto)",
)
##########
model_group = parser.add_argument_group("Advanced Model Settings")
model_group.add_argument(
"--openai-api-base",
metavar="OPENAI_API_BASE",
env_var="OPENAI_API_BASE",
help="Specify the api base url",
)
model_group.add_argument(
"--openai-api-type",
metavar="OPENAI_API_TYPE",
env_var="OPENAI_API_TYPE",
help="Specify the api_type",
)
model_group.add_argument(
"--openai-api-version",
metavar="OPENAI_API_VERSION",
env_var="OPENAI_API_VERSION",
help="Specify the api_version",
)
model_group.add_argument(
"--openai-api-deployment-id",
metavar="OPENAI_API_DEPLOYMENT_ID",
env_var="OPENAI_API_DEPLOYMENT_ID",
help="Specify the deployment_id",
)
model_group.add_argument(
"--openai-organization-id",
metavar="OPENAI_ORGANIZATION_ID",
env_var="OPENAI_ORGANIZATION_ID",
help="Specify the OpenAI organization ID",
)
model_group.add_argument(
"--edit-format",
metavar="EDIT_FORMAT",
default=None,
help="Specify what edit format GPT should use (default depends on model)",
)
core_group.add_argument(
"--weak-model",
metavar="WEAK_MODEL",
default=None,
help=(
"Specify the model to use for commit messages and chat history summarization (default"
" depends on --model)"
),
)
model_group.add_argument(
"--show-model-warnings",
action=argparse.BooleanOptionalAction,
default=True,
help="Only work with models that have meta-data available (default: True)",
)
model_group.add_argument(
"--map-tokens",
type=int,
default=1024,
help="Max number of tokens to use for repo map, use 0 to disable (default: 1024)",
)
##########
history_group = parser.add_argument_group("History Files")
default_input_history_file = (
os.path.join(git_root, ".aider.input.history") if git_root else ".aider.input.history"
)
default_chat_history_file = (
os.path.join(git_root, ".aider.chat.history.md") if git_root else ".aider.chat.history.md"
)
history_group.add_argument(
"--input-history-file",
metavar="INPUT_HISTORY_FILE",
default=default_input_history_file,
help=f"Specify the chat input history file (default: {default_input_history_file})",
)
history_group.add_argument(
"--chat-history-file",
metavar="CHAT_HISTORY_FILE",
default=default_chat_history_file,
help=f"Specify the chat history file (default: {default_chat_history_file})",
)
##########
output_group = parser.add_argument_group("Output Settings")
output_group.add_argument(
"--dark-mode",
action="store_true",
help="Use colors suitable for a dark terminal background (default: False)",
default=False,
)
output_group.add_argument(
"--light-mode",
action="store_true",
help="Use colors suitable for a light terminal background (default: False)",
default=False,
)
output_group.add_argument(
"--pretty",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable pretty, colorized output (default: True)",
)
output_group.add_argument(
"--stream",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable streaming responses (default: True)",
)
output_group.add_argument(
"--user-input-color",
default="#00cc00",
help="Set the color for user input (default: #00cc00)",
)
output_group.add_argument(
"--tool-output-color",
default=None,
help="Set the color for tool output (default: None)",
)
output_group.add_argument(
"--tool-error-color",
default="#FF2222",
help="Set the color for tool error messages (default: red)",
)
output_group.add_argument(
"--assistant-output-color",
default="#0088ff",
help="Set the color for assistant output (default: #0088ff)",
)
output_group.add_argument(
"--code-theme",
default="default",
help=(
"Set the markdown code theme (default: default, other options include monokai,"
" solarized-dark, solarized-light)"
),
)
output_group.add_argument(
"--show-diffs",
action="store_true",
help="Show diffs when committing changes (default: False)",
default=False,
)
##########
git_group = parser.add_argument_group("Git Settings")
git_group.add_argument(
"--git",
action=argparse.BooleanOptionalAction,
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(
"--auto-commits",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable auto commit of GPT changes (default: True)",
)
git_group.add_argument(
"--dirty-commits",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable/disable commits when repo is found dirty (default: True)",
)
git_group.add_argument(
"--dry-run",
action=argparse.BooleanOptionalAction,
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,
)
##########
other_group = parser.add_argument_group("Other Settings")
other_group.add_argument(
"--version",
action="version",
version=f"%(prog)s {__version__}",
help="Show the version number and exit",
)
other_group.add_argument(
"--check-update",
action="store_true",
help="Check for updates and return status in the exit code",
default=False,
)
other_group.add_argument(
"--skip-check-update",
action="store_true",
help="Skips checking for the update when the program runs",
)
other_group.add_argument(
"--apply",
metavar="FILE",
help="Apply the changes from the given file instead of running the chat (debug)",
)
other_group.add_argument(
"--yes",
action="store_true",
help="Always say yes to every confirmation",
default=None,
)
other_group.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
default=False,
)
other_group.add_argument(
"--show-repo-map",
action="store_true",
help="Print the repo map and exit (debug)",
default=False,
)
other_group.add_argument(
"--message",
"--msg",
"-m",
metavar="COMMAND",
help="Specify a single message to send GPT, process reply then exit (disables chat mode)",
)
other_group.add_argument(
"--message-file",
"-f",
metavar="MESSAGE_FILE",
help=(
"Specify a file containing the message to send GPT, process reply, then exit (disables"
" chat mode)"
),
)
other_group.add_argument(
"--encoding",
default="utf-8",
help="Specify the encoding for input and output (default: utf-8)",
)
other_group.add_argument(
"-c",
"--config",
is_config_file=True,
metavar="CONFIG_FILE",
help=(
"Specify the config file (default: search for .aider.conf.yml in git root, cwd"
" or home directory)"
),
)
parser = get_parser(default_config_files, git_root)
args = parser.parse_args(argv)
if args.gui and not return_coder:
launch_gui(argv)
return
if args.dark_mode:
args.user_input_color = "#32FF32"
args.tool_error_color = "#FF3333"
@ -513,7 +219,7 @@ def main(argv=None, input=None, output=None, force_git_root=None):
io = InputOutput(
args.pretty,
args.yes,
args.yes or return_coder, # Force --yes if return_coder
args.input_history_file,
args.chat_history_file,
input=input,
@ -554,20 +260,14 @@ def main(argv=None, input=None, output=None, force_git_root=None):
if args.git and not force_git_root:
right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname)
if right_repo_root:
return main(argv, input, output, right_repo_root)
io.tool_output(f"Aider v{__version__}")
return main(argv, input, output, right_repo_root, return_coder=return_coder)
if not args.skip_check_update:
check_version(io.tool_error)
if args.check_update:
update_available = check_version(lambda msg: None)
sys.exit(0 if not update_available else 1)
if "VSCODE_GIT_IPC_HANDLE" in os.environ:
args.pretty = False
io.tool_output("VSCode terminal detected, pretty output has been disabled.")
return 0 if not update_available else 1
if args.models:
matches = models.fuzzy_match_models(args.models)
@ -588,24 +288,13 @@ def main(argv=None, input=None, output=None, force_git_root=None):
if args.gitignore:
check_gitignore(git_root, io)
def scrub_sensitive_info(text):
# Replace sensitive information with placeholder
if text and args.openai_api_key:
text = text.replace(args.openai_api_key, "***")
if text and args.anthropic_api_key:
text = text.replace(args.anthropic_api_key, "***")
return text
if args.verbose:
show = scrub_sensitive_info(parser.format_values())
show = format_settings(parser, args)
io.tool_output(show)
io.tool_output("Option settings:")
for arg, val in sorted(vars(args).items()):
if val:
val = scrub_sensitive_info(str(val))
io.tool_output(f" - {arg}: {val}")
io.tool_output(*map(scrub_sensitive_info, sys.argv), log_only=True)
cmd_line = " ".join(sys.argv)
cmd_line = scrub_sensitive_info(args, cmd_line)
io.tool_output(cmd_line, log_only=True)
if args.anthropic_api_key:
os.environ["ANTHROPIC_API_KEY"] = args.anthropic_api_key
@ -652,6 +341,11 @@ def main(argv=None, input=None, output=None, force_git_root=None):
io.tool_error(str(err))
return 1
if return_coder:
return coder
coder.show_announcements()
if args.commit:
coder.commands.cmd_commit("")
return
@ -670,6 +364,10 @@ def main(argv=None, input=None, output=None, force_git_root=None):
coder.apply_updates()
return
if "VSCODE_GIT_IPC_HANDLE" in os.environ:
args.pretty = False
io.tool_output("VSCode terminal detected, pretty output has been disabled.")
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():
@ -685,7 +383,9 @@ def main(argv=None, input=None, output=None, force_git_root=None):
io.add_to_input_history(args.message)
io.tool_output()
coder.run(with_message=args.message)
elif args.message_file:
return
if args.message_file:
try:
message_from_file = io.read_text(args.message_file)
io.tool_output()
@ -696,8 +396,9 @@ def main(argv=None, input=None, output=None, force_git_root=None):
except IOError as e:
io.tool_error(f"Error reading message file: {e}")
return 1
else:
coder.run()
return
coder.run()
if __name__ == "__main__":

View file

@ -66,14 +66,14 @@ class Scraper:
except Exception:
self.playwright_available = False
def show_playwright_instructions(self):
def get_playwright_instructions(self):
if self.playwright_available in (True, None):
return
if self.playwright_instructions_shown:
return
self.playwright_instructions_shown = True
self.print_error(PLAYWRIGHT_INFO)
return PLAYWRIGHT_INFO
def scrape_with_httpx(self, url):
headers = {"User-Agent": f"Mozilla./5.0 ({aider_user_agent})"}