jarvis-ai-assistant 0.1.222__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.
Files changed (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -7,17 +7,17 @@ import tempfile
7
7
  from pathlib import Path
8
8
  from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple
9
9
 
10
- import yaml
10
+ import yaml # type: ignore[import-untyped]
11
11
 
12
12
  from jarvis.jarvis_mcp import McpClient
13
13
  from jarvis.jarvis_mcp.sse_mcp_client import SSEMcpClient
14
14
  from jarvis.jarvis_mcp.stdio_mcp_client import StdioMcpClient
15
15
  from jarvis.jarvis_mcp.streamable_mcp_client import StreamableMcpClient
16
16
  from jarvis.jarvis_tools.base import Tool
17
- from jarvis.jarvis_utils.config import get_data_dir
17
+ from jarvis.jarvis_utils.config import get_data_dir, get_tool_load_dirs
18
18
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
19
19
  from jarvis.jarvis_utils.tag import ct, ot
20
- from jarvis.jarvis_utils.utils import is_context_overflow
20
+ from jarvis.jarvis_utils.utils import is_context_overflow, daily_check_git_updates
21
21
 
22
22
  tool_call_help = f"""
23
23
  <tool_system_guide>
@@ -31,6 +31,7 @@ tool_call_help = f"""
31
31
  {ot("TOOL_CALL")}
32
32
  want: 想要从执行结果中获取到的信息,如果工具输出内容过长,会根据此字段尝试提取有效信息
33
33
  name: 工具名称
34
+
34
35
  arguments:
35
36
  param1: 值1
36
37
  param2: 值2
@@ -50,6 +51,7 @@ arguments:
50
51
  - 完全按照上述格式
51
52
  - 使用正确的YAML格式,2个空格作为缩进
52
53
  - 包含所有必需参数
54
+ - {ot("TOOL_CALL")} 和 {ct("TOOL_CALL")} 必须出现在行首
53
55
  </rule>
54
56
 
55
57
  <rule>
@@ -71,14 +73,15 @@ arguments:
71
73
 
72
74
  <string_format>
73
75
  # 📝 字符串参数格式
74
- 始终使用 |2 语法表示字符串参数,防止多行字符串行首空格引起歧义:
76
+ 使用 |2 语法表示字符串参数,防止多行字符串行首空格引起歧义。
75
77
 
76
78
  {ot("TOOL_CALL")}
77
79
  want: 当前的git状态,期望获取xxx的提交记录
78
80
  name: execute_script
81
+
79
82
  arguments:
80
83
  interpreter: bash
81
- script_cotent: |2
84
+ script_content: |
82
85
  git status --porcelain
83
86
  {ct("TOOL_CALL")}
84
87
  </string_format>
@@ -95,11 +98,11 @@ arguments:
95
98
  <common_errors>
96
99
  # ⚠️ 常见错误
97
100
  - 同时调用多个工具
98
- - 字符串参数缺少 |2
99
101
  - 假设工具结果
100
102
  - 创建虚构对话
101
103
  - 在没有所需信息的情况下继续
102
104
  - yaml 格式错误
105
+ - {ot("TOOL_CALL")} 和 {ct("TOOL_CALL")} 没有出现在行首
103
106
  </common_errors>
104
107
  </tool_system_guide>
105
108
  """
@@ -107,18 +110,21 @@ arguments:
107
110
 
108
111
  class OutputHandlerProtocol(Protocol):
109
112
  def name(self) -> str: ...
113
+
110
114
  def can_handle(self, response: str) -> bool: ...
115
+
111
116
  def prompt(self) -> str: ...
117
+
112
118
  def handle(self, response: str, agent: Any) -> Tuple[bool, Any]: ...
113
119
 
114
120
 
115
121
  class ToolRegistry(OutputHandlerProtocol):
116
-
117
122
  def name(self) -> str:
118
123
  return "TOOL_CALL"
119
124
 
120
125
  def can_handle(self, response: str) -> bool:
121
- return ToolRegistry._has_tool_calls_block(response)
126
+ # 仅当 {ot("TOOL_CALL")} 出现在行首时才认为可以处理
127
+ return re.search(rf'(?m){re.escape(ot("TOOL_CALL"))}', response) is not None
122
128
 
123
129
  def prompt(self) -> str:
124
130
  """加载工具"""
@@ -165,11 +171,33 @@ class ToolRegistry(OutputHandlerProtocol):
165
171
  return tools_prompt
166
172
  return ""
167
173
 
168
- def handle(self, response: str, agent: Any) -> Tuple[bool, Any]:
169
- tool_call, err_msg = self._extract_tool_calls(response)
170
- if err_msg:
171
- return False, err_msg
172
- return False, self.handle_tool_calls(tool_call, agent)
174
+ def handle(self, response: str, agent_: Any) -> Tuple[bool, Any]:
175
+ try:
176
+ tool_call, err_msg, auto_completed = self._extract_tool_calls(response)
177
+ if err_msg:
178
+ # 只要工具解析错误,追加工具使用帮助信息(相当于一次 <ToolUsage>)
179
+ try:
180
+ from jarvis.jarvis_agent import Agent
181
+ agent: Agent = agent_
182
+ tool_usage = agent.get_tool_usage_prompt()
183
+ return False, f"{err_msg}\n\n{tool_usage}"
184
+ except Exception:
185
+ # 兼容处理:无法获取Agent或ToolUsage时,至少返回工具系统帮助信息
186
+ return False, f"{err_msg}\n\n{tool_call_help}"
187
+ result = self.handle_tool_calls(tool_call, agent_)
188
+ if auto_completed:
189
+ # 如果自动补全了结束标签,在结果中添加说明信息
190
+ result = f"检测到工具调用缺少结束标签,已自动补全{ct('TOOL_CALL')}。请确保后续工具调用包含完整的开始和结束标签。\n\n{result}"
191
+ return False, result
192
+ except Exception as e:
193
+ PrettyOutput.print(f"工具调用处理失败: {str(e)}", OutputType.ERROR)
194
+ from jarvis.jarvis_agent import Agent
195
+
196
+ agent: Agent = agent_
197
+ return (
198
+ False,
199
+ f"工具调用处理失败: {str(e)}\n\n{agent.get_tool_usage_prompt()}",
200
+ )
173
201
 
174
202
  def __init__(self) -> None:
175
203
  """初始化工具注册表"""
@@ -178,30 +206,46 @@ class ToolRegistry(OutputHandlerProtocol):
178
206
  self._load_builtin_tools()
179
207
  self._load_external_tools()
180
208
  self._load_mcp_tools()
209
+ # 应用工具配置组过滤
210
+ self._apply_tool_config_filter()
181
211
 
182
212
  def _get_tool_stats(self) -> Dict[str, int]:
183
213
  """从数据目录获取工具调用统计"""
184
- stats_file = Path(get_data_dir()) / "tool_stat.yaml"
185
- if stats_file.exists():
186
- try:
187
- with open(stats_file, "r", encoding="utf-8") as f:
188
- return yaml.safe_load(f) or {}
189
- except Exception as e:
190
- PrettyOutput.print(
191
- f"加载工具调用统计失败: {str(e)}", OutputType.WARNING
192
- )
193
- return {}
214
+ from jarvis.jarvis_stats.stats import StatsManager
215
+ from datetime import datetime
216
+
217
+ # 获取所有工具的统计数据
218
+ tool_stats = {}
219
+ tools = self.get_all_tools()
220
+
221
+ # 获取所有历史数据(从很早的时间开始)
222
+ end_time = datetime.now()
223
+ start_time = datetime(2000, 1, 1) # 使用一个足够早的时间
224
+
225
+ for tool in tools:
226
+ tool_name = tool["name"]
227
+ # 获取该工具的统计数据
228
+ stats_data = StatsManager.get_stats(
229
+ metric_name=tool_name,
230
+ start_time=start_time,
231
+ end_time=end_time,
232
+ tags={"group": "tool"},
233
+ )
234
+
235
+ # 计算总调用次数
236
+ if stats_data and "records" in stats_data:
237
+ total_count = sum(record["value"] for record in stats_data["records"])
238
+ tool_stats[tool_name] = int(total_count)
239
+ else:
240
+ tool_stats[tool_name] = 0
241
+
242
+ return tool_stats
194
243
 
195
244
  def _update_tool_stats(self, name: str) -> None:
196
245
  """更新工具调用统计"""
197
- stats = self._get_tool_stats()
198
- stats[name] = stats.get(name, 0) + 1
199
- stats_file = Path(get_data_dir()) / "tool_stat.yaml"
200
- try:
201
- with open(stats_file, "w", encoding="utf-8") as f:
202
- yaml.safe_dump(stats, f, allow_unicode=True)
203
- except Exception as e:
204
- PrettyOutput.print(f"保存工具调用统计失败: {str(e)}", OutputType.WARNING)
246
+ from jarvis.jarvis_stats.stats import StatsManager
247
+
248
+ StatsManager.increment(name, group="tool")
205
249
 
206
250
  def use_tools(self, name: List[str]) -> None:
207
251
  """使用指定工具
@@ -231,6 +275,35 @@ class ToolRegistry(OutputHandlerProtocol):
231
275
  name: tool for name, tool in self.tools.items() if name not in names
232
276
  }
