jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.0__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 +289 -87
- jarvis/jarvis_agent/agent_manager.py +17 -8
- jarvis/jarvis_agent/edit_file_handler.py +374 -86
- jarvis/jarvis_agent/event_bus.py +1 -1
- jarvis/jarvis_agent/file_context_handler.py +79 -0
- jarvis/jarvis_agent/jarvis.py +601 -43
- jarvis/jarvis_agent/main.py +32 -2
- jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
- jarvis/jarvis_agent/run_loop.py +38 -5
- jarvis/jarvis_agent/share_manager.py +8 -1
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +5 -2
- jarvis/jarvis_agent/task_planner.py +496 -0
- jarvis/jarvis_agent/utils.py +5 -1
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +751 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +613 -0
- jarvis/jarvis_c2rust/collector.py +258 -0
- jarvis/jarvis_c2rust/library_replacer.py +1122 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
- jarvis/jarvis_c2rust/optimizer.py +960 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2325 -0
- jarvis/jarvis_code_agent/build_validation_config.py +133 -0
- jarvis/jarvis_code_agent/code_agent.py +1171 -94
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
- jarvis/jarvis_code_agent/lint.py +270 -8
- jarvis/jarvis_code_agent/utils.py +142 -0
- jarvis/jarvis_code_analysis/code_review.py +483 -569
- jarvis/jarvis_data/config_schema.json +97 -8
- jarvis/jarvis_git_utils/git_commiter.py +38 -26
- jarvis/jarvis_mcp/sse_mcp_client.py +2 -2
- jarvis/jarvis_mcp/stdio_mcp_client.py +1 -1
- jarvis/jarvis_memory_organizer/memory_organizer.py +1 -1
- jarvis/jarvis_multi_agent/__init__.py +239 -25
- jarvis/jarvis_multi_agent/main.py +37 -1
- jarvis/jarvis_platform/base.py +103 -51
- jarvis/jarvis_platform/openai.py +26 -1
- jarvis/jarvis_platform/yuanbao.py +1 -1
- jarvis/jarvis_platform_manager/service.py +2 -2
- jarvis/jarvis_rag/cli.py +4 -4
- jarvis/jarvis_sec/__init__.py +3605 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +116 -0
- jarvis/jarvis_sec/report.py +257 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/workflow.py +219 -0
- jarvis/jarvis_stats/cli.py +1 -1
- jarvis/jarvis_stats/stats.py +1 -1
- jarvis/jarvis_stats/visualizer.py +1 -1
- jarvis/jarvis_tools/cli/main.py +1 -0
- jarvis/jarvis_tools/execute_script.py +46 -9
- jarvis/jarvis_tools/generate_new_tool.py +3 -1
- jarvis/jarvis_tools/read_code.py +275 -12
- jarvis/jarvis_tools/read_symbols.py +141 -0
- jarvis/jarvis_tools/read_webpage.py +5 -3
- jarvis/jarvis_tools/registry.py +73 -35
- jarvis/jarvis_tools/search_web.py +15 -11
- jarvis/jarvis_tools/sub_agent.py +24 -42
- jarvis/jarvis_tools/sub_code_agent.py +14 -13
- jarvis/jarvis_tools/virtual_tty.py +1 -1
- jarvis/jarvis_utils/config.py +187 -35
- jarvis/jarvis_utils/embedding.py +3 -0
- jarvis/jarvis_utils/git_utils.py +181 -6
- jarvis/jarvis_utils/globals.py +3 -3
- jarvis/jarvis_utils/http.py +1 -1
- jarvis/jarvis_utils/input.py +78 -2
- jarvis/jarvis_utils/methodology.py +25 -19
- jarvis/jarvis_utils/utils.py +644 -359
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/METADATA +85 -1
- jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +4 -0
- jarvis/jarvis_agent/config.py +0 -92
- jarvis/jarvis_tools/edit_file.py +0 -179
- jarvis/jarvis_tools/rewrite_file.py +0 -191
- jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.0.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
|
"""处理文件编辑响应
|
|
@@ -54,20 +90,23 @@ class EditFileHandler(OutputHandler):
|
|
|
54
90
|
"""
|
|
55
91
|
patches = self._parse_patches(response)
|
|
56
92
|
if not patches:
|
|
93
|
+
# 当响应中存在 PATCH 标签但未能解析出合法的补丁内容时,提示格式错误并给出合法格式
|
|
94
|
+
has_patch_open = bool(re.search("<PATCH", response))
|
|
95
|
+
has_patch_close = bool(re.search(ct("PATCH"), response))
|
|
96
|
+
if has_patch_open and has_patch_close:
|
|
97
|
+
return False, f"PATCH格式错误。合法的格式如下:\n{self.prompt()}"
|
|
57
98
|
return False, "未找到有效的文件编辑指令"
|
|
58
99
|
|
|
59
|
-
# 记录
|
|
100
|
+
# 记录 PATCH 操作调用统计
|
|
60
101
|
from jarvis.jarvis_stats.stats import StatsManager
|
|
61
102
|
|
|
62
|
-
StatsManager.increment("
|
|
103
|
+
StatsManager.increment("patch", group="tool")
|
|
63
104
|
|
|
64
105
|
results = []
|
|
65
106
|
|
|
66
107
|
for file_path, diffs in patches.items():
|
|
67
108
|
file_path = os.path.abspath(file_path)
|
|
68
|
-
file_patches =
|
|
69
|
-
{"SEARCH": diff["SEARCH"], "REPLACE": diff["REPLACE"]} for diff in diffs
|
|
70
|
-
]
|
|
109
|
+
file_patches = diffs
|
|
71
110
|
|
|
72
111
|
success, result = self._fast_edit(file_path, file_patches)
|
|
73
112
|
|
|
@@ -88,7 +127,11 @@ class EditFileHandler(OutputHandler):
|
|
|
88
127
|
Returns:
|
|
89
128
|
bool: 返回是否能处理该响应
|
|
90
129
|
"""
|
|
91
|
-
|
|
130
|
+
# 只要检测到 PATCH 标签(包含起止标签),即认为可处理,
|
|
131
|
+
# 具体合法性由 handle() 内的解析与错误提示负责
|
|
132
|
+
has_patch_open = bool(re.search("<PATCH", response))
|
|
133
|
+
has_patch_close = bool(re.search(ct("PATCH"), response))
|
|
134
|
+
return has_patch_open and has_patch_close
|
|
92
135
|
|
|
93
136
|
def prompt(self) -> str:
|
|
94
137
|
"""获取处理器的提示信息
|
|
@@ -96,17 +139,48 @@ class EditFileHandler(OutputHandler):
|
|
|
96
139
|
Returns:
|
|
97
140
|
str: 返回处理器的提示字符串
|
|
98
141
|
"""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
from jarvis.jarvis_utils.config import get_patch_format
|
|
143
|
+
|
|
144
|
+
patch_format = get_patch_format()
|
|
145
|
+
|
|
146
|
+
search_prompt = f"""{ot("DIFF")}
|
|
147
|
+
{ot("RANGE")}起止行号(如: 10-50),可选{ct("RANGE")}
|
|
102
148
|
{ot("SEARCH")}原始代码{ct("SEARCH")}
|
|
103
149
|
{ot("REPLACE")}新代码{ct("REPLACE")}
|
|
104
|
-
{ct("DIFF")}
|
|
150
|
+
{ct("DIFF")}"""
|
|
151
|
+
|
|
152
|
+
search_range_prompt = f"""{ot("DIFF")}
|
|
153
|
+
{ot("RANGE")}起止行号(如: 10-50),可选{ct("RANGE")}
|
|
154
|
+
{ot("SEARCH_START")}起始标记{ct("SEARCH_START")}
|
|
155
|
+
{ot("SEARCH_END")}结束标记{ct("SEARCH_END")}
|
|
156
|
+
{ot("REPLACE")}替换内容{ct("REPLACE")}
|
|
157
|
+
{ct("DIFF")}"""
|
|
158
|
+
|
|
159
|
+
if patch_format == "search":
|
|
160
|
+
formats = search_prompt
|
|
161
|
+
supported_formats = "仅支持单点替换(SEARCH/REPLACE)"
|
|
162
|
+
usage_recommendation = ""
|
|
163
|
+
elif patch_format == "search_range":
|
|
164
|
+
formats = search_range_prompt
|
|
165
|
+
supported_formats = "仅支持区间替换(SEARCH_START/SEARCH_END/REPLACE),可选RANGE限定行号范围"
|
|
166
|
+
usage_recommendation = ""
|
|
167
|
+
else: # all
|
|
168
|
+
formats = f"{search_prompt}\n或\n{search_range_prompt}"
|
|
169
|
+
supported_formats = "支持两种DIFF块:单点替换(SEARCH/REPLACE)与区间替换(SEARCH_START/SEARCH_END/REPLACE)"
|
|
170
|
+
usage_recommendation = "\n推荐:优先使用单点替换(SEARCH/REPLACE)模式,除非需要大段修改,否则不要使用区间替换(SEARCH_START/SEARCH_END/REPLACE)模式"
|
|
171
|
+
|
|
172
|
+
return f"""文件编辑指令格式:
|
|
173
|
+
{ot("PATCH file=文件路径")}
|
|
174
|
+
{formats}
|
|
105
175
|
{ct("PATCH")}
|
|
106
176
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
177
|
+
注意:
|
|
178
|
+
- {ot("PATCH")} 和 {ct("PATCH")} 必须出现在行首,否则不生效(会被忽略)
|
|
179
|
+
- {supported_formats}{usage_recommendation}
|
|
180
|
+
- {ot("RANGE")}start-end{ct("RANGE")} 可用于单点替换(SEARCH/REPLACE)和区间替换(SEARCH_START/SEARCH_END)模式,表示只在指定行号范围内进行匹配与替换(1-based,闭区间);省略则在整个文件范围内处理
|
|
181
|
+
- 单点替换要求 SEARCH 在有效范围内唯一匹配(仅替换第一个匹配)
|
|
182
|
+
- 区间替换会从包含 {ot("SEARCH_START")} 的行首开始,到包含 {ot("SEARCH_END")} 的行尾结束,替换整个区域
|
|
183
|
+
否则编辑将失败。"""
|
|
110
184
|
|
|
111
185
|
def name(self) -> str:
|
|
112
186
|
"""获取处理器的名称
|
|
@@ -139,18 +213,77 @@ class EditFileHandler(OutputHandler):
|
|
|
139
213
|
}
|
|
140
214
|
"""
|
|
141
215
|
patches: Dict[str, List[Dict[str, str]]] = {}
|
|
216
|
+
|
|
142
217
|
for match in self.patch_pattern.finditer(response):
|
|
143
218
|
# Get the file path from the appropriate capture group
|
|
144
219
|
file_path = match.group(1) or match.group(2) or match.group(3)
|
|
145
|
-
diffs = []
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
220
|
+
diffs: List[Dict[str, str]] = []
|
|
221
|
+
|
|
222
|
+
# 逐块解析,保持 DIFF 顺序
|
|
223
|
+
diff_block_pattern = re.compile(ot("DIFF") + r"(.*?)" + ct("DIFF"), re.DOTALL)
|
|
224
|
+
for block_match in diff_block_pattern.finditer(match.group(0)):
|
|
225
|
+
block_text = block_match.group(1)
|
|
226
|
+
|
|
227
|
+
# 提取可选的行号范围
|
|
228
|
+
range_scope = None
|
|
229
|
+
range_scope_match = re.match(
|
|
230
|
+
r"^\s*" + ot("RANGE") + r"(.*?)" + ct("RANGE") + r"\s*",
|
|
231
|
+
block_text,
|
|
232
|
+
re.DOTALL,
|
|
233
|
+
)
|
|
234
|
+
if range_scope_match:
|
|
235
|
+
range_scope = range_scope_match.group(1).strip()
|
|
236
|
+
# 仅移除块首部的RANGE标签,避免误删内容中的同名标记
|
|
237
|
+
block_text = block_text[range_scope_match.end():]
|
|
238
|
+
# 统一按 all 解析:无视配置,始终尝试区间替换
|
|
239
|
+
range_match = re.search(
|
|
240
|
+
ot("SEARCH_START")
|
|
241
|
+
+ r"(.*?)"
|
|
242
|
+
+ ct("SEARCH_START")
|
|
243
|
+
+ r"\s*"
|
|
244
|
+
+ ot("SEARCH_END")
|
|
245
|
+
+ r"(.*?)"
|
|
246
|
+
+ ct("SEARCH_END")
|
|
247
|
+
+ r"\s*"
|
|
248
|
+
+ ot("REPLACE")
|
|
249
|
+
+ r"(.*?)"
|
|
250
|
+
+ ct("REPLACE"),
|
|
251
|
+
block_text,
|
|
252
|
+
re.DOTALL,
|
|
253
|
+
)
|
|
254
|
+
if range_match:
|
|
255
|
+
diff_item: Dict[str, str] = {
|
|
256
|
+
"SEARCH_START": range_match.group(1), # 原始SEARCH_START内容
|
|
257
|
+
"SEARCH_END": range_match.group(2), # 原始SEARCH_END内容
|
|
258
|
+
"REPLACE": range_match.group(3), # 原始REPLACE内容
|
|
152
259
|
}
|
|
260
|
+
if range_scope:
|
|
261
|
+
diff_item["RANGE"] = range_scope
|
|
262
|
+
diffs.append(diff_item)
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# 解析单点替换(统一按 all 解析:无视配置,始终尝试单点替换)
|
|
266
|
+
single_match = re.search(
|
|
267
|
+
ot("SEARCH")
|
|
268
|
+
+ r"(.*?)"
|
|
269
|
+
+ ct("SEARCH")
|
|
270
|
+
+ r"\s*"
|
|
271
|
+
+ ot("REPLACE")
|
|
272
|
+
+ r"(.*?)"
|
|
273
|
+
+ ct("REPLACE"),
|
|
274
|
+
block_text,
|
|
275
|
+
re.DOTALL,
|
|
153
276
|
)
|
|
277
|
+
if single_match:
|
|
278
|
+
diff_item = {
|
|
279
|
+
"SEARCH": single_match.group(1), # 原始SEARCH内容
|
|
280
|
+
"REPLACE": single_match.group(2), # 原始REPLACE内容
|
|
281
|
+
}
|
|
282
|
+
# SEARCH 模式支持 RANGE,如果存在则添加
|
|
283
|
+
if range_scope:
|
|
284
|
+
diff_item["RANGE"] = range_scope
|
|
285
|
+
diffs.append(diff_item)
|
|
286
|
+
|
|
154
287
|
if diffs:
|
|
155
288
|
if file_path in patches:
|
|
156
289
|
patches[file_path].extend(diffs)
|
|
@@ -194,85 +327,240 @@ class EditFileHandler(OutputHandler):
|
|
|
194
327
|
failed_patches: List[Dict[str, Any]] = []
|
|
195
328
|
successful_patches = 0
|
|
196
329
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
330
|
+
# 当存在RANGE时,确保按行号从后往前应用补丁,避免前面补丁影响后续RANGE的行号
|
|
331
|
+
ordered_patches: List[Dict[str, str]] = []
|
|
332
|
+
range_items: List[Tuple[int, int, int, Dict[str, str]]] = []
|
|
333
|
+
non_range_items: List[Tuple[int, Dict[str, str]]] = []
|
|
334
|
+
for idx, p in enumerate(patches):
|
|
335
|
+
r = p.get("RANGE")
|
|
336
|
+
if r and str(r).strip():
|
|
337
|
+
m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(r))
|
|
338
|
+
if m:
|
|
339
|
+
start_line = int(m.group(1))
|
|
340
|
+
end_line = int(m.group(2))
|
|
341
|
+
range_items.append((start_line, end_line, idx, p))
|
|
342
|
+
else:
|
|
343
|
+
# RANGE格式无效的补丁保持原有顺序
|
|
344
|
+
non_range_items.append((idx, p))
|
|
345
|
+
else:
|
|
346
|
+
# 无RANGE的补丁保持原有顺序
|
|
347
|
+
non_range_items.append((idx, p))
|
|
348
|
+
# 先应用RANGE补丁:按start_line、end_line、原始索引逆序
|
|
349
|
+
range_items.sort(key=lambda x: (x[0], x[1], x[2]), reverse=True)
|
|
350
|
+
ordered_patches = [item[3] for item in range_items] + [item[1] for item in non_range_items]
|
|
351
|
+
|
|
352
|
+
patch_count = len(ordered_patches)
|
|
353
|
+
for patch in ordered_patches:
|
|
204
354
|
found = False
|
|
205
355
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
356
|
+
# 处理可选的RANGE范围:格式 "start-end"(1-based, 闭区间)
|
|
357
|
+
scoped = False
|
|
358
|
+
prefix = suffix = ""
|
|
359
|
+
base_content = modified_content
|
|
360
|
+
if "RANGE" in patch and str(patch["RANGE"]).strip():
|
|
361
|
+
m = re.match(r"\s*(\d+)\s*-\s*(\d+)\s*$", str(patch["RANGE"]))
|
|
362
|
+
if not m:
|
|
363
|
+
error_msg = "RANGE格式无效,应为 'start-end' 的行号范围(1-based, 闭区间)"
|
|
364
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
365
|
+
# 不进行本补丁其它处理
|
|
366
|
+
continue
|
|
367
|
+
start_line = int(m.group(1))
|
|
368
|
+
end_line = int(m.group(2))
|
|
369
|
+
|
|
370
|
+
# 拆分为三段
|
|
371
|
+
lines = modified_content.splitlines(keepends=True)
|
|
372
|
+
total_lines = len(lines)
|
|
215
373
|
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
374
|
+
start_line < 1
|
|
375
|
+
or end_line < 1
|
|
376
|
+
or start_line > end_line
|
|
377
|
+
or start_line > total_lines
|
|
220
378
|
):
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
379
|
+
error_msg = f"RANGE行号无效(文件共有{total_lines}行)"
|
|
380
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
381
|
+
continue
|
|
382
|
+
# 截断end_line不超过总行数
|
|
383
|
+
end_line = min(end_line, total_lines)
|
|
384
|
+
|
|
385
|
+
prefix = "".join(lines[: start_line - 1])
|
|
386
|
+
base_content = "".join(lines[start_line - 1 : end_line])
|
|
387
|
+
suffix = "".join(lines[end_line:])
|
|
388
|
+
scoped = True
|
|
389
|
+
|
|
390
|
+
# 单点替换
|
|
391
|
+
if "SEARCH" in patch:
|
|
392
|
+
search_text = patch["SEARCH"]
|
|
393
|
+
replace_text = patch["REPLACE"]
|
|
394
|
+
|
|
395
|
+
# 精确匹配搜索文本(保留原始换行和空格)
|
|
396
|
+
exact_search = search_text
|
|
397
|
+
|
|
398
|
+
def _count_occurrences(haystack: str, needle: str) -> int:
|
|
399
|
+
if not needle:
|
|
400
|
+
return 0
|
|
401
|
+
return haystack.count(needle)
|
|
402
|
+
|
|
403
|
+
# 1) 精确匹配,要求唯一
|
|
404
|
+
cnt = _count_occurrences(base_content, exact_search)
|
|
405
|
+
if cnt == 1:
|
|
406
|
+
base_content = base_content.replace(exact_search, replace_text, 1)
|
|
407
|
+
found = True
|
|
408
|
+
elif cnt > 1:
|
|
409
|
+
# 提供更详细的错误信息,帮助调试
|
|
410
|
+
range_info = f"(RANGE: {patch.get('RANGE', '无')})" if "RANGE" in patch else ""
|
|
411
|
+
base_preview = base_content[:200] + "..." if len(base_content) > 200 else base_content
|
|
412
|
+
error_msg = (
|
|
413
|
+
f"SEARCH 在指定范围内出现多次,要求唯一匹配{range_info}。"
|
|
414
|
+
f"匹配次数: {cnt}。"
|
|
415
|
+
f"搜索内容: {repr(exact_search[:50])}。"
|
|
416
|
+
f"范围内容预览: {repr(base_preview)}"
|
|
417
|
+
)
|
|
418
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
419
|
+
# 不继续尝试其它变体
|
|
420
|
+
continue
|
|
421
|
+
else:
|
|
422
|
+
# 2) 若首尾均为换行,尝试去掉首尾换行后匹配,要求唯一
|
|
234
423
|
if (
|
|
235
|
-
|
|
236
|
-
and
|
|
237
|
-
and
|
|
238
|
-
and
|
|
424
|
+
search_text.startswith("\n")
|
|
425
|
+
and search_text.endswith("\n")
|
|
426
|
+
and replace_text.startswith("\n")
|
|
427
|
+
and replace_text.endswith("\n")
|
|
239
428
|
):
|
|
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
|
|
429
|
+
stripped_search = search_text[1:-1]
|
|
430
|
+
stripped_replace = replace_text[1:-1]
|
|
431
|
+
cnt2 = _count_occurrences(base_content, stripped_search)
|
|
432
|
+
if cnt2 == 1:
|
|
433
|
+
base_content = base_content.replace(
|
|
434
|
+
stripped_search, stripped_replace, 1
|
|
255
435
|
)
|
|
256
|
-
|
|
257
436
|
found = True
|
|
258
|
-
|
|
437
|
+
elif cnt2 > 1:
|
|
438
|
+
error_msg = "SEARCH 在指定范围内出现多次(去掉首尾换行后),要求唯一匹配"
|
|
439
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
# 3) 尝试缩进适配(1..16个空格),要求唯一
|
|
443
|
+
if not found:
|
|
444
|
+
current_search = search_text
|
|
445
|
+
current_replace = replace_text
|
|
446
|
+
if (
|
|
447
|
+
current_search.startswith("\n")
|
|
448
|
+
and current_search.endswith("\n")
|
|
449
|
+
and current_replace.startswith("\n")
|
|
450
|
+
and current_replace.endswith("\n")
|
|
451
|
+
):
|
|
452
|
+
current_search = current_search[1:-1]
|
|
453
|
+
current_replace = current_replace[1:-1]
|
|
454
|
+
|
|
455
|
+
for space_count in range(1, 17):
|
|
456
|
+
indented_search = "\n".join(
|
|
457
|
+
" " * space_count + line if line.strip() else line
|
|
458
|
+
for line in current_search.split("\n")
|
|
459
|
+
)
|
|
460
|
+
indented_replace = "\n".join(
|
|
461
|
+
" " * space_count + line if line.strip() else line
|
|
462
|
+
for line in current_replace.split("\n")
|
|
463
|
+
)
|
|
464
|
+
cnt3 = _count_occurrences(base_content, indented_search)
|
|
465
|
+
if cnt3 == 1:
|
|
466
|
+
base_content = base_content.replace(
|
|
467
|
+
indented_search, indented_replace, 1
|
|
468
|
+
)
|
|
469
|
+
found = True
|
|
470
|
+
break
|
|
471
|
+
elif cnt3 > 1:
|
|
472
|
+
error_msg = "SEARCH 在指定范围内出现多次(缩进适配后),要求唯一匹配"
|
|
473
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
474
|
+
# 多匹配直接失败,不再继续尝试其它缩进
|
|
475
|
+
found = False
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
if not found:
|
|
479
|
+
# 未找到任何可用的唯一匹配
|
|
480
|
+
failed_patches.append({"patch": patch, "error": "未找到唯一匹配的SEARCH"})
|
|
481
|
+
|
|
482
|
+
# 区间替换
|
|
483
|
+
elif "SEARCH_START" in patch and "SEARCH_END" in patch:
|
|
484
|
+
search_start = patch["SEARCH_START"]
|
|
485
|
+
search_end = patch["SEARCH_END"]
|
|
486
|
+
replace_text = patch["REPLACE"]
|
|
487
|
+
|
|
488
|
+
# 范围替换(包含边界),命中第一个起始标记及其后的第一个结束标记
|
|
489
|
+
start_idx = base_content.find(search_start)
|
|
490
|
+
if start_idx == -1:
|
|
491
|
+
error_msg = "未找到SEARCH_START"
|
|
492
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
493
|
+
else:
|
|
494
|
+
# 从 search_start 之后开始查找 search_end
|
|
495
|
+
end_idx = base_content.find(search_end, start_idx + len(search_start))
|
|
496
|
+
if end_idx == -1:
|
|
497
|
+
error_msg = "在SEARCH_START之后未找到SEARCH_END"
|
|
498
|
+
failed_patches.append({"patch": patch, "error": error_msg})
|
|
499
|
+
else:
|
|
500
|
+
# 将替换范围扩展到整行
|
|
501
|
+
# 找到 start_idx 所在行的行首
|
|
502
|
+
line_start_idx = base_content.rfind("\n", 0, start_idx) + 1
|
|
503
|
+
|
|
504
|
+
# 找到 end_idx 所在行的行尾
|
|
505
|
+
match_end_pos = end_idx + len(search_end)
|
|
506
|
+
line_end_idx = base_content.find("\n", match_end_pos)
|
|
507
|
+
|
|
508
|
+
if line_end_idx == -1:
|
|
509
|
+
# 如果没有找到换行符,说明是最后一行
|
|
510
|
+
end_of_range = len(base_content)
|
|
511
|
+
else:
|
|
512
|
+
# 包含换行符
|
|
513
|
+
end_of_range = line_end_idx + 1
|
|
514
|
+
|
|
515
|
+
final_replace_text = replace_text
|
|
516
|
+
original_slice = base_content[line_start_idx:end_of_range]
|
|
517
|
+
|
|
518
|
+
# 如果原始片段以换行符结尾,且替换内容不为空且不以换行符结尾,
|
|
519
|
+
# 则为替换内容添加换行符以保持格式
|
|
520
|
+
if (
|
|
521
|
+
final_replace_text
|
|
522
|
+
and original_slice.endswith("\n")
|
|
523
|
+
and not final_replace_text.endswith("\n")
|
|
524
|
+
):
|
|
525
|
+
final_replace_text += "\n"
|
|
526
|
+
|
|
527
|
+
base_content = (
|
|
528
|
+
base_content[:line_start_idx]
|
|
529
|
+
+ final_replace_text
|
|
530
|
+
+ base_content[end_of_range:]
|
|
531
|
+
)
|
|
532
|
+
found = True
|
|
259
533
|
|
|
260
|
-
if found:
|
|
261
|
-
successful_patches += 1
|
|
262
534
|
else:
|
|
263
|
-
error_msg = "
|
|
264
|
-
|
|
535
|
+
error_msg = "不支持的补丁格式"
|
|
265
536
|
failed_patches.append({"patch": patch, "error": error_msg})
|
|
266
537
|
|
|
538
|
+
# 若使用了RANGE,则将局部修改写回整体内容
|
|
539
|
+
if found:
|
|
540
|
+
if scoped:
|
|
541
|
+
modified_content = prefix + base_content + suffix
|
|
542
|
+
else:
|
|
543
|
+
modified_content = base_content
|
|
544
|
+
successful_patches += 1
|
|
545
|
+
|
|
267
546
|
# 写入修改后的内容
|
|
268
547
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
269
548
|
f.write(modified_content)
|
|
270
549
|
|
|
271
550
|
if failed_patches:
|
|
272
|
-
error_details = [
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
551
|
+
error_details = []
|
|
552
|
+
for p in failed_patches:
|
|
553
|
+
patch = p["patch"]
|
|
554
|
+
if "SEARCH" in patch:
|
|
555
|
+
patch_desc = patch["SEARCH"]
|
|
556
|
+
else:
|
|
557
|
+
patch_desc = (
|
|
558
|
+
"SEARCH_START:\n"
|
|
559
|
+
+ (patch.get("SEARCH_START", ""))
|
|
560
|
+
+ "\nSEARCH_END:\n"
|
|
561
|
+
+ (patch.get("SEARCH_END", ""))
|
|
562
|
+
)
|
|
563
|
+
error_details.append(f" - 失败的补丁: \n{patch_desc}\n 错误: {p['error']}")
|
|
276
564
|
if successful_patches == 0:
|
|
277
565
|
summary = (
|
|
278
566
|
f"文件 {file_path} 修改失败(全部失败)。\n"
|
jarvis/jarvis_agent/event_bus.py
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Tuple
|
|
5
|
+
|
|
6
|
+
from jarvis.jarvis_tools.read_code import ReadCodeTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_text_file(filepath: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if a file is a text file.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
15
|
+
f.read(1024) # Try to read a small chunk
|
|
16
|
+
return True
|
|
17
|
+
except (UnicodeDecodeError, IOError):
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def count_lines(filepath: str) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Count the number of lines in a file.
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
27
|
+
return sum(1 for _ in f)
|
|
28
|
+
except IOError:
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def file_context_handler(user_input: str, agent_: Any) -> Tuple[str, bool]:
|
|
33
|
+
"""
|
|
34
|
+
Extracts file paths from the input, reads their content if they are valid text files
|
|
35
|
+
and appends the content to the input.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
user_input: The user's input string.
|
|
39
|
+
agent_: The agent instance.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A tuple containing the modified user input and a boolean indicating if
|
|
43
|
+
further processing should be skipped.
|
|
44
|
+
"""
|
|
45
|
+
# Regex to find paths in single quotes
|
|
46
|
+
raw_paths = re.findall(r"'([^']+)'", user_input)
|
|
47
|
+
# Convert to absolute paths and de-duplicate by absolute path while preserving order
|
|
48
|
+
abs_to_raws: dict[str, list[str]] = {}
|
|
49
|
+
file_paths = []
|
|
50
|
+
for _raw in raw_paths:
|
|
51
|
+
abs_path = os.path.abspath(_raw)
|
|
52
|
+
if abs_path not in abs_to_raws:
|
|
53
|
+
abs_to_raws[abs_path] = []
|
|
54
|
+
file_paths.append(abs_path)
|
|
55
|
+
abs_to_raws[abs_path].append(_raw)
|
|
56
|
+
|
|
57
|
+
if not file_paths:
|
|
58
|
+
return user_input, False
|
|
59
|
+
|
|
60
|
+
added_context = ""
|
|
61
|
+
read_code_tool = ReadCodeTool()
|
|
62
|
+
|
|
63
|
+
for abs_path in file_paths:
|
|
64
|
+
if os.path.isfile(abs_path) and is_text_file(abs_path):
|
|
65
|
+
line_count = count_lines(abs_path)
|
|
66
|
+
if line_count > 0:
|
|
67
|
+
# Use ReadCodeTool to get formatted content
|
|
68
|
+
result = read_code_tool._handle_single_file(abs_path)
|
|
69
|
+
if result["success"]:
|
|
70
|
+
# Remove all original path tokens that map to this absolute path to avoid redundancy
|
|
71
|
+
for _raw in abs_to_raws.get(abs_path, []):
|
|
72
|
+
user_input = user_input.replace(f"'{_raw}'", "")
|
|
73
|
+
# Append the full, formatted output from the tool, which includes headers and line numbers
|
|
74
|
+
added_context += "\n" + result["stdout"]
|
|
75
|
+
|
|
76
|
+
if added_context:
|
|
77
|
+
user_input = user_input.strip() + added_context
|
|
78
|
+
|
|
79
|
+
return user_input, False
|