jarvis-ai-assistant 0.3.29__py3-none-any.whl → 0.3.31__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +116 -9
- jarvis/jarvis_agent/edit_file_handler.py +330 -83
- jarvis/jarvis_agent/run_loop.py +1 -3
- jarvis/jarvis_code_agent/code_agent.py +91 -8
- jarvis/jarvis_data/config_schema.json +46 -4
- jarvis/jarvis_tools/edit_file.py +39 -10
- jarvis/jarvis_tools/generate_new_tool.py +13 -2
- jarvis/jarvis_tools/read_code.py +12 -11
- jarvis/jarvis_tools/registry.py +16 -15
- jarvis/jarvis_utils/config.py +31 -0
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/git_utils.py +34 -18
- jarvis/jarvis_utils/utils.py +74 -18
- {jarvis_ai_assistant-0.3.29.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.3.29.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/RECORD +20 -20
- {jarvis_ai_assistant-0.3.29.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.29.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.29.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.29.dist-info → jarvis_ai_assistant-0.3.31.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,11 @@ class EditFileHandler(OutputHandler):
|
|
13
13
|
ot("PATCH file=(?:'([^']+)'|\"([^\"]+)\"|([^>]+))") + r"\s*"
|
14
14
|
r"(?:"
|
15
15
|
+ ot("DIFF")
|
16
|
-
+ r"\s*"
|
16
|
+
+ r"\s*(?:"
|
17
|
+
# 可选的RANGE标签,限制替换行号范围
|
18
|
+
+ r"(?:" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*)?"
|
19
|
+
+ r"(?:"
|
20
|
+
# 单点替换(SEARCH/REPLACE)
|
17
21
|
+ ot("SEARCH")
|
18
22
|
+ r"(.*?)"
|
19
23
|
+ ct("SEARCH")
|
@@ -21,15 +25,29 @@ class EditFileHandler(OutputHandler):
|
|
21
25
|
+ ot("REPLACE")
|
22
26
|
+ r"(.*?)"
|
23
27
|
+ ct("REPLACE")
|
28
|
+
+ r"|"
|
29
|
+
# 区间替换(SEARCH_START/SEARCH_END/REPLACE)
|
30
|
+
+ ot("SEARCH_START")
|
31
|
+
+ r"(.*?)"
|
32
|
+
+ ct("SEARCH_START")
|
33
|
+
+ r"\s*"
|
34
|
+
+ ot("SEARCH_END")
|
35
|
+
+ r"(.*?)"
|
36
|
+
+ ct("SEARCH_END")
|
24
37
|
+ r"\s*"
|
38
|
+
+ ot("REPLACE")
|
39
|
+
+ r"(.*?)"
|
40
|
+
+ ct("REPLACE")
|
41
|
+
+ r")"
|
42
|
+
+ r")\s*"
|
25
43
|
+ ct("DIFF")
|
26
44
|
+ r"\s*)+"
|
27
|
-
+ ct("PATCH"),
|
28
|
-
re.DOTALL,
|
45
|
+
+ r"^" + ct("PATCH"),
|
46
|
+
re.DOTALL | re.MULTILINE,
|
29
47
|
)
|
30
48
|
self.diff_pattern = re.compile(
|
31
49
|
ot("DIFF")
|
32
|
-
+ r"\s*"
|
50
|
+
+ r"\s*(?:" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*)?"
|
33
51
|
+ ot("SEARCH")
|
34
52
|
+ r"(.*?)"
|
35
53
|
+ ct("SEARCH")
|
@@ -41,6 +59,24 @@ class EditFileHandler(OutputHandler):
|
|
41
59
|
+ ct("DIFF"),
|
42
60
|
re.DOTALL,
|
43
61
|
)
|
62
|
+
self.diff_range_pattern = re.compile(
|
63
|
+
ot("DIFF")
|
64
|
+
+ r"\s*(?:" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*)?"
|
65
|
+
+ ot("SEARCH_START")
|
66
|
+
+ r"(.*?)"
|
67
|
+
+ ct("SEARCH_START")
|
68
|
+
+ r"\s*"
|
69
|
+
+ ot("SEARCH_END")
|
70
|
+
+ r"(.*?)"
|
71
|
+
+ ct("SEARCH_END")
|
72
|
+
+ r"\s*"
|
73
|
+
+ ot("REPLACE")
|
74
|
+
+ r"(.*?)"
|
75
|
+
+ ct("REPLACE")
|
76
|
+
+ r"\s*"
|
77
|
+
+ ct("DIFF"),
|
78
|
+
re.DOTALL,
|
79
|
+
)
|
44
80
|
|
45
81
|
def handle(self, response: str, agent: Any) -> Tuple[bool, str]:
|
46
82
|
"""处理文件编辑响应
|
@@ -65,9 +101,7 @@ class EditFileHandler(OutputHandler):
|
|
65
101
|
|
66
102
|
for file_path, diffs in patches.items():
|
67
103
|
file_path = os.path.abspath(file_path)
|
68
|
-
file_patches =
|
69
|
-
{"SEARCH": diff["SEARCH"], "REPLACE": diff["REPLACE"]} for diff in diffs
|
70
|
-
]
|
104
|
+
file_patches = diffs
|
71
105
|
|
72
106
|
success, result = self._fast_edit(file_path, file_patches)
|
73
107
|
|
@@ -96,17 +130,44 @@ class EditFileHandler(OutputHandler):
|
|
96
130
|
Returns:
|
97
131
|
str: 返回处理器的提示字符串
|
98
132
|
"""
|
99
|
-
|
100
|
-
|
101
|
-
|
133
|
+
from jarvis.jarvis_utils.config import get_patch_format
|
134
|
+
|
135
|
+
patch_format = get_patch_format()
|
136
|
+
|
137
|
+
search_prompt = f"""{ot("DIFF")}
|
102
138
|
{ot("SEARCH")}原始代码{ct("SEARCH")}
|
103
139
|
{ot("REPLACE")}新代码{ct("REPLACE")}
|
104
|
-
{ct("DIFF")}
|
140
|
+
{ct("DIFF")}"""
|
141
|
+
|
142
|
+
search_range_prompt = f"""{ot("DIFF")}
|
143
|
+
{ot("RANGE")}起止行号(如: 10-50),可选{ct("RANGE")}
|
144
|
+
{ot("SEARCH_START")}起始标记{ct("SEARCH_START")}
|
145
|
+
{ot("SEARCH_END")}结束标记{ct("SEARCH_END")}
|
146
|
+
{ot("REPLACE")}替换内容{ct("REPLACE")}
|
147
|
+
{ct("DIFF")}"""
|
148
|
+
|
149
|
+
if patch_format == "search":
|
150
|
+
formats = search_prompt
|
151
|
+
supported_formats = "仅支持单点替换(SEARCH/REPLACE)"
|
152
|
+
elif patch_format == "search_range":
|
153
|
+
formats = search_range_prompt
|
154
|
+
supported_formats = "仅支持区间替换(SEARCH_START/SEARCH_END/REPLACE),可选RANGE限定行号范围"
|
155
|
+
else: # all
|
156
|
+
formats = f"{search_prompt}\n或\n{search_range_prompt}"
|
157
|
+
supported_formats = "支持两种DIFF块:单点替换(SEARCH/REPLACE)与区间替换(SEARCH_START/SEARCH_END/REPLACE)"
|
158
|
+
|
159
|
+
return f"""文件编辑指令格式:
|
160
|
+
{ot("PATCH file=文件路径")}
|
161
|
+
{formats}
|
105
162
|
{ct("PATCH")}
|
106
163
|
|
107
|
-
|
108
|
-
|
109
|
-
|
164
|
+
注意:
|
165
|
+
- {ot("PATCH")} 和 {ct("PATCH")} 必须出现在行首,否则不生效(会被忽略)
|
166
|
+
- {supported_formats}
|
167
|
+
- {ot("RANGE")}start-end{ct("RANGE")} 仅用于区间替换模式(SEARCH_START/SEARCH_END),表示只在指定行号范围内进行匹配与替换(1-based,闭区间);省略则在整个文件范围内处理
|
168
|
+
- 单点替换要求 SEARCH 在有效范围内唯一匹配(仅替换第一个匹配)
|
169
|
+
- 区间替换命中有效范围内的第一个 {ot("SEARCH_START")} 及其后的第一个 {ot("SEARCH_END")}
|
170
|
+
否则编辑将失败。"""
|
110
171
|
|
111
172
|
def name(self) -> str:
|
112
173
|
"""获取处理器的名称
|
@@ -139,18 +200,75 @@ class EditFileHandler(OutputHandler):
|
|
139
200
|
}
|
140
201
|
"""
|
141
202
|
patches: Dict[str, List[Dict[str, str]]] = {}
|
203
|
+
|
142
204
|
for match in self.patch_pattern.finditer(response):
|
143
205
|
# Get the file path from the appropriate capture group
|
144
206
|
file_path = match.group(1) or match.group(2) or match.group(3)
|
145
|
-
diffs = []
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
207
|
+
diffs: List[Dict[str, str]] = []
|
208
|
+
|
209
|
+
# 逐块解析,保持 DIFF 顺序
|
210
|
+
diff_block_pattern = re.compile(ot("DIFF") + r"(.*?)" + ct("DIFF"), re.DOTALL)
|
211
|
+
for block_match in diff_block_pattern.finditer(match.group(0)):
|
212
|
+
block_text = block_match.group(1)
|
213
|
+
|
214
|
+
# 提取可选的行号范围
|
215
|
+
range_scope = None
|
216
|
+
range_scope_match = re.match(
|
217
|
+
r"^\s*" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*",
|
218
|
+
block_text,
|
219
|
+
re.DOTALL,
|
220
|
+
)
|
221
|
+
if range_scope_match:
|
222
|
+
range_scope = range_scope_match.group(1).strip()
|
223
|
+
# 仅移除块首部的RANGE标签,避免误删内容中的同名标记
|
224
|
+
block_text = block_text[range_scope_match.end():]
|
225
|
+
# 统一按 all 解析:无视配置,始终尝试区间替换
|
226
|
+
range_match = re.search(
|
227
|
+
ot("SEARCH_START")
|
228
|
+
+ r"(.*?)"
|
229
|
+
+ ct("SEARCH_START")
|
230
|
+
+ r"\s*"
|
231
|
+
+ ot("SEARCH_END")
|
232
|
+
+ r"(.*?)"
|
233
|
+
+ ct("SEARCH_END")
|
234
|
+
+ r"\s*"
|
235
|
+
+ ot("REPLACE")
|
236
|
+
+ r"(.*?)"
|
237
|
+
+ ct("REPLACE"),
|
238
|
+
block_text,
|
239
|
+
re.DOTALL,
|
240
|
+
)
|
241
|
+
if range_match:
|
242
|
+
diff_item: Dict[str, str] = {
|
243
|
+
"SEARCH_START": range_match.group(1), # 原始SEARCH_START内容
|
244
|
+
"SEARCH_END": range_match.group(2), # 原始SEARCH_END内容
|
245
|
+
"REPLACE": range_match.group(3), # 原始REPLACE内容
|
152
246
|
}
|
247
|
+
if range_scope:
|
248
|
+
diff_item["RANGE"] = range_scope
|
249
|
+
diffs.append(diff_item)
|
250
|
+
continue
|
251
|
+
|
252
|
+
# 解析单点替换(统一按 all 解析:无视配置,始终尝试单点替换)
|
253
|
+
single_match = re.search(
|
254
|
+
ot("SEARCH")
|
255
|
+
+ r"(.*?)"
|
256
|
+
+ ct("SEARCH")
|
257
|
+
+ r"\s*"
|
258
|
+
+ ot("REPLACE")
|
259
|
+
+ r"(.*?)"
|
260
|
+
+ ct("REPLACE"),
|
261
|
+
block_text,
|
262
|
+
re.DOTALL,
|
153
263
|
)
|
264
|
+
if single_match:
|
265
|
+
diff_item = {
|
266
|
+
"SEARCH": single_match.group(1), # 原始SEARCH内容
|
267
|
+
"REPLACE": single_match.group(2), # 原始REPLACE内容
|
268
|
+
}
|
269
|
+
# SEARCH 模式不支持 RANGE,直接忽略
|
270
|
+
diffs.append(diff_item)
|
271
|
+
|
154
272
|
if diffs:
|
155
273
|
if file_path in patches:
|
156
274
|
patches[file_path].extend(diffs)
|
@@ -194,85 +312,214 @@ class EditFileHandler(OutputHandler):
|
|
194
312
|
failed_patches: List[Dict[str, Any]] = []
|
195
313
|
successful_patches = 0
|
196
314
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
315
|
+
# 当存在RANGE时,确保按行号从后往前应用补丁,避免前面补丁影响后续RANGE的行号
|
316
|
+
ordered_patches: List[Dict[str, str]] = []
|
317
|
+
range_items: List[Tuple[int, int, int, Dict[str, str]]] = []
|
318
|
+
non_range_items: List[Tuple[int, Dict[str, str]]] = []
|
319
|
+
for idx, p in enumerate(patches):
|
320
|
+
r = p.get("RANGE")
|
321
|
+
if r and str(r).strip():
|
322
|
+
m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(r))
|
323
|
+
if m:
|
324
|
+
start_line = int(m.group(1))
|
325
|
+
end_line = int(m.group(2))
|
326
|
+
range_items.append((start_line, end_line, idx, p))
|
327
|
+
else:
|
328
|
+
# RANGE格式无效的补丁保持原有顺序
|
329
|
+
non_range_items.append((idx, p))
|
330
|
+
else:
|
331
|
+
# 无RANGE的补丁保持原有顺序
|
332
|
+
non_range_items.append((idx, p))
|
333
|
+
# 先应用RANGE补丁:按start_line、end_line、原始索引逆序
|
334
|
+
range_items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=True)
|
335
|
+
ordered_patches = [item[3] for item in range_items] + [item[1] for item in non_range_items]
|
336
|
+
|
337
|
+
patch_count = len(ordered_patches)
|
338
|
+
for patch in ordered_patches:
|
204
339
|
found = False
|
205
340
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
341
|
+
# 处理可选的RANGE范围:格式 "start-end"(1-based, 闭区间)
|
342
|
+
scoped = False
|
343
|
+
prefix = suffix = ""
|
344
|
+
base_content = modified_content
|
345
|
+
if "RANGE" in patch and str(patch["RANGE"]).strip():
|
346
|
+
m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(patch["RANGE"]))
|
347
|
+
if not m:
|
348
|
+
error_msg = "RANGE格式无效,应为 'start-end' 的行号范围(1-based, 闭区间)"
|
349
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
350
|
+
# 不进行本补丁其它处理
|
351
|
+
continue
|
352
|
+
start_line = int(m.group(1))
|
353
|
+
end_line = int(m.group(2))
|
354
|
+
|
355
|
+
# 拆分为三段
|
356
|
+
lines = modified_content.splitlines(keepends=True)
|
357
|
+
total_lines = len(lines)
|
215
358
|
if (
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
359
|
+
start_line < 1
|
360
|
+
or end_line < 1
|
361
|
+
or start_line > end_line
|
362
|
+
or start_line > total_lines
|
220
363
|
):
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
364
|
+
error_msg = f"RANGE行号无效(文件共有{total_lines}行)"
|
365
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
366
|
+
continue
|
367
|
+
# 截断end_line不超过总行数
|
368
|
+
end_line = min(end_line, total_lines)
|
369
|
+
|
370
|
+
prefix = "".join(lines[: start_line - 1])
|
371
|
+
base_content = "".join(lines[start_line - 1 : end_line])
|
372
|
+
suffix = "".join(lines[end_line:])
|
373
|
+
scoped = True
|
374
|
+
|
375
|
+
# 单点替换
|
376
|
+
if "SEARCH" in patch:
|
377
|
+
search_text = patch["SEARCH"]
|
378
|
+
replace_text = patch["REPLACE"]
|
379
|
+
|
380
|
+
# 精确匹配搜索文本(保留原始换行和空格)
|
381
|
+
exact_search = search_text
|
382
|
+
|
383
|
+
def _count_occurrences(haystack: str, needle: str) -> int:
|
384
|
+
if not needle:
|
385
|
+
return 0
|
386
|
+
return haystack.count(needle)
|
387
|
+
|
388
|
+
# 1) 精确匹配,要求唯一
|
389
|
+
cnt = _count_occurrences(base_content, exact_search)
|
390
|
+
if cnt == 1:
|
391
|
+
base_content = base_content.replace(exact_search, replace_text, 1)
|
392
|
+
found = True
|
393
|
+
elif cnt > 1:
|
394
|
+
error_msg = "SEARCH 在指定范围内出现多次,要求唯一匹配"
|
395
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
396
|
+
# 不继续尝试其它变体
|
397
|
+
continue
|
398
|
+
else:
|
399
|
+
# 2) 若首尾均为换行,尝试去掉首尾换行后匹配,要求唯一
|
234
400
|
if (
|
235
|
-
|
236
|
-
and
|
237
|
-
and
|
238
|
-
and
|
401
|
+
search_text.startswith("\n")
|
402
|
+
and search_text.endswith("\n")
|
403
|
+
and replace_text.startswith("\n")
|
404
|
+
and replace_text.endswith("\n")
|
239
405
|
):
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
for line in current_search.split("\n")
|
247
|
-
)
|
248
|
-
indented_replace = "\n".join(
|
249
|
-
" " * space_count + line if line.strip() else line
|
250
|
-
for line in current_replace.split("\n")
|
251
|
-
)
|
252
|
-
if indented_search in modified_content:
|
253
|
-
modified_content = modified_content.replace(
|
254
|
-
indented_search, indented_replace, 1
|
406
|
+
stripped_search = search_text[1:-1]
|
407
|
+
stripped_replace = replace_text[1:-1]
|
408
|
+
cnt2 = _count_occurrences(base_content, stripped_search)
|
409
|
+
if cnt2 == 1:
|
410
|
+
base_content = base_content.replace(
|
411
|
+
stripped_search, stripped_replace, 1
|
255
412
|
)
|
256
|
-
|
257
413
|
found = True
|
258
|
-
|
414
|
+
elif cnt2 > 1:
|
415
|
+
error_msg = "SEARCH 在指定范围内出现多次(去掉首尾换行后),要求唯一匹配"
|
416
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
417
|
+
continue
|
418
|
+
|
419
|
+
# 3) 尝试缩进适配(1..16个空格),要求唯一
|
420
|
+
if not found:
|
421
|
+
current_search = search_text
|
422
|
+
current_replace = replace_text
|
423
|
+
if (
|
424
|
+
current_search.startswith("\n")
|
425
|
+
and current_search.endswith("\n")
|
426
|
+
and current_replace.startswith("\n")
|
427
|
+
and current_replace.endswith("\n")
|
428
|
+
):
|
429
|
+
current_search = current_search[1:-1]
|
430
|
+
current_replace = current_replace[1:-1]
|
431
|
+
|
432
|
+
for space_count in range(1, 17):
|
433
|
+
indented_search = "\n".join(
|
434
|
+
" " * space_count + line if line.strip() else line
|
435
|
+
for line in current_search.split("\n")
|
436
|
+
)
|
437
|
+
indented_replace = "\n".join(
|
438
|
+
" " * space_count + line if line.strip() else line
|
439
|
+
for line in current_replace.split("\n")
|
440
|
+
)
|
441
|
+
cnt3 = _count_occurrences(base_content, indented_search)
|
442
|
+
if cnt3 == 1:
|
443
|
+
base_content = base_content.replace(
|
444
|
+
indented_search, indented_replace, 1
|
445
|
+
)
|
446
|
+
found = True
|
447
|
+
break
|
448
|
+
elif cnt3 > 1:
|
449
|
+
error_msg = "SEARCH 在指定范围内出现多次(缩进适配后),要求唯一匹配"
|
450
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
451
|
+
# 多匹配直接失败,不再继续尝试其它缩进
|
452
|
+
found = False
|
453
|
+
break
|
454
|
+
|
455
|
+
if not found:
|
456
|
+
# 未找到任何可用的唯一匹配
|
457
|
+
failed_patches.append({"patch": patch, "error": "未找到唯一匹配的SEARCH"})
|
458
|
+
|
459
|
+
# 区间替换
|
460
|
+
elif "SEARCH_START" in patch and "SEARCH_END" in patch:
|
461
|
+
search_start = patch["SEARCH_START"]
|
462
|
+
search_end = patch["SEARCH_END"]
|
463
|
+
replace_text = patch["REPLACE"]
|
464
|
+
|
465
|
+
# 范围替换(包含边界),命中第一个起始标记及其后的第一个结束标记
|
466
|
+
start_idx = base_content.find(search_start)
|
467
|
+
if start_idx == -1:
|
468
|
+
error_msg = "未找到SEARCH_START"
|
469
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
470
|
+
else:
|
471
|
+
end_idx = base_content.find(search_end, start_idx)
|
472
|
+
if end_idx == -1:
|
473
|
+
error_msg = "在SEARCH_START之后未找到SEARCH_END"
|
474
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
475
|
+
else:
|
476
|
+
# 避免额外空行:
|
477
|
+
# 若 REPLACE 以换行结尾且 SEARCH_END 后紧跟换行符,
|
478
|
+
# 则将该换行并入替换范围,防止出现双重换行导致“多一行”
|
479
|
+
end_of_range = end_idx + len(search_end)
|
480
|
+
if (
|
481
|
+
end_of_range < len(base_content)
|
482
|
+
and base_content[end_of_range] == "\n"
|
483
|
+
and replace_text.endswith("\n")
|
484
|
+
):
|
485
|
+
end_of_range += 1
|
486
|
+
base_content = (
|
487
|
+
base_content[:start_idx]
|
488
|
+
+ replace_text
|
489
|
+
+ base_content[end_of_range:]
|
490
|
+
)
|
491
|
+
found = True
|
259
492
|
|
260
|
-
if found:
|
261
|
-
successful_patches += 1
|
262
493
|
else:
|
263
|
-
error_msg = "
|
264
|
-
|
494
|
+
error_msg = "不支持的补丁格式"
|
265
495
|
failed_patches.append({"patch": patch, "error": error_msg})
|
266
496
|
|
497
|
+
# 若使用了RANGE,则将局部修改写回整体内容
|
498
|
+
if found:
|
499
|
+
if scoped:
|
500
|
+
modified_content = prefix + base_content + suffix
|
501
|
+
else:
|
502
|
+
modified_content = base_content
|
503
|
+
successful_patches += 1
|
504
|
+
|
267
505
|
# 写入修改后的内容
|
268
506
|
with open(file_path, "w", encoding="utf-8") as f:
|
269
507
|
f.write(modified_content)
|
270
508
|
|
271
509
|
if failed_patches:
|
272
|
-
error_details = [
|
273
|
-
|
274
|
-
|
275
|
-
|
510
|
+
error_details = []
|
511
|
+
for p in failed_patches:
|
512
|
+
patch = p["patch"]
|
513
|
+
if "SEARCH" in patch:
|
514
|
+
patch_desc = patch["SEARCH"]
|
515
|
+
else:
|
516
|
+
patch_desc = (
|
517
|
+
"SEARCH_START:\n"
|
518
|
+
+ (patch.get("SEARCH_START", ""))
|
519
|
+
+ "\nSEARCH_END:\n"
|
520
|
+
+ (patch.get("SEARCH_END", ""))
|
521
|
+
)
|
522
|
+
error_details.append(f" - 失败的补丁: \n{patch_desc}\n 错误: {p['error']}")
|
276
523
|
if successful_patches == 0:
|
277
524
|
summary = (
|
278
525
|
f"文件 {file_path} 修改失败(全部失败)。\n"
|
jarvis/jarvis_agent/run_loop.py
CHANGED
@@ -12,6 +12,7 @@ from typing import List, Optional, Tuple
|
|
12
12
|
import typer
|
13
13
|
|
14
14
|
from jarvis.jarvis_agent import Agent
|
15
|
+
from jarvis.jarvis_agent.events import AFTER_TOOL_CALL
|
15
16
|
from jarvis.jarvis_agent.builtin_input_handler import builtin_input_handler
|
16
17
|
from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
|
17
18
|
from jarvis.jarvis_agent.shell_input_handler import shell_input_handler
|
@@ -94,7 +95,7 @@ class CodeAgent:
|
|
94
95
|
use_analysis=False, # 禁用分析
|
95
96
|
)
|
96
97
|
|
97
|
-
self.agent.
|
98
|
+
self.agent.event_bus.subscribe(AFTER_TOOL_CALL, self._on_after_tool_call)
|
98
99
|
|
99
100
|
def _get_system_prompt(self) -> str:
|
100
101
|
"""获取代码工程师的系统提示词"""
|
@@ -528,8 +529,9 @@ class CodeAgent:
|
|
528
529
|
if self.agent.force_save_memory:
|
529
530
|
self.agent.memory_manager.prompt_memory_save()
|
530
531
|
elif start_commit:
|
531
|
-
|
532
|
-
|
532
|
+
if user_confirm("是否要重置到初始提交?", True):
|
533
|
+
os.system(f"git reset --hard {str(start_commit)}") # 确保转换为字符串
|
534
|
+
PrettyOutput.print("已重置到初始提交", OutputType.INFO)
|
533
535
|
|
534
536
|
def run(self, user_input: str, prefix: str = "", suffix: str = "") -> Optional[str]:
|
535
537
|
"""使用给定的用户输入运行代码代理。
|
@@ -598,14 +600,96 @@ class CodeAgent:
|
|
598
600
|
except RuntimeError as e:
|
599
601
|
return f"Error during execution: {str(e)}"
|
600
602
|
|
601
|
-
def
|
603
|
+
def _on_after_tool_call(self, agent: Agent, current_response=None, need_return=None, tool_prompt=None, **kwargs) -> None:
|
602
604
|
"""工具调用后回调函数。"""
|
603
605
|
final_ret = ""
|
604
606
|
diff = get_diff()
|
607
|
+
|
608
|
+
# 构造按文件的状态映射与差异文本,删除文件不展示diff,仅提示删除
|
609
|
+
def _build_name_status_map() -> dict:
|
610
|
+
status_map = {}
|
611
|
+
try:
|
612
|
+
head_exists = bool(get_latest_commit_hash())
|
613
|
+
# 临时 -N 以包含未跟踪文件的差异检测
|
614
|
+
subprocess.run(["git", "add", "-N", "."], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
615
|
+
cmd = ["git", "diff", "--name-status"] + (["HEAD"] if head_exists else [])
|
616
|
+
res = subprocess.run(
|
617
|
+
cmd,
|
618
|
+
capture_output=True,
|
619
|
+
text=True,
|
620
|
+
encoding="utf-8",
|
621
|
+
errors="replace",
|
622
|
+
check=False,
|
623
|
+
)
|
624
|
+
finally:
|
625
|
+
subprocess.run(["git", "reset"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
626
|
+
|
627
|
+
if res.returncode == 0 and res.stdout:
|
628
|
+
for line in res.stdout.splitlines():
|
629
|
+
if not line.strip():
|
630
|
+
continue
|
631
|
+
parts = line.split("\t")
|
632
|
+
if not parts:
|
633
|
+
continue
|
634
|
+
status = parts[0]
|
635
|
+
if status.startswith("R") or status.startswith("C"):
|
636
|
+
# 重命名/复制:使用新路径作为键
|
637
|
+
if len(parts) >= 3:
|
638
|
+
old_path, new_path = parts[1], parts[2]
|
639
|
+
status_map[new_path] = status
|
640
|
+
# 也记录旧路径,便于匹配 name-only 的结果
|
641
|
+
status_map[old_path] = status
|
642
|
+
elif len(parts) >= 2:
|
643
|
+
status_map[parts[-1]] = status
|
644
|
+
else:
|
645
|
+
if len(parts) >= 2:
|
646
|
+
status_map[parts[1]] = status
|
647
|
+
return status_map
|
648
|
+
|
649
|
+
def _get_file_diff(file_path: str) -> str:
|
650
|
+
"""获取单文件的diff,包含新增文件内容;失败时返回空字符串"""
|
651
|
+
head_exists = bool(get_latest_commit_hash())
|
652
|
+
try:
|
653
|
+
# 为了让未跟踪文件也能展示diff,临时 -N 该文件
|
654
|
+
subprocess.run(["git", "add", "-N", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
655
|
+
cmd = ["git", "diff"] + (["HEAD"] if head_exists else []) + ["--", file_path]
|
656
|
+
res = subprocess.run(
|
657
|
+
cmd,
|
658
|
+
capture_output=True,
|
659
|
+
text=True,
|
660
|
+
encoding="utf-8",
|
661
|
+
errors="replace",
|
662
|
+
check=False,
|
663
|
+
)
|
664
|
+
if res.returncode == 0:
|
665
|
+
return res.stdout or ""
|
666
|
+
return ""
|
667
|
+
finally:
|
668
|
+
subprocess.run(["git", "reset", "--", file_path], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
669
|
+
|
670
|
+
def _build_per_file_patch_preview(modified_files: List[str]) -> str:
|
671
|
+
status_map = _build_name_status_map()
|
672
|
+
lines: List[str] = []
|
673
|
+
for f in modified_files:
|
674
|
+
status = status_map.get(f, "")
|
675
|
+
# 删除文件:不展示diff,仅提示
|
676
|
+
if (status.startswith("D")) or (not os.path.exists(f)):
|
677
|
+
lines.append(f"- {f} 文件被删除")
|
678
|
+
continue
|
679
|
+
# 其它情况:展示该文件的diff
|
680
|
+
file_diff = _get_file_diff(f)
|
681
|
+
if file_diff.strip():
|
682
|
+
lines.append(f"文件: {f}\n```diff\n{file_diff}\n```")
|
683
|
+
else:
|
684
|
+
# 当无法获取到diff(例如重命名或特殊状态),避免空输出
|
685
|
+
lines.append(f"- {f} 变更已记录(无可展示的文本差异)")
|
686
|
+
return "\n".join(lines)
|
687
|
+
|
605
688
|
if diff:
|
606
689
|
start_hash = get_latest_commit_hash()
|
607
690
|
PrettyOutput.print(diff, OutputType.CODE, lang="diff")
|
608
691
|
modified_files = get_diff_file_list()
|
692
|
+
per_file_preview = _build_per_file_patch_preview(modified_files)
|
609
693
|
commited = handle_commit_workflow()
|
610
694
|
if commited:
|
611
695
|
# 统计代码行数变化
|
@@ -629,15 +713,14 @@ class CodeAgent:
|
|
629
713
|
|
630
714
|
StatsManager.increment("code_modifications", group="code_agent")
|
631
715
|
|
632
|
-
|
633
716
|
# 获取提交信息
|
634
717
|
end_hash = get_latest_commit_hash()
|
635
718
|
commits = get_commits_between(start_hash, end_hash)
|
636
719
|
|
637
|
-
# 添加提交信息到final_ret
|
720
|
+
# 添加提交信息到final_ret(按文件展示diff;删除文件仅提示)
|
638
721
|
if commits:
|
639
722
|
final_ret += (
|
640
|
-
f"\n\n代码已修改完成\n
|
723
|
+
f"\n\n代码已修改完成\n补丁内容(按文件):\n{per_file_preview}\n"
|
641
724
|
)
|
642
725
|
# 修改后的提示逻辑
|
643
726
|
lint_tools_info = "\n".join(
|
@@ -664,7 +747,7 @@ class CodeAgent:
|
|
664
747
|
final_ret += "\n\n修改没有生效\n"
|
665
748
|
else:
|
666
749
|
final_ret += "\n修改被拒绝\n"
|
667
|
-
final_ret += f"#
|
750
|
+
final_ret += f"# 补丁预览(按文件):\n{per_file_preview}"
|
668
751
|
else:
|
669
752
|
return
|
670
753
|
# 用户确认最终结果
|