233
277
 
278
+ def _apply_tool_config_filter(self) -> None:
279
+ """应用工具配置组的过滤规则"""
280
+ from jarvis.jarvis_utils.config import get_tool_use_list, get_tool_dont_use_list
281
+
282
+ use_list = get_tool_use_list()
283
+ dont_use_list = get_tool_dont_use_list()
284
+
285
+ # 如果配置了 use 列表,只保留列表中的工具
286
+ if use_list:
287
+ filtered_tools = {}
288
+ missing = []
289
+ for tool_name in use_list:
290
+ if tool_name in self.tools:
291
+ filtered_tools[tool_name] = self.tools[tool_name]
292
+ else:
293
+ missing.append(tool_name)
294
+ if missing:
295
+ PrettyOutput.print(
296
+ "警告: 配置的工具不存在: " + ", ".join(f"'{name}'" for name in missing),
297
+ OutputType.WARNING,
298
+ )
299
+ self.tools = filtered_tools
300
+
301
+ # 如果配置了 dont_use 列表,排除列表中的工具
302
+ if dont_use_list:
303
+ for tool_name in dont_use_list:
304
+ if tool_name in self.tools:
305
+ del self.tools[tool_name]
306
+
234
307
  def _load_mcp_tools(self) -> None:
235
308
  """加载MCP工具,优先从配置获取,其次从目录扫描"""
