mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-02 18:54:59 +00:00
merging from upstream main
This commit is contained in:
commit
179b648864
29 changed files with 3810 additions and 114 deletions
|
@ -1 +1 @@
|
|||
__version__ = "0.18.1-dev"
|
||||
__version__ = "0.18.2-dev"
|
||||
|
|
|
@ -2,6 +2,7 @@ from .base_coder import Coder
|
|||
from .editblock_coder import EditBlockCoder
|
||||
from .editblock_func_coder import EditBlockFunctionCoder
|
||||
from .single_wholefile_func_coder import SingleWholeFileFunctionCoder
|
||||
from .udiff_coder import UnifiedDiffCoder
|
||||
from .wholefile_coder import WholeFileCoder
|
||||
from .wholefile_func_coder import WholeFileFunctionCoder
|
||||
|
||||
|
@ -12,4 +13,5 @@ __all__ = [
|
|||
WholeFileFunctionCoder,
|
||||
EditBlockFunctionCoder,
|
||||
SingleWholeFileFunctionCoder,
|
||||
UnifiedDiffCoder,
|
||||
]
|
||||
|
|
|
@ -50,7 +50,10 @@ class Coder:
|
|||
functions = None
|
||||
total_cost = 0.0
|
||||
num_exhausted_context_windows = 0
|
||||
num_malformed_responses = 0
|
||||
last_keyboard_interrupt = None
|
||||
max_apply_update_errors = 3
|
||||
edit_format = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
|
@ -62,7 +65,7 @@ class Coder:
|
|||
skip_model_availabily_check=False,
|
||||
**kwargs,
|
||||
):
|
||||
from . import EditBlockCoder, WholeFileCoder
|
||||
from . import EditBlockCoder, UnifiedDiffCoder, WholeFileCoder
|
||||
|
||||
if not main_model:
|
||||
main_model = models.GPT4
|
||||
|
@ -84,6 +87,8 @@ class Coder:
|
|||
return EditBlockCoder(client, main_model, io, **kwargs)
|
||||
elif edit_format == "whole":
|
||||
return WholeFileCoder(client, main_model, io, **kwargs)
|
||||
elif edit_format == "udiff":
|
||||
return UnifiedDiffCoder(client, main_model, io, **kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown edit format {edit_format}")
|
||||
|
||||
|
@ -146,7 +151,7 @@ class Coder:
|
|||
|
||||
self.main_model = main_model
|
||||
|
||||
self.io.tool_output(f"Model: {main_model.name}")
|
||||
self.io.tool_output(f"Model: {main_model.name} using {self.edit_format} edit format")
|
||||
|
||||
self.show_diffs = show_diffs
|
||||
|
||||
|
@ -175,7 +180,8 @@ class Coder:
|
|||
|
||||
if self.repo:
|
||||
rel_repo_dir = self.repo.get_rel_repo_dir()
|
||||
self.io.tool_output(f"Git repo: {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")
|
||||
else:
|
||||
self.io.tool_output("Git repo: none")
|
||||
self.find_common_root()
|
||||
|
@ -298,7 +304,13 @@ class Coder:
|
|||
prompt += "\n"
|
||||
prompt += relative_fname
|
||||
prompt += f"\n{self.fence[0]}\n"
|
||||
|
||||
prompt += content
|
||||
|
||||
# lines = content.splitlines(keepends=True)
|
||||
# lines = [f"{i+1:03}:{line}" for i, line in enumerate(lines)]
|
||||
# prompt += "".join(lines)
|
||||
|
||||
prompt += f"{self.fence[1]}\n"
|
||||
|
||||
return prompt
|
||||
|
@ -376,7 +388,7 @@ class Coder:
|
|||
new_user_message = self.send_new_user_message(new_user_message)
|
||||
|
||||
if with_message:
|
||||
return
|
||||
return self.partial_response_content
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.keyboard_interrupt()
|
||||
|
@ -488,12 +500,12 @@ class Coder:
|
|||
# add the reminder anyway
|
||||
total_tokens = 0
|
||||
|
||||
messages += self.cur_messages
|
||||
|
||||
# Add the reminder prompt if we still have room to include it.
|
||||
if total_tokens < self.main_model.max_context_tokens:
|
||||
messages += reminder_message
|
||||
|
||||
messages += self.cur_messages
|
||||
|
||||
return messages
|
||||
|
||||
def send_new_user_message(self, inp):
|
||||
|
@ -882,19 +894,19 @@ class Coder:
|
|||
return set(edit[0] for edit in edits)
|
||||
|
||||
def apply_updates(self):
|
||||
max_apply_update_errors = 3
|
||||
|
||||
try:
|
||||
edited = self.update_files()
|
||||
except ValueError as err:
|
||||
self.num_malformed_responses += 1
|
||||
err = err.args[0]
|
||||
self.apply_update_errors += 1
|
||||
if self.apply_update_errors < max_apply_update_errors:
|
||||
if self.apply_update_errors < self.max_apply_update_errors:
|
||||
self.io.tool_error(f"Malformed response #{self.apply_update_errors}, retrying...")
|
||||
self.io.tool_error(str(err))
|
||||
return None, err
|
||||
else:
|
||||
self.io.tool_error(f"Malformed response #{self.apply_update_errors}, aborting.")
|
||||
self.io.tool_error(str(err))
|
||||
return False, None
|
||||
|
||||
except Exception as err:
|
||||
|
@ -902,11 +914,13 @@ class Coder:
|
|||
print()
|
||||
traceback.print_exc()
|
||||
self.apply_update_errors += 1
|
||||
if self.apply_update_errors < max_apply_update_errors:
|
||||
if self.apply_update_errors < self.max_apply_update_errors:
|
||||
self.io.tool_error(f"Update exception #{self.apply_update_errors}, retrying...")
|
||||
self.io.tool_error(str(err))
|
||||
return None, str(err)
|
||||
else:
|
||||
self.io.tool_error(f"Update exception #{self.apply_update_errors}, aborting")
|
||||
self.io.tool_error(str(err))
|
||||
return False, None
|
||||
|
||||
self.apply_update_errors = 0
|
||||
|
|
|
@ -9,6 +9,8 @@ from .editblock_prompts import EditBlockPrompts
|
|||
|
||||
|
||||
class EditBlockCoder(Coder):
|
||||
edit_format = "diff"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.gpt_prompts = EditBlockPrompts()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
769
aider/coders/search_replace.py
Executable file
769
aider/coders/search_replace.py
Executable file
|
@ -0,0 +1,769 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import git
|
||||
from diff_match_patch import diff_match_patch
|
||||
from tqdm import tqdm
|
||||
|
||||
from aider.dump import dump
|
||||
from aider.utils import GitTemporaryDirectory
|
||||
|
||||
|
||||
class RelativeIndenter:
|
||||
"""Rewrites text files to have relative indentation, which involves
|
||||
reformatting the leading white space on lines. This format makes
|
||||
it easier to search and apply edits to pairs of code blocks which
|
||||
may differ significantly in their overall level of indentation.
|
||||
|
||||
It removes leading white space which is shared with the preceding
|
||||
line.
|
||||
|
||||
Original:
|
||||
```
|
||||
Foo # indented 8
|
||||
Bar # indented 4 more than the previous line
|
||||
Baz # same indent as the previous line
|
||||
Fob # same indent as the previous line
|
||||
```
|
||||
|
||||
Becomes:
|
||||
```
|
||||
Foo # indented 8
|
||||
Bar # indented 4 more than the previous line
|
||||
Baz # same indent as the previous line
|
||||
Fob # same indent as the previous line
|
||||
```
|
||||
|
||||
If the current line is *less* indented then the previous line,
|
||||
uses a unicode character to indicate outdenting.
|
||||
|
||||
Original
|
||||
```
|
||||
Foo
|
||||
Bar
|
||||
Baz
|
||||
Fob # indented 4 less than the previous line
|
||||
```
|
||||
|
||||
Becomes:
|
||||
```
|
||||
Foo
|
||||
Bar
|
||||
Baz
|
||||
←←←←Fob # indented 4 less than the previous line
|
||||
```
|
||||
|
||||
This is a similar original to the last one, but every line has
|
||||
been uniformly outdented:
|
||||
```
|
||||
Foo
|
||||
Bar
|
||||
Baz
|
||||
Fob # indented 4 less than the previous line
|
||||
```
|
||||
|
||||
It becomes this result, which is very similar to the previous
|
||||
result. Only the white space on the first line differs. From the
|
||||
word Foo onwards, it is identical to the previous result.
|
||||
```
|
||||
Foo
|
||||
Bar
|
||||
Baz
|
||||
←←←←Fob # indented 4 less than the previous line
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, texts):
|
||||
"""
|
||||
Based on the texts, choose a unicode character that isn't in any of them.
|
||||
"""
|
||||
|
||||
chars = set()
|
||||
for text in texts:
|
||||
chars.update(text)
|
||||
|
||||
ARROW = "←"
|
||||
if ARROW not in chars:
|
||||
self.marker = ARROW
|
||||
else:
|
||||
self.marker = self.select_unique_marker(chars)
|
||||
|
||||
def select_unique_marker(self, chars):
|
||||
for codepoint in range(0x10FFFF, 0x10000, -1):
|
||||
marker = chr(codepoint)
|
||||
if marker not in chars:
|
||||
return marker
|
||||
|
||||
raise ValueError("Could not find a unique marker")
|
||||
|
||||
def make_relative(self, text):
|
||||
"""
|
||||
Transform text to use relative indents.
|
||||
"""
|
||||
|
||||
if self.marker in text:
|
||||
raise ValueError("Text already contains the outdent marker: {self.marker}")
|
||||
|
||||
lines = text.splitlines(keepends=True)
|
||||
|
||||
output = []
|
||||
prev_indent = ""
|
||||
for line in lines:
|
||||
line_without_end = line.rstrip("\n\r")
|
||||
|
||||
len_indent = len(line_without_end) - len(line_without_end.lstrip())
|
||||
indent = line[:len_indent]
|
||||
change = len_indent - len(prev_indent)
|
||||
if change > 0:
|
||||
cur_indent = indent[-change:]
|
||||
elif change < 0:
|
||||
cur_indent = self.marker * -change
|
||||
else:
|
||||
cur_indent = ""
|
||||
|
||||
out_line = cur_indent + "\n" + line[len_indent:]
|
||||
# dump(len_indent, change, out_line)
|
||||
# print(out_line)
|
||||
output.append(out_line)
|
||||
prev_indent = indent
|
||||
|
||||
res = "".join(output)
|
||||
return res
|
||||
|
||||
def make_absolute(self, text):
|
||||
"""
|
||||
Transform text from relative back to absolute indents.
|
||||
"""
|
||||
lines = text.splitlines(keepends=True)
|
||||
|
||||
output = []
|
||||
prev_indent = ""
|
||||
for i in range(0, len(lines), 2):
|
||||
dent = lines[i].rstrip("\r\n")
|
||||
non_indent = lines[i + 1]
|
||||
|
||||
if dent.startswith(self.marker):
|
||||
len_outdent = len(dent)
|
||||
cur_indent = prev_indent[:-len_outdent]
|
||||
else:
|
||||
cur_indent = prev_indent + dent
|
||||
|
||||
if not non_indent.rstrip("\r\n"):
|
||||
out_line = non_indent # don't indent a blank line
|
||||
else:
|
||||
out_line = cur_indent + non_indent
|
||||
|
||||
output.append(out_line)
|
||||
prev_indent = cur_indent
|
||||
|
||||
res = "".join(output)
|
||||
if self.marker in res:
|
||||
# dump(res)
|
||||
raise ValueError("Error transforming text back to absolute indents")
|
||||
|
||||
return res
|
||||
|
||||
|
||||
# The patches are created to change S->R.
|
||||
# So all the patch offsets are relative to S.
|
||||
# But O has a lot more content. So all the offsets are very wrong.
|
||||
#
|
||||
# But patch_apply() seems to imply that once patch N is located,
|
||||
# then it adjusts the offset of the next patch.
|
||||
#
|
||||
# This is great, because once we sync up after a big gap the nearby
|
||||
# patches are close to being located right.
|
||||
# Except when indentation has been changed by GPT.
|
||||
#
|
||||
# It would help to use the diff trick to build map_S_offset_to_O_offset().
|
||||
# Then update all the S offsets in the S->R patches to be O offsets.
|
||||
# Do we also need to update the R offsets?
|
||||
#
|
||||
# What if this gets funky/wrong?
|
||||
#
|
||||
|
||||
|
||||
def map_patches(texts, patches, debug):
|
||||
search_text, replace_text, original_text = texts
|
||||
|
||||
dmp = diff_match_patch()
|
||||
dmp.Diff_Timeout = 5
|
||||
|
||||
diff_s_o = dmp.diff_main(search_text, original_text)
|
||||
# diff_r_s = dmp.diff_main(replace_text, search_text)
|
||||
|
||||
# dmp.diff_cleanupSemantic(diff_s_o)
|
||||
# dmp.diff_cleanupEfficiency(diff_s_o)
|
||||
|
||||
if debug:
|
||||
html = dmp.diff_prettyHtml(diff_s_o)
|
||||
Path("tmp.html").write_text(html)
|
||||
|
||||
dump(len(search_text))
|
||||
dump(len(original_text))
|
||||
|
||||
for patch in patches:
|
||||
start1 = patch.start1
|
||||
start2 = patch.start2
|
||||
|
||||
patch.start1 = dmp.diff_xIndex(diff_s_o, start1)
|
||||
patch.start2 = dmp.diff_xIndex(diff_s_o, start2)
|
||||
|
||||
if debug:
|
||||
print()
|
||||
print(start1, repr(search_text[start1 : start1 + 50]))
|
||||
print(patch.start1, repr(original_text[patch.start1 : patch.start1 + 50]))
|
||||
print(patch.diffs)
|
||||
print()
|
||||
|
||||
return patches
|
||||
|
||||
|
||||
example = """Left
|
||||
Left
|
||||
4 in
|
||||
4 in
|
||||
8 in
|
||||
4 in
|
||||
Left
|
||||
"""
|
||||
|
||||
"""
|
||||
ri = RelativeIndenter([example])
|
||||
dump(example)
|
||||
|
||||
rel_example = ri.make_relative(example)
|
||||
dump(repr(rel_example))
|
||||
|
||||
abs_example = ri.make_absolute(rel_example)
|
||||
dump(abs_example)
|
||||
|
||||
|
||||
sys.exit()
|
||||
"""
|
||||
|
||||
|
||||
def relative_indent(texts):
|
||||
ri = RelativeIndenter(texts)
|
||||
texts = list(map(ri.make_relative, texts))
|
||||
|
||||
return ri, texts
|
||||
|
||||
|
||||
line_padding = 100
|
||||
|
||||
|
||||
def line_pad(text):
|
||||
padding = "\n" * line_padding
|
||||
return padding + text + padding
|
||||
|
||||
|
||||
def line_unpad(text):
|
||||
if set(text[:line_padding] + text[-line_padding:]) != set("\n"):
|
||||
return
|
||||
return text[line_padding:-line_padding]
|
||||
|
||||
|
||||
def dmp_apply(texts, remap=True):
|
||||
debug = False
|
||||
# debug = True
|
||||
|
||||
search_text, replace_text, original_text = texts
|
||||
|
||||
dmp = diff_match_patch()
|
||||
dmp.Diff_Timeout = 5
|
||||
# dmp.Diff_EditCost = 16
|
||||
|
||||
if remap:
|
||||
dmp.Match_Threshold = 0.95
|
||||
dmp.Match_Distance = 500
|
||||
dmp.Match_MaxBits = 128
|
||||
dmp.Patch_Margin = 32
|
||||
else:
|
||||
dmp.Match_Threshold = 0.5
|
||||
dmp.Match_Distance = 100_000
|
||||
dmp.Match_MaxBits = 32
|
||||
dmp.Patch_Margin = 8
|
||||
|
||||
diff = dmp.diff_main(search_text, replace_text, None)
|
||||
dmp.diff_cleanupSemantic(diff)
|
||||
dmp.diff_cleanupEfficiency(diff)
|
||||
|
||||
patches = dmp.patch_make(search_text, diff)
|
||||
|
||||
if debug:
|
||||
html = dmp.diff_prettyHtml(diff)
|
||||
Path("tmp.search_replace_diff.html").write_text(html)
|
||||
|
||||
for d in diff:
|
||||
print(d[0], repr(d[1]))
|
||||
|
||||
for patch in patches:
|
||||
start1 = patch.start1
|
||||
print()
|
||||
print(start1, repr(search_text[start1 : start1 + 10]))
|
||||
print(start1, repr(replace_text[start1 : start1 + 10]))
|
||||
print(patch.diffs)
|
||||
|
||||
# dump(original_text)
|
||||
# dump(search_text)
|
||||
|
||||
if remap:
|
||||
patches = map_patches(texts, patches, debug)
|
||||
|
||||
patches_text = dmp.patch_toText(patches)
|
||||
|
||||
new_text, success = dmp.patch_apply(patches, original_text)
|
||||
|
||||
all_success = False not in success
|
||||
|
||||
if debug:
|
||||
# dump(new_text)
|
||||
print(patches_text)
|
||||
|
||||
# print(new_text)
|
||||
dump(success)
|
||||
dump(all_success)
|
||||
|
||||
# print(new_text)
|
||||
|
||||
if not all_success:
|
||||
return
|
||||
|
||||
return new_text
|
||||
|
||||
|
||||
def lines_to_chars(lines, mapping):
|
||||
new_text = []
|
||||
for char in lines:
|
||||
new_text.append(mapping[ord(char)])
|
||||
|
||||
new_text = "".join(new_text)
|
||||
return new_text
|
||||
|
||||
|
||||
def dmp_lines_apply(texts, remap=True):
|
||||
debug = False
|
||||
# debug = True
|
||||
|
||||
for t in texts:
|
||||
assert t.endswith("\n"), t
|
||||
|
||||
search_text, replace_text, original_text = texts
|
||||
|
||||
dmp = diff_match_patch()
|
||||
dmp.Diff_Timeout = 5
|
||||
# dmp.Diff_EditCost = 16
|
||||
|
||||
dmp.Match_Threshold = 0.1
|
||||
dmp.Match_Distance = 100_000
|
||||
dmp.Match_MaxBits = 32
|
||||
dmp.Patch_Margin = 1
|
||||
|
||||
all_text = search_text + replace_text + original_text
|
||||
all_lines, _, mapping = dmp.diff_linesToChars(all_text, "")
|
||||
assert len(all_lines) == len(all_text.splitlines())
|
||||
|
||||
search_num = len(search_text.splitlines())
|
||||
replace_num = len(replace_text.splitlines())
|
||||
original_num = len(original_text.splitlines())
|
||||
|
||||
search_lines = all_lines[:search_num]
|
||||
replace_lines = all_lines[search_num : search_num + replace_num]
|
||||
original_lines = all_lines[search_num + replace_num :]
|
||||
|
||||
assert len(search_lines) == search_num
|
||||
assert len(replace_lines) == replace_num
|
||||
assert len(original_lines) == original_num
|
||||
|
||||
diff_lines = dmp.diff_main(search_lines, replace_lines, None)
|
||||
dmp.diff_cleanupSemantic(diff_lines)
|
||||
dmp.diff_cleanupEfficiency(diff_lines)
|
||||
|
||||
patches = dmp.patch_make(search_lines, diff_lines)
|
||||
|
||||
if debug:
|
||||
diff = list(diff_lines)
|
||||
dmp.diff_charsToLines(diff, mapping)
|
||||
dump(diff)
|
||||
html = dmp.diff_prettyHtml(diff)
|
||||
Path("tmp.search_replace_diff.html").write_text(html)
|
||||
|
||||
for d in diff:
|
||||
print(d[0], repr(d[1]))
|
||||
|
||||
new_lines, success = dmp.patch_apply(patches, original_lines)
|
||||
new_text = lines_to_chars(new_lines, mapping)
|
||||
|
||||
all_success = False not in success
|
||||
|
||||
if debug:
|
||||
# print(new_text)
|
||||
dump(success)
|
||||
dump(all_success)
|
||||
|
||||
# print(new_text)
|
||||
|
||||
if not all_success:
|
||||
return
|
||||
|
||||
return new_text
|
||||
|
||||
|
||||
def diff_lines(search_text, replace_text):
|
||||
dmp = diff_match_patch()
|
||||
dmp.Diff_Timeout = 5
|
||||
# dmp.Diff_EditCost = 16
|
||||
search_lines, replace_lines, mapping = dmp.diff_linesToChars(search_text, replace_text)
|
||||
|
||||
diff_lines = dmp.diff_main(search_lines, replace_lines, None)
|
||||
dmp.diff_cleanupSemantic(diff_lines)
|
||||
dmp.diff_cleanupEfficiency(diff_lines)
|
||||
|
||||
diff = list(diff_lines)
|
||||
dmp.diff_charsToLines(diff, mapping)
|
||||
dump(diff)
|
||||
|
||||
udiff = []
|
||||
for d, lines in diff:
|
||||
if d < 0:
|
||||
d = "-"
|
||||
elif d > 0:
|
||||
d = "+"
|
||||
else:
|
||||
d = " "
|
||||
for line in lines.splitlines(keepends=True):
|
||||
udiff.append(d + line)
|
||||
|
||||
return udiff
|
||||
|
||||
|
||||
def search_and_replace(texts):
|
||||
search_text, replace_text, original_text = texts
|
||||
|
||||
num = original_text.count(search_text)
|
||||
# if num > 1:
|
||||
# raise SearchTextNotUnique()
|
||||
if num == 0:
|
||||
return
|
||||
|
||||
new_text = original_text.replace(search_text, replace_text)
|
||||
|
||||
return new_text
|
||||
|
||||
|
||||
def git_cherry_pick_osr_onto_o(texts):
|
||||
search_text, replace_text, original_text = texts
|
||||
|
||||
with GitTemporaryDirectory() as dname:
|
||||
repo = git.Repo(dname)
|
||||
|
||||
fname = Path(dname) / "file.txt"
|
||||
|
||||
# Make O->S->R
|
||||
fname.write_text(original_text)
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "original")
|
||||
original_hash = repo.head.commit.hexsha
|
||||
|
||||
fname.write_text(search_text)
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "search")
|
||||
|
||||
fname.write_text(replace_text)
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "replace")
|
||||
replace_hash = repo.head.commit.hexsha
|
||||
|
||||
# go back to O
|
||||
repo.git.checkout(original_hash)
|
||||
|
||||
# cherry pick R onto original
|
||||
try:
|
||||
repo.git.cherry_pick(replace_hash, "--minimal")
|
||||
except git.exc.GitCommandError:
|
||||
# merge conflicts!
|
||||
return
|
||||
|
||||
new_text = fname.read_text()
|
||||
return new_text
|
||||
|
||||
|
||||
def git_cherry_pick_sr_onto_so(texts):
|
||||
search_text, replace_text, original_text = texts
|
||||
|
||||
with GitTemporaryDirectory() as dname:
|
||||
repo = git.Repo(dname)
|
||||
|
||||
fname = Path(dname) / "file.txt"
|
||||
|
||||
fname.write_text(search_text)
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "search")
|
||||
search_hash = repo.head.commit.hexsha
|
||||
|
||||
# make search->replace
|
||||
fname.write_text(replace_text)
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "replace")
|
||||
replace_hash = repo.head.commit.hexsha
|
||||
|
||||
# go back to search,
|
||||
repo.git.checkout(search_hash)
|
||||
|
||||
# make search->original
|
||||
fname.write_text(original_text)
|
||||
repo.git.add(str(fname))
|
||||
repo.git.commit("-m", "original")
|
||||
|
||||
# cherry pick replace onto original
|
||||
try:
|
||||
repo.git.cherry_pick(replace_hash, "--minimal")
|
||||
except git.exc.GitCommandError:
|
||||
# merge conflicts!
|
||||
return
|
||||
|
||||
new_text = fname.read_text()
|
||||
|
||||
return new_text
|
||||
|
||||
|
||||
class SearchTextNotUnique(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
all_preprocs = [
|
||||
# (strip_blank_lines, relative_indent, reverse_lines)
|
||||
(False, False, False),
|
||||
(True, False, False),
|
||||
(False, True, False),
|
||||
(True, True, False),
|
||||
# (False, False, True),
|
||||
# (True, False, True),
|
||||
# (False, True, True),
|
||||
# (True, True, True),
|
||||
]
|
||||
|
||||
always_relative_indent = [
|
||||
(False, True, False),
|
||||
(True, True, False),
|
||||
# (False, True, True),
|
||||
# (True, True, True),
|
||||
]
|
||||
|
||||
editblock_strategies = [
|
||||
(search_and_replace, all_preprocs),
|
||||
(git_cherry_pick_osr_onto_o, all_preprocs),
|
||||
(dmp_lines_apply, all_preprocs),
|
||||
]
|
||||
|
||||
never_relative = [
|
||||
(False, False),
|
||||
(True, False),
|
||||
]
|
||||
|
||||
udiff_strategies = [
|
||||
(search_and_replace, all_preprocs),
|
||||
(git_cherry_pick_osr_onto_o, all_preprocs),
|
||||
(dmp_lines_apply, all_preprocs),
|
||||
]
|
||||
|
||||
|
||||
def flexible_search_and_replace(texts, strategies):
|
||||
"""Try a series of search/replace methods, starting from the most
|
||||
literal interpretation of search_text. If needed, progress to more
|
||||
flexible methods, which can accommodate divergence between
|
||||
search_text and original_text and yet still achieve the desired
|
||||
edits.
|
||||
"""
|
||||
|
||||
for strategy, preprocs in strategies:
|
||||
for preproc in preprocs:
|
||||
res = try_strategy(texts, strategy, preproc)
|
||||
if res:
|
||||
return res
|
||||
|
||||
|
||||
def reverse_lines(text):
|
||||
lines = text.splitlines(keepends=True)
|
||||
lines.reverse()
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def try_strategy(texts, strategy, preproc):
|
||||
preproc_strip_blank_lines, preproc_relative_indent, preproc_reverse = preproc
|
||||
ri = None
|
||||
|
||||
if preproc_strip_blank_lines:
|
||||
texts = strip_blank_lines(texts)
|
||||
if preproc_relative_indent:
|
||||
ri, texts = relative_indent(texts)
|
||||
if preproc_reverse:
|
||||
texts = list(map(reverse_lines, texts))
|
||||
|
||||
res = strategy(texts)
|
||||
|
||||
if res and preproc_reverse:
|
||||
res = reverse_lines(res)
|
||||
|
||||
if res and preproc_relative_indent:
|
||||
try:
|
||||
res = ri.make_absolute(res)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def strip_blank_lines(texts):
|
||||
# strip leading and trailing blank lines
|
||||
texts = [text.strip("\n") + "\n" for text in texts]
|
||||
return texts
|
||||
|
||||
|
||||
def read_text(fname):
|
||||
text = Path(fname).read_text()
|
||||
return text
|
||||
|
||||
|
||||
def proc(dname):
|
||||
dname = Path(dname)
|
||||
|
||||
try:
|
||||
search_text = read_text(dname / "search")
|
||||
replace_text = read_text(dname / "replace")
|
||||
original_text = read_text(dname / "original")
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
####
|
||||
|
||||
texts = search_text, replace_text, original_text
|
||||
|
||||
strategies = [
|
||||
# (search_and_replace, all_preprocs),
|
||||
# (git_cherry_pick_osr_onto_o, all_preprocs),
|
||||
# (git_cherry_pick_sr_onto_so, all_preprocs),
|
||||
# (dmp_apply, all_preprocs),
|
||||
(dmp_lines_apply, all_preprocs),
|
||||
]
|
||||
|
||||
_strategies = editblock_strategies # noqa: F841
|
||||
|
||||
short_names = dict(
|
||||
search_and_replace="sr",
|
||||
git_cherry_pick_osr_onto_o="cp_o",
|
||||
git_cherry_pick_sr_onto_so="cp_so",
|
||||
dmp_apply="dmp",
|
||||
dmp_lines_apply="dmpl",
|
||||
)
|
||||
|
||||
patched = dict()
|
||||
for strategy, preprocs in strategies:
|
||||
for preproc in preprocs:
|
||||
method = strategy.__name__
|
||||
method = short_names[method]
|
||||
|
||||
strip_blank, rel_indent, rev_lines = preproc
|
||||
if strip_blank or rel_indent:
|
||||
method += "_"
|
||||
if strip_blank:
|
||||
method += "s"
|
||||
if rel_indent:
|
||||
method += "i"
|
||||
if rev_lines:
|
||||
method += "r"
|
||||
|
||||
res = try_strategy(texts, strategy, preproc)
|
||||
patched[method] = res
|
||||
|
||||
results = []
|
||||
for method, res in patched.items():
|
||||
out_fname = dname / f"original.{method}"
|
||||
if out_fname.exists():
|
||||
out_fname.unlink()
|
||||
|
||||
if res:
|
||||
out_fname.write_text(res)
|
||||
|
||||
correct = (dname / "correct").read_text()
|
||||
if res == correct:
|
||||
res = "pass"
|
||||
else:
|
||||
res = "WRONG"
|
||||
else:
|
||||
res = "fail"
|
||||
|
||||
results.append((method, res))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def colorize_result(result):
|
||||
colors = {
|
||||
"pass": "\033[102;30mpass\033[0m", # Green background, black text
|
||||
"WRONG": "\033[101;30mWRONG\033[0m", # Red background, black text
|
||||
"fail": "\033[103;30mfail\033[0m", # Yellow background, black text
|
||||
}
|
||||
return colors.get(result, result) # Default to original result if not found
|
||||
|
||||
|
||||
def main(dnames):
|
||||
all_results = []
|
||||
for dname in tqdm(dnames):
|
||||
dname = Path(dname)
|
||||
results = proc(dname)
|
||||
for method, res in results:
|
||||
all_results.append((dname, method, res))
|
||||
# print(dname, method, colorize_result(res))
|
||||
|
||||
# Create a 2D table with directories along the right and methods along the top
|
||||
# Collect all unique methods and directories
|
||||
methods = []
|
||||
for _, method, _ in all_results:
|
||||
if method not in methods:
|
||||
methods.append(method)
|
||||
|
||||
directories = dnames
|
||||
|
||||
# Sort directories by decreasing number of 'pass' results
|
||||
pass_counts = {
|
||||
dname: sum(
|
||||
res == "pass" for dname_result, _, res in all_results if str(dname) == str(dname_result)
|
||||
)
|
||||
for dname in directories
|
||||
}
|
||||
directories.sort(key=lambda dname: pass_counts[dname], reverse=True)
|
||||
|
||||
# Create a results matrix
|
||||
results_matrix = {dname: {method: "" for method in methods} for dname in directories}
|
||||
|
||||
# Populate the results matrix
|
||||
for dname, method, res in all_results:
|
||||
results_matrix[str(dname)][method] = res
|
||||
|
||||
# Print the 2D table
|
||||
# Print the header
|
||||
print("{:<20}".format("Directory"), end="")
|
||||
for method in methods:
|
||||
print("{:<9}".format(method), end="")
|
||||
print()
|
||||
|
||||
# Print the rows with colorized results
|
||||
for dname in directories:
|
||||
print("{:<20}".format(Path(dname).name), end="")
|
||||
for method in methods:
|
||||
res = results_matrix[dname][method]
|
||||
colorized_res = colorize_result(res)
|
||||
res_l = 9 + len(colorized_res) - len(res)
|
||||
fmt = "{:<" + str(res_l) + "}"
|
||||
print(fmt.format(colorized_res), end="")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
status = main(sys.argv[1:])
|
||||
sys.exit(status)
|
395
aider/coders/udiff_coder.py
Normal file
395
aider/coders/udiff_coder.py
Normal file
|
@ -0,0 +1,395 @@
|
|||
import difflib
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
|
||||
from ..dump import dump # noqa: F401
|
||||
from .base_coder import Coder
|
||||
from .search_replace import (
|
||||
SearchTextNotUnique,
|
||||
all_preprocs,
|
||||
diff_lines,
|
||||
flexible_search_and_replace,
|
||||
search_and_replace,
|
||||
)
|
||||
from .udiff_prompts import UnifiedDiffPrompts
|
||||
|
||||
no_match_error = """UnifiedDiffNoMatch: hunk failed to apply!
|
||||
|
||||
{path} does not contain lines that match the diff you provided!
|
||||
Try again.
|
||||
DO NOT skip blank lines, comments, docstrings, etc!
|
||||
The diff needs to apply cleanly to the lines in {path}!
|
||||
|
||||
{path} does not contain these {num_lines} exact lines in a row:
|
||||
```
|
||||
{original}```
|
||||
"""
|
||||
|
||||
|
||||
not_unique_error = """UnifiedDiffNotUnique: hunk failed to apply!
|
||||
|
||||
{path} contains multiple sets of lines that match the diff you provided!
|
||||
Try again.
|
||||
Use additional ` ` lines to provide context that uniquely indicates which code needs to be changed.
|
||||
The diff needs to apply to a unique set of lines in {path}!
|
||||
|
||||
{path} contains multiple copies of these {num_lines} lines:
|
||||
```
|
||||
{original}```
|
||||
"""
|
||||
|
||||
|
||||
class UnifiedDiffCoder(Coder):
|
||||
edit_format = "udiff"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.gpt_prompts = UnifiedDiffPrompts()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_edits(self):
|
||||
content = self.partial_response_content
|
||||
|
||||
# might raise ValueError for malformed ORIG/UPD blocks
|
||||
raw_edits = list(find_diffs(content))
|
||||
|
||||
last_path = None
|
||||
edits = []
|
||||
for path, hunk in raw_edits:
|
||||
if path:
|
||||
last_path = path
|
||||
else:
|
||||
path = last_path
|
||||
edits.append((path, hunk))
|
||||
|
||||
return edits
|
||||
|
||||
def apply_edits(self, edits):
|
||||
seen = set()
|
||||
uniq = []
|
||||
for path, hunk in edits:
|
||||
hunk = normalize_hunk(hunk)
|
||||
if not hunk:
|
||||
continue
|
||||
|
||||
this = [path + "\n"] + hunk
|
||||
this = "".join(this)
|
||||
|
||||
if this in seen:
|
||||
continue
|
||||
seen.add(this)
|
||||
|
||||
uniq.append((path, hunk))
|
||||
|
||||
errors = []
|
||||
for path, hunk in uniq:
|
||||
full_path = self.abs_root_path(path)
|
||||
content = self.io.read_text(full_path)
|
||||
|
||||
original, _ = hunk_to_before_after(hunk)
|
||||
|
||||
try:
|
||||
content = do_replace(full_path, content, hunk)
|
||||
except SearchTextNotUnique:
|
||||
errors.append(
|
||||
not_unique_error.format(
|
||||
path=path, original=original, num_lines=len(original.splitlines())
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if not content:
|
||||
errors.append(
|
||||
no_match_error.format(
|
||||
path=path, original=original, num_lines=len(original.splitlines())
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# SUCCESS!
|
||||
self.io.write_text(full_path, content)
|
||||
|
||||
if errors:
|
||||
errors = "\n\n".join(errors)
|
||||
raise ValueError(errors)
|
||||
|
||||
|
||||
def do_replace(fname, content, hunk):
|
||||
fname = Path(fname)
|
||||
|
||||
before_text, after_text = hunk_to_before_after(hunk)
|
||||
|
||||
# does it want to make a new file?
|
||||
if not fname.exists() and not before_text.strip():
|
||||
fname.touch()
|
||||
content = ""
|
||||
|
||||
if content is None:
|
||||
return
|
||||
|
||||
# TODO: handle inserting into new file
|
||||
if not before_text.strip():
|
||||
# append to existing file, or start a new file
|
||||
new_content = content + after_text
|
||||
return new_content
|
||||
|
||||
new_content = None
|
||||
|
||||
new_content = apply_hunk(content, hunk)
|
||||
if new_content:
|
||||
return new_content
|
||||
|
||||
|
||||
def collapse_repeats(s):
|
||||
return "".join(k for k, g in groupby(s))
|
||||
|
||||
|
||||
def apply_hunk(content, hunk):
|
||||
before_text, after_text = hunk_to_before_after(hunk)
|
||||
|
||||
res = directly_apply_hunk(content, hunk)
|
||||
if res:
|
||||
return res
|
||||
|
||||
hunk = make_new_lines_explicit(content, hunk)
|
||||
|
||||
# just consider space vs not-space
|
||||
ops = "".join([line[0] for line in hunk])
|
||||
ops = ops.replace("-", "x")
|
||||
ops = ops.replace("+", "x")
|
||||
ops = ops.replace("\n", " ")
|
||||
|
||||
cur_op = " "
|
||||
section = []
|
||||
sections = []
|
||||
|
||||
for i in range(len(ops)):
|
||||
op = ops[i]
|
||||
if op != cur_op:
|
||||
sections.append(section)
|
||||
section = []
|
||||
cur_op = op
|
||||
section.append(hunk[i])
|
||||
|
||||
sections.append(section)
|
||||
if cur_op != " ":
|
||||
sections.append([])
|
||||
|
||||
all_done = True
|
||||
for i in range(2, len(sections), 2):
|
||||
preceding_context = sections[i - 2]
|
||||
changes = sections[i - 1]
|
||||
following_context = sections[i]
|
||||
|
||||
res = apply_partial_hunk(content, preceding_context, changes, following_context)
|
||||
if res:
|
||||
content = res
|
||||
else:
|
||||
all_done = False
|
||||
# FAILED!
|
||||
# this_hunk = preceding_context + changes + following_context
|
||||
break
|
||||
|
||||
if all_done:
|
||||
return content
|
||||
|
||||
|
||||
def flexi_just_search_and_replace(texts):
|
||||
strategies = [
|
||||
(search_and_replace, all_preprocs),
|
||||
]
|
||||
|
||||
return flexible_search_and_replace(texts, strategies)
|
||||
|
||||
|
||||
def make_new_lines_explicit(content, hunk):
|
||||
before, after = hunk_to_before_after(hunk)
|
||||
|
||||
diff = diff_lines(before, content)
|
||||
|
||||
back_diff = []
|
||||
for line in diff:
|
||||
if line[0] == "+":
|
||||
continue
|
||||
# if line[0] == "-":
|
||||
# line = "+" + line[1:]
|
||||
|
||||
back_diff.append(line)
|
||||
|
||||
new_before = directly_apply_hunk(before, back_diff)
|
||||
if not new_before:
|
||||
return hunk
|
||||
|
||||
if len(new_before.strip()) < 10:
|
||||
return hunk
|
||||
|
||||
before = before.splitlines(keepends=True)
|
||||
new_before = new_before.splitlines(keepends=True)
|
||||
after = after.splitlines(keepends=True)
|
||||
|
||||
if len(new_before) < len(before) * 0.66:
|
||||
return hunk
|
||||
|
||||
new_hunk = difflib.unified_diff(new_before, after, n=max(len(new_before), len(after)))
|
||||
new_hunk = list(new_hunk)[3:]
|
||||
|
||||
return new_hunk
|
||||
|
||||
|
||||
def cleanup_pure_whitespace_lines(lines):
|
||||
res = [
|
||||
line if line.strip() else line[-(len(line) - len(line.rstrip("\r\n")))] for line in lines
|
||||
]
|
||||
return res
|
||||
|
||||
|
||||
def normalize_hunk(hunk):
|
||||
before, after = hunk_to_before_after(hunk, lines=True)
|
||||
|
||||
before = cleanup_pure_whitespace_lines(before)
|
||||
after = cleanup_pure_whitespace_lines(after)
|
||||
|
||||
diff = difflib.unified_diff(before, after, n=max(len(before), len(after)))
|
||||
diff = list(diff)[3:]
|
||||
return diff
|
||||
|
||||
|
||||
def directly_apply_hunk(content, hunk):
|
||||
before, after = hunk_to_before_after(hunk)
|
||||
|
||||
before_lines, _ = hunk_to_before_after(hunk, lines=True)
|
||||
before_lines = "".join([line.strip() for line in before_lines])
|
||||
|
||||
# Refuse to do a repeated search and replace on a tiny bit of non-whitespace context
|
||||
if len(before_lines) < 10 and content.count(before) > 1:
|
||||
return
|
||||
|
||||
try:
|
||||
new_content = flexi_just_search_and_replace([before, after, content])
|
||||
except SearchTextNotUnique:
|
||||
new_content = None
|
||||
|
||||
return new_content
|
||||
|
||||
|
||||
def apply_partial_hunk(content, preceding_context, changes, following_context):
|
||||
len_prec = len(preceding_context)
|
||||
len_foll = len(following_context)
|
||||
|
||||
use_all = len_prec + len_foll
|
||||
|
||||
for drop in range(use_all):
|
||||
use = use_all - drop
|
||||
|
||||
for use_prec in range(len_prec, -1, -1):
|
||||
if use_prec > use:
|
||||
continue
|
||||
|
||||
use_foll = use - use_prec
|
||||
if use_foll > len_foll:
|
||||
continue
|
||||
|
||||
if use_prec:
|
||||
this_prec = preceding_context[-use_prec:]
|
||||
else:
|
||||
this_prec = []
|
||||
|
||||
this_foll = following_context[:use_foll]
|
||||
|
||||
res = directly_apply_hunk(content, this_prec + changes + this_foll)
|
||||
if res:
|
||||
return res
|
||||
|
||||
|
||||
def find_diffs(content):
|
||||
# We can always use triple-quotes, because all the udiff content
|
||||
# is prefixed with +/-/space.
|
||||
|
||||
if not content.endswith("\n"):
|
||||
content = content + "\n"
|
||||
|
||||
lines = content.splitlines(keepends=True)
|
||||
line_num = 0
|
||||
edits = []
|
||||
while line_num < len(lines):
|
||||
while line_num < len(lines):
|
||||
line = lines[line_num]
|
||||
if line.startswith("```diff"):
|
||||
line_num, these_edits = process_fenced_block(lines, line_num + 1)
|
||||
edits += these_edits
|
||||
break
|
||||
line_num += 1
|
||||
|
||||
# For now, just take 1!
|
||||
# edits = edits[:1]
|
||||
|
||||
return edits
|
||||
|
||||
|
||||
def process_fenced_block(lines, start_line_num):
|
||||
for line_num in range(start_line_num, len(lines)):
|
||||
line = lines[line_num]
|
||||
if line.startswith("```"):
|
||||
break
|
||||
|
||||
block = lines[start_line_num:line_num]
|
||||
block.append("@@ @@")
|
||||
|
||||
if block[1].startswith("+++ "):
|
||||
fname = block[1].split()[1]
|
||||
block = block[2:]
|
||||
else:
|
||||
fname = None
|
||||
|
||||
edits = []
|
||||
|
||||
keeper = False
|
||||
hunk = []
|
||||
op = " "
|
||||
for line in block:
|
||||
hunk.append(line)
|
||||
if len(line) < 2:
|
||||
continue
|
||||
op = line[0]
|
||||
if op in "-+":
|
||||
keeper = True
|
||||
continue
|
||||
if op != "@":
|
||||
continue
|
||||
if not keeper:
|
||||
hunk = []
|
||||
continue
|
||||
|
||||
hunk = hunk[:-1]
|
||||
edits.append((fname, hunk))
|
||||
hunk = []
|
||||
|
||||
return line_num + 1, edits
|
||||
|
||||
|
||||
def hunk_to_before_after(hunk, lines=False):
|
||||
before = []
|
||||
after = []
|
||||
op = " "
|
||||
for line in hunk:
|
||||
if len(line) < 2:
|
||||
op = " "
|
||||
line = line
|
||||
else:
|
||||
op = line[0]
|
||||
line = line[1:]
|
||||
|
||||
if op == " ":
|
||||
before.append(line)
|
||||
after.append(line)
|
||||
elif op == "-":
|
||||
before.append(line)
|
||||
elif op == "+":
|
||||
after.append(line)
|
||||
|
||||
if lines:
|
||||
return before, after
|
||||
|
||||
before = "".join(before)
|
||||
after = "".join(after)
|
||||
|
||||
return before, after
|
107
aider/coders/udiff_prompts.py
Normal file
107
aider/coders/udiff_prompts.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
# flake8: noqa: E501
|
||||
|
||||
from .base_prompts import CoderPrompts
|
||||
|
||||
|
||||
class UnifiedDiffPrompts(CoderPrompts):
|
||||
main_system = """Act as an expert software developer.
|
||||
You are diligent and tireless, and you always COMPLETELY IMPLEMENT the needed code.
|
||||
Always use best practices when coding.
|
||||
Respect and use existing conventions, libraries, etc that are already present in the code base.
|
||||
|
||||
Take requests for changes to the supplied code.
|
||||
If the request is ambiguous, ask questions.
|
||||
|
||||
For each file that needs to be changed, write out the changes similar to a unified diff like `diff -U0` would produce. For example:
|
||||
|
||||
# Example conversation 1
|
||||
|
||||
## USER: Replace is_prime with a call to sympy.
|
||||
|
||||
## ASSISTANT: Ok, I will:
|
||||
|
||||
1. Add an imports of sympy.
|
||||
2. Remove the is_prime() function.
|
||||
3. Replace the existing call to is_prime() with a call to sympy.isprime().
|
||||
|
||||
Here are the diffs for those changes:
|
||||
|
||||
```diff
|
||||
--- mathweb/flask/app.py
|
||||
+++ mathweb/flask/app.py
|
||||
@@ ... @@
|
||||
-class MathWeb:
|
||||
+import sympy
|
||||
+
|
||||
+class MathWeb:
|
||||
@@ ... @@
|
||||
-def is_prime(x):
|
||||
- if x < 2:
|
||||
- return False
|
||||
- for i in range(2, int(math.sqrt(x)) + 1):
|
||||
- if x % i == 0:
|
||||
- return False
|
||||
- return True
|
||||
@@ ... @@
|
||||
-@app.route('/prime/<int:n>')
|
||||
-def nth_prime(n):
|
||||
- count = 0
|
||||
- num = 1
|
||||
- while count < n:
|
||||
- num += 1
|
||||
- if is_prime(num):
|
||||
- count += 1
|
||||
- return str(num)
|
||||
+@app.route('/prime/<int:n>')
|
||||
+def nth_prime(n):
|
||||
+ count = 0
|
||||
+ num = 1
|
||||
+ while count < n:
|
||||
+ num += 1
|
||||
+ if sympy.isprime(num):
|
||||
+ count += 1
|
||||
+ return str(num)
|
||||
```
|
||||
"""
|
||||
|
||||
system_reminder = """# File editing rules:
|
||||
|
||||
Return edits similar to unified diffs that `diff -U0` would produce.
|
||||
|
||||
Make sure you include the first 2 lines with the file paths.
|
||||
Don't include timestamps with the file paths.
|
||||
|
||||
Start each hunk of changes with a `@@ ... @@` line.
|
||||
Don't include line numbers like `diff -U0` does.
|
||||
The user's patch tool doesn't need them.
|
||||
|
||||
The user's patch tool needs CORRECT patches that apply cleanly against the current contents of the file!
|
||||
Think carefully and make sure you include and mark all lines that need to be removed or changed as `-` lines.
|
||||
Make sure you mark all new or modified lines with `+`.
|
||||
Don't leave out any lines or the diff patch won't apply correctly.
|
||||
|
||||
Indentation matters in the diffs!
|
||||
|
||||
Start a new hunk for each section of the file that needs changes.
|
||||
|
||||
Only output hunks that specify changes with `+` or `-` lines.
|
||||
Skip any hunks that are entirely unchanging ` ` lines.
|
||||
|
||||
Output hunks in whatever order makes the most sense.
|
||||
Hunks don't need to be in any particular order.
|
||||
|
||||
When editing a function, method, loop, etc use a hunk to replace the *entire* code block.
|
||||
Delete the entire existing version with `-` lines and then add a new, updated version with `+` lines.
|
||||
This will help you generate correct code and correct diffs.
|
||||
|
||||
To make a new file, show a diff from `--- /dev/null` to `+++ path/to/new/file.ext`.
|
||||
"""
|
||||
|
||||
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 = """Below here are summaries of other files present in this git repository.
|
||||
Do not propose changes to these files, they are *read-only*.
|
||||
To make a file *read-write*, ask the user to *add it to the chat*.
|
||||
"""
|
|
@ -8,6 +8,8 @@ from .wholefile_prompts import WholeFilePrompts
|
|||
|
||||
|
||||
class WholeFileCoder(Coder):
|
||||
edit_format = "whole"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.gpt_prompts = WholeFilePrompts()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -148,7 +148,7 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
core_group.add_argument(
|
||||
"--model",
|
||||
metavar="MODEL",
|
||||
default=models.GPT4.name,
|
||||
default=models.GPT4_0613.name,
|
||||
help=f"Specify the model to use for the main chat (default: {models.GPT4.name})",
|
||||
)
|
||||
core_group.add_argument(
|
||||
|
@ -157,6 +157,14 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
default=False,
|
||||
help="Override to skip model availability check (default: False)",
|
||||
)
|
||||
default_4_turbo_model = models.GPT4_1106_PREVIEW
|
||||
core_group.add_argument(
|
||||
"--4-turbo",
|
||||
action="store_const",
|
||||
dest="model",
|
||||
const=default_4_turbo_model.name,
|
||||
help=f"Use {default_4_turbo_model.name} model for the main chat (gpt-4 is better)",
|
||||
)
|
||||
default_3_model = models.GPT35_1106
|
||||
core_group.add_argument(
|
||||
"-3",
|
||||
|
@ -380,7 +388,10 @@ def main(argv=None, input=None, output=None, force_git_root=None):
|
|||
"--message-file",
|
||||
"-f",
|
||||
metavar="MESSAGE_FILE",
|
||||
help="Specify a file containing the message to send GPT, process reply, then exit (disables chat mode)",
|
||||
help=(
|
||||
"Specify a file containing the message to send GPT, process reply, then exit (disables"
|
||||
" chat mode)"
|
||||
),
|
||||
)
|
||||
other_group.add_argument(
|
||||
"--encoding",
|
||||
|
|
|
@ -3,6 +3,8 @@ from .openai import OpenAIModel
|
|||
from .openrouter import OpenRouterModel
|
||||
|
||||
GPT4 = Model.create("gpt-4")
|
||||
GPT4_0613 = Model.create("gpt-4-0613")
|
||||
GPT4_1106_PREVIEW = Model.create("gpt-4-1106-preview")
|
||||
GPT35 = Model.create("gpt-3.5-turbo")
|
||||
GPT35_1106 = Model.create("gpt-3.5-turbo-1106")
|
||||
GPT35_16k = Model.create("gpt-3.5-turbo-16k")
|
||||
|
|
|
@ -33,7 +33,11 @@ class OpenAIModel(Model):
|
|||
self.tokenizer = tiktoken.encoding_for_model(name)
|
||||
|
||||
if self.is_gpt4():
|
||||
self.edit_format = "diff"
|
||||
if name == "gpt-4-1106-preview":
|
||||
self.edit_format = "udiff"
|
||||
else:
|
||||
self.edit_format = "diff"
|
||||
|
||||
self.use_repo_map = True
|
||||
self.send_undo_reply = True
|
||||
|
||||
|
@ -44,11 +48,11 @@ class OpenAIModel(Model):
|
|||
elif tokens == 32:
|
||||
self.prompt_price = 0.06
|
||||
self.completion_price = 0.12
|
||||
self.max_chat_history_tokens = 3 * 1024
|
||||
self.max_chat_history_tokens = 2 * 1024
|
||||
elif tokens == 128:
|
||||
self.prompt_price = 0.01
|
||||
self.completion_price = 0.03
|
||||
self.max_chat_history_tokens = 4 * 1024
|
||||
self.max_chat_history_tokens = 2 * 1024
|
||||
|
||||
return
|
||||
|
||||
|
@ -60,7 +64,7 @@ class OpenAIModel(Model):
|
|||
if self.name == "gpt-3.5-turbo-1106":
|
||||
self.prompt_price = 0.001
|
||||
self.completion_price = 0.002
|
||||
self.max_chat_history_tokens = 3 * 1024
|
||||
self.max_chat_history_tokens = 2 * 1024
|
||||
elif tokens == 4:
|
||||
self.prompt_price = 0.0015
|
||||
self.completion_price = 0.002
|
||||
|
|
|
@ -1,9 +1,69 @@
|
|||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import git
|
||||
|
||||
# Set of image file extensions
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
|
||||
|
||||
from .dump import dump # noqa: F401
|
||||
from aider.dump import dump # noqa: F401
|
||||
|
||||
|
||||
class IgnorantTemporaryDirectory:
|
||||
def __init__(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
def __enter__(self):
|
||||
return self.temp_dir.__enter__()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
try:
|
||||
self.temp_dir.__exit__(exc_type, exc_val, exc_tb)
|
||||
except (OSError, PermissionError):
|
||||
pass # Ignore errors (Windows)
|
||||
|
||||
|
||||
class ChdirTemporaryDirectory(IgnorantTemporaryDirectory):
|
||||
def __init__(self):
|
||||
try:
|
||||
self.cwd = os.getcwd()
|
||||
except FileNotFoundError:
|
||||
self.cwd = None
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __enter__(self):
|
||||
res = super().__enter__()
|
||||
os.chdir(self.temp_dir.name)
|
||||
return res
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.cwd:
|
||||
try:
|
||||
os.chdir(self.cwd)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
super().__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
|
||||
class GitTemporaryDirectory(ChdirTemporaryDirectory):
|
||||
def __enter__(self):
|
||||
dname = super().__enter__()
|
||||
self.repo = make_repo(dname)
|
||||
return dname
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del self.repo
|
||||
super().__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
|
||||
def make_repo(path=None):
|
||||
if not path:
|
||||
path = "."
|
||||
repo = git.Repo.init(path)
|
||||
repo.config_writer().set_value("user", "name", "Test User").release()
|
||||
repo.config_writer().set_value("user", "email", "testuser@example.com").release()
|
||||
|
||||
return repo
|
||||
|
||||
def is_image_file(file_name):
|
||||
"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue