Merge branch 'main' into call-graph

This commit is contained in:
Paul Gauthier 2023-05-26 17:07:17 -07:00
commit 1e1feeaa21
9 changed files with 222 additions and 92 deletions

View file

@ -168,7 +168,10 @@ class Coder:
if self.abs_fnames:
files_content = prompts.files_content_prefix
files_content += self.get_files_content()
all_content += files_content
else:
files_content = prompts.files_no_full_files
all_content += files_content
other_files = set(self.get_all_abs_files()) - set(self.abs_fnames)
repo_content = self.repo_map.get_repo_map(self.abs_fnames, other_files)
@ -204,7 +207,7 @@ class Coder:
self.num_control_c += 1
if self.num_control_c >= 2:
break
self.io.tool_error("^C again to quit")
self.io.tool_error("^C again or /exit to quit")
except EOFError:
return

View file

@ -1,8 +1,8 @@
import sys
import os
import git
import subprocess
import shlex
from rich.prompt import Confirm
from prompt_toolkit.completion import Completion
from aider import prompts
@ -16,20 +16,8 @@ class Commands:
if inp[0] == "/":
return True
def help(self):
"Show help about all commands"
commands = self.get_commands()
for cmd in commands:
cmd_method_name = f"cmd_{cmd[1:]}"
cmd_method = getattr(self, cmd_method_name, None)
if cmd_method:
description = cmd_method.__doc__
self.io.tool(f"{cmd} {description}")
else:
self.io.tool(f"{cmd} No description available.")
def get_commands(self):
commands = ["/help"]
commands = []
for attr in dir(self):
if attr.startswith("cmd_"):
commands.append("/" + attr[4:])
@ -62,15 +50,15 @@ class Commands:
all_commands = self.get_commands()
matching_commands = [cmd for cmd in all_commands if cmd.startswith(first_word)]
if len(matching_commands) == 1:
if matching_commands[0] == "/help":
self.help()
else:
return self.do_run(matching_commands[0][1:], rest_inp)
return self.do_run(matching_commands[0][1:], rest_inp)
elif len(matching_commands) > 1:
self.io.tool_error("Ambiguous command: ', '.join(matching_commands)}")
self.io.tool_error(f"Ambiguous command: {', '.join(matching_commands)}")
else:
self.io.tool_error(f"Error: {first_word} is not a valid command.")
# any method called cmd_xxx becomes a command automatically.
# each one must take an args param.
def cmd_commit(self, args):
"Commit edits to the repo made outside the chat (commit message optional)"
@ -251,6 +239,10 @@ class Commands:
)
return msg
def cmd_exit(self, args):
"Exit the application"
sys.exit()
def cmd_ls(self, args):
"List all known files and those included in the chat session"
@ -274,3 +266,15 @@ class Commands:
self.io.tool("\nRepo files not in the chat:\n")
for file in other_files:
self.io.tool(f" {file}")
def cmd_help(self, args):
"Show help about all commands"
commands = sorted(self.get_commands())
for cmd in commands:
cmd_method_name = f"cmd_{cmd[1:]}"
cmd_method = getattr(self, cmd_method_name, None)
if cmd_method:
description = cmd_method.__doc__
self.io.tool(f"{cmd} {description}")
else:
self.io.tool(f"{cmd} No description available.")

View file

