myagent-ai 1.8.9 → 1.9.1

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 (87) hide show
  1. package/agents/main_agent.py +615 -1
  2. package/core/context_builder.py +459 -0
  3. package/core/deps_checker.py +0 -0
  4. package/core/llm.py +19 -0
  5. package/core/output_parser.py +415 -0
  6. package/install/uninstall.ps1 +0 -0
  7. package/install/uninstall.sh +0 -0
  8. package/main.py +6 -1
  9. package/memory/manager.py +1 -1
  10. package/package.json +1 -1
  11. package/skills/docx/LICENSE.txt +8 -25
  12. package/skills/docx/SKILL.md +158 -413
  13. package/skills/docx/scripts/add_toc_placeholders.py +609 -80
  14. package/skills/docx/scripts/document.py +55 -24
  15. package/skills/gui_skill.py +0 -0
  16. package/skills/pdf/LICENSE.txt +8 -25
  17. package/skills/pdf/SKILL.md +757 -1364
  18. package/skills/xlsx/LICENSE.txt +8 -25
  19. package/skills/xlsx/SKILL.md +163 -429
  20. package/web/api_server.py +96 -3
  21. package/web/ui/chat/chat.css +206 -0
  22. package/web/ui/chat/chat.js +1 -1
  23. package/web/ui/chat/chat_container.html +1 -1
  24. package/web/ui/chat/chat_main.js +16 -2
  25. package/web/ui/chat/flow_engine.js +179 -0
  26. package/web/ui/chat/groupchat.js +0 -0
  27. package/web/ui/chat/left_sessions.html +0 -0
  28. package/web/ui/chat/middle_chat.html +0 -0
  29. package/web/ui/chat/right_agents.html +0 -0
  30. package/web/ui/index.html +78 -12
  31. package/skills/pptx/LICENSE.txt +0 -30
  32. package/skills/pptx/SKILL.md +0 -507
  33. package/skills/pptx/html2pptx.md +0 -625
  34. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
  35. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
  36. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
  37. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
  38. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
  39. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
  40. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
  41. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
  42. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
  43. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
  44. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
  45. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
  46. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
  47. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
  48. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
  49. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
  50. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
  51. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
  52. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
  53. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
  54. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
  55. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
  56. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
  57. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
  58. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
  59. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
  60. package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
  61. package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
  62. package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
  63. package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
  64. package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
  65. package/skills/pptx/ooxml/schemas/mce/mc.xsd +0 -75
  66. package/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +0 -560
  67. package/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +0 -67
  68. package/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +0 -14
  69. package/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +0 -20
  70. package/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +0 -13
  71. package/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
  72. package/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +0 -8
  73. package/skills/pptx/ooxml/scripts/pack.py +0 -159
  74. package/skills/pptx/ooxml/scripts/unpack.py +0 -29
  75. package/skills/pptx/ooxml/scripts/validate.py +0 -69
  76. package/skills/pptx/ooxml/scripts/validation/__init__.py +0 -15
  77. package/skills/pptx/ooxml/scripts/validation/base.py +0 -951
  78. package/skills/pptx/ooxml/scripts/validation/docx.py +0 -274
  79. package/skills/pptx/ooxml/scripts/validation/pptx.py +0 -315
  80. package/skills/pptx/ooxml/scripts/validation/redlining.py +0 -279
  81. package/skills/pptx/ooxml.md +0 -427
  82. package/skills/pptx/scripts/html2pptx.js +0 -1044
  83. package/skills/pptx/scripts/inventory.py +0 -1020
  84. package/skills/pptx/scripts/rearrange.py +0 -231
  85. package/skills/pptx/scripts/replace.py +0 -385
  86. package/skills/pptx/scripts/thumbnail.py +0 -450
  87. package/web/__pycache__/api_server.cpython-312.pyc +0 -0
@@ -7,13 +7,15 @@ from __future__ import annotations
7
7
 
8
8
  import json
9
9
  import asyncio
10
- from typing import Any, Dict, List, Optional
10
+ from typing import Any, Callable, Dict, List, Optional
11
11
 
12
12
  from core.logger import get_logger
13
13
  from core.llm import LLMClient, LLMResponse, Message
14
14
  from agents.base import BaseAgent, AgentContext
15
15
  from core.utils import generate_id, timestamp, safe_json_parse, truncate_str
