auto-coder 0.1.330__py3-none-any.whl → 0.1.332__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.330.dist-info → auto_coder-0.1.332.dist-info}/METADATA +1 -1
- {auto_coder-0.1.330.dist-info → auto_coder-0.1.332.dist-info}/RECORD +47 -45
- autocoder/agent/agentic_filter.py +928 -0
- autocoder/agent/project_reader.py +1 -14
- autocoder/auto_coder.py +6 -47
- autocoder/auto_coder_runner.py +2 -0
- autocoder/command_args.py +1 -6
- autocoder/commands/auto_command.py +1 -1
- autocoder/commands/tools.py +68 -16
- autocoder/common/__init__.py +8 -3
- autocoder/common/auto_coder_lang.py +21 -1
- autocoder/common/code_auto_generate.py +6 -160
- autocoder/common/code_auto_generate_diff.py +5 -111
- autocoder/common/code_auto_generate_editblock.py +5 -95
- autocoder/common/code_auto_generate_strict_diff.py +6 -112
- autocoder/common/code_auto_merge_editblock.py +1 -45
- autocoder/common/code_modification_ranker.py +6 -2
- autocoder/common/command_templates.py +2 -9
- autocoder/common/conf_utils.py +36 -0
- autocoder/common/stream_out_type.py +7 -2
- autocoder/common/types.py +3 -2
- autocoder/common/v2/code_auto_generate.py +6 -4
- autocoder/common/v2/code_auto_generate_diff.py +4 -3
- autocoder/common/v2/code_auto_generate_editblock.py +9 -4
- autocoder/common/v2/code_auto_generate_strict_diff.py +182 -14
- autocoder/common/v2/code_auto_merge_diff.py +560 -306
- autocoder/common/v2/code_auto_merge_editblock.py +12 -45
- autocoder/common/v2/code_auto_merge_strict_diff.py +76 -7
- autocoder/common/v2/code_diff_manager.py +73 -6
- autocoder/common/v2/code_editblock_manager.py +534 -82
- autocoder/dispacher/actions/action.py +15 -28
- autocoder/dispacher/actions/plugins/action_regex_project.py +5 -9
- autocoder/helper/project_creator.py +0 -1
- autocoder/index/entry.py +35 -53
- autocoder/index/filter/normal_filter.py +0 -16
- autocoder/lang.py +2 -4
- autocoder/linters/shadow_linter.py +4 -0
- autocoder/pyproject/__init__.py +2 -19
- autocoder/rag/cache/simple_cache.py +31 -6
- autocoder/regexproject/__init__.py +4 -22
- autocoder/suffixproject/__init__.py +6 -24
- autocoder/tsproject/__init__.py +5 -22
- autocoder/version.py +1 -1
- {auto_coder-0.1.330.dist-info → auto_coder-0.1.332.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.330.dist-info → auto_coder-0.1.332.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.330.dist-info → auto_coder-0.1.332.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.330.dist-info → auto_coder-0.1.332.dist-info}/top_level.txt +0 -0
|
@@ -1,354 +1,608 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
|
-
from autocoder.common import AutoCoderArgs,
|
|
4
|
-
from
|
|
5
|
-
from autocoder.common.text import TextSimilarity
|
|
6
|
-
from autocoder.memory.active_context_manager import ActiveContextManager
|
|
2
|
+
import difflib
|
|
3
|
+
from autocoder.common import AutoCoderArgs,git_utils
|
|
4
|
+
from typing import List,Union,Tuple
|
|
7
5
|
import pydantic
|
|
8
6
|
import byzerllm
|
|
9
|
-
|
|
7
|
+
from autocoder.common.action_yml_file_manager import ActionYmlFileManager
|
|
8
|
+
from autocoder.common.printer import Printer
|
|
10
9
|
import hashlib
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from itertools import groupby
|
|
12
|
+
from autocoder.memory.active_context_manager import ActiveContextManager
|
|
13
|
+
from autocoder.common.search_replace import (
|
|
14
|
+
SearchTextNotUnique,
|
|
15
|
+
all_preprocs,
|
|
16
|
+
diff_lines,
|
|
17
|
+
flexible_search_and_replace,
|
|
18
|
+
search_and_replace,
|
|
19
|
+
)
|
|
18
20
|
from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
|
|
19
21
|
from autocoder.common.code_modification_ranker import CodeModificationRanker
|
|
20
22
|
from autocoder.common import files as FileUtils
|
|
21
|
-
from autocoder.common.printer import Printer
|
|
22
|
-
from autocoder.shadows.shadow_manager import ShadowManager
|
|
23
23
|
|
|
24
24
|
class PathAndCode(pydantic.BaseModel):
|
|
25
25
|
path: str
|
|
26
26
|
content: str
|
|
27
27
|
|
|
28
|
+
def safe_abs_path(res):
|
|
29
|
+
"Gives an abs path, which safely returns a full (not 8.3) windows path"
|
|
30
|
+
res = Path(res).resolve()
|
|
31
|
+
return str(res)
|
|
32
|
+
|
|
33
|
+
def do_replace(fname, content, hunk):
|
|
34
|
+
fname = Path(fname)
|
|
35
|
+
|
|
36
|
+
before_text, after_text = hunk_to_before_after(hunk)
|
|
37
|
+
|
|
38
|
+
# does it want to make a new file?
|
|
39
|
+
if not fname.exists() and not before_text.strip():
|
|
40
|
+
fname.touch()
|
|
41
|
+
content = ""
|
|
42
|
+
|
|
43
|
+
if content is None:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# TODO: handle inserting into new file
|
|
47
|
+
if not before_text.strip():
|
|
48
|
+
# append to existing file, or start a new file
|
|
49
|
+
new_content = content + after_text
|
|
50
|
+
return new_content
|
|
51
|
+
|
|
52
|
+
new_content = None
|
|
53
|
+
|
|
54
|
+
new_content = apply_hunk(content, hunk)
|
|
55
|
+
if new_content:
|
|
56
|
+
return new_content
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def collapse_repeats(s):
|
|
60
|
+
return "".join(k for k, g in groupby(s))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def apply_hunk(content, hunk):
|
|
64
|
+
before_text, after_text = hunk_to_before_after(hunk)
|
|
65
|
+
|
|
66
|
+
res = directly_apply_hunk(content, hunk)
|
|
67
|
+
if res:
|
|
68
|
+
return res
|
|
69
|
+
|
|
70
|
+
hunk = make_new_lines_explicit(content, hunk)
|
|
71
|
+
|
|
72
|
+
# just consider space vs not-space
|
|
73
|
+
ops = "".join([line[0] for line in hunk])
|
|
74
|
+
ops = ops.replace("-", "x")
|
|
75
|
+
ops = ops.replace("+", "x")
|
|
76
|
+
ops = ops.replace("\n", " ")
|
|
77
|
+
|
|
78
|
+
cur_op = " "
|
|
79
|
+
section = []
|
|
80
|
+
sections = []
|
|
81
|
+
|
|
82
|
+
for i in range(len(ops)):
|
|
83
|
+
op = ops[i]
|
|
84
|
+
if op != cur_op:
|
|
85
|
+
sections.append(section)
|
|
86
|
+
section = []
|
|
87
|
+
cur_op = op
|
|
88
|
+
section.append(hunk[i])
|
|
89
|
+
|
|
90
|
+
sections.append(section)
|
|
91
|
+
if cur_op != " ":
|
|
92
|
+
sections.append([])
|
|
93
|
+
|
|
94
|
+
all_done = True
|
|
95
|
+
for i in range(2, len(sections), 2):
|
|
96
|
+
preceding_context = sections[i - 2]
|
|
97
|
+
changes = sections[i - 1]
|
|
98
|
+
following_context = sections[i]
|
|
99
|
+
|
|
100
|
+
res = apply_partial_hunk(content, preceding_context, changes, following_context)
|
|
101
|
+
if res:
|
|
102
|
+
content = res
|
|
103
|
+
else:
|
|
104
|
+
all_done = False
|
|
105
|
+
# FAILED!
|
|
106
|
+
# this_hunk = preceding_context + changes + following_context
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if all_done:
|
|
110
|
+
return content
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def flexi_just_search_and_replace(texts):
|
|
114
|
+
strategies = [
|
|
115
|
+
(search_and_replace, all_preprocs),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
return flexible_search_and_replace(texts, strategies)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def make_new_lines_explicit(content, hunk):
|
|
122
|
+
before, after = hunk_to_before_after(hunk)
|
|
123
|
+
|
|
124
|
+
diff = diff_lines(before, content)
|
|
125
|
+
|
|
126
|
+
back_diff = []
|
|
127
|
+
for line in diff:
|
|
128
|
+
if line[0] == "+":
|
|
129
|
+
continue
|
|
130
|
+
# if line[0] == "-":
|
|
131
|
+
# line = "+" + line[1:]
|
|
132
|
+
|
|
133
|
+
back_diff.append(line)
|
|
134
|
+
|
|
135
|
+
new_before = directly_apply_hunk(before, back_diff)
|
|
136
|
+
if not new_before:
|
|
137
|
+
return hunk
|
|
138
|
+
|
|
139
|
+
if len(new_before.strip()) < 10:
|
|
140
|
+
return hunk
|
|
141
|
+
|
|
142
|
+
before = before.splitlines(keepends=True)
|
|
143
|
+
new_before = new_before.splitlines(keepends=True)
|
|
144
|
+
after = after.splitlines(keepends=True)
|
|
145
|
+
|
|
146
|
+
if len(new_before) < len(before) * 0.66:
|
|
147
|
+
return hunk
|
|
148
|
+
|
|
149
|
+
new_hunk = difflib.unified_diff(new_before, after, n=max(len(new_before), len(after)))
|
|
150
|
+
new_hunk = list(new_hunk)[3:]
|
|
151
|
+
|
|
152
|
+
return new_hunk
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cleanup_pure_whitespace_lines(lines):
|
|
156
|
+
res = [
|
|
157
|
+
line if line.strip() else line[-(len(line) - len(line.rstrip("\r\n")))] for line in lines
|
|
158
|
+
]
|
|
159
|
+
return res
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def normalize_hunk(hunk):
|
|
163
|
+
before, after = hunk_to_before_after(hunk, lines=True)
|
|
164
|
+
|
|
165
|
+
before = cleanup_pure_whitespace_lines(before)
|
|
166
|
+
after = cleanup_pure_whitespace_lines(after)
|
|
167
|
+
|
|
168
|
+
diff = difflib.unified_diff(before, after, n=max(len(before), len(after)))
|
|
169
|
+
diff = list(diff)[3:]
|
|
170
|
+
return diff
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def directly_apply_hunk(content, hunk):
|
|
174
|
+
before, after = hunk_to_before_after(hunk)
|
|
175
|
+
|
|
176
|
+
if not before:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
before_lines, _ = hunk_to_before_after(hunk, lines=True)
|
|
180
|
+
before_lines = "".join([line.strip() for line in before_lines])
|
|
181
|
+
|
|
182
|
+
# Refuse to do a repeated search and replace on a tiny bit of non-whitespace context
|
|
183
|
+
if len(before_lines) < 10 and content.count(before) > 1:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
new_content = flexi_just_search_and_replace([before, after, content])
|
|
188
|
+
except SearchTextNotUnique:
|
|
189
|
+
new_content = None
|
|
190
|
+
|
|
191
|
+
return new_content
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def apply_partial_hunk(content, preceding_context, changes, following_context):
|
|
195
|
+
len_prec = len(preceding_context)
|
|
196
|
+
len_foll = len(following_context)
|
|
197
|
+
|
|
198
|
+
use_all = len_prec + len_foll
|
|
199
|
+
|
|
200
|
+
# if there is a - in the hunk, we can go all the way to `use=0`
|
|
201
|
+
for drop in range(use_all + 1):
|
|
202
|
+
use = use_all - drop
|
|
203
|
+
|
|
204
|
+
for use_prec in range(len_prec, -1, -1):
|
|
205
|
+
if use_prec > use:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
use_foll = use - use_prec
|
|
209
|
+
if use_foll > len_foll:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if use_prec:
|
|
213
|
+
this_prec = preceding_context[-use_prec:]
|
|
214
|
+
else:
|
|
215
|
+
this_prec = []
|
|
216
|
+
|
|
217
|
+
this_foll = following_context[:use_foll]
|
|
218
|
+
|
|
219
|
+
res = directly_apply_hunk(content, this_prec + changes + this_foll)
|
|
220
|
+
if res:
|
|
221
|
+
return res
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def find_diffs(content):
|
|
225
|
+
# We can always fence with triple-quotes, because all the udiff content
|
|
226
|
+
# is prefixed with +/-/space.
|
|
227
|
+
|
|
228
|
+
if not content.endswith("\n"):
|
|
229
|
+
content = content + "\n"
|
|
230
|
+
|
|
231
|
+
lines = content.splitlines(keepends=True)
|
|
232
|
+
line_num = 0
|
|
233
|
+
edits = []
|
|
234
|
+
while line_num < len(lines):
|
|
235
|
+
while line_num < len(lines):
|
|
236
|
+
line = lines[line_num]
|
|
237
|
+
if line.startswith("```diff"):
|
|
238
|
+
line_num, these_edits = process_fenced_block(lines, line_num + 1)
|
|
239
|
+
edits += these_edits
|
|
240
|
+
break
|
|
241
|
+
line_num += 1
|
|
242
|
+
|
|
243
|
+
# For now, just take 1!
|
|
244
|
+
# edits = edits[:1]
|
|
245
|
+
|
|
246
|
+
return edits
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def process_fenced_block(lines, start_line_num):
|
|
250
|
+
for line_num in range(start_line_num, len(lines)):
|
|
251
|
+
line = lines[line_num]
|
|
252
|
+
if line.startswith("```"):
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
block = lines[start_line_num:line_num]
|
|
256
|
+
block.append("@@ @@")
|
|
257
|
+
|
|
258
|
+
if block[0].startswith("--- ") and block[1].startswith("+++ "):
|
|
259
|
+
# Extract the file path, considering that it might contain spaces
|
|
260
|
+
fname = block[1][4:].strip()
|
|
261
|
+
block = block[2:]
|
|
262
|
+
else:
|
|
263
|
+
fname = None
|
|
264
|
+
|
|
265
|
+
edits = []
|
|
266
|
+
|
|
267
|
+
keeper = False
|
|
268
|
+
hunk = []
|
|
269
|
+
op = " "
|
|
270
|
+
for line in block:
|
|
271
|
+
hunk.append(line)
|
|
272
|
+
if len(line) < 2:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
if line.startswith("+++ ") and hunk[-2].startswith("--- "):
|
|
276
|
+
if hunk[-3] == "\n":
|
|
277
|
+
hunk = hunk[:-3]
|
|
278
|
+
else:
|
|
279
|
+
hunk = hunk[:-2]
|
|
280
|
+
|
|
281
|
+
edits.append((fname, hunk))
|
|
282
|
+
hunk = []
|
|
283
|
+
keeper = False
|
|
284
|
+
|
|
285
|
+
fname = line[4:].strip()
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
op = line[0]
|
|
289
|
+
if op in "-+":
|
|
290
|
+
keeper = True
|
|
291
|
+
continue
|
|
292
|
+
if op != "@":
|
|
293
|
+
continue
|
|
294
|
+
if not keeper:
|
|
295
|
+
hunk = []
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
hunk = hunk[:-1]
|
|
299
|
+
edits.append((fname, hunk))
|
|
300
|
+
hunk = []
|
|
301
|
+
keeper = False
|
|
302
|
+
|
|
303
|
+
return line_num + 1, edits
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def hunk_to_before_after(hunk, lines=False):
|
|
307
|
+
before = []
|
|
308
|
+
after = []
|
|
309
|
+
op = " "
|
|
310
|
+
for line in hunk:
|
|
311
|
+
if len(line) < 2:
|
|
312
|
+
op = " "
|
|
313
|
+
line = line
|
|
314
|
+
else:
|
|
315
|
+
op = line[0]
|
|
316
|
+
line = line[1:]
|
|
317
|
+
|
|
318
|
+
if op == " ":
|
|
319
|
+
before.append(line)
|
|
320
|
+
after.append(line)
|
|
321
|
+
elif op == "-":
|
|
322
|
+
before.append(line)
|
|
323
|
+
elif op == "+":
|
|
324
|
+
after.append(line)
|
|
325
|
+
|
|
326
|
+
if lines:
|
|
327
|
+
return before, after
|
|
328
|
+
|
|
329
|
+
before = "".join(before)
|
|
330
|
+
after = "".join(after)
|
|
331
|
+
|
|
332
|
+
return before, after
|
|
333
|
+
|
|
334
|
+
no_match_error = """UnifiedDiffNoMatch: hunk failed to apply!
|
|
335
|
+
|
|
336
|
+
{path} does not contain lines that match the diff you provided!
|
|
337
|
+
Try again.
|
|
338
|
+
DO NOT skip blank lines, comments, docstrings, etc!
|
|
339
|
+
The diff needs to apply cleanly to the lines in {path}!
|
|
340
|
+
|
|
341
|
+
{path} does not contain these {num_lines} exact lines in a row:
|
|
342
|
+
```
|
|
343
|
+
{original}```
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
not_unique_error = """UnifiedDiffNotUnique: hunk failed to apply!
|
|
348
|
+
|
|
349
|
+
{path} contains multiple sets of lines that match the diff you provided!
|
|
350
|
+
Try again.
|
|
351
|
+
Use additional ` ` lines to provide context that uniquely indicates which code needs to be changed.
|
|
352
|
+
The diff needs to apply to a unique set of lines in {path}!
|
|
353
|
+
|
|
354
|
+
{path} contains multiple copies of these {num_lines} lines:
|
|
355
|
+
```
|
|
356
|
+
{original}```
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
other_hunks_applied = (
|
|
360
|
+
"Note: some hunks did apply successfully. See the updated source code shown above.\n\n"
|
|
361
|
+
)
|
|
28
362
|
|
|
29
363
|
class CodeAutoMergeDiff:
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
llm: byzerllm.ByzerLLM,
|
|
33
|
-
args: AutoCoderArgs,
|
|
34
|
-
):
|
|
364
|
+
def __init__(self, llm:byzerllm.ByzerLLM,args:AutoCoderArgs):
|
|
35
365
|
self.llm = llm
|
|
36
366
|
self.args = args
|
|
37
|
-
self.printer = Printer()
|
|
38
|
-
|
|
39
|
-
def run_pylint(self, code: str) -> tuple[bool, str]:
|
|
40
|
-
with tempfile.NamedTemporaryFile(
|
|
41
|
-
mode="w", suffix=".py", delete=False
|
|
42
|
-
) as temp_file:
|
|
43
|
-
temp_file.write(code)
|
|
44
|
-
temp_file_path = temp_file.name
|
|
45
|
-
|
|
46
|
-
try:
|
|
47
|
-
result = subprocess.run(
|
|
48
|
-
[
|
|
49
|
-
"pylint",
|
|
50
|
-
"--disable=all",
|
|
51
|
-
"--enable=E0001,W0311,W0312",
|
|
52
|
-
temp_file_path,
|
|
53
|
-
],
|
|
54
|
-
capture_output=True,
|
|
55
|
-
text=True,
|
|
56
|
-
check=False,
|
|
57
|
-
)
|
|
58
|
-
os.unlink(temp_file_path)
|
|
59
|
-
if result.returncode != 0:
|
|
60
|
-
error_message = result.stdout.strip() or result.stderr.strip()
|
|
61
|
-
self.printer.print_in_terminal("pylint_check_failed", error_message=error_message)
|
|
62
|
-
return False, error_message
|
|
63
|
-
return True, ""
|
|
64
|
-
except subprocess.CalledProcessError as e:
|
|
65
|
-
error_message = f"Error running pylint: {str(e)}"
|
|
66
|
-
self.printer.print_in_terminal("pylint_error", error_message=error_message)
|
|
67
|
-
os.unlink(temp_file_path)
|
|
68
|
-
return False, error_message
|
|
69
|
-
|
|
70
|
-
def get_edits(self, content: str):
|
|
71
|
-
edits = self.parse_whole_text(content)
|
|
72
|
-
result = []
|
|
73
|
-
for edit in edits:
|
|
74
|
-
result.append((edit.path, edit.content))
|
|
75
|
-
return result
|
|
76
|
-
|
|
77
|
-
def get_source_code_list_from_shadow_files(self, shadow_files: Dict[str, str]) -> SourceCodeList:
|
|
78
|
-
"""
|
|
79
|
-
将影子文件转换为SourceCodeList对象
|
|
80
|
-
|
|
81
|
-
参数:
|
|
82
|
-
shadow_files (Dict[str, str]): 映射 {影子文件路径: 内容}
|
|
83
|
-
|
|
84
|
-
返回:
|
|
85
|
-
SourceCodeList: 包含原始路径和内容的SourceCodeList对象
|
|
86
|
-
"""
|
|
87
|
-
sources = []
|
|
88
|
-
shadow_manager = ShadowManager(self.args.source_dir,event_file_id=self.args.event_file)
|
|
89
|
-
for shadow_path, content in shadow_files.items():
|
|
90
|
-
# 将影子路径转换回原始文件路径
|
|
91
|
-
file_path = shadow_manager.from_shadow_path(shadow_path)
|
|
92
|
-
# 创建SourceCode对象并添加到sources列表
|
|
93
|
-
source = SourceCode(module_name=file_path, source_code=content)
|
|
94
|
-
sources.append(source)
|
|
95
|
-
|
|
96
|
-
return SourceCodeList(sources)
|
|
367
|
+
self.printer = Printer()
|
|
97
368
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- list of (file_path, new_content) tuples for successfully merged blocks
|
|
102
|
-
- list of (file_path, hunk) tuples for failed to merge blocks"""
|
|
103
|
-
edits = self.get_edits(content)
|
|
104
|
-
file_content_mapping = {}
|
|
105
|
-
failed_blocks = []
|
|
369
|
+
def get_edits(self,content:str):
|
|
370
|
+
# might raise ValueError for malformed ORIG/UPD blocks
|
|
371
|
+
raw_edits = list(find_diffs(content))
|
|
106
372
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
373
|
+
last_path = None
|
|
374
|
+
edits = []
|
|
375
|
+
for path, hunk in raw_edits:
|
|
376
|
+
if path:
|
|
377
|
+
last_path = path
|
|
110
378
|
else:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
existing_content = file_content_mapping[path]
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
new_content = self.apply_hunk(existing_content, hunk)
|
|
117
|
-
if new_content:
|
|
118
|
-
file_content_mapping[path] = new_content
|
|
119
|
-
else:
|
|
120
|
-
failed_blocks.append((path, hunk))
|
|
121
|
-
except Exception as e:
|
|
122
|
-
failed_blocks.append((path, hunk))
|
|
379
|
+
path = last_path
|
|
380
|
+
edits.append((path, hunk))
|
|
123
381
|
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# Split hunk into lines
|
|
136
|
-
lines = hunk.splitlines()
|
|
137
|
-
|
|
138
|
-
# Extract file paths
|
|
139
|
-
old_path = lines[0].replace("--- ", "").strip()
|
|
140
|
-
new_path = lines[1].replace("+++ ", "").strip()
|
|
382
|
+
return edits
|
|
383
|
+
|
|
384
|
+
def merge_code(self, generate_result: CodeGenerateResult, force_skip_git: bool = False):
|
|
385
|
+
result = self.choose_best_choice(generate_result)
|
|
386
|
+
self._merge_code(result.contents[0], force_skip_git)
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
def choose_best_choice(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
|
|
390
|
+
if len(generate_result.contents) == 1:
|
|
391
|
+
return generate_result
|
|
141
392
|
|
|
142
|
-
|
|
143
|
-
|
|
393
|
+
merge_results = []
|
|
394
|
+
for content,conversations in zip(generate_result.contents,generate_result.conversations):
|
|
395
|
+
merge_result = self._merge_code_without_effect(content)
|
|
396
|
+
merge_results.append(merge_result)
|
|
397
|
+
|
|
398
|
+
# If all merge results are None, return first one
|
|
399
|
+
if all(len(result.failed_blocks) != 0 for result in merge_results):
|
|
400
|
+
self.printer.print_in_terminal("all_merge_results_failed")
|
|
401
|
+
return CodeGenerateResult(contents=[generate_result.contents[0]], conversations=[generate_result.conversations[0]])
|
|
144
402
|
|
|
145
|
-
#
|
|
146
|
-
for i,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
403
|
+
# If only one merge result is not None, return that one
|
|
404
|
+
not_none_indices = [i for i, result in enumerate(merge_results) if len(result.failed_blocks) == 0]
|
|
405
|
+
if len(not_none_indices) == 1:
|
|
406
|
+
idx = not_none_indices[0]
|
|
407
|
+
self.printer.print_in_terminal("only_one_merge_result_success")
|
|
408
|
+
return CodeGenerateResult(contents=[generate_result.contents[idx]], conversations=[generate_result.conversations[idx]])
|
|
409
|
+
|
|
410
|
+
# 最后,如果有多个,那么根据质量排序再返回
|
|
411
|
+
ranker = CodeModificationRanker(self.llm, self.args)
|
|
412
|
+
ranked_result = ranker.rank_modifications(generate_result,merge_results)
|
|
413
|
+
|
|
414
|
+
## 得到的结果,再做一次合并,第一个通过的返回 , 返回做合并有点重复低效,未来修改。
|
|
415
|
+
for content,conversations in zip(ranked_result.contents,ranked_result.conversations):
|
|
416
|
+
merge_result = self._merge_code_without_effect(content)
|
|
417
|
+
if not merge_result.failed_blocks:
|
|
418
|
+
return CodeGenerateResult(contents=[content], conversations=[conversations])
|
|
419
|
+
|
|
420
|
+
# 最后保底,但实际不会出现
|
|
421
|
+
return CodeGenerateResult(contents=[ranked_result.contents[0]], conversations=[ranked_result.conversations[0]])
|
|
422
|
+
|
|
423
|
+
@byzerllm.prompt(render="jinja2")
|
|
424
|
+
def git_require_msg(self,source_dir:str,error:str)->str:
|
|
425
|
+
'''
|
|
426
|
+
auto_merge only works for git repositories.
|
|
427
|
+
|
|
428
|
+
Try to use git init in the source directory.
|
|
153
429
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
fromfile=old_path,
|
|
159
|
-
tofile=new_path)
|
|
160
|
-
# Convert back to string
|
|
161
|
-
return "".join(result)
|
|
162
|
-
except Exception as e:
|
|
163
|
-
return None
|
|
164
|
-
|
|
165
|
-
def _merge_code(self, content: str, force_skip_git: bool = False):
|
|
166
|
-
file_content = FileUtils.read_file(self.args.file)
|
|
167
|
-
md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
|
|
168
|
-
file_name = os.path.basename(self.args.file)
|
|
430
|
+
```shell
|
|
431
|
+
cd {{ source_dir }}
|
|
432
|
+
git init .
|
|
433
|
+
```
|
|
169
434
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
435
|
+
Then try to run auto-coder again.
|
|
436
|
+
Error: {{ error }}
|
|
437
|
+
'''
|
|
438
|
+
|
|
439
|
+
def abs_root_path(self, path):
|
|
440
|
+
if path.startswith(self.args.source_dir):
|
|
441
|
+
return safe_abs_path(Path(path))
|
|
442
|
+
res = Path(self.args.source_dir) / path
|
|
443
|
+
return safe_abs_path(res)
|
|
175
444
|
|
|
176
|
-
|
|
177
|
-
|
|
445
|
+
def apply_edits(self, edits):
|
|
446
|
+
seen = set()
|
|
447
|
+
uniq = []
|
|
178
448
|
for path, hunk in edits:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if unmerged_blocks:
|
|
200
|
-
if self.args.request_id and not self.args.skip_events:
|
|
201
|
-
# collect unmerged blocks
|
|
202
|
-
event_data = []
|
|
203
|
-
for file_path, head, update, similarity in unmerged_blocks:
|
|
204
|
-
event_data.append(
|
|
205
|
-
{
|
|
206
|
-
"file_path": file_path,
|
|
207
|
-
"head": head,
|
|
208
|
-
"update": update,
|
|
209
|
-
"similarity": similarity,
|
|
210
|
-
}
|
|
211
|
-
)
|
|
212
|
-
return
|
|
449
|
+
hunk = normalize_hunk(hunk)
|
|
450
|
+
if not hunk:
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
this = [path + "\n"] + hunk
|
|
454
|
+
this = "".join(this)
|
|
455
|
+
|
|
456
|
+
if this in seen:
|
|
457
|
+
continue
|
|
458
|
+
seen.add(this)
|
|
459
|
+
|
|
460
|
+
uniq.append((path, hunk))
|
|
461
|
+
|
|
462
|
+
errors = []
|
|
463
|
+
for path, hunk in uniq:
|
|
464
|
+
full_path = self.abs_root_path(path)
|
|
465
|
+
|
|
466
|
+
if not os.path.exists(full_path):
|
|
467
|
+
with open(full_path, "w") as f:
|
|
468
|
+
f.write("")
|
|
213
469
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# lint check
|
|
219
|
-
for file_path, new_content in file_content_mapping.items():
|
|
220
|
-
if file_path.endswith(".py"):
|
|
221
|
-
pylint_passed, error_message = self.run_pylint(new_content)
|
|
222
|
-
if not pylint_passed:
|
|
223
|
-
self.printer.print_in_terminal("pylint_file_check_failed",
|
|
224
|
-
file_path=file_path,
|
|
225
|
-
error_message=error_message)
|
|
226
|
-
|
|
227
|
-
if changes_made and not force_skip_git and not self.args.skip_commit:
|
|
470
|
+
content = FileUtils.read_file(full_path)
|
|
471
|
+
|
|
472
|
+
original, _ = hunk_to_before_after(hunk)
|
|
473
|
+
|
|
228
474
|
try:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
style="red"
|
|
236
|
-
)
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
# Now, apply the changes
|
|
240
|
-
for file_path, new_content in file_content_mapping.items():
|
|
241
|
-
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
242
|
-
with open(file_path, "w") as f:
|
|
243
|
-
f.write(new_content)
|
|
244
|
-
|
|
245
|
-
if self.args.request_id and not self.args.skip_events:
|
|
246
|
-
# collect modified files
|
|
247
|
-
event_data = []
|
|
248
|
-
for code in merged_blocks:
|
|
249
|
-
file_path, head, update, similarity = code
|
|
250
|
-
event_data.append(
|
|
251
|
-
{
|
|
252
|
-
"file_path": file_path,
|
|
253
|
-
"head": head,
|
|
254
|
-
"update": update,
|
|
255
|
-
"similarity": similarity,
|
|
256
|
-
}
|
|
475
|
+
content = do_replace(full_path, content, hunk)
|
|
476
|
+
except SearchTextNotUnique:
|
|
477
|
+
errors.append(
|
|
478
|
+
not_unique_error.format(
|
|
479
|
+
path=path, original=original, num_lines=len(original.splitlines())
|
|
480
|
+
)
|
|
257
481
|
)
|
|
482
|
+
continue
|
|
258
483
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
self.args.source_dir,
|
|
264
|
-
f"{self.args.query}\nauto_coder_{file_name}",
|
|
484
|
+
if not content:
|
|
485
|
+
errors.append(
|
|
486
|
+
no_match_error.format(
|
|
487
|
+
path=path, original=original, num_lines=len(original.splitlines())
|
|
265
488
|
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
add_updated_urls.append(os.path.join(self.args.source_dir, file))
|
|
273
|
-
|
|
274
|
-
self.args.add_updated_urls = add_updated_urls
|
|
275
|
-
update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
|
|
276
|
-
if not update_yaml_success:
|
|
277
|
-
self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
|
|
278
|
-
|
|
279
|
-
if self.args.enable_active_context:
|
|
280
|
-
active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
|
|
281
|
-
task_id = active_context_manager.process_changes(self.args)
|
|
282
|
-
self.printer.print_in_terminal("active_context_background_task",
|
|
283
|
-
style="blue",
|
|
284
|
-
task_id=task_id)
|
|
285
|
-
git_utils.print_commit_info(commit_result=commit_result)
|
|
286
|
-
except Exception as e:
|
|
287
|
-
self.printer.print_str_in_terminal(
|
|
288
|
-
self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
|
|
289
|
-
style="red"
|
|
290
|
-
)
|
|
291
|
-
else:
|
|
292
|
-
self.print_merged_blocks(merged_blocks)
|
|
293
|
-
|
|
294
|
-
self.printer.print_in_terminal("merge_success",
|
|
295
|
-
num_files=len(file_content_mapping.keys()),
|
|
296
|
-
num_changes=len(changes_to_make),
|
|
297
|
-
total_blocks=len(edits))
|
|
298
|
-
|
|
299
|
-
else:
|
|
300
|
-
self.printer.print_in_terminal("no_changes_made")
|
|
301
|
-
|
|
302
|
-
def _print_unmerged_blocks(self, unmerged_blocks: List[tuple]):
|
|
303
|
-
self.printer.print_in_terminal("unmerged_blocks_title", style="bold red")
|
|
304
|
-
for file_path, head, update, similarity in unmerged_blocks:
|
|
305
|
-
self.printer.print_str_in_terminal(
|
|
306
|
-
f"\n{self.printer.get_message_from_key_with_format('unmerged_file_path',file_path=file_path)}",
|
|
307
|
-
style="bold blue"
|
|
308
|
-
)
|
|
309
|
-
self.printer.print_str_in_terminal(
|
|
310
|
-
f"\n{self.printer.get_message_from_key_with_format('unmerged_search_block',similarity=similarity)}",
|
|
311
|
-
style="bold green"
|
|
312
|
-
)
|
|
313
|
-
syntax = Syntax(head, "python", theme="monokai", line_numbers=True)
|
|
314
|
-
self.printer.console.print(Panel(syntax, expand=False))
|
|
315
|
-
self.printer.print_in_terminal("unmerged_replace_block", style="bold yellow")
|
|
316
|
-
syntax = Syntax(update, "python", theme="monokai", line_numbers=True)
|
|
317
|
-
self.printer.console.print(Panel(syntax, expand=False))
|
|
318
|
-
self.printer.print_in_terminal("unmerged_blocks_total", num_blocks=len(unmerged_blocks), style="bold red")
|
|
489
|
+
)
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
# SUCCESS!
|
|
493
|
+
with open(full_path, "w") as f:
|
|
494
|
+
f.write(content)
|
|
319
495
|
|
|
496
|
+
if errors:
|
|
497
|
+
errors = "\n\n".join(errors)
|
|
498
|
+
if len(errors) < len(uniq):
|
|
499
|
+
errors += other_hunks_applied
|
|
500
|
+
raise ValueError(errors)
|
|
320
501
|
|
|
321
|
-
def
|
|
322
|
-
"""
|
|
502
|
+
def _merge_code_without_effect(self, content: str) -> MergeCodeWithoutEffect:
|
|
503
|
+
"""Merge code without any side effects like git operations or file writing.
|
|
504
|
+
Returns a tuple of:
|
|
505
|
+
- list of (file_path, new_content) tuples for successfully merged blocks
|
|
506
|
+
- list of (file_path, hunk) tuples for failed to merge blocks"""
|
|
507
|
+
edits = self.get_edits(content)
|
|
508
|
+
file_content_mapping = {}
|
|
509
|
+
failed_blocks = []
|
|
510
|
+
|
|
511
|
+
for path, hunk in edits:
|
|
512
|
+
full_path = self.abs_root_path(path)
|
|
513
|
+
if not os.path.exists(full_path):
|
|
514
|
+
_, after = hunk_to_before_after(hunk)
|
|
515
|
+
file_content_mapping[full_path] = after
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
if full_path not in file_content_mapping:
|
|
519
|
+
file_content_mapping[full_path] = FileUtils.read_file(full_path)
|
|
520
|
+
|
|
521
|
+
content = file_content_mapping[full_path]
|
|
522
|
+
new_content = do_replace(full_path, content, hunk)
|
|
523
|
+
if new_content:
|
|
524
|
+
file_content_mapping[full_path] = new_content
|
|
525
|
+
else:
|
|
526
|
+
failed_blocks.append((full_path, "\n".join(hunk)))
|
|
527
|
+
|
|
528
|
+
return MergeCodeWithoutEffect(
|
|
529
|
+
success_blocks=[(path, content) for path, content in file_content_mapping.items()],
|
|
530
|
+
failed_blocks=failed_blocks
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
def print_edits(self, edits: List[Tuple[str, List[str]]]):
|
|
534
|
+
"""Print diffs for user review using rich library"""
|
|
323
535
|
from rich.syntax import Syntax
|
|
324
536
|
from rich.panel import Panel
|
|
325
537
|
|
|
326
|
-
# Group
|
|
327
|
-
|
|
328
|
-
for
|
|
329
|
-
if
|
|
330
|
-
|
|
331
|
-
|
|
538
|
+
# Group edits by file path
|
|
539
|
+
file_edits = {}
|
|
540
|
+
for path, hunk in edits:
|
|
541
|
+
if path not in file_edits:
|
|
542
|
+
file_edits[path] = []
|
|
543
|
+
file_edits[path].append(hunk)
|
|
332
544
|
|
|
333
545
|
# Generate formatted text for each file
|
|
334
546
|
formatted_text = ""
|
|
335
|
-
for
|
|
336
|
-
formatted_text += f"##File: {
|
|
337
|
-
for
|
|
338
|
-
formatted_text += "
|
|
339
|
-
formatted_text += head + "\n"
|
|
340
|
-
formatted_text += "=======\n"
|
|
341
|
-
formatted_text += update + "\n"
|
|
342
|
-
formatted_text += ">>>>>>> REPLACE\n"
|
|
547
|
+
for path, hunks in file_edits.items():
|
|
548
|
+
formatted_text += f"##File: {path}\n"
|
|
549
|
+
for hunk in hunks:
|
|
550
|
+
formatted_text += "".join(hunk)
|
|
343
551
|
formatted_text += "\n"
|
|
344
552
|
|
|
345
553
|
# Print with rich panel
|
|
346
|
-
self.printer.print_in_terminal("
|
|
554
|
+
self.printer.print_in_terminal("edits_title", style="bold green")
|
|
347
555
|
self.printer.console.print(
|
|
348
556
|
Panel(
|
|
349
557
|
Syntax(formatted_text, "diff", theme="monokai"),
|
|
350
|
-
title="
|
|
558
|
+
title="Edits",
|
|
351
559
|
border_style="green",
|
|
352
560
|
expand=False
|
|
353
561
|
)
|
|
354
|
-
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def _merge_code(self, content: str,force_skip_git:bool=False):
|
|
565
|
+
total = 0
|
|
566
|
+
|
|
567
|
+
file_content = FileUtils.read_file(self.args.file)
|
|
568
|
+
md5 = hashlib.md5(file_content.encode('utf-8')).hexdigest()
|
|
569
|
+
# get the file name
|
|
570
|
+
file_name = os.path.basename(self.args.file)
|
|
571
|
+
|
|
572
|
+
if not force_skip_git and not self.args.skip_commit:
|
|
573
|
+
try:
|
|
574
|
+
git_utils.commit_changes(self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}")
|
|
575
|
+
except Exception as e:
|
|
576
|
+
self.printer.print_in_terminal("git_init_required", style="red", source_dir=self.args.source_dir, error=str(e))
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
edits = self.get_edits(content)
|
|
580
|
+
self.apply_edits(edits)
|
|
581
|
+
|
|
582
|
+
self.printer.print_in_terminal("files_merged_total", total=total)
|
|
583
|
+
if not force_skip_git and not self.args.skip_commit:
|
|
584
|
+
commit_result = git_utils.commit_changes(self.args.source_dir, f"{self.args.query}\nauto_coder_{file_name}")
|
|
585
|
+
|
|
586
|
+
action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
|
|
587
|
+
action_file_name = os.path.basename(self.args.file)
|
|
588
|
+
add_updated_urls = []
|
|
589
|
+
commit_result.changed_files
|
|
590
|
+
for file in commit_result.changed_files:
|
|
591
|
+
add_updated_urls.append(os.path.join(self.args.source_dir, file))
|
|
592
|
+
|
|
593
|
+
self.args.add_updated_urls = add_updated_urls
|
|
594
|
+
update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
|
|
595
|
+
if not update_yaml_success:
|
|
596
|
+
self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
|
|
597
|
+
|
|
598
|
+
if self.args.enable_active_context:
|
|
599
|
+
active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
|
|
600
|
+
task_id = active_context_manager.process_changes(self.args)
|
|
601
|
+
self.printer.print_in_terminal("active_context_background_task",
|
|
602
|
+
style="blue",
|
|
603
|
+
task_id=task_id)
|
|
604
|
+
|
|
605
|
+
git_utils.print_commit_info(commit_result=commit_result)
|
|
606
|
+
else:
|
|
607
|
+
# Print edits for review
|
|
608
|
+
self.print_edits(edits)
|