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
@@ -3,9 +3,18 @@
3
3
  import datetime
4
4
  import os
5
5
  import platform
6
- from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from enum import Enum
10
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
11
+
7
12
 
8
13
  # 第三方库导入
14
+ from rich.align import Align
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.text import Text
9
18
 
10
19
  # 本地库导入
11
20
  # jarvis_agent 相关
@@ -13,11 +22,44 @@ from jarvis.jarvis_agent.prompt_builder import build_action_prompt
13
22
  from jarvis.jarvis_agent.protocols import OutputHandlerProtocol
14
23
  from jarvis.jarvis_agent.session_manager import SessionManager
15
24
  from jarvis.jarvis_agent.tool_executor import execute_tool_call
25
+ from jarvis.jarvis_agent.memory_manager import MemoryManager
26
+ from jarvis.jarvis_memory_organizer.memory_organizer import MemoryOrganizer
27
+ from jarvis.jarvis_agent.task_analyzer import TaskAnalyzer
28
+ from jarvis.jarvis_agent.task_planner import TaskPlanner
29
+ from jarvis.jarvis_agent.file_methodology_manager import FileMethodologyManager
16
30
  from jarvis.jarvis_agent.prompts import (
17
31
  DEFAULT_SUMMARY_PROMPT,
18
32
  SUMMARY_REQUEST_PROMPT,
19
- TASK_ANALYSIS_PROMPT,
33
+ TASK_ANALYSIS_PROMPT as TASK_ANALYSIS_PROMPT,
34
+ )
35
+ from jarvis.jarvis_tools.registry import ToolRegistry
36
+ from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
37
+ from jarvis.jarvis_agent.rewrite_file_handler import RewriteFileHandler
38
+ from jarvis.jarvis_agent.prompt_manager import PromptManager
39
+ from jarvis.jarvis_agent.event_bus import EventBus
40
+ from jarvis.jarvis_agent.run_loop import AgentRunLoop
41
+ from jarvis.jarvis_agent.events import (
42
+ BEFORE_SUMMARY,
43
+ AFTER_SUMMARY,
44
+ TASK_COMPLETED,
45
+ TASK_STARTED,
46
+ BEFORE_ADDON_PROMPT,
47
+ AFTER_ADDON_PROMPT,
48
+ BEFORE_HISTORY_CLEAR,
49
+ AFTER_HISTORY_CLEAR,
50
+ BEFORE_MODEL_CALL,
51
+ AFTER_MODEL_CALL,
52
+ INTERRUPT_TRIGGERED,
53
+ BEFORE_TOOL_FILTER,
54
+ TOOL_FILTERED,
55
+ AFTER_TOOL_CALL,
20
56
  )
57
+ from jarvis.jarvis_agent.user_interaction import UserInteractionHandler
58
+ from jarvis.jarvis_agent.utils import join_prompts
59
+ from jarvis.jarvis_utils.methodology import _load_all_methodologies
60
+ from jarvis.jarvis_agent.shell_input_handler import shell_input_handler
61
+ from jarvis.jarvis_agent.file_context_handler import file_context_handler
62
+ from jarvis.jarvis_agent.builtin_input_handler import builtin_input_handler
21
63
 
22
64
  # jarvis_platform 相关
23
65
  from jarvis.jarvis_platform.base import BasePlatform
@@ -25,14 +67,18 @@ from jarvis.jarvis_platform.registry import PlatformRegistry
25
67
 
26
68
  # jarvis_utils 相关
27
69
  from jarvis.jarvis_utils.config import (
28
- get_max_token_count,
70
+ get_data_dir,
29
71
  get_normal_model_name,
30
72
  get_normal_platform_name,
31
- get_thinking_model_name,
32
- get_thinking_platform_name,
33
73
  is_execute_tool_confirm,
74
+ is_force_save_memory,
34
75
  is_use_analysis,
35
76
  is_use_methodology,
77
+ get_tool_filter_threshold,
78
+ get_after_tool_call_cb_dirs,
79
+ get_plan_max_depth,
80
+ is_plan_enabled,
81
+ get_addon_prompt_threshold,
36
82
  )
37
83
  from jarvis.jarvis_utils.embedding import get_context_token_count