16
16
  from core.context_manager import ContextManager, ContextConfig, estimate_tokens
17
+ from core.context_builder import ContextBuilder
18
+ from core.output_parser import ParsedOutput, parse_output, validate_output, extract_surrounding_text
17
19
 
18
20
  logger = get_logger("myagent.agent.main")
19
21
 
@@ -171,6 +173,45 @@ status 取值:
171
173
  - 绝对不要在回复开头进行自我介绍
172
174
  - 不要重复问候"""
173
175
 
176
+ # =========================================================================
177
+ # V2 系统提示词 — 结构化输出格式
178
+ # =========================================================================
179
+ SYSTEM_PROMPT_V2 = """你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:
180
+
181
+ 严格格式化输出以下内容:
182
+ <output>
183
+ <usersays_correct>根据用户输入的"usersays"内容,结合上下文优化为新的用户输入,如果"usersays"为空,这里输出为空。</usersays_correct>
184
+ <task_plan>如"context"包含非空"task_plan",则更新它,变为当前输出。否则,根据"context", 以MD 的格式,制定新任务列表。</task_plan>
185
+
186
+ <toolstocal>
187
+ <tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。示例:首先,要调用网页查询工具,找到网页内容。</beforecalltext><toolname>工具名</toolname><parms>参数</parms><timeout>预估超时时限</timeout><callback>true/false,要求解析器在该工具执行完后是否要回调llm大模型,将所有工具输出结果+新构造的"context"输入给llm</callback></tool>
188
+
189
+ <tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。示例:接下来,要调用命令行工具,获得ip地址。</beforecalltext><toolname>工具名</toolname><parms>参数</parms><timeout>预估超时时限</timeout><callback>true/false</callback></tool>
190
+ </toolstocal>
191
+ <remember>根据"context,提取需要存入记忆库的内容,这里要搜简上记忆库</remember>
192
+ <recall>下一轮执行需要调取的记忆,这里要设计接上记忆库</recall>
193
+ <get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
194
+ <askuser>需要询问用户的内容,如无,则为空</askuser>
195
+ <finish>true/false,是否结束循环调用llm。如"askuser"为非空,则"finish"输出true。否则,根据"context"判断任务是否已完成,是否结束llm回调</finish>
196
+
197
+ </output>
198
+
199
+ ## 核心规则
200
+ 1. 你必须且只能输出 <output> XML 结构,不要输出任何其他文本
201
+ 2. <usersays_correct>: 如果 context 中 usersays 非空,则根据对话语境优化为更准确的用户意图表达
202
+ 3. <task_plan>: 使用 Markdown 列表格式,每项包含任务描述和完成状态标记 [x]/[ ]
203
+ 4. <toolstocal>: 列出所有需要执行的工具调用,每个工具包含完整的参数说明
204
+ 5. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
205
+ 6. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
206
+ 7. <remember>: 提取本轮对话中值得长期记忆的关键信息(用户偏好、重要结论、错误经验等)
207
+ 8. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
208
+ 9. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
209
+ 10. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
210
+ 11. <finish>: 当任务已完成或需要等待用户回应时为 true;否则为 false 继续执行
211
+ 12. 使用中文输出所有内容
212
+ 13. 优先使用技能系统(skill)而非直接写代码执行
213
+ """
214
+
174
215
  def __init__(self, tool_agent=None, memory_agent=None, **kwargs):
175
216
  super().__init__(**kwargs)
176
217
  self.tool_agent = tool_agent
@@ -183,12 +224,23 @@ status 取值:
183
224
  self._org_context_mtime: float = 0 # 文件修改时间,用于检测变更
184
225
  # Token 上下文管理器 (滚动摘要 + 预算控制)
185
226
  self.context_manager = ContextManager(ContextConfig())
227
+ # V2 Context Builder (结构化上下文构建)
228
+ self.context_builder: Optional[ContextBuilder] = None
186
229
  # 执行事件追踪(用于前端展示命令执行过程)
187
230
  self._execution_events: List[Dict] = []
188
231
  self._exec_event_counter: int = 0
189
232
  # 活跃会话上下文追踪(用于消息注入)
190
233
  self.active_contexts: Dict[str, AgentContext] = {}
191
234
 
235
+ def init_context_builder(self, memory_manager=None, skill_registry=None, knowledge_base_dir=None):
236
+ """初始化 V2 Context Builder(在系统启动后调用,注入依赖)"""
237
+ self.context_builder = ContextBuilder(
238
+ memory_manager=memory_manager,
239
+ skill_registry=skill_registry,
240
+ knowledge_base_dir=knowledge_base_dir,
241
+ )
242
+ logger.info("V2 Context Builder 已初始化" + (f" (知识库: {knowledge_base_dir})" if knowledge_base_dir else ""))
243
+
192
244
  def _add_exec_event(self, event_type: str, data: Dict):
193
245
  """记录一个执行事件(供前端展示)"""
194
246
  import time as _time
@@ -241,6 +293,10 @@ status 取值:
241
293
  logger.info(f"[{task_id}] 开始处理用户请求: {context.user_message[:100]}")
242
294
 
243
295
  try:
296
+ # V2 路由: 如果 context_builder 已初始化,使用 V2 结构化输出循环
297
+ if self.context_builder is not None:
298
+ logger.info(f"[{task_id}] 检测到 V2 Context Builder,使用 V2 执行循环")
299
+ return await self.process_v2(context)
244
300
  return await self._process_inner(context, task_id)
245
301
  finally:
246
302
  # 移除活跃上下文
@@ -479,6 +535,44 @@ status 取值:
479
535
  content=final_response,
480
536
  )
481
537
 
538
+ # ── 保存工具调用过程到短期记忆(供历史记录查看)──
539
+ if self.memory and self._execution_events:
540
+ try:
541
+ tool_summary_parts = []
542
+ for evt in self._execution_events:
543
+ etype = evt.get("type", "")
544
+ if etype in ("tool_call", "skill_call"):
545
+ tool_name = evt.get("tool_name") or evt.get("skill_name", "")
546
+ args = evt.get("arguments") or evt.get("params", {})
547
+ tool_summary_parts.append(f"🔧 调用工具: {tool_name}({json.dumps(args, ensure_ascii=False)[:200]})")
548
+ elif etype in ("tool_result", "skill_result"):
549
+ tool_name = evt.get("tool_name") or evt.get("skill_name", "")
550
+ success = evt.get("success", False)
551
+ summary = evt.get("summary", "")
552
+ status = "✅" if success else "❌"
553
+ tool_summary_parts.append(f" {status} 结果: {summary[:300]}")
554
+ elif etype == "code_exec":
555
+ lang = evt.get("language", "")
556
+ code_preview = evt.get("code_preview", "")
557
+ stdout = evt.get("stdout", "")
558
+ success = evt.get("success", False)
559
+ status = "✅" if success else "❌"
560
+ tool_summary_parts.append(f"💻 执行 {lang}: {code_preview[:150]}")
561
+ if stdout:
562
+ tool_summary_parts.append(f" {status} 输出: {stdout[:300]}")
563
+ elif etype == "code_result":
564
+ pass # code_result is duplicate of code_exec with final status
565
+ if tool_summary_parts:
566
+ exec_log = "\n".join(tool_summary_parts)
567
+ self.memory.add_short_term(
568
+ session_id=context.session_id,
569
+ role="tool",
570
+ content=exec_log,
571
+ importance=0.3,
572
+ )
573
+ except Exception as e:
574
+ logger.warning(f"保存工具调用过程失败: {e}")
575
+
482
576
  # 清理工作记忆
483
577
  context.working_memory["final_response"] = final_response
484
578
  context.working_memory["iterations"] = self._iteration_count
@@ -1026,3 +1120,523 @@ status 取值:
1026
1120
  except Exception as e:
1027
1121
  logger.debug(f"构建组织上下文失败: {e}")
1028
1122
  return ""
1123
+
1124
+ # =========================================================================
1125
+ # V2 执行循环 — 结构化输出 + Context Builder + Output Parser
1126
+ # =========================================================================
1127
+
1128
+ async def _emit_v2_event(self, event_type: str, data: Dict, stream_callback: Optional[Callable] = None):
1129
+ """发送 V2 SSE 事件。如果 stream_callback 不存在则仅记录日志。"""
1130
+ event = {"type": event_type, **data}
1131
+ if stream_callback is not None:
1132
+ try:
1133
+ if asyncio.iscoroutinefunction(stream_callback):
1134
+ await stream_callback(event)
1135
+ else:
1136
+ stream_callback(event)
1137
+ except Exception as e:
1138
+ logger.debug(f"V2 SSE 事件发送失败 ({event_type}): {e}")
1139
+ else:
1140
+ logger.debug(f"[v2-event] {event_type}: {data}")
1141
+
1142
+ async def process_v2(
1143
+ self,
1144
+ context: AgentContext,
1145
+ agent_name: str = "助手",
1146
+ agent_description: str = "通用AI助手",
1147
+ agent_override_prompt: Optional[str] = None,
1148
+ stream_callback: Optional[Callable] = None,
1149
+ stream_response=None,
1150
+ text_delta_callback=None,
1151
+ ) -> AgentContext:
1152
+ """
1153
+ V2 主处理循环 — 使用结构化输出格式。
1154
+
1155
+ 核心流程:
1156
+ 1. 使用 ContextBuilder 构建 <context> XML
1157
+ 2. 将 context 注入 SYSTEM_PROMPT_V2,调用 LLM
1158
+ 3. 使用 OutputParser 解析 <output> XML
1159
+ 4. 根据 parsed.tools_to_call 依次执行工具
1160
+ 5. 任一工具超时 → 强制回调 LLM
1161
+ 6. 根据 callback 标志决定是否回调 LLM
1162
+ 7. 处理 remember/recall/askuser/finish
1163
+
1164
+ Args:
1165
+ context: Agent 上下文
1166
+ agent_name: Agent 名称(用于 ContextBuilder)
1167
+ agent_description: Agent 描述
1168
+ agent_override_prompt: 可选的 Agent 身份覆盖提示词
1169
+ stream_callback: 可选的 SSE 事件回调 (callable 或 async callable)
1170
+ stream_response: 可选的流式响应对象(用于 LLM 流式输出)
1171
+ text_delta_callback: 可选的文本增量回调
1172
+ """
1173
+ task_id = context.task_id or generate_id("task")
1174
+ context.task_id = task_id
1175
+ self._iteration_count = 0
1176
+ self._current_task_id = task_id
1177
+ self.clear_execution_events()
1178
+
1179
+ if not self.context_builder:
1180
+ logger.warning("Context Builder 未初始化,回退到 V1 执行循环")
1181
+ return await self._process_inner(context, task_id)
1182
+
1183
+ logger.info(f"[{task_id}] V2 执行循环启动: {context.user_message[:100]}")
1184
+
1185
+ try:
1186
+ return await self._process_v2_inner(
1187
+ context, task_id, agent_name, agent_description,
1188
+ agent_override_prompt, stream_callback, stream_response, text_delta_callback
1189
+ )
1190
+ except Exception as e:
1191
+ logger.error(f"[{task_id}] V2 执行循环异常: {e}", exc_info=True)
1192
+ context.working_memory["final_response"] = f"执行异常: {str(e)}"
1193
+ await self._emit_v2_event("v2_reasoning", {"content": f"执行异常: {str(e)}"}, stream_callback)
1194
+ return context
1195
+
1196
+ async def _process_v2_inner(
1197
+ self,
1198
+ context: AgentContext,
1199
+ task_id: str,
1200
+ agent_name: str,
1201
+ agent_description: str,
1202
+ agent_override_prompt: Optional[str],
1203
+ stream_callback: Optional[Callable] = None,
1204
+ stream_response=None,
1205
+ text_delta_callback=None,
1206
+ ) -> AgentContext:
1207
+ """V2 内部循环逻辑 — 结构化输出 + 工具调度 + SSE 事件推送"""
1208
+ max_iter = self.config.agent.max_iterations
1209
+ current_task_plan = ""
1210
+ all_tool_outputs = ""
1211
+ recall_content = ""
1212
+ get_knowledge_content = ""
1213
+
1214
+ conversation_history = list(context.conversation_history or [])
1215
+
1216
+ # 保存用户消息到短期记忆
1217
+ if self.memory:
1218
+ self.memory.add_short_term(
1219
+ session_id=context.session_id,
1220
+ role="user",
1221
+ content=context.user_message,
1222
+ )
1223
+
1224
+ # 加载相关记忆 (recall from previous round or initial load)
1225
+ if self.memory_agent and context.user_message:
1226
+ mem_ctx = AgentContext(
1227
+ task_id=task_id,
1228
+ session_id=context.session_id,
1229
+ user_message=context.user_message,
1230
+ metadata={"memory_action": "get_relevant"},
1231
+ )
1232
+ await self.memory_agent.process(mem_ctx)
1233
+ if "memory_context_prompt" in mem_ctx.working_memory:
1234
+ context.working_memory["memory_context_prompt"] = \
1235
+ mem_ctx.working_memory["memory_context_prompt"]
1236
+
1237
+ while self._iteration_count < max_iter:
1238
+ self._iteration_count += 1
1239
+ logger.info(f"[{task_id}] V2 迭代 {self._iteration_count}/{max_iter}")
1240
+
1241
+ # ── 检查配置热加载广播 ──
1242
+ if self.config_broadcaster:
1243
+ reloaded, reload_type = await self.config_broadcaster.check_and_wait(task_id)
1244
+ if reloaded:
1245
+ logger.info(f"[{task_id}] V2 迭代 {self._iteration_count}: {reload_type}已热更新")
1246
+
1247
+ # ── 检查并处理注入的消息 ──
1248
+ if context.pending_injected_messages:
1249
+ injected = context.pending_injected_messages.copy()
1250
+ context.pending_injected_messages.clear()
1251
+ for msg_text in injected:
1252
+ logger.info(f"[{task_id}] 注入消息到对话历史: {msg_text[:50]}...")
1253
+ conversation_history.append(
1254
+ Message(role="user", content=f"[用户中断/补充]: {msg_text}")
1255
+ )
1256
+
1257
+ # Step 1: 构建 Context XML
1258
+ context_xml = self.context_builder.build_context(
1259
+ agent_name=agent_name,
1260
+ agent_description=agent_description,
1261
+ session_id=context.session_id,
1262
+ conversation_history=conversation_history,
1263
+ user_typed_text=context.user_message,
1264
+ user_voice_text="",
1265
+ task_plan=current_task_plan,
1266
+ agent_override_prompt=agent_override_prompt,
1267
+ get_knowledge=get_knowledge_content,
1268
+ )
1269
+
1270
+ await self._emit_v2_event(
1271
+ "v2_context",
1272
+ {"context": truncate_str(context_xml, 8000)},
1273
+ stream_callback,
1274
+ )
1275
+
1276
+ # Step 2: 构建系统消息 (SYSTEM_PROMPT_V2 + context XML)
1277
+ system_content = self.SYSTEM_PROMPT_V2 + "\n\n" + context_xml
1278
+
1279
+ # Step 3: 调用 LLM
1280
+ messages = [Message(role="system", content=system_content)]
1281
+
1282
+ if all_tool_outputs:
1283
+ messages.append(Message(
1284
+ role="user",
1285
+ content=f"[上一轮工具执行结果汇总]\n{truncate_str(all_tool_outputs, 15000)}"
1286
+ ))
1287
+ all_tool_outputs = ""
1288
+
1289
+ if stream_response and self.llm:
1290
+ response = await self._call_llm_stream(
1291
+ messages, text_delta_callback=text_delta_callback,
1292
+ stream_response=stream_response,
1293
+ )
1294
+ else:
1295
+ response = await self._call_llm(messages)
1296
+
1297
+ if not response.success:
1298
+ logger.error(f"[{task_id}] LLM 调用失败: {response.error}")
1299
+ error_msg = f"LLM 调用失败: {response.error}"
1300
+ context.working_memory["final_response"] = error_msg
1301
+ await self._emit_v2_event("v2_reasoning", {"content": error_msg}, stream_callback)
1302
+ break
1303
+
1304
+ llm_raw = response.content
1305
+ logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
1306
+
1307
+ # Step 4: 解析结构化输出
1308
+ parsed = parse_output(llm_raw)
1309
+
1310
+ await self._emit_v2_event(
1311
+ "v2_output_parsed",
1312
+ {"data": {
1313
+ "usersays_correct": parsed.usersays_correct,
1314
+ "task_plan": truncate_str(parsed.task_plan, 500),
1315
+ "tools_count": len(parsed.tools_to_call),
1316
+ "remember": truncate_str(parsed.remember, 200),
1317
+ "ask_user": truncate_str(parsed.ask_user, 200),
1318
+ "finish": parsed.finish,
1319
+ "parse_success": parsed.parse_success,
1320
+ }},
1321
+ stream_callback,
1322
+ )
1323
+
1324
+ if not parsed.parse_success:
1325
+ logger.warning(f"[{task_id}] XML 解析失败,尝试提取周边文本")
1326
+ before, after = extract_surrounding_text(llm_raw)
1327
+ if before.strip() or after.strip():
1328
+ final_text = (before + "\n" + after).strip()
1329
+ context.working_memory["final_response"] = final_text
1330
+ await self._emit_v2_event("v2_reasoning", {"content": final_text}, stream_callback)
1331
+ if self.memory:
1332
+ self.memory.add_short_term(
1333
+ session_id=context.session_id,
1334
+ role="assistant",
1335
+ content=final_text,
1336
+ )
1337
+ break
1338
+ else:
1339
+ context.working_memory["final_response"] = llm_raw
1340
+ break
1341
+
1342
+ warnings = validate_output(parsed)
1343
+ for w in warnings:
1344
+ logger.debug(f"[{task_id}] 验证警告: {w}")
1345
+
1346
+ # Step 5: 处理 usersays_correct — 存入上下文供后续使用
1347
+ if parsed.usersays_correct:
1348
+ context.working_memory["usersays_correct"] = parsed.usersays_correct
1349
+
1350
+ # Step 6: 处理 remember — 存入长期记忆
1351
+ if parsed.remember and self.memory_agent:
1352
+ try:
1353
+ mem_ctx = AgentContext(
1354
+ task_id=task_id,
1355
+ session_id=context.session_id,
1356
+ metadata={
1357
+ "memory_action": "save",
1358
+ "content": parsed.remember,
1359
+ },
1360
+ )
1361
+ await self.memory_agent.process(mem_ctx)
1362
+ await self._emit_v2_event(
1363
+ "v2_memory_saved",
1364
+ {"content": truncate_str(parsed.remember, 200)},
1365
+ stream_callback,
1366
+ )
1367
+ except Exception as e:
1368
+ logger.warning(f"[{task_id}] 存入记忆失败: {e}")
1369
+ elif parsed.remember and self.memory:
1370
+ try:
1371
+ self.memory.add_long_term(
1372
+ session_id=context.session_id,
1373
+ key="conversation_insight",
1374
+ content=parsed.remember,
1375
+ summary=truncate_str(parsed.remember, 200),
1376
+ importance=0.7,
1377
+ )
1378
+ await self._emit_v2_event(
1379
+ "v2_memory_saved",
1380
+ {"content": truncate_str(parsed.remember, 200)},
1381
+ stream_callback,
1382
+ )
1383
+ except Exception as e:
1384
+ logger.warning(f"[{task_id}] 存入记忆失败: {e}")
1385
+
1386
+ # Step 7: 处理 recall — 记录下一轮需要检索的记忆内容
1387
+ if parsed.recall:
1388
+ recall_content = parsed.recall
1389
+
1390
+ # Step 7.5: 处理 get_knowledge — 记录下一轮需要 RAG 搜索的知识关键词
1391
+ if parsed.get_knowledge:
1392
+ get_knowledge_content = parsed.get_knowledge
1393
+ await self._emit_v2_event(
1394
+ "v2_knowledge_request",
1395
+ {"query": truncate_str(parsed.get_knowledge, 200)},
1396
+ stream_callback,
1397
+ )
1398
+
1399
+ # Step 8: 更新任务计划
1400
+ if parsed.task_plan:
1401
+ current_task_plan = parsed.task_plan
1402
+ await self._emit_v2_event(
1403
+ "v2_task_plan",
1404
+ {"plan": truncate_str(current_task_plan, 2000)},
1405
+ stream_callback,
1406
+ )
1407
+
1408
+ # Step 9: 处理 askuser(askuser 非空时,finish 必为 true)
1409
+ if parsed.askuser:
1410
+ logger.info(f"[{task_id}] 需要询问用户: {parsed.askuser[:100]}")
1411
+ context.working_memory["final_response"] = parsed.askuser
1412
+ context.working_memory["ask_user"] = parsed.askuser
1413
+ await self._emit_v2_event(
1414
+ "v2_ask_user",
1415
+ {"question": parsed.askuser},
1416
+ stream_callback,
1417
+ )
1418
+ if self.memory:
1419
+ self.memory.add_short_term(
1420
+ session_id=context.session_id,
1421
+ role="assistant",
1422
+ content=parsed.askuser,
1423
+ )
1424
+ break
1425
+
1426
+ # Step 10: 执行工具调用(无论 finish 值如何,先执行工具)
1427
+ if not parsed.tools_to_call:
1428
+ # 无工具调用: 直接根据 finish 判断是否结束
1429
+ if parsed.finish:
1430
+ logger.info(f"[{task_id}] finish=true 且无工具调用,结束循环")
1431
+ before, after = extract_surrounding_text(llm_raw)
1432
+ final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "任务已完成。"
1433
+ context.working_memory["final_response"] = final_text
1434
+ await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1435
+ if self.memory:
1436
+ self.memory.add_short_term(
1437
+ session_id=context.session_id,
1438
+ role="assistant",
1439
+ content=final_text,
1440
+ )
1441
+ else:
1442
+ logger.info(f"[{task_id}] 无工具调用且 finish=false,结束")
1443
+ before, after = extract_surrounding_text(llm_raw)
1444
+ final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "处理完毕。"
1445
+ context.working_memory["final_response"] = final_text
1446
+ await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1447
+ if self.memory:
1448
+ self.memory.add_short_term(
1449
+ session_id=context.session_id,
1450
+ role="assistant",
1451
+ content=final_text,
1452
+ )
1453
+ break
1454
+
1455
+ # Step 11: 有工具调用 — 先执行所有工具,再根据 finish 决定回调
1456
+ need_callback = False
1457
+ tool_outputs_parts = []
1458
+
1459
+ for tool_info in parsed.tools_to_call:
1460
+ tool_name = tool_info.get("toolname", "").strip()
1461
+ before_call = tool_info.get("beforecalltext", "")
1462
+ parms = tool_info.get("parms", "")
1463
+ timeout = tool_info.get("timeout", 120)
1464
+ should_callback = tool_info.get("callback", True)
1465
+
1466
+ if not tool_name:
1467
+ continue
1468
+
1469
+ logger.info(f"[{task_id}] 执行工具: {tool_name} (timeout={timeout}s, callback={should_callback})")
1470
+
1471
+ # 发送 beforecalltext 作为显示文本
1472
+ if before_call:
1473
+ await self._emit_v2_event(
1474
+ "v2_reasoning",
1475
+ {"content": before_call},
1476
+ stream_callback,
1477
+ )
1478
+
1479
+ # 发送工具开始事件
1480
+ await self._emit_v2_event(
1481
+ "v2_tool_start",
1482
+ {"tool": {
1483
+ "toolname": tool_name,
1484
+ "parms": truncate_str(parms, 500),
1485
+ "timeout": timeout,
1486
+ "callback": should_callback,
1487
+ }},
1488
+ stream_callback,
1489
+ )
1490
+
1491
+ self._add_exec_event("tool_call", {
1492
+ "title": f"调用工具: {tool_name}",
1493
+ "tool_name": tool_name,
1494
+ "arguments": parms,
1495
+ })
1496
+
1497
+ tool_result = await self._execute_v2_tool(
1498
+ tool_name, parms, timeout, context, task_id
1499
+ )
1500
+
1501
+ # 发送工具结果事件
1502
+ await self._emit_v2_event(
1503
+ "v2_tool_result",
1504
+ {"tool": {"toolname": tool_name}, "result": {
1505
+ "success": tool_result.get("success", False),
1506
+ "output": truncate_str(tool_result.get("output", ""), 3000),
1507
+ "error": truncate_str(tool_result.get("error", ""), 1000),
1508
+ "timed_out": tool_result.get("timed_out", False),
1509
+ }},
1510
+ stream_callback,
1511
+ )
1512
+
1513
+ self._add_exec_event("tool_result", {
1514
+ "title": f"工具结果: {tool_name}",
1515
+ "tool_name": tool_name,
1516
+ "success": tool_result.get("success", False),
1517
+ "summary": truncate_str(str(tool_result.get("output", tool_result.get("error", ""))), 500),
1518
+ "result": tool_result,
1519
+ })
1520
+
1521
+ is_timeout = tool_result.get("timed_out", False)
1522
+ if is_timeout:
1523
+ need_callback = True
1524
+ logger.warning(f"[{task_id}] 工具 {tool_name} 超时 ({timeout}s)")
1525
+ elif should_callback:
1526
+ need_callback = True
1527
+
1528
+ output_str = tool_result.get("output", "") or tool_result.get("error", "")
1529
+ tool_outputs_parts.append(
1530
+ f"### {before_call}\n"
1531
+ f"**工具**: {tool_name}\n"
1532
+ f"**结果**: {'成功' if tool_result.get('success') else '失败'}\n"
1533
+ f"{truncate_str(output_str, 2000)}\n"
1534
+ )
1535
+
1536
+ conversation_history.append(Message(
1537
+ role="assistant",
1538
+ content=f"执行工具 {tool_name}:\n{truncate_str(output_str, 3000)}",
1539
+ ))
1540
+ conversation_history.append(Message(
1541
+ role="user",
1542
+ content=f"[工具 {tool_name} 执行完成] {'成功' if tool_result.get('success') else '失败'}",
1543
+ ))
1544
+
1545
+ all_tool_outputs = "\n".join(tool_outputs_parts)
1546
+
1547
+ # Step 12: 工具执行完毕后,根据 finish 标志决定是否回调 LLM
1548
+ # 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
1549
+ if parsed.finish:
1550
+ logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
1551
+ final_text = "已完成所有操作。"
1552
+ if current_task_plan:
1553
+ final_text += f"\n\n任务计划:\n{current_task_plan}"
1554
+ context.working_memory["final_response"] = final_text
1555
+ await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1556
+ if self.memory:
1557
+ self.memory.add_short_term(
1558
+ session_id=context.session_id,
1559
+ role="assistant",
1560
+ content=final_text,
1561
+ )
1562
+ break
1563
+
1564
+ # finish=false: 根据工具的 callback 标志决定是否回调
1565
+ if not need_callback:
1566
+ logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
1567
+ final_text = "已完成所有操作。"
1568
+ if current_task_plan:
1569
+ final_text += f"\n\n任务计划:\n{current_task_plan}"
1570
+ context.working_memory["final_response"] = final_text
1571
+ await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1572
+ if self.memory:
1573
+ self.memory.add_short_term(
1574
+ session_id=context.session_id,
1575
+ role="assistant",
1576
+ content=final_text,
1577
+ )
1578
+ break
1579
+
1580
+ logger.info(f"[{task_id}] finish=false 且 need_callback=true,回调 LLM...")
1581
+
1582
+ context.working_memory["iterations"] = self._iteration_count
1583
+ if current_task_plan:
1584
+ context.working_memory["task_plan"] = current_task_plan
1585
+
1586
+ logger.info(f"[{task_id}] V2 循环完成 (共 {self._iteration_count} 次迭代)")
1587
+ return context
1588
+
1589
+ async def _execute_v2_tool(
1590
+ self,
1591
+ tool_name: str,
1592
+ parms_str: str,
1593
+ timeout: int,
1594
+ context: AgentContext,
1595
+ task_id: str,
1596
+ ) -> Dict[str, Any]:
1597
+ """V2 工具执行"""
1598
+ result = {"success": False, "output": "", "error": ""}
1599
+
1600
+ try:
1601
+ import json as _json
1602
+ try:
1603
+ params = _json.loads(parms_str) if parms_str else {}
1604
+ except (_json.JSONDecodeError, TypeError):
1605
+ params = {"raw_input": parms_str}
1606
+
1607
+ if tool_name == "code" or tool_name.startswith("code_"):
1608
+ code_lang = params.get("language", "python")
1609
+ code_text = params.get("code", parms_str)
1610
+ if self.executor:
1611
+ exec_result = await self.executor.execute(
1612
+ language=code_lang,
1613
+ code=code_text,
1614
+ timeout=timeout,
1615
+ )
1616
+ result = exec_result.to_dict()
1617
+ else:
1618
+ result["error"] = "执行引擎未初始化"
1619
+
1620
+ elif tool_name == "command" or tool_name == "command_run":
1621
+ code_text = params.get("command", parms_str)
1622
+ if self.executor:
1623
+ exec_result = await self.executor.execute(
1624
+ language="shell",
1625
+ code=code_text,
1626
+ timeout=timeout,
1627
+ )
1628
+ result = exec_result.to_dict()
1629
+ else:
1630
+ result["error"] = "执行引擎未初始化"
1631
+
1632
+ elif self.skills:
1633
+ exec_result = await self.skills.execute(tool_name, **params)
1634
+ result = exec_result.to_dict()
1635
+ else:
1636
+ result["error"] = f"未知工具: {tool_name}"
1637
+
1638
+ except Exception as e:
1639
+ result["error"] = str(e)
1640
+ logger.error(f"[{task_id}] 工具 {tool_name} 异常: {e}")
1641
+
1642
+ return result