236
309
  from jarvis.jarvis_utils.config import get_mcp_config
@@ -254,14 +327,15 @@ class ToolRegistry(OutputHandlerProtocol):
254
327
  )
255
328
 
256
329
  # 遍历目录中的所有.yaml文件
330
+ error_lines = []
257
331
  for file_path in mcp_tools_dir.glob("*.yaml"):
258
332
  try:
259
333
  config = yaml.safe_load(open(file_path, "r", encoding="utf-8"))
260
334
  self.register_mcp_tool_by_config(config)
261
335
  except Exception as e:
262
- PrettyOutput.print(
263
- f"文件 {file_path} 加载失败: {str(e)}", OutputType.WARNING
264
- )
336
+ error_lines.append(f"文件 {file_path} 加载失败: {str(e)}")
337
+ if error_lines:
338
+ PrettyOutput.print("\n".join(error_lines), OutputType.WARNING)
265
339
 
266
340
  def _load_builtin_tools(self) -> None:
267
341
  """从内置工具目录加载工具"""
@@ -276,18 +350,52 @@ class ToolRegistry(OutputHandlerProtocol):
276
350
  self.register_tool_by_file(str(file_path))
277
351
 
278
352
  def _load_external_tools(self) -> None:
279
- """从jarvis_data/tools加载外部工具"""
280
- external_tools_dir = Path(get_data_dir()) / "tools"
281
- if not external_tools_dir.exists():
282
- return
353
+ """从jarvis_data/tools和配置的目录加载外部工具"""
354
+ from jarvis.jarvis_utils.config import get_central_tool_repo
355
+
356
+ tool_dirs = [str(Path(get_data_dir()) / "tools")] + get_tool_load_dirs()
357
+
358
+ # 如果配置了中心工具仓库,将其添加到加载路径
359
+ central_repo = get_central_tool_repo()
360
+ if central_repo:
361
+ # 支持本地目录路径或Git仓库URL
362
+ expanded = os.path.expanduser(os.path.expandvars(central_repo))
363
+ if os.path.isdir(expanded):
364
+ # 直接使用本地目录(支持Git仓库的子目录)
365
+ tool_dirs.append(expanded)
366
+ else:
367
+ # 中心工具仓库存储在数据目录下的特定位置
368
+ central_repo_path = os.path.join(get_data_dir(), "central_tool_repo")
369
+ tool_dirs.append(central_repo_path)
283
370
 