@ -1,47 +1,85 @@
import os
import sys
import argparse
import git
import configargparse
from dotenv import load_dotenv
from aider.coder import Coder
from aider.io import InputOutput
def get_git_root():
try:
repo = git.Repo(search_parent_directories=True)
return repo.working_tree_dir
except git.InvalidGitRepositoryError:
return None
def main(args=None, input=None, output=None):
if args is None:
args = sys.argv[1:]
load_dotenv()
env_prefix = "AIDER_"
parser = argparse.ArgumentParser(description="aider - chat with GPT about your code")
default_config_files = [
os.path.expanduser("~/.aider.conf.yml"),
]
git_root = get_git_root()
if git_root:
default_config_files.insert(0, os.path.join(git_root, ".aider.conf.yml"))
parser = configargparse.ArgumentParser(
description="aider - chat with GPT about your code",
add_config_file_help=True,
default_config_files=default_config_files,
config_file_parser_class=configargparse.YAMLConfigFileParser,
)
parser.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 or home"
" directory)"
),
)
parser.add_argument(
"files",
metavar="FILE",
nargs="*",
help="a list of source code files (optional)",
)
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"
)
parser.add_argument(
"--input-history-file",
metavar="INPUT_HISTORY_FILE",
default=os.environ.get(f"{env_prefix}INPUT_HISTORY_FILE", ".aider.input.history"),
help=(
"Specify the chat input history file (default: .aider.input.history,"
f" ${env_prefix}INPUT_HISTORY_FILE)"
),
env_var=f"{env_prefix}INPUT_HISTORY_FILE",
default=default_input_history_file,
help=f"Specify the chat input history file (default: {default_input_history_file})",
)
parser.add_argument(
"--chat-history-file",
metavar="CHAT_HISTORY_FILE",
default=os.environ.get(f"{env_prefix}CHAT_HISTORY_FILE", ".aider.chat.history.md"),
help=(
"Specify the chat history file (default: .aider.chat.history.md,"
f" ${env_prefix}CHAT_HISTORY_FILE)"
),
env_var=f"{env_prefix}CHAT_HISTORY_FILE",
default=default_chat_history_file,
help=f"Specify the chat history file (default: {default_chat_history_file})",
)
parser.add_argument(
"--model",
metavar="MODEL",
default=os.environ.get(f"{env_prefix}MODEL", "gpt-4"),
help=f"Specify the model to use for the main chat (default: gpt-4, ${env_prefix}MODEL)",
env_var=f"{env_prefix}MODEL",
default="gpt-4",
help="Specify the model to use for the main chat (default: gpt-4)",
)
parser.add_argument(
"-3",
@ -50,24 +88,38 @@ def main(args=None, input=None, output=None):
const="gpt-3.5-turbo",
help="Use gpt-3.5-turbo model for the main chat (not advised)",
)
parser.add_argument(
"--pretty",
action="store_true",
env_var=f"{env_prefix}PRETTY",
default=True,
help="Enable pretty, colorized output (default: True)",
)
parser.add_argument(
"--no-pretty",
action="store_false",
dest="pretty",
help=f"Disable pretty, colorized output (${env_prefix}PRETTY)",
default=bool(int(os.environ.get(f"{env_prefix}PRETTY", 1))),
help="Disable pretty, colorized output",
)
parser.add_argument(
"--apply",
metavar="FILE",
help="Apply the changes from the given file instead of running the chat (debug)",
)
parser.add_argument(
"--auto-commits",
action="store_true",
env_var=f"{env_prefix}AUTO_COMMIT",
default=True,
help="Enable auto commit of changes (default: True)",
)
parser.add_argument(
"--no-auto-commits",
action="store_false",
dest="auto_commits",
help=f"Disable auto commit of changes (${env_prefix}AUTO_COMMITS)",
default=bool(int(os.environ.get(f"{env_prefix}AUTO_COMMITS", 1))),
dest="auto_commit",
help="Disable auto commit of changes",
)
parser.add_argument(
"--dry-run",
@ -78,17 +130,21 @@ def main(args=None, input=None, output=None):
parser.add_argument(
"--show-diffs",
action="store_true",
help=f"Show diffs when committing changes (default: False, ${env_prefix}SHOW_DIFFS)",
default=bool(int(os.environ.get(f"{env_prefix}SHOW_DIFFS", 0))),
env_var=f"{env_prefix}SHOW_DIFFS",
help="Show diffs when committing changes (default: False)",
default=False,
)
parser.add_argument(
"--ctags",
action="store_true",
type=lambda x: (str(x).lower() == "true"),
nargs="?",
const=True,
default=None,
env_var=f"{env_prefix}CTAGS",
help=(
"Add ctags to the chat to help GPT understand the codebase (default: False,"
f" ${env_prefix}CTAGS)"
"Add ctags to the chat to help GPT understand the codebase (default: check for ctags"
" executable)"
),
default=bool(int(os.environ.get(f"{env_prefix}CTAGS", 0))),
)
parser.add_argument(
"--yes",
@ -97,7 +153,8 @@ def main(args=None, input=None, output=None):
default=False,
)
parser.add_argument(
"-v", "--verbose",
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
default=False,

View file

@ -8,14 +8,12 @@ Take requests for changes to the supplied code.
If the request is ambiguous, ask questions.
Once you understand the request you MUST:
1. List the files you need to modify.
1. List the files you need to modify. If they are *read-only* ask the user to make them *read-write* using the file's full path name.
2. Think step-by-step and explain the needed changes.
3. Describe each change with an *edit block* per the example below.
"""
system_reminder = """Base any edits off the files shown in the user's last msg.
You MUST format EVERY code change with an *edit block* like this:
system_reminder = """You MUST format EVERY code change with an *edit block* like this:
```python
some/dir/example.py
@ -29,11 +27,11 @@ some/dir/example.py
def add(a,b):
>>>>>>> UPDATED
Every *edit block* must be fenced w/triple backticks with the correct code language.
Every *edit block* must start with the full path! *NEVER* propose edit blocks for *read-only* files.
The ORIGINAL section must be an *exact* set of lines from the file:
- NEVER SKIP LINES!
- Include all original leading spaces and indentation!
Every *edit block* must be fenced w/triple backticks with the correct code language.
Every *edit block* must start with the full path!
Edits to different parts of a file each need their own *edit block*.
@ -54,11 +52,13 @@ files_content_gpt_no_edits = "I didn't see any properly formatted edits in your
files_content_local_edits = "I edited the files myself."
files_content_prefix = "Propose changes to *only* these files (ask before editing others):\n"
files_content_prefix = "These are the *read-write* files:\n"
files_no_full_files = "I am not sharing any *read-write* files yet."
repo_content_prefix = (
"Here is a map of all the {other}files{ctags_msg}. You *must* ask with the"
" full path before editing these:\n\n"
"All the files below here are *read-only* files. Notice that files in directories are indented."
" Use their parent dirs to build their full path.\n"
)

View file

@ -3,6 +3,7 @@ import json
import sys
import subprocess
import tiktoken
import tempfile
from collections import defaultdict
from aider import prompts, utils
@ -48,14 +49,20 @@ def fname_to_components(fname, with_colon):
class RepoMap:
def __init__(self, use_ctags=True, root=None, main_model="gpt-4"):
ctags_cmd = ["ctags", "--fields=+S", "--extras=-F", "--output-format=json"]
def __init__(self, use_ctags=None, root=None, main_model="gpt-4"):
if not root:
root = os.getcwd()
self.use_ctags = use_ctags
self.tokenizer = tiktoken.encoding_for_model(main_model)
self.root = root
if use_ctags is None:
self.use_ctags = self.check_for_ctags()
else:
self.use_ctags = use_ctags
self.tokenizer = tiktoken.encoding_for_model(main_model)
def get_repo_map(self, chat_files, other_files):
res = self.choose_files_listing(other_files)
if not res:
@ -123,7 +130,7 @@ class RepoMap:
def split_path(self, path):
path = os.path.relpath(path, self.root)
return fname_to_components(path, True)
return [path + ":"]
def run_ctags(self, filename):
# Check if the file is in the cache and if the modification time has not changed
@ -132,7 +139,7 @@ class RepoMap:
if cache_key in TAGS_CACHE and TAGS_CACHE[cache_key]["mtime"] == file_mtime:
return TAGS_CACHE[cache_key]["data"]
cmd = ["ctags", "--fields=+S", "--extras=-F", "--output-format=json", filename]
cmd = self.ctags_cmd + [filename]
output = subprocess.check_output(cmd).decode("utf-8")
output = output.splitlines()
@ -169,6 +176,17 @@ class RepoMap:
return tags
def check_for_ctags(self):
try:
with tempfile.TemporaryDirectory() as tempdir:
hello_py = os.path.join(tempdir, "hello.py")
with open(hello_py, "w") as f:
f.write("def hello():\n print('Hello, world!')\n")
self.get_tags(hello_py)
except Exception:
return False
return True
def find_py_files(directory):
if not os.path.isdir(directory):
@ -197,6 +215,7 @@ def call_map():
"""
rm = RepoMap()
# res = rm.get_tags_map(fnames)
# print(res)
@ -222,7 +241,7 @@ def call_map():
# dump("ref", fname, ident)
references[ident].append(show_fname)
for ident,fname in defines.items():
for ident, fname in defines.items():
dump(fname, ident)
idents = set(defines.keys()).intersection(set(references.keys()))
@ -256,7 +275,9 @@ def call_map():
ranked = nx.pagerank(G, weight="weight")
# drop low weight edges for plotting
edges_to_remove = [(node1, node2) for node1, node2, data in G.edges(data=True) if data['weight'] < 1]
edges_to_remove = [
(node1, node2) for node1, node2, data in G.edges(data=True) if data["weight"] < 1
]
G.remove_edges_from(edges_to_remove)
# Remove isolated nodes (nodes with no edges)
dump(G.nodes())
@ -272,8 +293,8 @@ def call_map():
dot.node(fname, penwidth=str(pen))
max_w = max(edges.values())
for refs,defs,data in G.edges(data=True):
weight = data['weight']
for refs, defs, data in G.edges(data=True):
weight = data["weight"]
r = random.randint(0, 255)
g = random.randint(0, 255)
@ -286,7 +307,7 @@ def call_map():
print()
print(name)
for ident in sorted(labels[name]):
print('\t', ident)
print("\t", ident)
# print(f"{refs} -{weight}-> {defs}")
top_rank = sorted([(rank, node) for (node, rank) in ranked.items()], reverse=True)
@ -296,5 +317,6 @@ def call_map():
dot.render("tmp", format="pdf", view=True)
if __name__ == "__main__":
call_map()