38
84
  from jarvis.jarvis_utils.globals import (
@@ -43,9 +89,99 @@ from jarvis.jarvis_utils.globals import (
43
89
  set_interrupt,
44
90
  )
45
91
  from jarvis.jarvis_utils.input import get_multiline_input, user_confirm
46
- from jarvis.jarvis_utils.methodology import load_methodology, upload_methodology
47
92
  from jarvis.jarvis_utils.output import OutputType, PrettyOutput
48
- from jarvis.jarvis_utils.tag import ct, ot
93
+ from jarvis.jarvis_utils.tag import ot
94
+
95
+
96
+ def show_agent_startup_stats(
97
+ agent_name: str,
98
+ model_name: str,
99
+ tool_registry_instance: Optional[Any] = None,
100
+ platform_name: Optional[str] = None,
101
+ ) -> None:
102
+ """输出启动时的统计信息
103
+
104
+ 参数:
105
+ agent_name: Agent的名称
106
+ model_name: 使用的模型名称
107
+ """
108
+ try:
109
+ methodologies = _load_all_methodologies()
110
+ methodology_count = len(methodologies)
111
+
112
+ # 获取工具数量
113
+ # 创建一个临时的工具注册表类来获取所有工具(不应用过滤)
114
+ class TempToolRegistry(ToolRegistry):
115
+ def _apply_tool_config_filter(self) -> None:
116
+ """重写过滤方法,不执行任何过滤"""
117
+ pass
118
+
119
+ # 获取所有工具的数量
120
+ tool_registry_all = TempToolRegistry()
121
+ total_tool_count = len(tool_registry_all.tools)
122
+
123
+ # 获取可用工具的数量(应用过滤)
124
+ if tool_registry_instance is not None:
125
+ available_tool_count = len(tool_registry_instance.get_all_tools())
126
+ else:
127
+ tool_registry = ToolRegistry()
128
+ available_tool_count = len(tool_registry.get_all_tools())
129
+
130
+ global_memory_dir = Path(get_data_dir()) / "memory" / "global_long_term"
131
+ global_memory_count = 0
132
+ if global_memory_dir.exists():
133
+ global_memory_count = len(list(global_memory_dir.glob("*.json")))
134
+
135
+ # 检查项目记忆
136
+ project_memory_dir = Path(".jarvis/memory")
137
+ project_memory_count = 0
138
+ if project_memory_dir.exists():
139
+ project_memory_count = len(list(project_memory_dir.glob("*.json")))
140
+
141
+ # 获取当前工作目录
142
+ current_dir = os.getcwd()
143
+
144
+ # 构建欢迎信息
145
+ platform = platform_name or get_normal_platform_name()
146
+ welcome_message = f"{agent_name} 初始化完成 - 使用 {platform} 平台 {model_name} 模型"
147
+
148
+ stats_parts = [
149
+ f"📚 本地方法论: [bold cyan]{methodology_count}[/bold cyan]",
150
+ f"🛠️ 工具: [bold green]{available_tool_count}/{total_tool_count}[/bold green] (可用/全部)",
151
+ f"🧠 全局记忆: [bold yellow]{global_memory_count}[/bold yellow]",
152
+ ]
153
+
154
+ # 如果有项目记忆,添加到统计信息中
155
+ if project_memory_count > 0:
156
+ stats_parts.append(
157
+ f"📝 项目记忆: [bold magenta]{project_memory_count}[/bold magenta]"
158
+ )
159
+
160
+ stats_text = Text.from_markup(" | ".join(stats_parts), justify="center")
161
+
162
+ # 创建包含欢迎信息和统计信息的面板内容
163
+ panel_content = Text()
164
+ panel_content.append(welcome_message, style="bold white")
165
+ panel_content.append("\n")
166
+ panel_content.append(f"📁 工作目录: {current_dir}", style="dim white")
167
+ panel_content.append("\n\n")
168
+ panel_content.append(stats_text)
169
+ panel_content.justify = "center"
170
+
171
+ panel = Panel(
172
+ panel_content,
173
+ title="✨ Jarvis 资源概览 ✨",
174
+ title_align="center",
175
+ border_style="blue",
176
+ expand=False,
177
+ )
178
+
179
+ console = Console()
180
+ console.print(Align.center(panel))
181
+
182
+ except Exception as e:
183
+ PrettyOutput.print(f"加载统计信息失败: {e}", OutputType.WARNING)
184
+
49
185
 
50
186
  origin_agent_system_prompt = f"""
51
187
  <role>
@@ -73,6 +209,21 @@ origin_agent_system_prompt = f"""
73
209
  4. **完成**: 验证任务是否达成目标,并进行总结。
74
210
  </workflow>
75
211
 
212
+ <sub_agents_guide>
213
+ # 子任务工具使用建议
214
+ - 使用 sub_code_agent(代码子Agent)当:
215
+ - 需要在当前任务下并行推进较大且相对独立的代码改造
216
+ - 涉及多文件/多模块的大范围变更,或需要较长的工具调用链
217
+ - 需要隔离上下文以避免污染当前对话(如探索性改动、PoC)
218
+ - 需要专注于单一代码子问题,阶段性产出可复用的结果
219
+ - 使用 sub_agent(通用子Agent)当:
220
+ - 子任务不是以代码改造为主(如调研、方案撰写、评审总结、用例设计、文档生成等)
221
+ - 只是需要短期分流一个轻量的辅助性子任务
222
+ 说明:
223
+ - 两者仅需参数 task(可选 background 提供上下文),完成后返回结果给父Agent
224
+ - 子Agent将自动完成并生成总结,请在上层根据返回结果继续编排
225
+ </sub_agents_guide>
226
+
76
227
  <system_info>
77
228
  # 系统信息
78
229
  - OS: {platform.platform()} {platform.version()}
@@ -81,134 +232,332 @@ origin_agent_system_prompt = f"""
81
232
  """
82
233
 
83
234
 
235
+ class LoopAction(Enum):
236
+ SKIP_TURN = "skip_turn"
237
+ CONTINUE = "continue"
238
+ COMPLETE = "complete"
239
+
240
+
84
241
  class Agent:
85
- def clear(self):
242
+ # Attribute type annotations to satisfy static type checkers
243
+ event_bus: EventBus
244
+ memory_manager: MemoryManager
245
+ task_analyzer: TaskAnalyzer
246
+ file_methodology_manager: FileMethodologyManager
247
+ prompt_manager: PromptManager
248
+ model: BasePlatform
249
+ session: SessionManager
250
+
251
+ def clear_history(self):
86
252
  """
87
253
  Clears the current conversation history by delegating to the session manager.
254
+ Emits BEFORE_HISTORY_CLEAR/AFTER_HISTORY_CLEAR and reapplies system prompt to preserve constraints.
88
255
  """
89
- self.session.clear()
256
+ # 广播清理历史前事件(不影响主流程)
257
+ try:
258
+ self.event_bus.emit(BEFORE_HISTORY_CLEAR, agent=self)
259
+ except Exception:
260
+ pass
261
+
262
+ # 清理会话历史并重置模型状态
263
+ self.session.clear_history()
264
+ # 重置 addon_prompt 跳过轮数计数器
265
+ self._addon_prompt_skip_rounds = 0
266
+
267
+ # 重置后重新设置系统提示词,确保系统约束仍然生效
268
+ try:
269
+ self._setup_system_prompt()
270
+ except Exception:
271
+ pass
272
+
273
+ # 广播清理历史后的事件
274
+ try:
275
+ self.event_bus.emit(AFTER_HISTORY_CLEAR, agent=self)
276
+ except Exception:
277
+ pass
90
278
 
91
279
  def __del__(self):
92
280
  # 只有在记录启动时才停止记录
93
- delete_agent(self.name)
281
+ try:
282
+ name = getattr(self, "name", None)
283
+ if name:
284
+ delete_agent(name)
285
+ except Exception:
286
+ pass
287
+
288
+ def get_tool_usage_prompt(self) -> str:
289
+ """获取工具使用提示"""
290
+ return build_action_prompt(self.output_handler) # type: ignore
291
+
292
+ def __new__(cls, *args, **kwargs):
293
+ if kwargs.get("agent_type") == "code":
294
+ try:
295
+ from jarvis.jarvis_code_agent.code_agent import CodeAgent
296
+ except ImportError as e:
297
+ raise RuntimeError(
298
+ "CodeAgent could not be imported. Please ensure jarvis_code_agent is installed correctly."
299
+ ) from e
300
+
301
+ # 移除 agent_type 避免无限循环,并传递所有其他参数
302
+ kwargs.pop("agent_type", None)
303
+ return CodeAgent(**kwargs)
304
+ else:
305
+ return super().__new__(cls)
94
306
 
95
307
  def __init__(
96
308
  self,
97
309
  system_prompt: str,
98
310
  name: str = "Jarvis",
99
311
  description: str = "",
100
- llm_type: str = "normal",
312
+ model_group: Optional[str] = None,
101
313
  summary_prompt: Optional[str] = None,
102
314
  auto_complete: bool = False,
103
- output_handler: List[OutputHandlerProtocol] = [],
104
- use_tools: List[str] = [],
105
- input_handler: Optional[List[Callable[[str, Any], Tuple[str, bool]]]] = None,
315
+ output_handler: Optional[List[OutputHandlerProtocol]] = None,
316
+ use_tools: Optional[List[str]] = None,
106
317
  execute_tool_confirm: Optional[bool] = None,
107
318
  need_summary: bool = True,
319
+ auto_summary_rounds: Optional[int] = None,
108
320
  multiline_inputer: Optional[Callable[[str], str]] = None,
109
321
  use_methodology: Optional[bool] = None,
110
322
  use_analysis: Optional[bool] = None,
111
- files: List[str] = [],
323
+ force_save_memory: Optional[bool] = None,
324
+ disable_file_edit: bool = False,
325
+ files: Optional[List[str]] = None,
326
+ confirm_callback: Optional[Callable[[str, bool], bool]] = None,
327
+ non_interactive: Optional[bool] = None,
328
+ in_multi_agent: Optional[bool] = None,
329
+ plan: Optional[bool] = None,
330
+ plan_max_depth: Optional[int] = None,
331
+ plan_depth: int = 0,
332
+ agent_type: str = "normal",
333
+ **kwargs,
112
334
  ):
113
- self.files = files
114
335
  """初始化Jarvis Agent实例
115
336
 
116
337
  参数:
117
338
  system_prompt: 系统提示词,定义Agent的行为准则
118
339
  name: Agent名称,默认为"Jarvis"
119
340
  description: Agent描述信息
120
- llm_type: LLM类型,可以是 'normal' 或 'thinking'
341
+
121
342
  summary_prompt: 任务总结提示模板
122
343
  auto_complete: 是否自动完成任务
123
- output_handler: 输出处理器列表
124
- input_handler: 输入处理器列表
125
- max_context_length: 最大上下文长度
126
344
  execute_tool_confirm: 执行工具前是否需要确认
127
345
  need_summary: 是否需要生成总结
128
346
  multiline_inputer: 多行输入处理器
129
347
  use_methodology: 是否使用方法论
130
348
  use_analysis: 是否使用任务分析
349
+ force_save_memory: 是否强制保存记忆
350
+ confirm_callback: 用户确认回调函数,签名为 (tip: str, default: bool) -> bool;默认使用CLI的user_confirm
351
+ non_interactive: 是否以非交互模式运行(优先级最高,覆盖环境变量与配置)
352
+ plan: 是否启用任务规划与子任务拆分(默认从配置加载;启用后在进入主循环前评估是否需要将任务拆分为 <SUB_TASK> 列表,逐一由子Agent执行并汇总结果)
353
+ plan_max_depth: 任务规划的最大层数(默认3,可通过配置 JARVIS_PLAN_MAX_DEPTH 或入参覆盖)
354
+ plan_depth: 当前规划层数(内部用于递归控制,子Agent会在父基础上+1)
131
355
  """
356
+ # 基础属性初始化(仅根据入参设置原始值;实际生效的默认回退在 _init_config 中统一解析)
357
+ # 标识与描述
132
358
  self.name = make_agent_name(name)
133
359
  self.description = description
134
- # 初始化平台和模型
135
- if llm_type == "thinking":
136
- platform_name = get_thinking_platform_name()
137
- model_name = get_thinking_model_name()
138
- else: # 默认为 normal
139
- platform_name = get_normal_platform_name()
140
- model_name = get_normal_model_name()
141
-
142
- self.model = PlatformRegistry().create_platform(platform_name)
143
- if self.model is None:
144
- PrettyOutput.print(
145
- f"平台 {platform_name} 不存在,将使用普通模型", OutputType.WARNING
360
+ self.system_prompt = system_prompt
361
+ # 行为控制开关(原始入参值)
362
+ self.auto_complete = bool(auto_complete)
363
+ self.need_summary = bool(need_summary)
364
+ # 自动摘要轮次:None 表示使用配置文件中的默认值,由 AgentRunLoop 决定最终取值
365
+ self.auto_summary_rounds = auto_summary_rounds
366
+ self.use_methodology = use_methodology
367
+ self.use_analysis = use_analysis
368
+ self.execute_tool_confirm = execute_tool_confirm
369
+ self.summary_prompt = summary_prompt
370
+ self.force_save_memory = force_save_memory
371
+ self.disable_file_edit = bool(disable_file_edit)
372
+ # 资源与环境
373
+ self.model_group = model_group
374
+ self.files = files or []
375
+ self.use_tools = use_tools
376
+ self.non_interactive = non_interactive
377
+ # 多智能体运行标志:用于控制非交互模式下的自动完成行为
378
+ self.in_multi_agent = bool(in_multi_agent)
379
+ # 任务规划:优先使用入参,否则回退到配置
380
+ self.plan = bool(plan) if plan is not None else is_plan_enabled()
381
+ # 规划深度与上限
382
+ try:
383
+ self.plan_max_depth = (
384
+ int(plan_max_depth) if plan_max_depth is not None else int(get_plan_max_depth())
146
385
  )
147
- self.model = PlatformRegistry().get_normal_platform()
386
+ except Exception:
387
+ self.plan_max_depth = 2
388
+ try:
389
+ self.plan_depth = int(plan_depth)
390
+ except Exception:
391
+ self.plan_depth = 0
392
+ # 运行时状态
393
+ self.first = True
394
+ self.run_input_handlers_next_turn = False
395
+ self.user_data: Dict[str, Any] = {}
396
+ # 记录连续未添加 addon_prompt 的轮数
397
+ self._addon_prompt_skip_rounds: int = 0
148
398
 
149
- if model_name:
150
- self.model.set_model_name(model_name)
151
399
 
152
- self.user_data: Dict[str, Any] = {}
400
+ # 用户确认回调:默认使用 CLI user_confirm,可由外部注入以支持 TUI/GUI
401
+ self.confirm_callback: Callable[[str, bool], bool] = (
402
+ confirm_callback or user_confirm # type: ignore[assignment]
403
+ )
153
404
 
154
- self.model.set_suppress_output(False)
405
+ # 初始化模型和会话
406
+ self._init_model(model_group)
407
+ self._init_session()
408
+
409
+ # 初始化处理器
410
+ self._init_handlers(
411
+ multiline_inputer,
412
+ output_handler,
413
+ use_tools or [],
414
+ )
415
+ # 初始化用户交互封装,保持向后兼容
416
+ self.user_interaction = UserInteractionHandler(self.multiline_inputer, self.confirm_callback)
417
+ # 将确认函数指向封装后的 confirm,保持既有调用不变
418
+ self.confirm_callback = self.user_interaction.confirm # type: ignore[assignment]
419
+ # 非交互模式参数支持:允许通过构造参数显式控制,便于其他Agent调用时设置
420
+ try:
421
+ # 优先使用构造参数,其次回退到环境变量
422
+ self.non_interactive = (
423
+ bool(non_interactive)
424
+ if non_interactive is not None
425
+ else str(os.environ.get("JARVIS_NON_INTERACTIVE", "")).lower() in ("1", "true", "yes")
426
+ )
427
+ # 如果构造参数显式提供,则同步到环境变量与全局配置,供下游组件读取
428
+ if non_interactive is not None:
429
+ os.environ["JARVIS_NON_INTERACTIVE"] = "true" if self.non_interactive else "false"
155
430
 
156
- # Initialize the session manager
157
- self.session = SessionManager(model=self.model, agent_name=self.name)
431
+ except Exception:
432
+ # 防御式回退
433
+ self.non_interactive = False
158
434
 
159
- from jarvis.jarvis_tools.registry import ToolRegistry
435
+ # 初始化配置(直接解析,不再依赖 _init_config)
436
+ try:
437
+ resolved_use_methodology = bool(use_methodology if use_methodology is not None else is_use_methodology())
438
+ except Exception:
439
+ resolved_use_methodology = bool(use_methodology) if use_methodology is not None else True
160
440
 
161
- self.output_handler = output_handler if output_handler else [ToolRegistry()]
162
- self.set_use_tools(use_tools)
441
+ try:
442
+ resolved_use_analysis = bool(use_analysis if use_analysis is not None else is_use_analysis())
443
+ except Exception:
444
+ resolved_use_analysis = bool(use_analysis) if use_analysis is not None else True
163
445
 
164
- self.multiline_inputer = (
165
- multiline_inputer if multiline_inputer else get_multiline_input
166
- )
446
+ try:
447
+ resolved_execute_tool_confirm = bool(execute_tool_confirm if execute_tool_confirm is not None else is_execute_tool_confirm())
448
+ except Exception:
449
+ resolved_execute_tool_confirm = bool(execute_tool_confirm) if execute_tool_confirm is not None else False
167
450
 
168
- # 如果有上传文件,自动禁用方法论
169
- self.use_methodology = (
170
- False
171
- if files
172
- else (
173
- use_methodology if use_methodology is not None else is_use_methodology()
174
- )
175
- )
176
- self.use_analysis = (
177
- use_analysis if use_analysis is not None else is_use_analysis()
451
+ try:
452
+ resolved_force_save_memory = bool(force_save_memory if force_save_memory is not None else is_force_save_memory())
453
+ except Exception:
454
+ resolved_force_save_memory = bool(force_save_memory) if force_save_memory is not None else False
455
+
456
+ self.use_methodology = resolved_use_methodology
457
+ self.use_analysis = resolved_use_analysis
458
+ self.execute_tool_confirm = resolved_execute_tool_confirm
459
+ self.summary_prompt = (summary_prompt or DEFAULT_SUMMARY_PROMPT)
460
+ self.force_save_memory = resolved_force_save_memory
461
+ # 多智能体模式下,默认不自动完成(即使是非交互),仅在明确传入 auto_complete=True 时开启
462
+ if self.in_multi_agent:
463
+ self.auto_complete = bool(self.auto_complete)
464
+ else:
465
+ # 非交互模式下默认自动完成;否则保持传入的 auto_complete 值
466
+ self.auto_complete = bool(self.auto_complete or (self.non_interactive or False))
467
+
468
+ # 初始化事件总线需先于管理器,以便管理器在构造中安全订阅事件
469
+ self.event_bus = EventBus()
470
+ # 初始化管理器
471
+ self.memory_manager = MemoryManager(self)
472
+ self.task_analyzer = TaskAnalyzer(self)
473
+ self.file_methodology_manager = FileMethodologyManager(self)
474
+ self.prompt_manager = PromptManager(self)
475
+ # 任务规划器:封装规划与子任务调度逻辑
476
+ self.task_planner = TaskPlanner(self, plan_depth=self.plan_depth, plan_max_depth=self.plan_max_depth)
477
+
478
+ # 设置系统提示词
479
+ self._setup_system_prompt()
480
+
481
+ # 输出统计信息(包含欢迎信息)
482
+ show_agent_startup_stats(
483
+ name,
484
+ self.model.name(),
485
+ self.get_tool_registry(), # type: ignore
486
+ platform_name=self.model.platform_name(), # type: ignore
178
487
  )
179
- self.system_prompt = system_prompt
180
- self.input_handler = input_handler if input_handler is not None else []
181
- self.need_summary = need_summary
182
- # Load configuration from environment variables
488
+ # 动态加载工具调用后回调
489
+ self._load_after_tool_callbacks()
183
490
 
184
- self.after_tool_call_cb: Optional[Callable[[Agent], None]] = None
491
+ def _init_model(self, model_group: Optional[str]):
492
+ """初始化模型平台(统一使用 normal 平台/模型)"""
493
+ platform_name = get_normal_platform_name(model_group)
494
+ model_name = get_normal_model_name(model_group)
185
495
 
186
- self.execute_tool_confirm = (
187
- execute_tool_confirm
188
- if execute_tool_confirm is not None
189
- else is_execute_tool_confirm()
190
- )
496
+ maybe_model = PlatformRegistry().create_platform(platform_name)
497
+ if maybe_model is None:
498
+ PrettyOutput.print(
499
+ f"平台 {platform_name} 不存在,将使用普通模型", OutputType.WARNING
500
+ )
501
+ maybe_model = PlatformRegistry().get_normal_platform()
191
502
 
192
- self.summary_prompt = (
193
- summary_prompt if summary_prompt else DEFAULT_SUMMARY_PROMPT
194
- )
503
+ # 在此处收敛为非可选类型,确保后续赋值满足类型检查
504
+ self.model = maybe_model
195
505
 
196
- self.max_token_count = get_max_token_count()
197
- self.auto_complete = auto_complete
198
- welcome_message = f"{name} 初始化完成 - 使用 {self.model.name()} 模型"
506
+ if model_name:
507
+ self.model.set_model_name(model_name)
508
+
509
+ self.model.set_model_group(model_group)
510
+ self.model.set_suppress_output(False)
199
511
 
200
- PrettyOutput.print(welcome_message, OutputType.SYSTEM)
512
+ def _init_session(self):
513
+ """初始化会话管理器"""
514
+ self.session = SessionManager(model=self.model, agent_name=self.name) # type: ignore
201
515
 
202
- action_prompt = build_action_prompt(self.output_handler) # type: ignore
516
+ def _init_handlers(
517
+ self,
518
+ multiline_inputer: Optional[Callable[[str], str]],
519
+ output_handler: Optional[List[OutputHandlerProtocol]],
520
+ use_tools: List[str],
521
+ ):
522
+ """初始化各种处理器"""
523
+ default_handlers: List[Any] = [ToolRegistry()]
524
+ if not getattr(self, "disable_file_edit", False):
525
+ default_handlers.extend([EditFileHandler(), RewriteFileHandler()])
526
+ handlers = output_handler or default_handlers
527
+ if getattr(self, "disable_file_edit", False):
528
+ handlers = [h for h in handlers if not isinstance(h, (EditFileHandler, RewriteFileHandler))]
529
+ self.output_handler = handlers
530
+ self.set_use_tools(use_tools)
531
+ self.input_handler = [
532
+ builtin_input_handler,
533
+ shell_input_handler,
534
+ file_context_handler,
535
+ ]
536
+ self.multiline_inputer = multiline_inputer or get_multiline_input
537
+
538
+ def _setup_system_prompt(self):
539
+ """设置系统提示词"""
540
+ try:
541
+ if hasattr(self, "prompt_manager"):
542
+ prompt_text = self.prompt_manager.build_system_prompt()
543
+ else:
544
+ action_prompt = self.get_tool_usage_prompt()
545
+ prompt_text = f"""
546
+ {self.system_prompt}
203
547
 
204
- self.model.set_system_prompt(
205
- f"""
548
+ {action_prompt}
549
+ """
550
+ self.model.set_system_prompt(prompt_text) # type: ignore
551
+ except Exception:
552
+ # 回退到原始行为,确保兼容性
553
+ action_prompt = self.get_tool_usage_prompt()
554
+ self.model.set_system_prompt( # type: ignore
555
+ f"""
206
556
  {self.system_prompt}
207
557
 
208
558
  {action_prompt}
209
559
  """
210
- )
211
- self.first = True
560
+ )
212
561
 
213
562
  def set_user_data(self, key: str, value: Any):
214
563
  """Sets user data in the session."""
@@ -220,8 +569,6 @@ class Agent:
220
569
 
221
570
  def set_use_tools(self, use_tools):
222
571
  """设置要使用的工具列表"""
223
- from jarvis.jarvis_tools.registry import ToolRegistry
224
-
225
572
  for handler in self.output_handler:
226
573
  if isinstance(handler, ToolRegistry):
227
574
  if use_tools:
@@ -232,13 +579,115 @@ class Agent:
232
579
  """Sets the addon prompt in the session."""
233
580
  self.session.set_addon_prompt(addon_prompt)
234
581
 
235
- def set_after_tool_call_cb(self, cb: Callable[[Any], None]): # type: ignore
236
- """设置工具调用后回调函数。
582
+ def set_run_input_handlers_next_turn(self, value: bool):
583
+ """Sets the flag to run input handlers on the next turn."""
584
+ self.run_input_handlers_next_turn = value
237
585
 
238
- 参数:
239
- cb: 回调函数
586
+ def _multiline_input(self, tip: str, print_on_empty: bool) -> str:
587
+ """
588
+ Safe wrapper for multiline input to optionally suppress empty-input notice.
589
+ If the configured multiline_inputer supports 'print_on_empty' keyword, pass it;
590
+ otherwise, fall back to calling with a single argument for compatibility.
240
591
  """
241
- self.after_tool_call_cb = cb
592
+ # 优先通过用户交互封装,便于未来替换 UI
593
+ if hasattr(self, "user_interaction"):
594
+ return self.user_interaction.multiline_input(tip, print_on_empty)
595
+ try:
596
+ # Try to pass the keyword for enhanced input handler
597
+ return self.multiline_inputer(tip, print_on_empty=print_on_empty) # type: ignore
598
+ except TypeError:
599
+ # Fallback for custom handlers that only accept one argument
600
+ return self.multiline_inputer(tip) # type: ignore
601
+
602
+ def _load_after_tool_callbacks(self) -> None:
603
+ """
604
+ 扫描 JARVIS_AFTER_TOOL_CALL_CB_DIRS 中的 Python 文件并动态注册回调。
605
+ 约定优先级(任一命中即注册):
606
+ - 模块级可调用对象: after_tool_call_cb
607
+ - 工厂方法返回单个或多个可调用对象: get_after_tool_call_cb(), register_after_tool_call_cb()
608
+ """
609
+ try:
610
+ dirs = get_after_tool_call_cb_dirs()
611
+ if not dirs:
612
+ return
613
+ for d in dirs:
614
+ p_dir = Path(d)
615
+ if not p_dir.exists() or not p_dir.is_dir():
616
+ continue
617
+ for file_path in p_dir.glob("*.py"):
618
+ if file_path.name == "__init__.py":
619
+ continue
620
+ parent_dir = str(file_path.parent)
621
+ added_path = False
622
+ try:
623
+ if parent_dir not in sys.path:
624
+ sys.path.insert(0, parent_dir)
625
+ added_path = True
626
+ module_name = file_path.stem
627
+ module = __import__(module_name)
628
+
629
+ candidates: List[Callable[[Any], None]] = []
630
+
631
+ # 1) 直接导出的回调
632
+ if hasattr(module, "after_tool_call_cb"):
633
+ obj = getattr(module, "after_tool_call_cb")
634
+ if callable(obj):
635
+ candidates.append(obj) # type: ignore[arg-type]
636
+
637
+ # 2) 工厂方法:get_after_tool_call_cb()
638
+ if hasattr(module, "get_after_tool_call_cb"):
639
+ factory = getattr(module, "get_after_tool_call_cb")
640
+ if callable(factory):
641
+ try:
642
+ ret = factory()
643
+ if callable(ret):
644
+ candidates.append(ret)
645
+ elif isinstance(ret, (list, tuple)):
646
+ for c in ret:
647
+ if callable(c):
648
+ candidates.append(c)
649
+ except Exception:
650
+ pass
651
+
652
+ # 3) 工厂方法:register_after_tool_call_cb()
653
+ if hasattr(module, "register_after_tool_call_cb"):
654
+ factory2 = getattr(module, "register_after_tool_call_cb")
655
+ if callable(factory2):
656
+ try:
657
+ ret2 = factory2()
658
+ if callable(ret2):
659
+ candidates.append(ret2)
660
+ elif isinstance(ret2, (list, tuple)):
661
+ for c in ret2:
662
+ if callable(c):
663
+ candidates.append(c)
664
+ except Exception:
665
+ pass
666
+
667
+ for cb in candidates:
668
+ try:
669
+ def _make_wrapper(callback):
670
+ def _wrapper(**kwargs: Any) -> None:
671
+ try:
672
+ agent = kwargs.get("agent")
673
+ callback(agent)
674
+ except Exception:
675
+ pass
676
+ return _wrapper
677
+ self.event_bus.subscribe(AFTER_TOOL_CALL, _make_wrapper(cb))
678
+ except Exception:
679
+ pass
680
+
681
+ except Exception as e:
682
+ PrettyOutput.print(f"从 {file_path} 加载回调失败: {e}", OutputType.WARNING)
683
+ finally:
684
+ if added_path:
685
+ try:
686
+ sys.path.remove(parent_dir)
687
+ except ValueError:
688
+ pass
689
+ except Exception as e:
690
+ PrettyOutput.print(f"加载回调目录时发生错误: {e}", OutputType.WARNING)
242
691
 
243
692
  def save_session(self) -> bool:
244
693
  """Saves the current session state by delegating to the session manager."""
@@ -253,19 +702,24 @@ class Agent:
253
702
 
254
703
  def get_tool_registry(self) -> Optional[Any]:
255
704
  """获取工具注册表实例"""
256
- from jarvis.jarvis_tools.registry import ToolRegistry
257
-
258
705
  for handler in self.output_handler:
259
706
  if isinstance(handler, ToolRegistry):
260
707
  return handler
261
708
  return None
262
709
 
263
- def _call_model(self, message: str, need_complete: bool = False) -> str:
710
+ def get_event_bus(self) -> EventBus:
711
+ """获取事件总线实例"""
712
+ return self.event_bus
713
+
714
+ def _call_model(
715
+ self, message: str, need_complete: bool = False, run_input_handlers: bool = True
716
+ ) -> str:
264
717
  """调用AI模型并实现重试逻辑
265
718
 
266
719
  参数:
267
720
  message: 输入给模型的消息
268
721
  need_complete: 是否需要完成任务标记
722
+ run_input_handlers: 是否运行输入处理器
269
723
 
270
724
  返回:
271
725
  str: 模型的响应
@@ -276,27 +730,136 @@ class Agent:
276
730
  3. 会自动添加附加提示
277
731
  4. 会检查并处理上下文长度限制
278
732
  """
733
+ # 处理输入
734
+ if run_input_handlers:
735
+ message = self._process_input(message)
736
+ if not message:
737
+ return ""
738
+
739
+ # 添加附加提示
740
+ message = self._add_addon_prompt(message, need_complete)
741
+
742
+ # 管理对话长度
743
+ message = self._manage_conversation_length(message)
744
+
745
+ # 调用模型
746
+ response = self._invoke_model(message)
747
+
748
+ return response
749
+
750
+ def _process_input(self, message: str) -> str:
751
+ """处理输入消息"""
279
752
  for handler in self.input_handler:
280
753
  message, need_return = handler(message, self)
281
754
  if need_return:
755
+ self._last_handler_returned = True
282
756
  return message
757
+ self._last_handler_returned = False
758
+ return message
759
+
760
+ def _add_addon_prompt(self, message: str, need_complete: bool) -> str:
761
+ """添加附加提示到消息
762
+
763
+ 规则:
764
+ 1. 如果 session.addon_prompt 存在,优先使用它
765
+ 2. 如果消息长度超过阈值,添加默认 addon_prompt
766
+ 3. 如果连续10轮都没有添加过 addon_prompt,强制添加一次
767
+ """
768
+ # 广播添加附加提示前事件(不影响主流程)
769
+ try:
770
+ self.event_bus.emit(
771
+ BEFORE_ADDON_PROMPT,
772
+ agent=self,
773
+ need_complete=need_complete,
774
+ current_message=message,
775
+ has_session_addon=bool(self.session.addon_prompt),
776
+ )
777
+ except Exception:
778
+ pass
283
779
 
780
+ addon_text = ""
781
+ should_add = False
782
+
284
783
  if self.session.addon_prompt:
285
- message += f"\n\n{self.session.addon_prompt}"
784
+ # 优先使用 session 中设置的 addon_prompt
785
+ addon_text = self.session.addon_prompt
786
+ message = join_prompts([message, addon_text])
286
787
  self.session.addon_prompt = ""
788
+ should_add = True
287
789
  else:
288
- message += f"\n\n{self.make_default_addon_prompt(need_complete)}"
790
+ threshold = get_addon_prompt_threshold()
791
+ # 条件1:消息长度超过阈值
792
+ if len(message) > threshold:
793
+ addon_text = self.make_default_addon_prompt(need_complete)
794
+ message = join_prompts([message, addon_text])
795
+ should_add = True
796
+ # 条件2:连续10轮都没有添加过 addon_prompt,强制添加一次
797
+ elif self._addon_prompt_skip_rounds >= 10:
798
+ addon_text = self.make_default_addon_prompt(need_complete)
799
+ message = join_prompts([message, addon_text])
800
+ should_add = True
801
+
802
+ # 更新计数器:如果添加了 addon_prompt,重置计数器;否则递增
803
+ if should_add:
804
+ self._addon_prompt_skip_rounds = 0
805
+ else:
806
+ self._addon_prompt_skip_rounds += 1
807
+
808
+ # 广播添加附加提示后事件(不影响主流程)
809
+ try:
810
+ self.event_bus.emit(
811
+ AFTER_ADDON_PROMPT,
812
+ agent=self,
813
+ need_complete=need_complete,
814
+ addon_text=addon_text,
815
+ final_message=message,
816
+ )
817
+ except Exception:
818
+ pass
819
+ return message
289
820
 
290
- # 累加对话长度
821
+ def _manage_conversation_length(self, message: str) -> str:
822
+ """管理对话长度计数;摘要触发由轮次在 AgentRunLoop 中统一处理。"""
291
823
  self.session.conversation_length += get_context_token_count(message)
292
824
 
293
- if self.session.conversation_length > self.max_token_count:
294
- message = self._summarize_and_clear_history() + "\n\n" + message
295
- self.session.conversation_length += get_context_token_count(message)
296
825
 
826
+ return message
827
+
828
+ def _invoke_model(self, message: str) -> str:
829
+ """实际调用模型获取响应"""
297
830
  if not self.model:
298
831
  raise RuntimeError("Model not initialized")
832
+
833
+ # 事件:模型调用前
834
+ try:
835
+ self.event_bus.emit(
836
+ BEFORE_MODEL_CALL,
837
+ agent=self,
838
+ message=message,
839
+ )
840
+ except Exception:
841
+ pass
842
+
299
843
  response = self.model.chat_until_success(message) # type: ignore
844
+ # 防御: 模型可能返回空响应(None或空字符串),统一为空字符串并告警
845
+ if not response:
846
+ try:
847
+ PrettyOutput.print("模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
848
+ except Exception:
849
+ pass
850
+ response = ""
851
+
852
+ # 事件:模型调用后
853
+ try:
854
+ self.event_bus.emit(
855
+ AFTER_MODEL_CALL,
856
+ agent=self,
857
+ message=message,
858
+ response=response,
859
+ )
860
+ except Exception:
861
+ pass
862
+
300
863
  self.session.conversation_length += get_context_token_count(response)
301
864
 
302
865
  return response
@@ -310,28 +873,40 @@ class Agent:
310
873
  注意:
311
874
  仅生成摘要,不修改对话状态
312
875
  """
313
- print("📄 正在总结对话历史...")
876
+
314
877
  try:
315
878
  if not self.model:
316
879
  raise RuntimeError("Model not initialized")
317
- summary = self.model.chat_until_success(
318
- self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
319
- ) # type: ignore
320
- print("✅ 总结对话历史完成")
880
+ # 优先使用外部传入的 summary_prompt;如为空则回退到默认的会话摘要请求
881
+ safe_summary_prompt = self.summary_prompt or ""
882
+ if isinstance(safe_summary_prompt, str) and safe_summary_prompt.strip() != "":
883
+ prompt_to_use = safe_summary_prompt
884
+ else:
885
+ prompt_to_use = self.session.prompt + "\n" + SUMMARY_REQUEST_PROMPT
886
+
887
+ summary = self.model.chat_until_success(prompt_to_use) # type: ignore
888
+ # 防御: 可能返回空响应(None或空字符串),统一为空字符串并告警
889
+ if not summary:
890
+ try:
891
+ PrettyOutput.print("总结模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
892
+ except Exception:
893
+ pass
894
+ summary = ""
321
895
  return summary
322
- except Exception as e:
323
- print("总结对话历史失败")
896
+ except Exception:
897
+ PrettyOutput.print("总结对话历史失败", OutputType.ERROR)
324
898
  return ""
325
899
 
326
900
  def _summarize_and_clear_history(self) -> str:
327
901
  """总结当前对话并清理历史记录
328
902
 
329
903
  该方法将:
330
- 1. 调用_generate_summary生成摘要
331
- 2. 清除对话历史
332
- 3. 保留系统消息
333
- 4. 添加摘要作为新上下文
334
- 5. 重置对话长度计数器
904
+ 1. 提示用户保存重要记忆
905
+ 2. 调用 generate_summary 生成摘要
906
+ 3. 清除对话历史
907
+ 4. 保留系统消息
908
+ 5. 添加摘要作为新上下文
909
+ 6. 重置对话长度计数器
335
910
 
336
911
  返回:
337
912
  str: 包含对话摘要的字符串
@@ -339,25 +914,72 @@ class Agent:
339
914
  注意:
340
915
  当上下文长度超过最大值时使用
341
916
  """
342
- need_summary = True
343
- tmp_file_name = ""
344
- try:
345
- if self.model and self.model.support_upload_files():
346
- need_summary = False
347
- if need_summary:
348
- summary = self.generate_summary()
349
- else:
350
- import tempfile
917
+ # 在清理历史之前,提示用户保存重要记忆(事件驱动触发实际保存)
918
+ if self.force_save_memory:
919
+ PrettyOutput.print(
920
+ "对话历史即将被总结和清理,请先保存重要信息...", OutputType.INFO
921
+ )
351
922
 
352
- tmp_file = tempfile.NamedTemporaryFile(delete=False)
353
- tmp_file_name = tmp_file.name
354
- self.clear_history() # type: ignore
923
+ if self._should_use_file_upload():
924
+ return self._handle_history_with_file_upload()
925
+ else:
926
+ return self._handle_history_with_summary()
927
+
928
+ def _should_use_file_upload(self) -> bool:
929
+ """判断是否应该使用文件上传方式处理历史"""
930
+ return bool(self.model and self.model.support_upload_files())
931
+
932
+ def _handle_history_with_summary(self) -> str:
933
+ """使用摘要方式处理历史"""
934
+ summary = self.generate_summary()
935
+
936
+ # 先获取格式化的摘要消息
937
+ formatted_summary = ""
938
+ if summary:
939
+ formatted_summary = self._format_summary_message(summary)
940
+
941
+ # 清理历史(但不清理prompt,因为prompt会在builtin_input_handler中设置)
942
+ if self.model:
943
+ # 广播清理历史前事件
944
+ try:
945
+ self.event_bus.emit(BEFORE_HISTORY_CLEAR, agent=self)
946
+ except Exception:
947
+ pass
948
+ self.model.reset()
949
+ # 重置后重新设置系统提示词,确保系统约束仍然生效
950
+ self._setup_system_prompt()
951
+ # 重置会话
952
+ self.session.clear_history()
953
+ # 重置 addon_prompt 跳过轮数计数器
954
+ self._addon_prompt_skip_rounds = 0
955
+ # 广播清理历史后的事件
956
+ try:
957
+ self.event_bus.emit(AFTER_HISTORY_CLEAR, agent=self)
958
+ except Exception:
959
+ pass
355
960
 
356
- if need_summary:
357
- if not summary:
358
- return ""
961
+ return formatted_summary
359
962
 
360
- return f"""
963
+ def _handle_history_with_file_upload(self) -> str:
964
+ """使用文件上传方式处理历史"""
965
+ # 广播清理历史前事件
966
+ try:
967
+ self.event_bus.emit(BEFORE_HISTORY_CLEAR, agent=self)
968
+ except Exception:
969
+ pass
970
+ result = self.file_methodology_manager.handle_history_with_file_upload()
971
+ # 重置 addon_prompt 跳过轮数计数器
972
+ self._addon_prompt_skip_rounds = 0
973
+ # 广播清理历史后的事件
974
+ try:
975
+ self.event_bus.emit(AFTER_HISTORY_CLEAR, agent=self)
976
+ except Exception:
977
+ pass
978
+ return result
979
+
980
+ def _format_summary_message(self, summary: str) -> str:
981
+ """格式化摘要消息"""
982
+ return f"""
361
983
  以下是之前对话的关键信息总结:
362
984
 
363
985
  <content>
@@ -366,14 +988,6 @@ class Agent:
366
988
 
367
989
  请基于以上信息继续完成任务。请注意,这是之前对话的摘要,上下文长度已超过限制而被重置。请直接继续任务,无需重复已完成的步骤。如有需要,可以询问用户以获取更多信息。
368
990
  """
369
- else:
370
- if self.model and self.model.upload_files([tmp_file_name]):
371
- return "上传的文件是历史对话信息,请基于历史对话信息继续完成任务。"
372
- else:
373
- return ""
374
- finally:
375
- if tmp_file_name:
376
- os.remove(tmp_file_name)
377
991
 
378
992
  def _call_tools(self, response: str) -> Tuple[bool, Any]:
379
993
  """
@@ -381,7 +995,7 @@ class Agent:
381
995
  """
382
996
  return execute_tool_call(response, self)
383
997
 
384
- def _complete_task(self) -> str:
998
+ def _complete_task(self, auto_completed: bool = False) -> str:
385
999
  """完成任务并生成总结(如果需要)
386
1000
 
387
1001
  返回:
@@ -392,33 +1006,68 @@ class Agent:
392
1006
  2. 对于子Agent: 可能会生成总结(如果启用)
393
1007
  3. 使用spinner显示生成状态
394
1008
  """
395
- if self.use_analysis:
396
- self._analysis_task()
397
- if self.need_summary:
398
- print("📄 正在生成总结...")
399
- self.session.prompt = self.summary_prompt
400
- if not self.model:
401
- raise RuntimeError("Model not initialized")
402
- ret = self.model.chat_until_success(self.session.prompt) # type: ignore
403
- print("✅ 总结生成完成")
404
- return ret
1009
+ # 事件驱动方式:
1010
+ # - TaskAnalyzer 通过订阅 before_summary/task_completed 事件执行分析与满意度收集
1011
+ # - MemoryManager 通过订阅 before_history_clear/task_completed 事件执行记忆保存(受 force_save_memory 控制)
1012
+ # 为减少耦合,这里不再直接调用上述组件,保持行为由事件触发
1013
+ self._check_and_organize_memory()
405
1014
 
406
- return "任务完成"
1015
+ result = "任务完成"
407
1016
 
408
- def _analysis_task(self):
409
- print("🔍 正在分析任务...")
410
- try:
411
- # 让模型判断是否需要生成方法论
412
- analysis_prompt = TASK_ANALYSIS_PROMPT
1017
+ if self.need_summary:
1018
+
1019
+ # 确保总结提示词非空:若为None或仅空白,则回退到默认提示词
1020
+ safe_summary_prompt = self.summary_prompt or ""
1021
+ if isinstance(safe_summary_prompt, str) and safe_summary_prompt.strip() == "":
1022
+ safe_summary_prompt = DEFAULT_SUMMARY_PROMPT
1023
+ # 注意:不要写回 session.prompt,避免 BEFORE_SUMMARY 事件回调修改/清空后导致使用空prompt
1024
+ # 广播将要生成总结事件
1025
+ try:
1026
+ self.event_bus.emit(
1027
+ BEFORE_SUMMARY,
1028
+ agent=self,
1029
+ prompt=safe_summary_prompt,
1030
+ auto_completed=auto_completed,
1031
+ need_summary=self.need_summary,
1032
+ )
1033
+ except Exception:
1034
+ pass
413
1035
 
414
- self.session.prompt = analysis_prompt
415
1036
  if not self.model:
416
1037
  raise RuntimeError("Model not initialized")
417
- response = self.model.chat_until_success(self.session.prompt) # type: ignore
418
- self._call_tools(response)
419
- print("✅ 分析完成")
420
- except Exception as e:
421
- print("❌ 分析失败")
1038
+ # 直接使用本地变量,避免受事件回调影响
1039
+ ret = self.model.chat_until_success(safe_summary_prompt) # type: ignore
1040
+ # 防御: 总结阶段模型可能返回空响应(None或空字符串),统一为空字符串并告警
1041
+ if not ret:
1042
+ try:
1043
+ PrettyOutput.print("总结阶段模型返回空响应,已使用空字符串回退。", OutputType.WARNING)
1044
+ except Exception:
1045
+ pass
1046
+ ret = ""
1047
+ result = ret
1048
+
1049
+ # 广播完成总结事件
1050
+ try:
1051
+ self.event_bus.emit(
1052
+ AFTER_SUMMARY,
1053
+ agent=self,
1054
+ summary=result,
1055
+ )
1056
+ except Exception:
1057
+ pass
1058
+
1059
+ # 广播任务完成事件(不影响主流程)
1060
+ try:
1061
+ self.event_bus.emit(
1062
+ TASK_COMPLETED,
1063
+ agent=self,
1064
+ auto_completed=auto_completed,
1065
+ need_summary=self.need_summary,
1066
+ )
1067
+ except Exception:
1068
+ pass
1069
+
1070
+ return result
422
1071
 
423
1072
  def make_default_addon_prompt(self, need_complete: bool) -> str:
424
1073
  """生成附加提示。
@@ -427,6 +1076,10 @@ class Agent:
427
1076
  need_complete: 是否需要完成任务
428
1077
 
429
1078
  """
1079
+ # 优先使用 PromptManager 以保持逻辑集中
1080
+ if hasattr(self, "prompt_manager"):
1081
+ return self.prompt_manager.build_default_addon_prompt(need_complete)
1082
+
430
1083
  # 结构化系统指令
431
1084
  action_handlers = ", ".join([handler.name() for handler in self.output_handler])
432
1085
 
@@ -437,6 +1090,12 @@ class Agent:
437
1090
  else ""
438
1091
  )
439
1092
 
1093
+ # 检查工具列表并添加记忆工具相关提示
1094
+ tool_registry = self.get_tool_registry()
1095
+ memory_prompts = self.memory_manager.add_memory_prompts_to_addon(
1096
+ "", tool_registry
1097
+ )
1098
+
440
1099
  addon_prompt = f"""
441
1100
  <system_prompt>
442
1101
  请判断是否已经完成任务,如果已经完成:
@@ -446,7 +1105,7 @@ class Agent:
446
1105
  - 仅包含一个操作
447
1106
  - 如果信息不明确,请请求用户补充
448
1107
  - 如果执行过程中连续失败5次,请使用ask_user询问用户操作
449
- - 操作列表:{action_handlers}
1108
+ - 操作列表:{action_handlers}{memory_prompts}
450
1109
  </system_prompt>
451
1110
 
452
1111
  请继续。
@@ -469,114 +1128,353 @@ class Agent:
469
1128
  3. 包含错误处理和恢复逻辑
470
1129
  4. 自动加载相关方法论(如果是首次运行)
471
1130
  """
472
-
473
1131
  self.session.prompt = f"{user_input}"
474
1132
  try:
475
1133
  set_agent(self.name, self)
476
-
477
- while True:
478
- if self.first:
479
- self._first_run()
1134
+ # 广播任务开始事件(不影响主流程)
1135
+ try:
1136
+ self.event_bus.emit(
1137
+ TASK_STARTED,
1138
+ agent=self,
1139
+ name=self.name,
1140
+ description=self.description,
1141
+ user_input=self.session.prompt,
1142
+ )
1143
+ except Exception:
1144
+ pass
1145
+ # 如启用规划模式,先判断是否需要拆分并调度子任务
1146
+ if self.plan:
480
1147
  try:
481
- current_response = self._call_model(self.session.prompt, True)
482
- self.session.prompt = ""
483
-
484
- if get_interrupt():
485
- set_interrupt(False)
486
- user_input = self.multiline_inputer(
487
- f"模型交互期间被中断,请输入用户干预信息:"
488
- )
489
- if user_input:
490
- # 如果有工具调用且用户确认继续,则将干预信息和工具执行结果拼接为prompt
491
- if any(
492
- handler.can_handle(current_response)
493
- for handler in self.output_handler
494
- ):
495
- if user_confirm(
496
- "检测到有工具调用,是否继续处理工具调用?", True
497
- ):
498
- self.session.prompt = (
499
- f"{user_input}\n\n{current_response}"
500
- )
501
- continue
502
- self.session.prompt += f"{user_input}"
503
- continue
504
-
505
- need_return, self.session.prompt = self._call_tools(
506
- current_response
507
- )
1148
+ self._maybe_plan_and_dispatch(self.session.prompt)
1149
+ except Exception:
1150
+ # 防御式处理,规划失败不影响主流程
1151
+ pass
1152
+ return self._main_loop()
1153
+ except Exception as e:
1154
+ PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
1155
+ return f"Task failed: {str(e)}"
508
1156
 
509
- if need_return:
510
- return self.session.prompt
1157
+ def _main_loop(self) -> Any:
1158
+ """主运行循环"""
1159
+ # 委派至独立的运行循环类,保持行为一致
1160
+ loop = AgentRunLoop(self)
1161
+ return loop.run()
511
1162
 
512
- if self.after_tool_call_cb:
513
- self.after_tool_call_cb(self)
1163
+ def _handle_run_interrupt(self, current_response: str) -> Optional[Union[Any, "LoopAction"]]:
1164
+ """处理运行中的中断
514
1165
 
515
- if self.session.prompt or self.session.addon_prompt:
516
- continue
1166
+ 返回:
1167
+ None: 无中断,或中断后允许继续执行当前响应
1168
+ Any: 需要返回的最终结果
1169
+ LoopAction.SKIP_TURN: 中断后需要跳过当前响应,并立即开始下一次循环
1170
+ """
1171
+ if not get_interrupt():
1172
+ return None
517
1173
 
518
- if self.auto_complete and ot("!!!COMPLETE!!!") in current_response:
519
- return self._complete_task()
1174
+ set_interrupt(False)
1175
+ user_input = self._multiline_input(
1176
+ "模型交互期间被中断,请输入用户干预信息:", False
1177
+ )
1178
+ # 广播中断事件(包含用户输入,可能为空字符串)
1179
+ try:
1180
+ self.event_bus.emit(
1181
+ INTERRUPT_TRIGGERED,
1182
+ agent=self,
1183
+ current_response=current_response,
1184
+ user_input=user_input,
1185
+ )
1186
+ except Exception:
1187
+ pass
1188
+
1189
+ self.run_input_handlers_next_turn = True
1190
+
1191
+ if not user_input:
1192
+ # 用户输入为空,完成任务
1193
+ return self._complete_task(auto_completed=False)
1194
+
1195
+ if any(handler.can_handle(current_response) for handler in self.output_handler):
1196
+ if self.confirm_callback("检测到有工具调用,是否继续处理工具调用?", True):
1197
+ self.session.prompt = join_prompts([
1198
+ f"被用户中断,用户补充信息为:{user_input}",
1199
+ "用户同意继续工具调用。"
1200
+ ])
1201
+ return None # 继续执行工具调用
1202
+ else:
1203
+ self.session.prompt = join_prompts([
1204
+ f"被用户中断,用户补充信息为:{user_input}",
1205
+ "检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
1206
+ ])
1207
+ return LoopAction.SKIP_TURN # 请求主循环 continue
1208
+ else:
1209
+ self.session.prompt = f"被用户中断,用户补充信息为:{user_input}"
1210
+ return LoopAction.SKIP_TURN # 请求主循环 continue
520
1211
 
521
- # 获取用户输入
522
- user_input = self.multiline_inputer(
523
- f"{self.name}: 请输入,或输入空行来结束当前任务:"
524
- )
1212
+ def _get_next_user_action(self) -> Union[str, "LoopAction"]:
1213
+ """获取用户下一步操作
525
1214
 
526
- if user_input:
527
- self.session.prompt = user_input
528
- continue
1215
+ 返回:
1216
+ LoopAction.CONTINUE 或 LoopAction.COMPLETE(兼容旧字符串值 "continue"/"complete")
1217
+ """
1218
+ user_input = self._multiline_input(
1219
+ f"{self.name}: 请输入,或输入空行来结束当前任务:", False
1220
+ )
529
1221
 
530
- if not user_input:
531
- return self._complete_task()
1222
+ if user_input:
1223
+ self.session.prompt = user_input
1224
+ # 使用显式动作信号,保留返回类型注释以保持兼容
1225
+ return LoopAction.CONTINUE # type: ignore[return-value]
1226
+ else:
1227
+ return LoopAction.COMPLETE # type: ignore[return-value]
532
1228
 
533
- except Exception as e:
534
- PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
535
- return f"Task failed: {str(e)}"
1229
+ def _first_run(self):
1230
+ """首次运行初始化"""
1231
+ # 如果工具过多,使用AI进行筛选
1232
+ if self.session.prompt:
1233
+ self._filter_tools_if_needed(self.session.prompt)
536
1234
 
537
- except Exception as e:
538
- PrettyOutput.print(f"任务失败: {str(e)}", OutputType.ERROR)
539
- return f"Task failed: {str(e)}"
1235
+ # 准备记忆标签提示
1236
+ memory_tags_prompt = self.memory_manager.prepare_memory_tags_prompt()
540
1237
 
541
- def _first_run(self):
542
- # 如果有上传文件,先上传文件
543
- if self.model and self.model.support_upload_files():
544
- if self.use_methodology:
545
- if not upload_methodology(self.model, other_files=self.files):
546
- if self.files:
547
- PrettyOutput.print(
548
- "文件上传失败,将忽略文件列表", OutputType.WARNING
549
- )
550
- # 上传失败则回退到本地加载
551
- msg = self.session.prompt
552
- for handler in self.input_handler:
553
- msg, _ = handler(msg, self)
554
- self.session.prompt = f"{self.session.prompt}\n\n以下是历史类似问题的执行经验,可参考:\n{load_methodology(msg, self.get_tool_registry())}"
555
- else:
556
- if self.files:
557
- self.session.prompt = f"{self.session.prompt}\n\n上传的文件包含历史对话信息和方法论文件,可以从中获取一些经验信息。"
558
- else:
559
- self.session.prompt = f"{self.session.prompt}\n\n上传的文件包含历史对话信息,可以从中获取一些经验信息。"
560
- elif self.files:
561
- if not self.model.upload_files(self.files):
562
- PrettyOutput.print(
563
- "文件上传失败,将忽略文件列表", OutputType.WARNING
564
- )
565
- else:
566
- self.session.prompt = f"{self.session.prompt}\n\n上传的文件包含历史对话信息,可以从中获取一些经验信息。"
567
- else:
568
- if self.files:
569
- PrettyOutput.print("不支持上传文件,将忽略文件列表", OutputType.WARNING)
570
- if self.use_methodology:
571
- msg = self.session.prompt
572
- for handler in self.input_handler:
573
- msg, _ = handler(msg, self)
574
- self.session.prompt = f"{self.session.prompt}\n\n以下是历史类似问题的执行经验,可参考:\n{load_methodology(msg, self.get_tool_registry())}"
1238
+ # 处理文件上传和方法论加载
1239
+ self.file_methodology_manager.handle_files_and_methodology()
1240
+
1241
+ # 添加记忆标签提示
1242
+ if memory_tags_prompt:
1243
+ self.session.prompt = f"{self.session.prompt}{memory_tags_prompt}"
575
1244
 
576
1245
  self.first = False
577
1246
 
578
- def clear_history(self):
1247
+ def _create_temp_model(self, system_prompt: str) -> BasePlatform:
1248
+ """创建一个用于执行一次性任务的临时模型实例,以避免污染主会话。"""
1249
+ temp_model = PlatformRegistry().create_platform(
1250
+ self.model.platform_name() # type: ignore
1251
+ )
1252
+ if not temp_model:
1253
+ raise RuntimeError("创建临时模型失败。")
1254
+
1255
+ temp_model.set_model_name(self.model.name()) # type: ignore
1256
+ temp_model.set_system_prompt(system_prompt)
1257
+ return temp_model
1258
+
1259
+ def _build_child_agent_params(self, name: str, description: str) -> Dict[str, Any]:
1260
+ """构建子Agent参数,尽量继承父Agent配置,并确保子Agent非交互自动完成。"""
1261
+ use_tools_param: Optional[List[str]] = None
1262
+ try:
1263
+ tr = self.get_tool_registry()
1264
+ if isinstance(tr, ToolRegistry):
1265
+ selected_tools = tr.get_all_tools()
1266
+ use_tools_param = [t["name"] for t in selected_tools]
1267
+ except Exception:
1268
+ use_tools_param = None
1269
+
1270
+ return {
1271
+ "system_prompt": origin_agent_system_prompt,
1272
+ "name": name,
1273
+ "description": description,
1274
+ "model_group": self.model_group,
1275
+ "summary_prompt": self.summary_prompt,
1276
+ "auto_complete": True,
1277
+ "use_tools": use_tools_param,
1278
+ "execute_tool_confirm": self.execute_tool_confirm,
1279
+ "need_summary": self.need_summary,
1280
+ "auto_summary_rounds": self.auto_summary_rounds,
1281
+ "multiline_inputer": self.multiline_inputer,
1282
+ "use_methodology": self.use_methodology,
1283
+ "use_analysis": self.use_analysis,
1284
+ "force_save_memory": self.force_save_memory,
1285
+ "disable_file_edit": self.disable_file_edit,
1286
+ "files": self.files,
1287
+ "confirm_callback": self.confirm_callback,
1288
+ "non_interactive": True,
1289
+ "in_multi_agent": True,
1290
+ "plan": self.plan, # 继承父Agent的规划开关
1291
+ "plan_depth": self.plan_depth + 1, # 子Agent层数+1
1292
+ "plan_max_depth": self.plan_max_depth, # 继承上限
1293
+ }
1294
+
1295
+ def _maybe_plan_and_dispatch(self, task_text: str) -> None:
1296
+ """委托给 TaskPlanner 执行任务规划与子任务调度,保持向后兼容。"""
1297
+ try:
1298
+ if hasattr(self, "task_planner") and self.task_planner:
1299
+ # 优先使用初始化时注入的规划器
1300
+ self.task_planner.maybe_plan_and_dispatch(task_text) # type: ignore[attr-defined]
1301
+ else:
1302
+ # 防御式回退:临时创建规划器以避免因未初始化导致的崩溃
1303
+ from jarvis.jarvis_agent.task_planner import TaskPlanner
1304
+ TaskPlanner(self, plan_depth=self.plan_depth, plan_max_depth=self.plan_max_depth).maybe_plan_and_dispatch(task_text)
1305
+ except Exception:
1306
+ # 规划失败不影响主流程
1307
+ pass
1308
+
1309
+ def _filter_tools_if_needed(self, task: str):
1310
+ """如果工具数量超过阈值,使用大模型筛选相关工具"""
1311
+ tool_registry = self.get_tool_registry()
1312
+ if not isinstance(tool_registry, ToolRegistry):
1313
+ return
1314
+
1315
+ all_tools = tool_registry.get_all_tools()
1316
+ threshold = get_tool_filter_threshold()
1317
+ if len(all_tools) <= threshold:
1318
+ return
1319
+
1320
+ # 为工具选择构建提示
1321
+ tools_prompt_part = ""
1322
+ tool_names = []
1323
+ for i, tool in enumerate(all_tools, 1):
1324
+ tool_names.append(tool["name"])
1325
+ tools_prompt_part += f"{i}. {tool['name']}: {tool['description']}\n"
1326
+
1327
+ selection_prompt = f"""
1328
+ 用户任务是:
1329
+ <task>
1330
+ {task}
1331
+ </task>
1332
+
1333
+ 这是一个可用工具的列表:
1334
+ <tools>
1335
+ {tools_prompt_part}
1336
+ </tools>
1337
+
1338
+ 请根据用户任务,从列表中选择最相关的工具。
1339
+ 请仅返回所选工具的编号,以逗号分隔。例如:1, 5, 12
1340
+ """
1341
+ PrettyOutput.print(
1342
+ f"工具数量超过{threshold}个,正在使用AI筛选相关工具...", OutputType.INFO
1343
+ )
1344
+ # 广播工具筛选开始事件
1345
+ try:
1346
+ self.event_bus.emit(
1347
+ BEFORE_TOOL_FILTER,
1348
+ agent=self,
1349
+ task=task,
1350
+ total_tools=len(all_tools),
1351
+ threshold=threshold,
1352
+ )
1353
+ except Exception:
1354
+ pass
1355
+
1356
+ # 使用临时模型实例调用模型,以避免污染历史记录
1357
+ try:
1358
+ temp_model = self._create_temp_model("你是一个帮助筛选工具的助手。")
1359
+ selected_tools_str = temp_model.chat_until_success(
1360
+ selection_prompt
1361
+ ) # type: ignore
1362
+
1363
+ # 解析响应并筛选工具
1364
+ selected_indices = [
1365
+ int(i.strip()) for i in re.findall(r"\d+", selected_tools_str)
1366
+ ]
1367
+ selected_tool_names = [
1368
+ tool_names[i - 1]
1369
+ for i in selected_indices
1370
+ if 0 < i <= len(tool_names)
1371
+ ]
1372
+
1373
+ if selected_tool_names:
1374
+ # 移除重复项
1375
+ selected_tool_names = sorted(list(set(selected_tool_names)))
1376
+ tool_registry.use_tools(selected_tool_names)
1377
+ # 使用筛选后的工具列表重新设置系统提示
1378
+ self._setup_system_prompt()
1379
+ PrettyOutput.print(
1380
+ f"已筛选出 {len(selected_tool_names)} 个相关工具: {', '.join(selected_tool_names)}",
1381
+ OutputType.SUCCESS,
1382
+ )
1383
+ # 广播工具筛选事件
1384
+ try:
1385
+ self.event_bus.emit(
1386
+ TOOL_FILTERED,
1387
+ agent=self,
1388
+ task=task,
1389
+ selected_tools=selected_tool_names,
1390
+ total_tools=len(all_tools),
1391
+ threshold=threshold,
1392
+ )
1393
+ except Exception:
1394
+ pass
1395
+ else:
1396
+ PrettyOutput.print(
1397
+ "AI 未能筛选出任何相关工具,将使用所有工具。", OutputType.WARNING
1398
+ )
1399
+ # 广播工具筛选事件(无筛选结果)
1400
+ try:
1401
+ self.event_bus.emit(
1402
+ TOOL_FILTERED,
1403
+ agent=self,
1404
+ task=task,
1405
+ selected_tools=[],
1406
+ total_tools=len(all_tools),
1407
+ threshold=threshold,
1408
+ )
1409
+ except Exception:
1410
+ pass
1411
+
1412
+ except Exception as e:
1413
+ PrettyOutput.print(
1414
+ f"工具筛选失败: {e},将使用所有工具。", OutputType.ERROR
1415
+ )
1416
+
1417
+ def _check_and_organize_memory(self):
579
1418
  """
580
- Clears conversation history by delegating to the session manager.
1419
+ 检查记忆库状态,如果满足条件则提示用户整理。
1420
+ 每天只检测一次。
581
1421
  """
582
- self.session.clear_history()
1422
+ try:
1423
+ # 检查项目记忆
1424
+ self._perform_memory_check("project_long_term", Path(".jarvis"), "project")
1425
+ # 检查全局记忆
1426
+ self._perform_memory_check(
1427
+ "global_long_term",
1428
+ Path(get_data_dir()),
1429
+ "global",
1430
+ )
1431
+ except Exception as e:
1432
+ PrettyOutput.print(f"检查记忆库时发生意外错误: {e}", OutputType.WARNING)
1433
+
1434
+ def _perform_memory_check(self, memory_type: str, base_path: Path, scope_name: str):
1435
+ """执行特定范围的记忆检查和整理"""
1436
+ check_file = base_path / ".last_memory_organizer_check"
1437
+ now = datetime.datetime.now()
1438
+
1439
+ if check_file.exists():
1440
+ try:
1441
+ last_check_time = datetime.datetime.fromisoformat(
1442
+ check_file.read_text()
1443
+ )
1444
+ if (now - last_check_time).total_seconds() < 24 * 3600:
1445
+ return # 24小时内已检查
1446
+ except (ValueError, FileNotFoundError):
1447
+ # 文件内容无效或文件在读取时被删除,继续执行检查
1448
+ pass
1449
+
1450
+ # 立即更新检查时间,防止并发或重复检查
1451
+ base_path.mkdir(parents=True, exist_ok=True)
1452
+ check_file.write_text(now.isoformat())
1453
+
1454
+ organizer = MemoryOrganizer()
1455
+ # NOTE: 使用受保护方法以避免重复实现逻辑
1456
+ memories = organizer._load_memories(memory_type)
1457
+
1458
+ if len(memories) < 200:
1459
+ return
1460
+
1461
+ # NOTE: 使用受保护方法以避免重复实现逻辑
1462
+ overlap_groups = organizer._find_overlapping_memories(memories, min_overlap=3)
1463
+ has_significant_overlap = any(groups for groups in overlap_groups.values())
1464
+
1465
+ if not has_significant_overlap:
1466
+ return
1467
+
1468
+ prompt = (
1469
+ f"检测到您的 '{scope_name}' 记忆库中包含 {len(memories)} 条记忆,"
1470
+ f"并且存在3个以上标签重叠的记忆。\n"
1471
+ f"是否立即整理记忆库以优化性能和相关性?"
1472
+ )
1473
+ if self.confirm_callback(prompt, True):
1474
+ PrettyOutput.print(
1475
+ f"正在开始整理 '{scope_name}' ({memory_type}) 记忆库...",
1476
+ OutputType.INFO,
1477
+ )
1478
+ organizer.organize_memories(memory_type, min_overlap=3)
1479
+ else:
1480
+ PrettyOutput.print(f"已取消 '{scope_name}' 记忆库整理。", OutputType.INFO)