284
- # 遍历目录中的所有.py文件
285
- for file_path in external_tools_dir.glob("*.py"):
286
- # 跳过__init__.py
287
- if file_path.name == "__init__.py":
371
+ # 确保中心工具仓库被克隆/更新
372
+ if not os.path.exists(central_repo_path):
373
+ try:
374
+ import subprocess
375
+
376
+ subprocess.run(
377
+ ["git", "clone", central_repo, central_repo_path], check=True
378
+ )
379
+ except Exception as e:
380
+ PrettyOutput.print(
381
+ f"克隆中心工具仓库失败: {str(e)}", OutputType.ERROR
382
+ )
383
+
384
+ # --- 全局每日更新检查 ---
385
+ daily_check_git_updates(tool_dirs, "tools")
386
+
387
+ for tool_dir in tool_dirs:
388
+ p_tool_dir = Path(tool_dir)
389
+ if not p_tool_dir.exists() or not p_tool_dir.is_dir():
288
390
  continue
289
391
 
290
- self.register_tool_by_file(str(file_path))
392
+ # 遍历目录中的所有.py文件
393
+ for file_path in p_tool_dir.glob("*.py"):
394
+ # 跳过__init__.py
395
+ if file_path.name == "__init__.py":
396
+ continue
397
+
398
+ self.register_tool_by_file(str(file_path))
291
399
 
292
400
  def register_mcp_tool_by_config(self, config: Dict[str, Any]) -> bool:
