autocoder-nano 0.1.26__py3-none-any.whl → 0.1.28__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.
- autocoder_nano/agent/agent_base.py +376 -63
- autocoder_nano/auto_coder_nano.py +89 -1106
- autocoder_nano/edit/__init__.py +20 -0
- autocoder_nano/edit/actions.py +136 -0
- autocoder_nano/edit/code/__init__.py +0 -0
- autocoder_nano/edit/code/generate_editblock.py +403 -0
- autocoder_nano/edit/code/merge_editblock.py +418 -0
- autocoder_nano/edit/code/modification_ranker.py +90 -0
- autocoder_nano/edit/text.py +38 -0
- autocoder_nano/index/index_manager.py +1 -0
- autocoder_nano/index/symbols_utils.py +43 -0
- autocoder_nano/llm_types.py +1 -0
- autocoder_nano/project/pyproject.py +1 -1
- autocoder_nano/project/suffixproject.py +1 -1
- autocoder_nano/version.py +1 -1
- autocoder_nano-0.1.28.dist-info/METADATA +445 -0
- {autocoder_nano-0.1.26.dist-info → autocoder_nano-0.1.28.dist-info}/RECORD +21 -13
- autocoder_nano-0.1.26.dist-info/METADATA +0 -432
- {autocoder_nano-0.1.26.dist-info → autocoder_nano-0.1.28.dist-info}/LICENSE +0 -0
- {autocoder_nano-0.1.26.dist-info → autocoder_nano-0.1.28.dist-info}/WHEEL +0 -0
- {autocoder_nano-0.1.26.dist-info → autocoder_nano-0.1.28.dist-info}/entry_points.txt +0 -0
- {autocoder_nano-0.1.26.dist-info → autocoder_nano-0.1.28.dist-info}/top_level.txt +0 -0
@@ -2,21 +2,18 @@ import argparse
|
|
2
2
|
import glob
|
3
3
|
import hashlib
|
4
4
|
import os
|
5
|
-
import re
|
6
5
|
import json
|
7
6
|
import shutil
|
8
7
|
import subprocess
|
9
|
-
import tempfile
|
10
8
|
import textwrap
|
11
9
|
import time
|
12
|
-
import traceback
|
13
10
|
import uuid
|
14
|
-
from difflib import SequenceMatcher
|
15
11
|
|
16
|
-
from autocoder_nano.
|
12
|
+
from autocoder_nano.edit import Dispacher
|
17
13
|
from autocoder_nano.helper import show_help
|
18
14
|
from autocoder_nano.index.entry import build_index_and_filter_files
|
19
15
|
from autocoder_nano.index.index_manager import IndexManager
|
16
|
+
from autocoder_nano.index.symbols_utils import extract_symbols
|
20
17
|
from autocoder_nano.llm_client import AutoLLM
|
21
18
|
from autocoder_nano.version import __version__
|
22
19
|
from autocoder_nano.llm_types import *
|
@@ -55,7 +52,7 @@ base_persist_dir = os.path.join(project_root, ".auto-coder", "plugins", "chat-au
|
|
55
52
|
# ".vscode", ".idea", ".hg"]
|
56
53
|
commands = [
|
57
54
|
"/add_files", "/remove_files", "/list_files", "/conf", "/coding", "/chat", "/revert", "/index/query",
|
58
|
-
"/index/build", "/exclude_dirs", "/help", "/shell", "/exit", "/mode", "/models", "/commit", "/new"
|
55
|
+
"/index/build", "/exclude_dirs", "/exclude_files", "/help", "/shell", "/exit", "/mode", "/models", "/commit", "/new"
|
59
56
|
]
|
60
57
|
|
61
58
|
memory = {
|
@@ -77,29 +74,6 @@ memory = {
|
|
77
74
|
args: AutoCoderArgs = AutoCoderArgs()
|
78
75
|
|
79
76
|
|
80
|
-
def extract_symbols(text: str) -> SymbolsInfo:
|
81
|
-
patterns = {
|
82
|
-
"usage": r"用途:(.+)",
|
83
|
-
"functions": r"函数:(.+)",
|
84
|
-
"variables": r"变量:(.+)",
|
85
|
-
"classes": r"类:(.+)",
|
86
|
-
"import_statements": r"导入语句:(.+)",
|
87
|
-
}
|
88
|
-
|
89
|
-
info = SymbolsInfo()
|
90
|
-
for field, pattern in patterns.items():
|
91
|
-
match = re.search(pattern, text)
|
92
|
-
if match:
|
93
|
-
value = match.group(1).strip()
|
94
|
-
if field == "import_statements":
|
95
|
-
value = [v.strip() for v in value.split("^^")]
|
96
|
-
elif field == "functions" or field == "variables" or field == "classes":
|
97
|
-
value = [v.strip() for v in value.split(",")]
|
98
|
-
setattr(info, field, value)
|
99
|
-
|
100
|
-
return info
|
101
|
-
|
102
|
-
|
103
77
|
def get_all_file_names_in_project() -> List[str]:
|
104
78
|
file_names = []
|
105
79
|
final_exclude_dirs = default_exclude_dirs + memory.get("exclude_dirs", [])
|
@@ -223,7 +197,9 @@ COMMANDS = {
|
|
223
197
|
"/conf": "",
|
224
198
|
"/mode": "",
|
225
199
|
"/models": ""
|
226
|
-
}
|
200
|
+
},
|
201
|
+
"/exclude_files": {"/list": "", "/drop": ""},
|
202
|
+
"/exclude_dirs": {}
|
227
203
|
}
|
228
204
|
|
229
205
|
|
@@ -701,6 +677,15 @@ class CommandCompleter(Completer):
|
|
701
677
|
if current_word and current_word in file_name:
|
702
678
|
yield Completion(file_name, start_position=-len(current_word))
|
703
679
|
|
680
|
+
elif words[0] == "/exclude_files":
|
681
|
+
new_text = text[len("/exclude_files"):]
|
682
|
+
parser = CommandTextParser(new_text, words[0])
|
683
|
+
parser.add_files()
|
684
|
+
current_word = parser.current_word()
|
685
|
+
for command in parser.get_sub_commands():
|
686
|
+
if command.startswith(current_word):
|
687
|
+
yield Completion(command, start_position=-len(current_word))
|
688
|
+
|
704
689
|
elif words[0] == "/models":
|
705
690
|
new_text = text[len("/models"):]
|
706
691
|
parser = CommandTextParser(new_text, words[0])
|
@@ -798,20 +783,69 @@ def load_memory():
|
|
798
783
|
completer.update_current_files(memory["current_files"]["files"])
|
799
784
|
|
800
785
|
|
801
|
-
def
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
786
|
+
def exclude_dirs(dir_names: List[str]):
|
787
|
+
new_dirs = dir_names
|
788
|
+
existing_dirs = memory.get("exclude_dirs", [])
|
789
|
+
dirs_to_add = [d for d in new_dirs if d not in existing_dirs]
|
790
|
+
|
791
|
+
if dirs_to_add:
|
792
|
+
existing_dirs.extend(dirs_to_add)
|
793
|
+
if "exclude_dirs" not in memory:
|
794
|
+
memory["exclude_dirs"] = existing_dirs
|
795
|
+
print(f"Added exclude dirs: {dirs_to_add}")
|
796
|
+
for d in dirs_to_add:
|
797
|
+
exclude_files(f"regex://.*/{d}/*.")
|
798
|
+
# exclude_files([f"regex://.*/{d}/*." for d in dirs_to_add])
|
799
|
+
else:
|
800
|
+
print("All specified dirs are already in the exclude list.")
|
801
|
+
save_memory()
|
802
|
+
completer.refresh_files()
|
813
803
|
|
814
|
-
|
804
|
+
|
805
|
+
def exclude_files(query: str):
|
806
|
+
if "/list" in query:
|
807
|
+
query = query.replace("/list", "", 1).strip()
|
808
|
+
existing_file_patterns = memory.get("exclude_files", [])
|
809
|
+
|
810
|
+
# 打印表格
|
811
|
+
table = Table(title="Exclude Files")
|
812
|
+
table.add_column("File Pattern")
|
813
|
+
for file_pattern in existing_file_patterns:
|
814
|
+
table.add_row(file_pattern)
|
815
|
+
console.print(table)
|
816
|
+
return
|
817
|
+
|
818
|
+
if "/drop" in query:
|
819
|
+
query = query.replace("/drop", "", 1).strip()
|
820
|
+
existing_file_patterns = memory.get("exclude_files", [])
|
821
|
+
existing_file_patterns.remove(query.strip())
|
822
|
+
memory["exclude_files"] = existing_file_patterns
|
823
|
+
if query.startswith("regex://.*/") and query.endswith("/*."):
|
824
|
+
existing_dirs_patterns = memory.get("exclude_dirs", [])
|
825
|
+
dir_query = query.replace("regex://.*/", "", 1).replace("/*.", "", 1)
|
826
|
+
if dir_query in existing_dirs_patterns:
|
827
|
+
existing_dirs_patterns.remove(dir_query.strip())
|
828
|
+
save_memory()
|
829
|
+
completer.refresh_files()
|
830
|
+
return
|
831
|
+
|
832
|
+
new_file_patterns = query.strip().split(",")
|
833
|
+
|
834
|
+
existing_file_patterns = memory.get("exclude_files", [])
|
835
|
+
file_patterns_to_add = [f for f in new_file_patterns if f not in existing_file_patterns]
|
836
|
+
|
837
|
+
for file_pattern in file_patterns_to_add:
|
838
|
+
if not file_pattern.startswith("regex://"):
|
839
|
+
raise
|
840
|
+
|
841
|
+
if file_patterns_to_add:
|
842
|
+
existing_file_patterns.extend(file_patterns_to_add)
|
843
|
+
if "exclude_files" not in memory:
|
844
|
+
memory["exclude_files"] = existing_file_patterns
|
845
|
+
save_memory()
|
846
|
+
print(f"Added exclude files: {file_patterns_to_add}")
|
847
|
+
else:
|
848
|
+
print("All specified files are already in the exclude list.")
|
815
849
|
|
816
850
|
|
817
851
|
def index_command(llm):
|
@@ -1007,7 +1041,8 @@ def update_config_to_args(query, delete_execute_file: bool = False):
|
|
1007
1041
|
"skip_confirm": conf.get("skip_confirm", "true") == "true",
|
1008
1042
|
"chat_model": conf.get("chat_model", ""),
|
1009
1043
|
"code_model": conf.get("code_model", ""),
|
1010
|
-
"auto_merge": conf.get("auto_merge", "editblock")
|
1044
|
+
"auto_merge": conf.get("auto_merge", "editblock"),
|
1045
|
+
"exclude_files": memory.get("exclude_files", [])
|
1011
1046
|
}
|
1012
1047
|
current_files = memory["current_files"]["files"]
|
1013
1048
|
yaml_config["urls"] = current_files
|
@@ -1220,30 +1255,6 @@ def chat(query: str, llm: AutoLLM):
|
|
1220
1255
|
return
|
1221
1256
|
|
1222
1257
|
|
1223
|
-
def git_print_commit_info(commit_result: CommitResult):
|
1224
|
-
table = Table(
|
1225
|
-
title="Commit Information (Use /revert to revert this commit)", show_header=True, header_style="bold magenta"
|
1226
|
-
)
|
1227
|
-
table.add_column("Attribute", style="cyan", no_wrap=True)
|
1228
|
-
table.add_column("Value", style="green")
|
1229
|
-
|
1230
|
-
table.add_row("Commit Hash", commit_result.commit_hash)
|
1231
|
-
table.add_row("Commit Message", commit_result.commit_message)
|
1232
|
-
table.add_row("Changed Files", "\n".join(commit_result.changed_files))
|
1233
|
-
|
1234
|
-
console.print(
|
1235
|
-
Panel(table, expand=False, border_style="green", title="Git Commit Summary")
|
1236
|
-
)
|
1237
|
-
|
1238
|
-
if commit_result.diffs:
|
1239
|
-
for file, diff in commit_result.diffs.items():
|
1240
|
-
console.print(f"\n[bold blue]File: {file}[/bold blue]")
|
1241
|
-
syntax = Syntax(diff, "diff", theme="monokai", line_numbers=True)
|
1242
|
-
console.print(
|
1243
|
-
Panel(syntax, expand=False, border_style="yellow", title="File Diff")
|
1244
|
-
)
|
1245
|
-
|
1246
|
-
|
1247
1258
|
def init_project():
|
1248
1259
|
if not args.project_type:
|
1249
1260
|
logger.error(
|
@@ -1316,1040 +1327,6 @@ def load_include_files(config, base_path, max_depth=10, current_depth=0):
|
|
1316
1327
|
return config
|
1317
1328
|
|
1318
1329
|
|
1319
|
-
class CodeAutoGenerateEditBlock:
|
1320
|
-
def __init__(self, llm: AutoLLM, action=None, fence_0: str = "```", fence_1: str = "```"):
|
1321
|
-
self.llm = llm
|
1322
|
-
# self.llm.setup_default_model_name(memory["conf"]["current_code_model"])
|
1323
|
-
self.args = args
|
1324
|
-
self.action = action
|
1325
|
-
self.fence_0 = fence_0
|
1326
|
-
self.fence_1 = fence_1
|
1327
|
-
if not self.llm:
|
1328
|
-
raise ValueError("Please provide a valid model instance to use for code generation.")
|
1329
|
-
self.llms = [self.llm]
|
1330
|
-
|
1331
|
-
@prompt()
|
1332
|
-
def single_round_instruction(self, instruction: str, content: str, context: str = ""):
|
1333
|
-
"""
|
1334
|
-
如果你需要生成代码,对于每个需要更改的文件,你需要按 *SEARCH/REPLACE block* 的格式进行生成。
|
1335
|
-
|
1336
|
-
# *SEARCH/REPLACE block* Rules:
|
1337
|
-
|
1338
|
-
Every *SEARCH/REPLACE block* must use this format:
|
1339
|
-
1. The opening fence and code language, eg: {{ fence_0 }}python
|
1340
|
-
2. The file path alone on a line, starting with "##File:" and verbatim. No bold asterisks, no quotes around it,
|
1341
|
-
no escaping of characters, etc.
|
1342
|
-
3. The start of search block: <<<<<<< SEARCH
|
1343
|
-
4. A contiguous chunk of lines to search for in the existing source code
|
1344
|
-
5. The dividing line: =======
|
1345
|
-
6. The lines to replace into the source code
|
1346
|
-
7. The end of the replacement block: >>>>>>> REPLACE
|
1347
|
-
8. The closing fence: {{ fence_1 }}
|
1348
|
-
|
1349
|
-
Every *SEARCH* section must *EXACTLY MATCH* the existing source code, character for character,
|
1350
|
-
including all comments, docstrings, etc.
|
1351
|
-
|
1352
|
-
*SEARCH/REPLACE* blocks will replace *all* matching occurrences.
|
1353
|
-
Include enough lines to make the SEARCH blocks unique.
|
1354
|
-
|
1355
|
-
Include *ALL* the code being searched and replaced!
|
1356
|
-
|
1357
|
-
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location,
|
1358
|
-
1 to insert it in the new location.
|
1359
|
-
|
1360
|
-
If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
|
1361
|
-
- A new file path, including dir name if needed
|
1362
|
-
- An empty `SEARCH` section
|
1363
|
-
- The new file's contents in the `REPLACE` section
|
1364
|
-
|
1365
|
-
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
|
1366
|
-
|
1367
|
-
下面我们来看一个例子:
|
1368
|
-
|
1369
|
-
当前项目目录结构:
|
1370
|
-
1. 项目根目录: /tmp/projects/mathweb
|
1371
|
-
2. 项目子目录/文件列表(类似tree 命令输出)
|
1372
|
-
flask/
|
1373
|
-
app.py
|
1374
|
-
templates/
|
1375
|
-
index.html
|
1376
|
-
static/
|
1377
|
-
style.css
|
1378
|
-
|
1379
|
-
用户需求: Change get_factorial() to use math.factorial
|
1380
|
-
|
1381
|
-
回答: To make this change we need to modify `/tmp/projects/mathweb/flask/app.py` to:
|
1382
|
-
|
1383
|
-
1. Import the math package.
|
1384
|
-
2. Remove the existing factorial() function.
|
1385
|
-
3. Update get_factorial() to call math.factorial instead.
|
1386
|
-
|
1387
|
-
Here are the *SEARCH/REPLACE* blocks:
|
1388
|
-
|
1389
|
-
```python
|
1390
|
-
##File: /tmp/projects/mathweb/flask/app.py
|
1391
|
-
<<<<<<< SEARCH
|
1392
|
-
from flask import Flask
|
1393
|
-
=======
|
1394
|
-
import math
|
1395
|
-
from flask import Flask
|
1396
|
-
>>>>>>> REPLACE
|
1397
|
-
```
|
1398
|
-
|
1399
|
-
```python
|
1400
|
-
##File: /tmp/projects/mathweb/flask/app.py
|
1401
|
-
<<<<<<< SEARCH
|
1402
|
-
def factorial(n):
|
1403
|
-
"compute factorial"
|
1404
|
-
|
1405
|
-
if n == 0:
|
1406
|
-
return 1
|
1407
|
-
else:
|
1408
|
-
return n * factorial(n-1)
|
1409
|
-
|
1410
|
-
=======
|
1411
|
-
>>>>>>> REPLACE
|
1412
|
-
```
|
1413
|
-
|
1414
|
-
```python
|
1415
|
-
##File: /tmp/projects/mathweb/flask/app.py
|
1416
|
-
<<<<<<< SEARCH
|
1417
|
-
return str(factorial(n))
|
1418
|
-
=======
|
1419
|
-
return str(math.factorial(n))
|
1420
|
-
>>>>>>> REPLACE
|
1421
|
-
```
|
1422
|
-
|
1423
|
-
用户需求: Refactor hello() into its own file.
|
1424
|
-
|
1425
|
-
回答:To make this change we need to modify `main.py` and make a new file `hello.py`:
|
1426
|
-
|
1427
|
-
1. Make a new hello.py file with hello() in it.
|
1428
|
-
2. Remove hello() from main.py and replace it with an import.
|
1429
|
-
|
1430
|
-
Here are the *SEARCH/REPLACE* blocks:
|
1431
|
-
|
1432
|
-
```python
|
1433
|
-
##File: /tmp/projects/mathweb/hello.py
|
1434
|
-
<<<<<<< SEARCH
|
1435
|
-
=======
|
1436
|
-
def hello():
|
1437
|
-
"print a greeting"
|
1438
|
-
|
1439
|
-
print("hello")
|
1440
|
-
>>>>>>> REPLACE
|
1441
|
-
```
|
1442
|
-
|
1443
|
-
```python
|
1444
|
-
##File: /tmp/projects/mathweb/main.py
|
1445
|
-
<<<<<<< SEARCH
|
1446
|
-
def hello():
|
1447
|
-
"print a greeting"
|
1448
|
-
|
1449
|
-
print("hello")
|
1450
|
-
=======
|
1451
|
-
from hello import hello
|
1452
|
-
>>>>>>> REPLACE
|
1453
|
-
```
|
1454
|
-
|
1455
|
-
现在让我们开始一个新的任务:
|
1456
|
-
|
1457
|
-
{%- if structure %}
|
1458
|
-
{{ structure }}
|
1459
|
-
{%- endif %}
|
1460
|
-
|
1461
|
-
{%- if content %}
|
1462
|
-
下面是一些文件路径以及每个文件对应的源码:
|
1463
|
-
<files>
|
1464
|
-
{{ content }}
|
1465
|
-
</files>
|
1466
|
-
{%- endif %}
|
1467
|
-
|
1468
|
-
{%- if context %}
|
1469
|
-
<extra_context>
|
1470
|
-
{{ context }}
|
1471
|
-
</extra_context>
|
1472
|
-
{%- endif %}
|
1473
|
-
|
1474
|
-
下面是用户的需求:
|
1475
|
-
|
1476
|
-
{{ instruction }}
|
1477
|
-
|
1478
|
-
"""
|
1479
|
-
|
1480
|
-
@prompt()
|
1481
|
-
def auto_implement_function(self, instruction: str, content: str) -> str:
|
1482
|
-
"""
|
1483
|
-
下面是一些文件路径以及每个文件对应的源码:
|
1484
|
-
|
1485
|
-
{{ content }}
|
1486
|
-
|
1487
|
-
请参考上面的内容,重新实现所有文件下方法体标记了如下内容的方法:
|
1488
|
-
|
1489
|
-
```python
|
1490
|
-
raise NotImplementedError("This function should be implemented by the model.")
|
1491
|
-
```
|
1492
|
-
|
1493
|
-
{{ instruction }}
|
1494
|
-
|
1495
|
-
"""
|
1496
|
-
|
1497
|
-
def single_round_run(self, query: str, source_content: str) -> CodeGenerateResult:
|
1498
|
-
init_prompt = ''
|
1499
|
-
if self.args.template == "common":
|
1500
|
-
init_prompt = self.single_round_instruction.prompt(
|
1501
|
-
instruction=query, content=source_content, context=self.args.context
|
1502
|
-
)
|
1503
|
-
elif self.args.template == "auto_implement":
|
1504
|
-
init_prompt = self.auto_implement_function.prompt(
|
1505
|
-
instruction=query, content=source_content
|
1506
|
-
)
|
1507
|
-
|
1508
|
-
with open(self.args.target_file, "w") as file:
|
1509
|
-
file.write(init_prompt)
|
1510
|
-
|
1511
|
-
conversations = [{"role": "user", "content": init_prompt}]
|
1512
|
-
|
1513
|
-
conversations_list = []
|
1514
|
-
results = []
|
1515
|
-
|
1516
|
-
for llm in self.llms:
|
1517
|
-
v = llm.chat_ai(conversations=conversations, model=args.code_model)
|
1518
|
-
results.append(v.output)
|
1519
|
-
for result in results:
|
1520
|
-
conversations_list.append(conversations + [{"role": "assistant", "content": result}])
|
1521
|
-
|
1522
|
-
return CodeGenerateResult(contents=results, conversations=conversations_list)
|
1523
|
-
|
1524
|
-
@prompt()
|
1525
|
-
def multi_round_instruction(self, instruction: str, content: str, context: str = "") -> str:
|
1526
|
-
"""
|
1527
|
-
如果你需要生成代码,对于每个需要更改的文件,你需要按 *SEARCH/REPLACE block* 的格式进行生成。
|
1528
|
-
|
1529
|
-
# *SEARCH/REPLACE block* Rules:
|
1530
|
-
|
1531
|
-
Every *SEARCH/REPLACE block* must use this format:
|
1532
|
-
1. The opening fence and code language, eg: {{ fence_0 }}python
|
1533
|
-
2. The file path alone on a line, starting with "##File:" and verbatim. No bold asterisks, no quotes around it,
|
1534
|
-
no escaping of characters, etc.
|
1535
|
-
3. The start of search block: <<<<<<< SEARCH
|
1536
|
-
4. A contiguous chunk of lines to search for in the existing source code
|
1537
|
-
5. The dividing line: =======
|
1538
|
-
6. The lines to replace into the source code
|
1539
|
-
7. The end of the replacement block: >>>>>>> REPLACE
|
1540
|
-
8. The closing fence: {{ fence_1 }}
|
1541
|
-
|
1542
|
-
Every *SEARCH* section must *EXACTLY MATCH* the existing source code, character for character,
|
1543
|
-
including all comments, docstrings, etc.
|
1544
|
-
|
1545
|
-
*SEARCH/REPLACE* blocks will replace *all* matching occurrences.
|
1546
|
-
Include enough lines to make the SEARCH blocks unique.
|
1547
|
-
|
1548
|
-
Include *ALL* the code being searched and replaced!
|
1549
|
-
|
1550
|
-
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location,
|
1551
|
-
1 to insert it in the new location.
|
1552
|
-
|
1553
|
-
If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
|
1554
|
-
- A new file path, including dir name if needed
|
1555
|
-
- An empty `SEARCH` section
|
1556
|
-
- The new file's contents in the `REPLACE` section
|
1557
|
-
|
1558
|
-
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
|
1559
|
-
|
1560
|
-
下面我们来看一个例子:
|
1561
|
-
|
1562
|
-
当前项目目录结构:
|
1563
|
-
1. 项目根目录: /tmp/projects/mathweb
|
1564
|
-
2. 项目子目录/文件列表(类似tree 命令输出)
|
1565
|
-
flask/
|
1566
|
-
app.py
|
1567
|
-
templates/
|
1568
|
-
index.html
|
1569
|
-
static/
|
1570
|
-
style.css
|
1571
|
-
|
1572
|
-
用户需求: Change get_factorial() to use math.factorial
|
1573
|
-
|
1574
|
-
回答: To make this change we need to modify `/tmp/projects/mathweb/flask/app.py` to:
|
1575
|
-
|
1576
|
-
1. Import the math package.
|
1577
|
-
2. Remove the existing factorial() function.
|
1578
|
-
3. Update get_factorial() to call math.factorial instead.
|
1579
|
-
|
1580
|
-
Here are the *SEARCH/REPLACE* blocks:
|
1581
|
-
|
1582
|
-
{{ fence_0 }}python
|
1583
|
-
##File: /tmp/projects/mathweb/flask/app.py
|
1584
|
-
<<<<<<< SEARCH
|
1585
|
-
from flask import Flask
|
1586
|
-
=======
|
1587
|
-
import math
|
1588
|
-
from flask import Flask
|
1589
|
-
>>>>>>> REPLACE
|
1590
|
-
{{ fence_1 }}
|
1591
|
-
|
1592
|
-
{{ fence_0 }}python
|
1593
|
-
##File: /tmp/projects/mathweb/flask/app.py
|
1594
|
-
<<<<<<< SEARCH
|
1595
|
-
def factorial(n):
|
1596
|
-
"compute factorial"
|
1597
|
-
|
1598
|
-
if n == 0:
|
1599
|
-
return 1
|
1600
|
-
else:
|
1601
|
-
return n * factorial(n-1)
|
1602
|
-
|
1603
|
-
=======
|
1604
|
-
>>>>>>> REPLACE
|
1605
|
-
{{ fence_1 }}
|
1606
|
-
|
1607
|
-
{{ fence_0 }}python
|
1608
|
-
##File: /tmp/projects/mathweb/flask/app.py
|
1609
|
-
<<<<<<< SEARCH
|
1610
|
-
return str(factorial(n))
|
1611
|
-
=======
|
1612
|
-
return str(math.factorial(n))
|
1613
|
-
>>>>>>> REPLACE
|
1614
|
-
{{ fence_1 }}
|
1615
|
-
|
1616
|
-
用户需求: Refactor hello() into its own file.
|
1617
|
-
|
1618
|
-
回答:To make this change we need to modify `main.py` and make a new file `hello.py`:
|
1619
|
-
|
1620
|
-
1. Make a new hello.py file with hello() in it.
|
1621
|
-
2. Remove hello() from main.py and replace it with an import.
|
1622
|
-
|
1623
|
-
Here are the *SEARCH/REPLACE* blocks:
|
1624
|
-
|
1625
|
-
|
1626
|
-
{{ fence_0 }}python
|
1627
|
-
##File: /tmp/projects/mathweb/hello.py
|
1628
|
-
<<<<<<< SEARCH
|
1629
|
-
=======
|
1630
|
-
def hello():
|
1631
|
-
"print a greeting"
|
1632
|
-
|
1633
|
-
print("hello")
|
1634
|
-
>>>>>>> REPLACE
|
1635
|
-
{{ fence_1 }}
|
1636
|
-
|
1637
|
-
{{ fence_0 }}python
|
1638
|
-
##File: /tmp/projects/mathweb/main.py
|
1639
|
-
<<<<<<< SEARCH
|
1640
|
-
def hello():
|
1641
|
-
"print a greeting"
|
1642
|
-
|
1643
|
-
print("hello")
|
1644
|
-
=======
|
1645
|
-
from hello import hello
|
1646
|
-
>>>>>>> REPLACE
|
1647
|
-
{{ fence_1 }}
|
1648
|
-
|
1649
|
-
现在让我们开始一个新的任务:
|
1650
|
-
|
1651
|
-
{%- if structure %}
|
1652
|
-
{{ structure }}
|
1653
|
-
{%- endif %}
|
1654
|
-
|
1655
|
-
{%- if content %}
|
1656
|
-
下面是一些文件路径以及每个文件对应的源码:
|
1657
|
-
<files>
|
1658
|
-
{{ content }}
|
1659
|
-
</files>
|
1660
|
-
{%- endif %}
|
1661
|
-
|
1662
|
-
{%- if context %}
|
1663
|
-
<extra_context>
|
1664
|
-
{{ context }}
|
1665
|
-
</extra_context>
|
1666
|
-
{%- endif %}
|
1667
|
-
|
1668
|
-
下面是用户的需求:
|
1669
|
-
|
1670
|
-
{{ instruction }}
|
1671
|
-
|
1672
|
-
每次生成一个文件的*SEARCH/REPLACE* blocks,然后询问我是否继续,当我回复继续,
|
1673
|
-
继续生成下一个文件的*SEARCH/REPLACE* blocks。当没有后续任务时,请回复 "__完成__" 或者 "__EOF__"。
|
1674
|
-
"""
|
1675
|
-
|
1676
|
-
def multi_round_run(self, query: str, source_content: str, max_steps: int = 3) -> CodeGenerateResult:
|
1677
|
-
init_prompt = ''
|
1678
|
-
if self.args.template == "common":
|
1679
|
-
init_prompt = self.multi_round_instruction.prompt(
|
1680
|
-
instruction=query, content=source_content, context=self.args.context
|
1681
|
-
)
|
1682
|
-
elif self.args.template == "auto_implement":
|
1683
|
-
init_prompt = self.auto_implement_function.prompt(
|
1684
|
-
instruction=query, content=source_content
|
1685
|
-
)
|
1686
|
-
|
1687
|
-
with open(self.args.target_file, "w") as file:
|
1688
|
-
file.write(init_prompt)
|
1689
|
-
|
1690
|
-
results = []
|
1691
|
-
conversations = [{"role": "user", "content": init_prompt}]
|
1692
|
-
|
1693
|
-
code_llm = self.llms[0]
|
1694
|
-
v = code_llm.chat_ai(conversations=conversations, model=args.code_model)
|
1695
|
-
results.append(v.output)
|
1696
|
-
|
1697
|
-
conversations.append({"role": "assistant", "content": v.output})
|
1698
|
-
|
1699
|
-
if "__完成__" in v.output or "/done" in v.output or "__EOF__" in v.output:
|
1700
|
-
return CodeGenerateResult(contents=["\n\n".join(results)], conversations=[conversations])
|
1701
|
-
|
1702
|
-
current_step = 0
|
1703
|
-
|
1704
|
-
while current_step < max_steps:
|
1705
|
-
conversations.append({"role": "user", "content": "继续"})
|
1706
|
-
|
1707
|
-
with open(self.args.target_file, "w") as file:
|
1708
|
-
file.write("继续")
|
1709
|
-
|
1710
|
-
t = code_llm.chat_ai(conversations=conversations, model=args.code_model)
|
1711
|
-
|
1712
|
-
results.append(t.output)
|
1713
|
-
conversations.append({"role": "assistant", "content": t.output})
|
1714
|
-
current_step += 1
|
1715
|
-
|
1716
|
-
if "__完成__" in t.output or "/done" in t.output or "__EOF__" in t.output:
|
1717
|
-
return CodeGenerateResult(contents=["\n\n".join(results)], conversations=[conversations])
|
1718
|
-
|
1719
|
-
return CodeGenerateResult(contents=["\n\n".join(results)], conversations=[conversations])
|
1720
|
-
|
1721
|
-
|
1722
|
-
class CodeModificationRanker:
|
1723
|
-
def __init__(self, llm: AutoLLM):
|
1724
|
-
self.llm = llm
|
1725
|
-
self.llm.setup_default_model_name(args.code_model)
|
1726
|
-
self.args = args
|
1727
|
-
self.llms = [self.llm]
|
1728
|
-
|
1729
|
-
@prompt()
|
1730
|
-
def _rank_modifications(self, s: CodeGenerateResult) -> str:
|
1731
|
-
"""
|
1732
|
-
对一组代码修改进行质量评估并排序。
|
1733
|
-
|
1734
|
-
下面是修改需求:
|
1735
|
-
|
1736
|
-
<edit_requirement>
|
1737
|
-
{{ s.conversations[0][-2]["content"] }}
|
1738
|
-
</edit_requirement>
|
1739
|
-
|
1740
|
-
下面是相应的代码修改:
|
1741
|
-
{% for content in s.contents %}
|
1742
|
-
<edit_block id="{{ loop.index0 }}">
|
1743
|
-
{{content}}
|
1744
|
-
</edit_block>
|
1745
|
-
{% endfor %}
|
1746
|
-
|
1747
|
-
请输出如下格式的评估结果,只包含 JSON 数据:
|
1748
|
-
|
1749
|
-
```json
|
1750
|
-
{
|
1751
|
-
"rank_result": [id1, id2, id3] // id 为 edit_block 的 id,按质量从高到低排序
|
1752
|
-
}
|
1753
|
-
```
|
1754
|
-
|
1755
|
-
注意:
|
1756
|
-
1. 只输出前面要求的 Json 格式就好,不要输出其他内容,Json 需要使用 ```json ```包裹
|
1757
|
-
"""
|
1758
|
-
|
1759
|
-
def rank_modifications(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
|
1760
|
-
import time
|
1761
|
-
from collections import defaultdict
|
1762
|
-
|
1763
|
-
start_time = time.time()
|
1764
|
-
logger.info(f"开始对 {len(generate_result.contents)} 个候选结果进行排序")
|
1765
|
-
|
1766
|
-
try:
|
1767
|
-
results = []
|
1768
|
-
for llm in self.llms:
|
1769
|
-
v = self._rank_modifications.with_llm(llm).with_return_type(RankResult).run(generate_result)
|
1770
|
-
results.append(v.rank_result)
|
1771
|
-
|
1772
|
-
if not results:
|
1773
|
-
raise Exception("All ranking requests failed")
|
1774
|
-
|
1775
|
-
# 计算每个候选人的分数
|
1776
|
-
candidate_scores = defaultdict(float)
|
1777
|
-
for rank_result in results:
|
1778
|
-
for idx, candidate_id in enumerate(rank_result):
|
1779
|
-
# Score is 1/(position + 1) since position starts from 0
|
1780
|
-
candidate_scores[candidate_id] += 1.0 / (idx + 1)
|
1781
|
-
# 按分数降序对候选人进行排序
|
1782
|
-
sorted_candidates = sorted(candidate_scores.keys(),
|
1783
|
-
key=lambda x: candidate_scores[x],
|
1784
|
-
reverse=True)
|
1785
|
-
|
1786
|
-
elapsed = time.time() - start_time
|
1787
|
-
score_details = ", ".join([f"candidate {i}: {candidate_scores[i]:.2f}" for i in sorted_candidates])
|
1788
|
-
logger.info(
|
1789
|
-
f"排序完成,耗时 {elapsed:.2f} 秒,最佳候选索引: {sorted_candidates[0]},评分详情: {score_details}"
|
1790
|
-
)
|
1791
|
-
|
1792
|
-
rerank_contents = [generate_result.contents[i] for i in sorted_candidates]
|
1793
|
-
rerank_conversations = [generate_result.conversations[i] for i in sorted_candidates]
|
1794
|
-
|
1795
|
-
return CodeGenerateResult(contents=rerank_contents, conversations=rerank_conversations)
|
1796
|
-
|
1797
|
-
except Exception as e:
|
1798
|
-
logger.error(f"排序过程失败: {str(e)}")
|
1799
|
-
logger.debug(traceback.format_exc())
|
1800
|
-
elapsed = time.time() - start_time
|
1801
|
-
logger.warning(f"排序失败,耗时 {elapsed:.2f} 秒,将使用原始顺序")
|
1802
|
-
return generate_result
|
1803
|
-
|
1804
|
-
|
1805
|
-
class TextSimilarity:
|
1806
|
-
"""
|
1807
|
-
找到 text_b 中与 text_a 最相似的部分(滑动窗口)
|
1808
|
-
返回相似度分数和最相似的文本片段
|
1809
|
-
"""
|
1810
|
-
|
1811
|
-
def __init__(self, text_a, text_b):
|
1812
|
-
self.text_a = text_a
|
1813
|
-
self.text_b = text_b
|
1814
|
-
self.lines_a = self._split_into_lines(text_a)
|
1815
|
-
self.lines_b = self._split_into_lines(text_b)
|
1816
|
-
self.m = len(self.lines_a)
|
1817
|
-
self.n = len(self.lines_b)
|
1818
|
-
|
1819
|
-
@staticmethod
|
1820
|
-
def _split_into_lines(text):
|
1821
|
-
return text.splitlines()
|
1822
|
-
|
1823
|
-
@staticmethod
|
1824
|
-
def _levenshtein_ratio(s1, s2):
|
1825
|
-
return SequenceMatcher(None, s1, s2).ratio()
|
1826
|
-
|
1827
|
-
def get_best_matching_window(self):
|
1828
|
-
best_similarity = 0
|
1829
|
-
best_window = []
|
1830
|
-
|
1831
|
-
for i in range(self.n - self.m + 1): # 滑动窗口
|
1832
|
-
window_b = self.lines_b[i:i + self.m]
|
1833
|
-
similarity = self._levenshtein_ratio("\n".join(self.lines_a), "\n".join(window_b))
|
1834
|
-
|
1835
|
-
if similarity > best_similarity:
|
1836
|
-
best_similarity = similarity
|
1837
|
-
best_window = window_b
|
1838
|
-
|
1839
|
-
return best_similarity, "\n".join(best_window)
|
1840
|
-
|
1841
|
-
|
1842
|
-
class CodeAutoMergeEditBlock:
|
1843
|
-
def __init__(self, llm: AutoLLM, fence_0: str = "```", fence_1: str = "```"):
|
1844
|
-
self.llm = llm
|
1845
|
-
self.llm.setup_default_model_name(args.code_model)
|
1846
|
-
self.args = args
|
1847
|
-
self.fence_0 = fence_0
|
1848
|
-
self.fence_1 = fence_1
|
1849
|
-
|
1850
|
-
@staticmethod
|
1851
|
-
def run_pylint(code: str) -> tuple[bool, str]:
|
1852
|
-
"""
|
1853
|
-
--disable=all 禁用所有 Pylint 的检查规则
|
1854
|
-
--enable=E0001,W0311,W0312 启用指定的 Pylint 检查规则,
|
1855
|
-
E0001:语法错误(Syntax Error),
|
1856
|
-
W0311:代码缩进使用了 Tab 而不是空格(Bad indentation)
|
1857
|
-
W0312:代码缩进不一致(Mixed indentation)
|
1858
|
-
:param code:
|
1859
|
-
:return:
|
1860
|
-
"""
|
1861
|
-
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
1862
|
-
temp_file.write(code)
|
1863
|
-
temp_file_path = temp_file.name
|
1864
|
-
|
1865
|
-
try:
|
1866
|
-
result = subprocess.run(
|
1867
|
-
["pylint", "--disable=all", "--enable=E0001,W0311,W0312", temp_file_path,],
|
1868
|
-
capture_output=True,
|
1869
|
-
text=True,
|
1870
|
-
check=False,
|
1871
|
-
)
|
1872
|
-
os.unlink(temp_file_path)
|
1873
|
-
if result.returncode != 0:
|
1874
|
-
error_message = result.stdout.strip() or result.stderr.strip()
|
1875
|
-
logger.warning(f"Pylint 检查代码失败: {error_message}")
|
1876
|
-
return False, error_message
|
1877
|
-
return True, ""
|
1878
|
-
except subprocess.CalledProcessError as e:
|
1879
|
-
error_message = f"运行 Pylint 时发生错误: {str(e)}"
|
1880
|
-
logger.error(error_message)
|
1881
|
-
os.unlink(temp_file_path)
|
1882
|
-
return False, error_message
|
1883
|
-
|
1884
|
-
def parse_whole_text(self, text: str) -> List[PathAndCode]:
|
1885
|
-
"""
|
1886
|
-
从文本中抽取如下格式代码(two_line_mode):
|
1887
|
-
|
1888
|
-
```python
|
1889
|
-
##File: /project/path/src/autocoder/index/index.py
|
1890
|
-
<<<<<<< SEARCH
|
1891
|
-
=======
|
1892
|
-
>>>>>>> REPLACE
|
1893
|
-
```
|
1894
|
-
|
1895
|
-
或者 (one_line_mode)
|
1896
|
-
|
1897
|
-
```python:/project/path/src/autocoder/index/index.py
|
1898
|
-
<<<<<<< SEARCH
|
1899
|
-
=======
|
1900
|
-
>>>>>>> REPLACE
|
1901
|
-
```
|
1902
|
-
"""
|
1903
|
-
HEAD = "<<<<<<< SEARCH"
|
1904
|
-
DIVIDER = "======="
|
1905
|
-
UPDATED = ">>>>>>> REPLACE"
|
1906
|
-
lines = text.split("\n")
|
1907
|
-
lines_len = len(lines)
|
1908
|
-
start_marker_count = 0
|
1909
|
-
block = []
|
1910
|
-
path_and_code_list = []
|
1911
|
-
# two_line_mode or one_line_mode
|
1912
|
-
current_editblock_mode = "two_line_mode"
|
1913
|
-
current_editblock_path = None
|
1914
|
-
|
1915
|
-
def guard(_index):
|
1916
|
-
return _index + 1 < lines_len
|
1917
|
-
|
1918
|
-
def start_marker(_line, _index):
|
1919
|
-
nonlocal current_editblock_mode
|
1920
|
-
nonlocal current_editblock_path
|
1921
|
-
if _line.startswith(self.fence_0) and guard(_index) and ":" in _line and lines[_index + 1].startswith(HEAD):
|
1922
|
-
current_editblock_mode = "one_line_mode"
|
1923
|
-
current_editblock_path = _line.split(":", 1)[1].strip()
|
1924
|
-
return True
|
1925
|
-
if _line.startswith(self.fence_0) and guard(_index) and lines[_index + 1].startswith("##File:"):
|
1926
|
-
current_editblock_mode = "two_line_mode"
|
1927
|
-
current_editblock_path = None
|
1928
|
-
return True
|
1929
|
-
return False
|
1930
|
-
|
1931
|
-
def end_marker(_line, _index):
|
1932
|
-
return _line.startswith(self.fence_1) and UPDATED in lines[_index - 1]
|
1933
|
-
|
1934
|
-
for index, line in enumerate(lines):
|
1935
|
-
if start_marker(line, index) and start_marker_count == 0:
|
1936
|
-
start_marker_count += 1
|
1937
|
-
elif end_marker(line, index) and start_marker_count == 1:
|
1938
|
-
start_marker_count -= 1
|
1939
|
-
if block:
|
1940
|
-
if current_editblock_mode == "two_line_mode":
|
1941
|
-
path = block[0].split(":", 1)[1].strip()
|
1942
|
-
content = "\n".join(block[1:])
|
1943
|
-
else:
|
1944
|
-
path = current_editblock_path
|
1945
|
-
content = "\n".join(block)
|
1946
|
-
block = []
|
1947
|
-
path_and_code_list.append(PathAndCode(path=path, content=content))
|
1948
|
-
elif start_marker_count > 0:
|
1949
|
-
block.append(line)
|
1950
|
-
|
1951
|
-
return path_and_code_list
|
1952
|
-
|
1953
|
-
def get_edits(self, content: str):
|
1954
|
-
edits = self.parse_whole_text(content)
|
1955
|
-
HEAD = "<<<<<<< SEARCH"
|
1956
|
-
DIVIDER = "======="
|
1957
|
-
UPDATED = ">>>>>>> REPLACE"
|
1958
|
-
result = []
|
1959
|
-
for edit in edits:
|
1960
|
-
heads = []
|
1961
|
-
updates = []
|
1962
|
-
c = edit.content
|
1963
|
-
in_head = False
|
1964
|
-
in_updated = False
|
1965
|
-
for line in c.splitlines():
|
1966
|
-
if line.strip() == HEAD:
|
1967
|
-
in_head = True
|
1968
|
-
continue
|
1969
|
-
if line.strip() == DIVIDER:
|
1970
|
-
in_head = False
|
1971
|
-
in_updated = True
|
1972
|
-
continue
|
1973
|
-
if line.strip() == UPDATED:
|
1974
|
-
in_head = False
|
1975
|
-
in_updated = False
|
1976
|
-
continue
|
1977
|
-
if in_head:
|
1978
|
-
heads.append(line)
|
1979
|
-
if in_updated:
|
1980
|
-
updates.append(line)
|
1981
|
-
result.append((edit.path, "\n".join(heads), "\n".join(updates)))
|
1982
|
-
return result
|
1983
|
-
|
1984
|
-
@prompt()
|
1985
|
-
def git_require_msg(self, source_dir: str, error: str) -> str:
|
1986
|
-
"""
|
1987
|
-
auto_merge only works for git repositories.
|
1988
|
-
|
1989
|
-
Try to use git init in the source directory.
|
1990
|
-
|
1991
|
-
```shell
|
1992
|
-
cd {{ source_dir }}
|
1993
|
-
git init .
|
1994
|
-
```
|
1995
|
-
|
1996
|
-
Then try to run auto-coder again.
|
1997
|
-
Error: {{ error }}
|
1998
|
-
"""
|
1999
|
-
|
2000
|
-
def _merge_code_without_effect(self, content: str) -> MergeCodeWithoutEffect:
|
2001
|
-
"""
|
2002
|
-
合并代码时不会产生任何副作用,例如 Git 操作、代码检查或文件写入。
|
2003
|
-
返回一个元组,包含:
|
2004
|
-
- 成功合并的代码块的列表,每个元素是一个 (file_path, new_content) 元组,
|
2005
|
-
其中 file_path 是文件路径,new_content 是合并后的新内容。
|
2006
|
-
- 合并失败的代码块的列表,每个元素是一个 (file_path, head, update) 元组,
|
2007
|
-
其中:file_path 是文件路径,head 是原始内容,update 是尝试合并的内容。
|
2008
|
-
"""
|
2009
|
-
codes = self.get_edits(content)
|
2010
|
-
file_content_mapping = {}
|
2011
|
-
failed_blocks = []
|
2012
|
-
|
2013
|
-
for block in codes:
|
2014
|
-
file_path, head, update = block
|
2015
|
-
if not os.path.exists(file_path):
|
2016
|
-
file_content_mapping[file_path] = update
|
2017
|
-
else:
|
2018
|
-
if file_path not in file_content_mapping:
|
2019
|
-
with open(file_path, "r") as f:
|
2020
|
-
temp = f.read()
|
2021
|
-
file_content_mapping[file_path] = temp
|
2022
|
-
existing_content = file_content_mapping[file_path]
|
2023
|
-
|
2024
|
-
# First try exact match
|
2025
|
-
new_content = (
|
2026
|
-
existing_content.replace(head, update, 1)
|
2027
|
-
if head
|
2028
|
-
else existing_content + "\n" + update
|
2029
|
-
)
|
2030
|
-
|
2031
|
-
# If exact match fails, try similarity match
|
2032
|
-
if new_content == existing_content and head:
|
2033
|
-
similarity, best_window = TextSimilarity(
|
2034
|
-
head, existing_content
|
2035
|
-
).get_best_matching_window()
|
2036
|
-
if similarity > self.args.editblock_similarity:
|
2037
|
-
new_content = existing_content.replace(
|
2038
|
-
best_window, update, 1
|
2039
|
-
)
|
2040
|
-
|
2041
|
-
if new_content != existing_content:
|
2042
|
-
file_content_mapping[file_path] = new_content
|
2043
|
-
else:
|
2044
|
-
failed_blocks.append((file_path, head, update))
|
2045
|
-
return MergeCodeWithoutEffect(
|
2046
|
-
success_blocks=[(path, content) for path, content in file_content_mapping.items()],
|
2047
|
-
failed_blocks=failed_blocks
|
2048
|
-
)
|
2049
|
-
|
2050
|
-
def choose_best_choice(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
|
2051
|
-
""" 选择最佳代码 """
|
2052
|
-
if len(generate_result.contents) == 1: # 仅一份代码立即返回
|
2053
|
-
logger.info("仅有一个候选结果,跳过排序")
|
2054
|
-
return generate_result
|
2055
|
-
|
2056
|
-
ranker = CodeModificationRanker(self.llm)
|
2057
|
-
ranked_result = ranker.rank_modifications(generate_result)
|
2058
|
-
# 过滤掉包含失败块的内容
|
2059
|
-
for content, conversations in zip(ranked_result.contents, ranked_result.conversations):
|
2060
|
-
merge_result = self._merge_code_without_effect(content)
|
2061
|
-
if not merge_result.failed_blocks:
|
2062
|
-
return CodeGenerateResult(contents=[content], conversations=[conversations])
|
2063
|
-
# 如果所有内容都包含失败块,则返回第一个
|
2064
|
-
return CodeGenerateResult(contents=[ranked_result.contents[0]], conversations=[ranked_result.conversations[0]])
|
2065
|
-
|
2066
|
-
def _merge_code(self, content: str, force_skip_git: bool = False):
|
2067
|
-
file_content = open(self.args.file).read()
|
2068
|
-
md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
|
2069
|
-
file_name = os.path.basename(self.args.file)
|
2070
|
-
|
2071
|
-
codes = self.get_edits(content)
|
2072
|
-
changes_to_make = []
|
2073
|
-
changes_made = False
|
2074
|
-
unmerged_blocks = []
|
2075
|
-
merged_blocks = []
|
2076
|
-
|
2077
|
-
# First, check if there are any changes to be made
|
2078
|
-
file_content_mapping = {}
|
2079
|
-
for block in codes:
|
2080
|
-
file_path, head, update = block
|
2081
|
-
if not os.path.exists(file_path):
|
2082
|
-
changes_to_make.append((file_path, None, update))
|
2083
|
-
file_content_mapping[file_path] = update
|
2084
|
-
merged_blocks.append((file_path, "", update, 1))
|
2085
|
-
changes_made = True
|
2086
|
-
else:
|
2087
|
-
if file_path not in file_content_mapping:
|
2088
|
-
with open(file_path, "r") as f:
|
2089
|
-
temp = f.read()
|
2090
|
-
file_content_mapping[file_path] = temp
|
2091
|
-
existing_content = file_content_mapping[file_path]
|
2092
|
-
new_content = (
|
2093
|
-
existing_content.replace(head, update, 1)
|
2094
|
-
if head
|
2095
|
-
else existing_content + "\n" + update
|
2096
|
-
)
|
2097
|
-
if new_content != existing_content:
|
2098
|
-
changes_to_make.append(
|
2099
|
-
(file_path, existing_content, new_content))
|
2100
|
-
file_content_mapping[file_path] = new_content
|
2101
|
-
merged_blocks.append((file_path, head, update, 1))
|
2102
|
-
changes_made = True
|
2103
|
-
else:
|
2104
|
-
# If the SEARCH BLOCK is not found exactly, then try to use
|
2105
|
-
# the similarity ratio to find the best matching block
|
2106
|
-
similarity, best_window = TextSimilarity(head, existing_content).get_best_matching_window()
|
2107
|
-
if similarity > self.args.editblock_similarity: # 相似性比较
|
2108
|
-
new_content = existing_content.replace(
|
2109
|
-
best_window, update, 1)
|
2110
|
-
if new_content != existing_content:
|
2111
|
-
changes_to_make.append(
|
2112
|
-
(file_path, existing_content, new_content)
|
2113
|
-
)
|
2114
|
-
file_content_mapping[file_path] = new_content
|
2115
|
-
merged_blocks.append(
|
2116
|
-
(file_path, head, update, similarity))
|
2117
|
-
changes_made = True
|
2118
|
-
else:
|
2119
|
-
unmerged_blocks.append((file_path, head, update, similarity))
|
2120
|
-
|
2121
|
-
if unmerged_blocks:
|
2122
|
-
if self.args.request_id and not self.args.skip_events:
|
2123
|
-
# collect unmerged blocks
|
2124
|
-
event_data = []
|
2125
|
-
for file_path, head, update, similarity in unmerged_blocks:
|
2126
|
-
event_data.append(
|
2127
|
-
{
|
2128
|
-
"file_path": file_path,
|
2129
|
-
"head": head,
|
2130
|
-
"update": update,
|
2131
|
-
"similarity": similarity,
|
2132
|
-
}
|
2133
|
-
)
|
2134
|
-
return
|
2135
|
-
logger.warning(f"发现 {len(unmerged_blocks)} 个未合并的代码块,更改将不会应用,请手动检查这些代码块后重试。")
|
2136
|
-
self._print_unmerged_blocks(unmerged_blocks)
|
2137
|
-
return
|
2138
|
-
|
2139
|
-
# lint check
|
2140
|
-
for file_path, new_content in file_content_mapping.items():
|
2141
|
-
if file_path.endswith(".py"):
|
2142
|
-
pylint_passed, error_message = self.run_pylint(new_content)
|
2143
|
-
if not pylint_passed:
|
2144
|
-
logger.warning(f"代码文件 {file_path} 的 Pylint 检查未通过,本次更改未应用。错误信息: {error_message}")
|
2145
|
-
|
2146
|
-
if changes_made and not force_skip_git and not self.args.skip_commit:
|
2147
|
-
try:
|
2148
|
-
commit_changes(self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}")
|
2149
|
-
except Exception as e:
|
2150
|
-
logger.error(
|
2151
|
-
self.git_require_msg(
|
2152
|
-
source_dir=self.args.source_dir, error=str(e))
|
2153
|
-
)
|
2154
|
-
return
|
2155
|
-
# Now, apply the changes
|
2156
|
-
for file_path, new_content in file_content_mapping.items():
|
2157
|
-
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
2158
|
-
with open(file_path, "w") as f:
|
2159
|
-
f.write(new_content)
|
2160
|
-
|
2161
|
-
if self.args.request_id and not self.args.skip_events:
|
2162
|
-
# collect modified files
|
2163
|
-
event_data = []
|
2164
|
-
for code in merged_blocks:
|
2165
|
-
file_path, head, update, similarity = code
|
2166
|
-
event_data.append(
|
2167
|
-
{
|
2168
|
-
"file_path": file_path,
|
2169
|
-
"head": head,
|
2170
|
-
"update": update,
|
2171
|
-
"similarity": similarity,
|
2172
|
-
}
|
2173
|
-
)
|
2174
|
-
|
2175
|
-
if changes_made:
|
2176
|
-
if not force_skip_git and not self.args.skip_commit:
|
2177
|
-
try:
|
2178
|
-
commit_result = commit_changes(self.args.source_dir, f"auto_coder_{file_name}_{md5}")
|
2179
|
-
git_print_commit_info(commit_result=commit_result)
|
2180
|
-
except Exception as e:
|
2181
|
-
logger.error(
|
2182
|
-
self.git_require_msg(
|
2183
|
-
source_dir=self.args.source_dir, error=str(e)
|
2184
|
-
)
|
2185
|
-
)
|
2186
|
-
logger.info(
|
2187
|
-
f"已在 {len(file_content_mapping.keys())} 个文件中合并更改,"
|
2188
|
-
f"完成 {len(changes_to_make)}/{len(codes)} 个代码块。"
|
2189
|
-
)
|
2190
|
-
else:
|
2191
|
-
logger.warning("未对任何文件进行更改。")
|
2192
|
-
|
2193
|
-
def merge_code(self, generate_result: CodeGenerateResult, force_skip_git: bool = False):
|
2194
|
-
result = self.choose_best_choice(generate_result)
|
2195
|
-
self._merge_code(result.contents[0], force_skip_git)
|
2196
|
-
return result
|
2197
|
-
|
2198
|
-
@staticmethod
|
2199
|
-
def _print_unmerged_blocks(unmerged_blocks: List[tuple]):
|
2200
|
-
console.print(f"\n[bold red]未合并的代码块:[/bold red]")
|
2201
|
-
for file_path, head, update, similarity in unmerged_blocks:
|
2202
|
-
console.print(f"\n[bold blue]文件:[/bold blue] {file_path}")
|
2203
|
-
console.print(
|
2204
|
-
f"\n[bold green]搜索代码块(相似度:{similarity}):[/bold green]")
|
2205
|
-
syntax = Syntax(head, "python", theme="monokai", line_numbers=True)
|
2206
|
-
console.print(Panel(syntax, expand=False))
|
2207
|
-
console.print("\n[bold yellow]替换代码块:[/bold yellow]")
|
2208
|
-
syntax = Syntax(update, "python", theme="monokai",
|
2209
|
-
line_numbers=True)
|
2210
|
-
console.print(Panel(syntax, expand=False))
|
2211
|
-
console.print(f"\n[bold red]未合并的代码块总数: {len(unmerged_blocks)}[/bold red]")
|
2212
|
-
|
2213
|
-
|
2214
|
-
class BaseAction:
|
2215
|
-
@staticmethod
|
2216
|
-
def _get_content_length(content: str) -> int:
|
2217
|
-
return len(content)
|
2218
|
-
|
2219
|
-
|
2220
|
-
class ActionPyProject(BaseAction):
|
2221
|
-
def __init__(self, llm: Optional[AutoLLM] = None) -> None:
|
2222
|
-
self.args = args
|
2223
|
-
self.llm = llm
|
2224
|
-
self.pp = None
|
2225
|
-
|
2226
|
-
def run(self):
|
2227
|
-
if self.args.project_type != "py":
|
2228
|
-
return False
|
2229
|
-
pp = PyProject(llm=self.llm, args=args)
|
2230
|
-
self.pp = pp
|
2231
|
-
pp.run()
|
2232
|
-
source_code = pp.output()
|
2233
|
-
if self.llm:
|
2234
|
-
source_code = build_index_and_filter_files(args=args, llm=self.llm, sources=pp.sources)
|
2235
|
-
self.process_content(source_code)
|
2236
|
-
return True
|
2237
|
-
|
2238
|
-
def process_content(self, content: str):
|
2239
|
-
# args = self.args
|
2240
|
-
if self.args.execute and self.llm:
|
2241
|
-
content_length = self._get_content_length(content)
|
2242
|
-
if content_length > self.args.model_max_input_length:
|
2243
|
-
logger.warning(
|
2244
|
-
f"发送给模型的内容长度为 {content_length} 个 token(可能收集了过多文件),"
|
2245
|
-
f"已超过最大输入长度限制 {self.args.model_max_input_length}。"
|
2246
|
-
)
|
2247
|
-
|
2248
|
-
if args.execute:
|
2249
|
-
logger.info("正在自动生成代码...")
|
2250
|
-
start_time = time.time()
|
2251
|
-
# diff, strict_diff, editblock 是代码自动生成或合并的不同策略, 通常用于处理代码的变更或生成
|
2252
|
-
# diff 模式,基于差异生成代码,生成最小的变更集,适用于局部优化,代码重构
|
2253
|
-
# strict_diff 模式,严格验证差异,确保生成的代码符合规则,适用于代码审查,自动化测试
|
2254
|
-
# editblock 模式,基于编辑块生成代码,支持较大范围的修改,适用于代码重构,功能扩展
|
2255
|
-
if args.auto_merge == "editblock":
|
2256
|
-
generate = CodeAutoGenerateEditBlock(llm=self.llm, action=self)
|
2257
|
-
else:
|
2258
|
-
generate = None
|
2259
|
-
|
2260
|
-
if self.args.enable_multi_round_generate:
|
2261
|
-
generate_result = generate.multi_round_run(query=args.query, source_content=content)
|
2262
|
-
else:
|
2263
|
-
generate_result = generate.single_round_run(query=args.query, source_content=content)
|
2264
|
-
logger.info(f"代码生成完成,耗时 {time.time() - start_time:.2f} 秒")
|
2265
|
-
|
2266
|
-
if args.auto_merge:
|
2267
|
-
logger.info("正在自动合并代码...")
|
2268
|
-
if args.auto_merge == "editblock":
|
2269
|
-
code_merge = CodeAutoMergeEditBlock(llm=self.llm)
|
2270
|
-
merge_result = code_merge.merge_code(generate_result=generate_result)
|
2271
|
-
else:
|
2272
|
-
merge_result = None
|
2273
|
-
|
2274
|
-
content = merge_result.contents[0]
|
2275
|
-
else:
|
2276
|
-
content = generate_result.contents[0]
|
2277
|
-
with open(args.target_file, "w") as file:
|
2278
|
-
file.write(content)
|
2279
|
-
|
2280
|
-
|
2281
|
-
class ActionSuffixProject(BaseAction):
|
2282
|
-
def __init__(self, llm: Optional[AutoLLM] = None) -> None:
|
2283
|
-
self.args = args
|
2284
|
-
self.llm = llm
|
2285
|
-
self.pp = None
|
2286
|
-
|
2287
|
-
def run(self):
|
2288
|
-
pp = SuffixProject(llm=self.llm, args=args)
|
2289
|
-
self.pp = pp
|
2290
|
-
pp.run()
|
2291
|
-
source_code = pp.output()
|
2292
|
-
if self.llm:
|
2293
|
-
source_code = build_index_and_filter_files(args=args, llm=self.llm, sources=pp.sources)
|
2294
|
-
self.process_content(source_code)
|
2295
|
-
|
2296
|
-
def process_content(self, content: str):
|
2297
|
-
if self.args.execute and self.llm:
|
2298
|
-
content_length = self._get_content_length(content)
|
2299
|
-
if content_length > self.args.model_max_input_length:
|
2300
|
-
logger.warning(
|
2301
|
-
f"发送给模型的内容长度为 {content_length} 个 token(可能收集了过多文件),"
|
2302
|
-
f"已超过最大输入长度限制 {self.args.model_max_input_length}。"
|
2303
|
-
)
|
2304
|
-
|
2305
|
-
if args.execute:
|
2306
|
-
logger.info("正在自动生成代码...")
|
2307
|
-
start_time = time.time()
|
2308
|
-
# diff, strict_diff, editblock 是代码自动生成或合并的不同策略, 通常用于处理代码的变更或生成
|
2309
|
-
# diff 模式,基于差异生成代码,生成最小的变更集,适用于局部优化,代码重构
|
2310
|
-
# strict_diff 模式,严格验证差异,确保生成的代码符合规则,适用于代码审查,自动化测试
|
2311
|
-
# editblock 模式,基于编辑块生成代码,支持较大范围的修改,适用于代码重构,功能扩展
|
2312
|
-
if args.auto_merge == "editblock":
|
2313
|
-
generate = CodeAutoGenerateEditBlock(llm=self.llm, action=self)
|
2314
|
-
else:
|
2315
|
-
generate = None
|
2316
|
-
|
2317
|
-
if self.args.enable_multi_round_generate:
|
2318
|
-
generate_result = generate.multi_round_run(query=args.query, source_content=content)
|
2319
|
-
else:
|
2320
|
-
generate_result = generate.single_round_run(query=args.query, source_content=content)
|
2321
|
-
logger.info(f"代码生成完成,耗时 {time.time() - start_time:.2f} 秒")
|
2322
|
-
|
2323
|
-
if args.auto_merge:
|
2324
|
-
logger.info("正在自动合并代码...")
|
2325
|
-
if args.auto_merge == "editblock":
|
2326
|
-
code_merge = CodeAutoMergeEditBlock(llm=self.llm)
|
2327
|
-
merge_result = code_merge.merge_code(generate_result=generate_result)
|
2328
|
-
else:
|
2329
|
-
merge_result = None
|
2330
|
-
|
2331
|
-
content = merge_result.contents[0]
|
2332
|
-
else:
|
2333
|
-
content = generate_result.contents[0]
|
2334
|
-
with open(args.target_file, "w") as file:
|
2335
|
-
file.write(content)
|
2336
|
-
|
2337
|
-
|
2338
|
-
class Dispacher:
|
2339
|
-
def __init__(self, llm: Optional[AutoLLM] = None):
|
2340
|
-
self.args = args
|
2341
|
-
self.llm = llm
|
2342
|
-
|
2343
|
-
def dispach(self):
|
2344
|
-
actions = [
|
2345
|
-
ActionPyProject(llm=self.llm),
|
2346
|
-
ActionSuffixProject(llm=self.llm)
|
2347
|
-
]
|
2348
|
-
for action in actions:
|
2349
|
-
if action.run():
|
2350
|
-
return
|
2351
|
-
|
2352
|
-
|
2353
1330
|
def prepare_chat_yaml():
|
2354
1331
|
# auto_coder_main(["next", "chat_action"]) 准备聊天 yaml 文件
|
2355
1332
|
actions_dir = os.path.join(args.source_dir, "actions")
|
@@ -2476,7 +1453,7 @@ def coding(query: str, llm: AutoLLM):
|
|
2476
1453
|
f.write(yaml_content)
|
2477
1454
|
convert_yaml_to_config(execute_file)
|
2478
1455
|
|
2479
|
-
dispacher = Dispacher(llm)
|
1456
|
+
dispacher = Dispacher(args=args, llm=llm)
|
2480
1457
|
dispacher.dispach()
|
2481
1458
|
else:
|
2482
1459
|
logger.warning("创建新的 YAML 文件失败。")
|
@@ -3609,6 +2586,12 @@ def main():
|
|
3609
2586
|
print(f"{memory['mode']} [{MODES[memory['mode']]}]")
|
3610
2587
|
else:
|
3611
2588
|
memory["mode"] = conf
|
2589
|
+
elif user_input.startswith("/exclude_dirs"):
|
2590
|
+
dir_names = user_input[len("/exclude_dirs"):].strip().split(",")
|
2591
|
+
exclude_dirs(dir_names)
|
2592
|
+
elif user_input.startswith("/exclude_files"):
|
2593
|
+
query = user_input[len("/exclude_files"):].strip()
|
2594
|
+
exclude_files(query)
|
3612
2595
|
else:
|
3613
2596
|
command = user_input
|
3614
2597
|
if user_input.startswith("/shell"):
|
@@ -3628,9 +2611,9 @@ def main():
|
|
3628
2611
|
break
|
3629
2612
|
except Exception as e:
|
3630
2613
|
print(f"\033[91m发生异常:\033[0m \033[93m{type(e).__name__}\033[0m - {str(e)}")
|
3631
|
-
|
3632
|
-
|
3633
|
-
|
2614
|
+
if runing_args and runing_args.debug:
|
2615
|
+
import traceback
|
2616
|
+
traceback.print_exc()
|
3634
2617
|
|
3635
2618
|
|
3636
2619
|
if __name__ == '__main__':
|