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.
- package/agents/main_agent.py +615 -1
- package/core/context_builder.py +459 -0
- package/core/deps_checker.py +0 -0
- package/core/llm.py +19 -0
- package/core/output_parser.py +415 -0
- package/install/uninstall.ps1 +0 -0
- package/install/uninstall.sh +0 -0
- package/main.py +6 -1
- package/memory/manager.py +1 -1
- package/package.json +1 -1
- package/skills/docx/LICENSE.txt +8 -25
- package/skills/docx/SKILL.md +158 -413
- package/skills/docx/scripts/add_toc_placeholders.py +609 -80
- package/skills/docx/scripts/document.py +55 -24
- package/skills/gui_skill.py +0 -0
- package/skills/pdf/LICENSE.txt +8 -25
- package/skills/pdf/SKILL.md +757 -1364
- package/skills/xlsx/LICENSE.txt +8 -25
- package/skills/xlsx/SKILL.md +163 -429
- package/web/api_server.py +96 -3
- package/web/ui/chat/chat.css +206 -0
- package/web/ui/chat/chat.js +1 -1
- package/web/ui/chat/chat_container.html +1 -1
- package/web/ui/chat/chat_main.js +16 -2
- package/web/ui/chat/flow_engine.js +179 -0
- package/web/ui/chat/groupchat.js +0 -0
- package/web/ui/chat/left_sessions.html +0 -0
- package/web/ui/chat/middle_chat.html +0 -0
- package/web/ui/chat/right_agents.html +0 -0
- package/web/ui/index.html +78 -12
- package/skills/pptx/LICENSE.txt +0 -30
- package/skills/pptx/SKILL.md +0 -507
- package/skills/pptx/html2pptx.md +0 -625
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
- package/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
- package/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
- package/skills/pptx/ooxml/schemas/mce/mc.xsd +0 -75
- package/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd +0 -560
- package/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd +0 -67
- package/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd +0 -14
- package/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +0 -20
- package/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +0 -13
- package/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
- package/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +0 -8
- package/skills/pptx/ooxml/scripts/pack.py +0 -159
- package/skills/pptx/ooxml/scripts/unpack.py +0 -29
- package/skills/pptx/ooxml/scripts/validate.py +0 -69
- package/skills/pptx/ooxml/scripts/validation/__init__.py +0 -15
- package/skills/pptx/ooxml/scripts/validation/base.py +0 -951
- package/skills/pptx/ooxml/scripts/validation/docx.py +0 -274
- package/skills/pptx/ooxml/scripts/validation/pptx.py +0 -315
- package/skills/pptx/ooxml/scripts/validation/redlining.py +0 -279
- package/skills/pptx/ooxml.md +0 -427
- package/skills/pptx/scripts/html2pptx.js +0 -1044
- package/skills/pptx/scripts/inventory.py +0 -1020
- package/skills/pptx/scripts/rearrange.py +0 -231
- package/skills/pptx/scripts/replace.py +0 -385
- package/skills/pptx/scripts/thumbnail.py +0 -450
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
package/agents/main_agent.py
CHANGED
|
@@ -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
|