293
401
  """从配置字典加载并注册工具
@@ -307,10 +415,7 @@ class ToolRegistry(OutputHandlerProtocol):
307
415
 
308
416
  # 检查enable标志
309
417
  if not config.get("enable", True):
310
- PrettyOutput.print(
311
- f"MCP配置{config.get('name', '')}已禁用(enable=false),跳过注册",
312
- OutputType.INFO,
313
- )
418
+
314
419
  return False
315
420
 
316
421
  name = config.get("name", "mcp")
@@ -322,10 +427,7 @@ class ToolRegistry(OutputHandlerProtocol):
322
427
  args.pop("agent", None)
323
428
  args.pop("want", None)
324
429
  ret = client.get_resource_list()
325
- PrettyOutput.print(
326
- f"MCP {name} 资源列表:\n{yaml.safe_dump(ret, allow_unicode=True)}",
327
- OutputType.TOOL,
328
- )
430
+
329
431
  return {
330
432
  "success": True,
331
433
  "stdout": yaml.safe_dump(ret, allow_unicode=True),
@@ -346,10 +448,7 @@ class ToolRegistry(OutputHandlerProtocol):
346
448
  "stderr": "缺少必需的uri参数",
347
449
  }
348
450
  ret = client.get_resource(args["uri"])
349
- PrettyOutput.print(
350
- f"MCP {name} 获取资源:\n{yaml.safe_dump(ret, allow_unicode=True)}",
351
- OutputType.TOOL,
352
- )
451
+
353
452
  return ret
354
453
 
355
454
  return execute
@@ -360,10 +459,7 @@ class ToolRegistry(OutputHandlerProtocol):
360
459
  args.pop("agent", None)
361
460
  args.pop("want", None)
362
461
  ret = client.execute(tool_name, args)
363
- PrettyOutput.print(
364
- f"MCP {name} {tool_name} 执行结果:\n{yaml.safe_dump(ret, allow_unicode=True)}",
365
- OutputType.TOOL,
366
- )
462
+
367
463
  return ret
368
464
 
369
465
  return execute
@@ -396,12 +492,13 @@ class ToolRegistry(OutputHandlerProtocol):
396
492
  return False
397
493
 
398
494
  # 创建MCP客户端
495
+ mcp_client: McpClient
399
496
  if config["type"] == "stdio":
400
- mcp_client: McpClient = StdioMcpClient(config)
497
+ mcp_client = StdioMcpClient(config)
401
498
  elif config["type"] == "sse":
402
- mcp_client: McpClient = SSEMcpClient(config)
499
+ mcp_client = SSEMcpClient(config)
403
500
  elif config["type"] == "streamable":
404
- mcp_client: McpClient = StreamableMcpClient(config)
501
+ mcp_client = StreamableMcpClient(config)
405
502
  else:
406
503
  raise ValueError(f"不支持的MCP客户端类型: {config['type']}")
407
504
 
@@ -491,7 +588,6 @@ class ToolRegistry(OutputHandlerProtocol):
491
588
  and hasattr(item, "execute")
492
589
  and item.name == module_name
493
590
  ):
494
-
495
591
  if hasattr(item, "check"):
496
592
  if not item.check():
497
593
  continue
@@ -505,6 +601,9 @@ class ToolRegistry(OutputHandlerProtocol):
505
601
  description=tool_instance.description,
506
602
  parameters=tool_instance.parameters,
507
603
  func=tool_instance.execute,
604
+ protocol_version=getattr(
605
+ tool_instance, "protocol_version", "1.0"
606
+ ),
508
607
  )
509
608
  tool_found = True
510
609
  break
@@ -526,29 +625,72 @@ class ToolRegistry(OutputHandlerProtocol):
526
625
 
527
626
  @staticmethod
528
627
  def _has_tool_calls_block(content: str) -> bool:
529
- """从内容中提取工具调用块"""
530
- return (
531
- re.search(ot("TOOL_CALL") + r"(.*?)" + ct("TOOL_CALL"), content, re.DOTALL)
532
- is not None
533
- )
628
+ """从内容中提取工具调用块(仅匹配行首标签)"""
629
+ pattern = rf'(?ms){re.escape(ot("TOOL_CALL"))}(.*?)^{re.escape(ct("TOOL_CALL"))}'
630
+ return re.search(pattern, content) is not None
534
631
 
535
632
  @staticmethod
536
- def _extract_tool_calls(content: str) -> Tuple[Dict[str, Dict[str, Any]], str]:
633
+ def _extract_tool_calls(
634
+ content: str,
635
+ ) -> Tuple[Dict[str, Dict[str, Any]], str, bool]:
537
636
  """从内容中提取工具调用。
538
637
 
539
638
  参数:
540
639
  content: 包含工具调用的内容
541
640
 
542
641
  返回:
543
- List[Dict]: 包含名称和参数的提取工具调用列表
642
+ Tuple[Dict[str, Dict[str, Any]], str, bool]:
643
+ - 第一个元素是提取的工具调用字典
644
+ - 第二个元素是错误消息字符串(成功时为"")
645
+ - 第三个元素是是否自动补全了结束标签
544
646
 
545
647
  异常:
546
648
  Exception: 如果工具调用缺少必要字段
547
649
  """
650
+ # 如果</TOOL_CALL>出现在响应的末尾,但是前面没有换行符,自动插入一个换行符进行修复
651
+ if content.rstrip().endswith(ct("TOOL_CALL")):
652
+ pos = content.rfind(ct("TOOL_CALL"))
653
+ if pos > 0 and content[pos - 1] not in ("\n", "\r"):
654
+ content = content[:pos] + "\n" + content[pos:]
655
+
548
656
  # 将内容拆分为行
549
- data = re.findall(
550
- ot("TOOL_CALL") + r"(.*?)" + ct("TOOL_CALL"), content, re.DOTALL
551
- )
657
+ pattern = rf'(?ms){re.escape(ot("TOOL_CALL"))}(.*?)^{re.escape(ct("TOOL_CALL"))}'
658
+ data = re.findall(pattern, content)
659
+ auto_completed = False
660
+ if not data:
661
+ # can_handle 确保 ot("TOOL_CALL") 在内容中(行首)。
662
+ # 如果数据为空,则表示行首的 ct("TOOL_CALL") 可能丢失。
663
+ has_open_at_bol = re.search(rf'(?m){re.escape(ot("TOOL_CALL"))}', content) is not None
664
+ has_close_at_bol = re.search(rf'(?m)^{re.escape(ct("TOOL_CALL"))}', content) is not None
665
+ if has_open_at_bol and not has_close_at_bol:
666
+ # 尝试通过附加结束标签来修复它(确保结束标签位于行首)
667
+ fixed_content = content.strip() + f"\n{ct('TOOL_CALL')}"
668
+
669
+ # 再次提取,并检查YAML是否有效
670
+ temp_data = re.findall(
671
+ pattern,
672
+ fixed_content,
673
+ )
674
+
675
+ if temp_data:
676
+ try:
677
+ yaml.safe_load(temp_data[0]) # Check if valid YAML
678
+
679
+ # Ask user for confirmation
680
+
681
+ data = temp_data
682
+ auto_completed = True
683
+ except (yaml.YAMLError, EOFError, KeyboardInterrupt):
684
+ # Even after fixing, it's not valid YAML, or user cancelled.
685
+ # Fall through to the original error.
686
+ pass
687
+
688
+ if not data:
689
+ return (
690
+ {},
691
+ f"只有{ot('TOOL_CALL')}标签,未找到{ct('TOOL_CALL')}标签,调用格式错误,请检查工具调用格式。\n{tool_call_help}",
692
+ False,
693
+ )
552
694
  ret = []
553
695
  for item in data:
554
696
  try:
@@ -560,6 +702,7 @@ class ToolRegistry(OutputHandlerProtocol):
560
702
  {e}
561
703
 
562
704
  {tool_call_help}""",
705
+ False,
563
706
  )
564
707
 
565
708
  if "name" in msg and "arguments" in msg and "want" in msg:
@@ -570,10 +713,11 @@ class ToolRegistry(OutputHandlerProtocol):
570
713
  f"""工具调用格式错误,请检查工具调用格式(缺少name、arguments、want字段)。
571
714
 
572
715
  {tool_call_help}""",
716
+ False,
573
717
  )
574
718
  if len(ret) > 1:
575
- return {}, "检测到多个工具调用,请一次只处理一个工具调用。"
576
- return ret[0] if ret else {}, ""
719
+ return {}, "检测到多个工具调用,请一次只处理一个工具调用。", False
720
+ return ret[0] if ret else {}, "", auto_completed
577
721
 
578
722
  def register_tool(
579
723
  self,
@@ -581,6 +725,7 @@ class ToolRegistry(OutputHandlerProtocol):
581
725
  description: str,
582
726
  parameters: Any,
583
727
  func: Callable[..., Dict[str, Any]],
728
+ protocol_version: str = "1.0",
584
729
  ) -> None:
585
730
  """注册新工具
586
731
 
@@ -590,7 +735,11 @@ class ToolRegistry(OutputHandlerProtocol):
590
735
  parameters: 工具参数定义
591
736
  func: 工具执行函数
592
737
  """
593
- self.tools[name] = Tool(name, description, parameters, func)
738
+ if name in self.tools:
739
+ PrettyOutput.print(
740
+ f"警告: 工具 '{name}' 已存在,将被覆盖", OutputType.WARNING
741
+ )
742
+ self.tools[name] = Tool(name, description, parameters, func, protocol_version)
594
743
 
595
744
  def get_tool(self, name: str) -> Optional[Tool]:
596
745
  """获取工具
@@ -611,12 +760,15 @@ class ToolRegistry(OutputHandlerProtocol):
611
760
  """
612
761
  return [tool.to_dict() for tool in self.tools.values()]
613
762
 
614
- def execute_tool(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
763
+ def execute_tool(
764
+ self, name: str, arguments: Dict[str, Any], agent: Optional[Any] = None
765
+ ) -> Dict[str, Any]:
615
766
  """执行指定工具
616
767
 
617
768
  参数:
618
769
  name: 工具名称
619
770
  arguments: 工具参数
771
+ agent: 智能体实例(由系统内部传递,用于v2.0分离agent与参数)
620
772
 
621
773
  返回:
622
774
  Dict[str, Any]: 包含执行结果的字典,包含success、stdout和stderr字段
@@ -632,7 +784,23 @@ class ToolRegistry(OutputHandlerProtocol):
632
784
  # 更新工具调用统计
633
785
  self._update_tool_stats(name)
634
786
 
635
- return tool.execute(arguments)
787
+ # 根据工具实现声明的协议版本分发调用方式
788
+ try:
789
+ if getattr(tool, "protocol_version", "1.0") == "2.0":
790
+ # v2.0: agent与参数分离传递
791
+ return tool.func(arguments, agent) # type: ignore[misc]
792
+ else:
793
+ # v1.0: 兼容旧实现,将agent注入到arguments(如果提供)
794
+ args_to_call = arguments.copy() if isinstance(arguments, dict) else {}
795
+ if agent is not None:
796
+ args_to_call["agent"] = agent
797
+ return tool.execute(args_to_call)
798
+ except TypeError:
799
+ # 兼容处理:如果函数签名不匹配,回退到旧方式
800
+ args_to_call = arguments.copy() if isinstance(arguments, dict) else {}
801
+ if agent is not None:
802
+ args_to_call["agent"] = agent
803
+ return tool.execute(args_to_call)
636
804
 
637
805
  def _format_tool_output(self, stdout: str, stderr: str) -> str:
638
806
  """格式化工具输出为可读字符串
@@ -671,25 +839,51 @@ class ToolRegistry(OutputHandlerProtocol):
671
839
  def handle_tool_calls(self, tool_call: Dict[str, Any], agent: Any) -> str:
672
840
  try:
673
841
  name = tool_call["name"] # 确保name是str类型
674
- args = tool_call["arguments"] # args已经是Dict[str, Any]
842
+ args = tool_call["arguments"] # 原始参数(来自外部协议)
675
843
  want = tool_call["want"]
676
- args["agent"] = agent
677
844
 
678
845
  from jarvis.jarvis_agent import Agent
679
846
 
680
847
  agent_instance: Agent = agent
681
848
 
849
+ # 如果args是字符串,尝试解析为JSON
682
850
  if isinstance(args, str):
683
851
  try:
684
852
  args = json.loads(args)
685
853
  except json.JSONDecodeError:
686
- PrettyOutput.print(
687
- f"工具参数格式无效: {name} {tool_call_help}", OutputType.ERROR
688
- )
689
- return ""
854
+ # 返回错误并附带完整的工具使用提示,指导下一次正确调用
855
+ try:
856
+ usage_prompt = agent_instance.get_tool_usage_prompt()
857
+ except Exception:
858
+ usage_prompt = tool_call_help
859
+ return f"工具参数格式无效: {name}。arguments 应为可解析的 JSON 或对象,请按工具调用格式提供。\n\n{usage_prompt}"
690
860
 
691
- # 执行工具调用
692
- result = self.execute_tool(name, args) # 修正参数传递
861
+ # 执行工具调用(根据工具实现的协议版本,由系统在内部决定agent的传递方式)
862
+ result = self.execute_tool(name, args, agent)
863
+
864
+ # 记录本轮实际执行的工具,供上层逻辑(如记忆保存判定)使用
865
+ try:
866
+ from jarvis.jarvis_agent import Agent # 延迟导入避免循环依赖
867
+ agent_instance_for_record: Agent = agent_instance
868
+ # 记录最后一次执行的工具
869
+ agent_instance_for_record.set_user_data("__last_executed_tool__", name) # type: ignore
870
+ # 记录本轮累计执行的工具列表
871
+ executed_list = agent_instance_for_record.get_user_data("__executed_tools__") # type: ignore
872
+ if not isinstance(executed_list, list):
873
+ executed_list = []
874
+ executed_list.append(name)
875
+ agent_instance_for_record.set_user_data("__executed_tools__", executed_list) # type: ignore
876
+ except Exception:
877
+ pass
878
+
879
+ # 如果执行失败,附带工具使用提示返回
880
+ if not result.get("success", False):
881
+ try:
882
+ usage_prompt = agent_instance.get_tool_usage_prompt()
883
+ except Exception:
884
+ usage_prompt = tool_call_help
885
+ err_output = self._format_tool_output(result.get("stdout", ""), result.get("stderr", ""))
886
+ return f"{err_output}\n\n{usage_prompt}"
693
887
 
694
888
  # 格式化输出
695
889
  output = self._format_tool_output(
@@ -697,7 +891,10 @@ class ToolRegistry(OutputHandlerProtocol):
697
891
  )
698
892
 
699
893
  # 检查内容是否过大
700
- is_large_content = is_context_overflow(output)
894
+ model_group = None
895
+ if agent_instance.model:
896
+ model_group = agent_instance.model.model_group
897
+ is_large_content = is_context_overflow(output, model_group)
701
898
 
702
899
  if is_large_content:
703
900
  # 创建临时文件
@@ -719,8 +916,9 @@ class ToolRegistry(OutputHandlerProtocol):
719
916
  [output_file]
720
917
  )
721
918
  if upload_success:
722
- # 删除args的agent
723
- args.pop("agent", None)
919
+ # 删除args的agent键(保持协议v2.0的“参数与agent分离”在可视化中的一致性)
920
+ if isinstance(args, dict):
921
+ args.pop("agent", None)
724
922
  prompt = f"""
725
923
  以下是之前对话的关键信息总结:
726
924
 
@@ -747,4 +945,10 @@ class ToolRegistry(OutputHandlerProtocol):
747
945
 
748
946
  except Exception as e:
749
947
  PrettyOutput.print(f"工具执行失败:{str(e)}", OutputType.ERROR)
750
- return f"工具调用失败: {str(e)}"
948
+ try:
949
+ from jarvis.jarvis_agent import Agent # 延迟导入避免循环依赖
950
+ agent_instance_for_prompt: Agent = agent # type: ignore
951
+ usage_prompt = agent_instance_for_prompt.get_tool_usage_prompt()
952
+ except Exception:
953
+ usage_prompt = tool_call_help
954
+ return f"工具调用失败: {str(e)}\n\n{usage_prompt}"