myagent-ai 1.9.5 → 1.9.7
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 +154 -1001
- package/main.py +5 -0
- package/memory/manager.py +82 -16
- package/package.json +1 -1
- package/web/api_server.py +7 -2
- package/web/ui/chat/flow_engine.js +8 -3
package/agents/main_agent.py
CHANGED
|
@@ -5,15 +5,13 @@ agents/main_agent.py - 主 Agent
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
import json
|
|
9
8
|
import asyncio
|
|
10
9
|
from typing import Any, Callable, Dict, List, Optional
|
|
11
10
|
|
|
12
11
|
from core.logger import get_logger
|
|
13
12
|
from core.llm import LLMClient, LLMResponse, Message
|
|
14
13
|
from agents.base import BaseAgent, AgentContext
|
|
15
|
-
from core.utils import generate_id, timestamp,
|
|
16
|
-
from core.context_manager import ContextManager, ContextConfig, estimate_tokens
|
|
14
|
+
from core.utils import generate_id, timestamp, truncate_str
|
|
17
15
|
from core.context_builder import ContextBuilder
|
|
18
16
|
from core.output_parser import ParsedOutput, parse_output, validate_output, extract_surrounding_text
|
|
19
17
|
|
|
@@ -36,147 +34,10 @@ class MainAgent(BaseAgent):
|
|
|
36
34
|
name = "main_agent"
|
|
37
35
|
description = "AI助手主控Agent,负责理解用户意图、规划任务、调度执行"
|
|
38
36
|
|
|
39
|
-
SYSTEM_PROMPT = """你是 MyAgent - 一个强大的本地桌面AI助手。你拥有以下能力:
|
|
40
|
-
|
|
41
|
-
## 核心能力
|
|
42
|
-
1. **代码执行**: 可以运行 Python、Shell/Bash、PowerShell 代码
|
|
43
|
-
2. **文件操作**: 读取、写入、搜索、管理文件
|
|
44
|
-
3. **网络搜索**: 搜索互联网、读取网页
|
|
45
|
-
4. **系统操作**: 查询系统信息、管理进程
|
|
46
|
-
5. **浏览器操作**: 自动化浏览器(如已安装 Playwright)
|
|
47
|
-
6. **记忆系统**: 记住用户偏好、历史任务、避免重复犯错
|
|
48
|
-
|
|
49
|
-
## 工作方式(遵循智能体循环规范)
|
|
50
|
-
你必须严格按照 **思考→执行→观察→思考** 的循环模式工作:
|
|
51
|
-
1. **思考**: 分析当前状态,确定下一步要做什么,用自然语言说明你的思路
|
|
52
|
-
2. **执行**: 执行操作(一个命令或一个技能调用)
|
|
53
|
-
3. **观察**: 查看执行结果,分析是否成功
|
|
54
|
-
4. **继续思考**: 基于结果决定下一步,重复以上循环
|
|
55
|
-
|
|
56
|
-
**执行模式**:
|
|
57
|
-
- **step 模式**(默认):每次只执行一个 action,等待结果后再决定下一步。适用于:
|
|
58
|
-
- 后续操作依赖前一步结果(如先读文件再修改)
|
|
59
|
-
- 操作可能有副作用需要确认(如删除、写入)
|
|
60
|
-
- 不确定操作是否会成功
|
|
61
|
-
- **batch 模式**:一次执行多个互不依赖的 action。仅适用于:
|
|
62
|
-
- 所有操作之间完全独立,不需要前一步的结果
|
|
63
|
-
- 都是简单的只读操作(如读取多个文件、查看多个系统信息)
|
|
64
|
-
- 你有很高信心所有操作都会成功
|
|
65
|
-
|
|
66
|
-
**关键原则**:
|
|
67
|
-
- 默认使用 step 模式(安全优先)
|
|
68
|
-
- 只有当你确信多个操作互不依赖时才用 batch 模式
|
|
69
|
-
- 每个操作前,用 thought 字段说明你为什么要执行这一步
|
|
70
|
-
- 如果前一步失败了,先分析原因,再尝试修复或换一种方法
|
|
71
|
-
- 不要猜测结果,始终基于实际执行结果来判断
|
|
72
|
-
|
|
73
|
-
## ⏰ 超时控制规则(强制要求)
|
|
74
|
-
对于每个需要执行的命令(action type="code"),你**必须**在 action 中包含 "timeout_seconds" 字段,
|
|
75
|
-
指定该命令的预估最大执行超时时间(秒)。这是一项强制要求,不能省略。
|
|
76
|
-
|
|
77
|
-
超时时间设定指南:
|
|
78
|
-
- 简单打印/计算: 10-30秒
|
|
79
|
-
- 文件读写/搜索: 30-60秒
|
|
80
|
-
- 网络请求/API调用: 60-120秒
|
|
81
|
-
- 数据处理/批量操作: 120-300秒
|
|
82
|
-
- 大规模编译/安装: 300-600秒
|
|
83
|
-
- 如果不确定,默认设为 120秒
|
|
84
|
-
|
|
85
|
-
**如果命令执行超时**: 系统会自动将超时信息反馈给你,包含命令已产生的部分输出。
|
|
86
|
-
你需要分析超时原因并给出改进方案(如优化算法、添加超时参数、分批处理等)。
|
|
87
|
-
|
|
88
|
-
## 输出格式(重要:流式友好)
|
|
89
|
-
|
|
90
|
-
**核心原则:先用纯文本说明你的思路,只在执行操作时才输出 JSON。**
|
|
91
|
-
|
|
92
|
-
### 情况 1:需要执行操作
|
|
93
|
-
先用纯文本/markdown简要说明你的分析和计划(用户会实时看到),然后用 JSON 输出操作指令:
|
|
94
|
-
|
|
95
|
-
让我先检查一下文件内容...
|
|
96
|
-
|
|
97
|
-
```action
|
|
98
|
-
{"thought": "需要查看文件内容", "mode": "step", "actions": [{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}}]}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 情况 2:不需要执行操作
|
|
102
|
-
直接用纯文本/markdown回复即可,不需要任何 JSON。
|
|
103
|
-
|
|
104
|
-
### 情况 3:执行完操作后的总结
|
|
105
|
-
操作完成后,直接用纯文本/markdown总结结果,不要再用 JSON。
|
|
106
|
-
|
|
107
|
-
### JSON 操作指令格式
|
|
108
|
-
将 JSON 放在 ```action``` 代码块中,系统会自动识别并执行:
|
|
109
|
-
|
|
110
|
-
**step 模式**(默认,逐步执行):
|
|
111
|
-
```action
|
|
112
|
-
{"thought": "说明你当前的分析和下一步计划", "mode": "step", "actions": [{"type": "code", "language": "python", "code": "代码", "timeout_seconds": 60}]}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
**batch 模式**(多个独立操作):
|
|
116
|
-
```action
|
|
117
|
-
{"thought": "说明为什么可以批量执行", "mode": "batch", "actions": [{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}}, {"type": "skill", "name": "file_read", "params": {"path": "/b.txt"}}]}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
action type="code" 必须包含 "timeout_seconds" 字段。action type="skill" 或 "memory" 不需要此字段。
|
|
121
|
-
省略 mode 字段时默认为 "step"。
|
|
122
|
-
|
|
123
|
-
**注意**:你也可以直接输出裸 JSON(不用 ```action``` 包裹),系统同样能识别。但推荐使用 ```action``` 格式,这样你的分析文本能实时流式展示给用户。
|
|
124
|
-
|
|
125
|
-
## 执行模式任务追踪(重要:强制 JSON 格式)
|
|
126
|
-
|
|
127
|
-
当你处于**执行模式**(Execution Mode)时,你**必须**在每次回复中包含一个 JSON 格式的任务进度列表。
|
|
128
|
-
这个 JSON 必须放在 ```tasklist``` 代码块中,系统会自动解析并实时展示给用户。
|
|
129
|
-
|
|
130
|
-
格式要求:
|
|
131
|
-
```tasklist
|
|
132
|
-
[
|
|
133
|
-
{"text": "分析需求", "status": "done"},
|
|
134
|
-
{"text": "检查文件结构", "status": "done"},
|
|
135
|
-
{"text": "修改代码", "status": "running"},
|
|
136
|
-
{"text": "测试验证", "status": "pending"}
|
|
137
|
-
]
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
status 取值:
|
|
141
|
-
- `"pending"` — 待执行
|
|
142
|
-
- `"running"` — 正在执行(当前步骤,每次只能有一个)
|
|
143
|
-
- `"done"` — 已完成
|
|
144
|
-
- `"blocked"` — 被阻塞(等待依赖或其他原因)
|
|
145
|
-
|
|
146
|
-
规则:
|
|
147
|
-
1. 每次回复开头先用纯文本简要分析当前状态(这段文本会实时流式展示给用户)
|
|
148
|
-
2. 然后输出 ```tasklist``` 代码块更新任务进度
|
|
149
|
-
3. 如果需要执行操作,在 tasklist 之后输出 ```action``` 代码块
|
|
150
|
-
4. 如果不需要执行操作(纯文本回复),在 tasklist 之后直接写总结
|
|
151
|
-
5. 如果是简单任务(一步完成),也必须输出任务列表(只有一个条目)
|
|
152
|
-
6. 每次只将一个任务标记为 `running`,其余为 `pending` 或 `done`
|
|
153
|
-
7. 不要手动输出 markdown 格式的任务列表,只用 JSON 格式
|
|
154
|
-
|
|
155
|
-
回复结构示例:
|
|
156
|
-
```
|
|
157
|
-
让我先检查一下文件内容...
|
|
158
|
-
|
|
159
|
-
```tasklist
|
|
160
|
-
[{"text": "检查文件内容", "status": "running"}, {"text": "根据结果修改", "status": "pending"}]
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
```action
|
|
164
|
-
{"thought": "需要查看文件", "mode": "step", "actions": [{"type": "skill", "name": "file_read", "params": {"path": "/a.txt"}}]}
|
|
165
|
-
```
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## 重要规则
|
|
169
|
-
- 优先使用技能系统完成操作,而不是直接写代码
|
|
170
|
-
- 执行危险操作前先警告用户
|
|
171
|
-
- 保持回复简洁明了
|
|
172
|
-
- 用中文回复
|
|
173
|
-
- 绝对不要在回复开头进行自我介绍
|
|
174
|
-
- 不要重复问候"""
|
|
175
|
-
|
|
176
37
|
# =========================================================================
|
|
177
|
-
#
|
|
38
|
+
# 系统提示词 — 结构化输出格式
|
|
178
39
|
# =========================================================================
|
|
179
|
-
|
|
40
|
+
SYSTEM_PROMPT = """你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:
|
|
180
41
|
|
|
181
42
|
严格格式化输出以下内容:
|
|
182
43
|
<output>
|
|
@@ -219,12 +80,7 @@ status 取值:
|
|
|
219
80
|
self._iteration_count = 0
|
|
220
81
|
self._current_task_id: str = ""
|
|
221
82
|
self._registered_task: bool = False
|
|
222
|
-
#
|
|
223
|
-
self._org_context_cache: str = ""
|
|
224
|
-
self._org_context_mtime: float = 0 # 文件修改时间,用于检测变更
|
|
225
|
-
# Token 上下文管理器 (滚动摘要 + 预算控制)
|
|
226
|
-
self.context_manager = ContextManager(ContextConfig())
|
|
227
|
-
# V2 Context Builder (结构化上下文构建)
|
|
83
|
+
# Context Builder (结构化上下文构建)
|
|
228
84
|
self.context_builder: Optional[ContextBuilder] = None
|
|
229
85
|
# 执行事件追踪(用于前端展示命令执行过程)
|
|
230
86
|
self._execution_events: List[Dict] = []
|
|
@@ -233,13 +89,13 @@ status 取值:
|
|
|
233
89
|
self.active_contexts: Dict[str, AgentContext] = {}
|
|
234
90
|
|
|
235
91
|
def init_context_builder(self, memory_manager=None, skill_registry=None, knowledge_base_dir=None):
|
|
236
|
-
"""初始化
|
|
92
|
+
"""初始化 Context Builder(在系统启动后调用,注入依赖)"""
|
|
237
93
|
self.context_builder = ContextBuilder(
|
|
238
94
|
memory_manager=memory_manager,
|
|
239
95
|
skill_registry=skill_registry,
|
|
240
96
|
knowledge_base_dir=knowledge_base_dir,
|
|
241
97
|
)
|
|
242
|
-
logger.info("
|
|
98
|
+
logger.info("Context Builder 已初始化" + (f" (知识库: {knowledge_base_dir})" if knowledge_base_dir else ""))
|
|
243
99
|
|
|
244
100
|
def _add_exec_event(self, event_type: str, data: Dict):
|
|
245
101
|
"""记录一个执行事件(供前端展示)"""
|
|
@@ -293,11 +149,7 @@ status 取值:
|
|
|
293
149
|
logger.info(f"[{task_id}] 开始处理用户请求: {context.user_message[:100]}")
|
|
294
150
|
|
|
295
151
|
try:
|
|
296
|
-
|
|
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)
|
|
300
|
-
return await self._process_inner(context, task_id)
|
|
152
|
+
return await self.process_v2(context)
|
|
301
153
|
finally:
|
|
302
154
|
# 移除活跃上下文
|
|
303
155
|
self.active_contexts.pop(context.session_id, None)
|
|
@@ -306,838 +158,98 @@ status 取值:
|
|
|
306
158
|
self.config_broadcaster.unregister(task_id)
|
|
307
159
|
self._registered_task = False
|
|
308
160
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
# Step 1: 加载相关记忆
|
|
313
|
-
if self.memory_agent:
|
|
314
|
-
mem_context = AgentContext(
|
|
315
|
-
task_id=task_id,
|
|
316
|
-
session_id=context.session_id,
|
|
317
|
-
user_message=context.user_message,
|
|
318
|
-
metadata={"memory_action": "get_relevant"},
|
|
319
|
-
)
|
|
320
|
-
await self.memory_agent.process(mem_context)
|
|
321
|
-
if "memory_context_prompt" in mem_context.working_memory:
|
|
322
|
-
context.working_memory["memory_context_prompt"] = \
|
|
323
|
-
mem_context.working_memory["memory_context_prompt"]
|
|
324
|
-
|
|
325
|
-
# Step 2: 保存用户消息到短期记忆
|
|
326
|
-
if self.memory:
|
|
327
|
-
self.memory.add_short_term(
|
|
328
|
-
session_id=context.session_id,
|
|
329
|
-
role="user",
|
|
330
|
-
content=context.user_message,
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
# Step 3: 主循环(计划-执行-反思)
|
|
334
|
-
final_response = ""
|
|
335
|
-
max_iter = self.config.agent.max_iterations
|
|
336
|
-
|
|
337
|
-
while self._iteration_count < max_iter:
|
|
338
|
-
self._iteration_count += 1
|
|
339
|
-
logger.debug(f"[{task_id}] 迭代 {self._iteration_count}/{max_iter}")
|
|
340
|
-
|
|
341
|
-
# 检查配置热加载广播(每次迭代前检查)
|
|
342
|
-
if self.config_broadcaster:
|
|
343
|
-
reloaded, reload_type = await self.config_broadcaster.check_and_wait(task_id)
|
|
344
|
-
if reloaded:
|
|
345
|
-
logger.info(f"[{task_id}] 🔄 {reload_type}已热更新,继续执行 (iteration={self._iteration_count})")
|
|
346
|
-
# 更新迭代检查点
|
|
347
|
-
if self.config_broadcaster._active_tasks.get(task_id):
|
|
348
|
-
self.config_broadcaster._active_tasks[task_id].iteration = self._iteration_count
|
|
349
|
-
# 代码热更新后,LLM 客户端可能已被刷新,下次调用会自动重建
|
|
350
|
-
if reload_type == "code":
|
|
351
|
-
logger.info(f"[{task_id}] 代码模块已热更新,LLM 客户端将在下次调用时自动重建")
|
|
352
|
-
|
|
353
|
-
# ── 检查并处理注入的消息 ──
|
|
354
|
-
if context.pending_injected_messages:
|
|
355
|
-
injected = context.pending_injected_messages.copy()
|
|
356
|
-
context.pending_injected_messages.clear()
|
|
357
|
-
for msg_text in injected:
|
|
358
|
-
logger.info(f"[{task_id}] 注入消息到对话历史: {msg_text[:50]}...")
|
|
359
|
-
# 如果不是第一轮,且历史最后一条不是 user 消息,则注入
|
|
360
|
-
context.conversation_history.append(
|
|
361
|
-
Message(role="user", content=f"[用户中断/补充]: {msg_text}")
|
|
362
|
-
)
|
|
363
|
-
# 同时也保存到短期记忆
|
|
364
|
-
if self.memory:
|
|
365
|
-
self.memory.add_short_term(
|
|
366
|
-
session_id=context.session_id,
|
|
367
|
-
role="user",
|
|
368
|
-
content=f"[中断补充]: {msg_text}",
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
# 构建消息列表
|
|
372
|
-
messages = self._build_messages(context)
|
|
373
|
-
|
|
374
|
-
# 添加可用工具
|
|
375
|
-
tools = self._get_tools()
|
|
376
|
-
|
|
377
|
-
# 调用 LLM
|
|
378
|
-
response = await self._call_llm(messages, tools=tools)
|
|
379
|
-
|
|
380
|
-
if not response.success:
|
|
381
|
-
final_response = f"⚠️ LLM 调用失败: {response.error}"
|
|
382
|
-
break
|
|
383
|
-
|
|
384
|
-
# 解析响应
|
|
385
|
-
content = response.content
|
|
386
|
-
|
|
387
|
-
# 检查是否有工具调用 (OpenAI function calling)
|
|
388
|
-
if response.tool_calls:
|
|
389
|
-
# 先将 assistant 的 tool_calls 消息加入历史(OpenAI 格式要求)
|
|
390
|
-
context.conversation_history.append(
|
|
391
|
-
Message(role="assistant",
|
|
392
|
-
content=response.content or "",
|
|
393
|
-
tool_calls=response.tool_calls)
|
|
394
|
-
)
|
|
395
|
-
# 记录工具调用事件
|
|
396
|
-
for tc in response.tool_calls:
|
|
397
|
-
self._add_exec_event("tool_call", {
|
|
398
|
-
"title": f"调用工具: {tc['name']}",
|
|
399
|
-
"tool_name": tc["name"],
|
|
400
|
-
"arguments": tc.get("arguments", {}),
|
|
401
|
-
})
|
|
402
|
-
# 执行工具调用
|
|
403
|
-
tool_results = await self._handle_tool_calls(
|
|
404
|
-
response.tool_calls, context, task_id
|
|
405
|
-
)
|
|
406
|
-
# 记录工具结果事件
|
|
407
|
-
for tc, result in tool_results:
|
|
408
|
-
self._add_exec_event("tool_result", {
|
|
409
|
-
"title": f"工具结果: {tc['name']}",
|
|
410
|
-
"tool_name": tc["name"],
|
|
411
|
-
"success": result.get("success", False),
|
|
412
|
-
"summary": truncate_str(result.get("output", result.get("error", "")), 500),
|
|
413
|
-
"result": result,
|
|
414
|
-
})
|
|
415
|
-
# 再将工具结果加入消息历史
|
|
416
|
-
for tc, result in tool_results:
|
|
417
|
-
context.conversation_history.append(
|
|
418
|
-
Message(role="tool", content=json.dumps(result, ensure_ascii=False),
|
|
419
|
-
tool_call_id=tc["id"], name=tc["name"])
|
|
420
|
-
)
|
|
421
|
-
# 继续循环,让 LLM 处理工具结果
|
|
422
|
-
continue
|
|
423
|
-
|
|
424
|
-
# 尝试解析 JSON 操作指令
|
|
425
|
-
action_data = safe_json_parse(content)
|
|
426
|
-
|
|
427
|
-
if action_data and isinstance(action_data, dict):
|
|
428
|
-
# 有结构化的操作指令
|
|
429
|
-
if "actions" in action_data:
|
|
430
|
-
action_mode = action_data.get("mode", "step")
|
|
431
|
-
|
|
432
|
-
# step 模式安全保护:只执行第一个 action
|
|
433
|
-
if action_mode == "step" and len(action_data.get("actions", [])) > 1:
|
|
434
|
-
action_data = {
|
|
435
|
-
"thought": action_data.get("thought", ""),
|
|
436
|
-
"mode": "step",
|
|
437
|
-
"actions": [action_data["actions"][0]],
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
# 执行操作列表
|
|
441
|
-
results = await self._execute_actions(
|
|
442
|
-
action_data, context, task_id
|
|
443
|
-
)
|
|
444
|
-
# 将结果反馈给 LLM
|
|
445
|
-
context.conversation_history.append(
|
|
446
|
-
Message(role="assistant", content=content)
|
|
447
|
-
)
|
|
448
|
-
result_summary = self._summarize_action_results(results)
|
|
449
|
-
|
|
450
|
-
# 如果有超时诊断,追加详细信息供 LLM 分析
|
|
451
|
-
has_timeout = any(r.get("timed_out") for r in results)
|
|
452
|
-
timeout_detail = ""
|
|
453
|
-
if has_timeout:
|
|
454
|
-
timeout_details = []
|
|
455
|
-
for i, r in enumerate(results, 1):
|
|
456
|
-
if r.get("timed_out"):
|
|
457
|
-
diag = r.get("timeout_diagnosis", {})
|
|
458
|
-
timeout_details.append(
|
|
459
|
-
f"### 命令 {i} 超时诊断\n"
|
|
460
|
-
f"- 原因: {diag.get('diagnosis', '未知')}\n"
|
|
461
|
-
f"- 进展: {diag.get('progress', '未知')}\n"
|
|
462
|
-
f"- 是否建议重试: {'是' if diag.get('should_retry') else '否'}\n"
|
|
463
|
-
f"- 重试策略: {diag.get('retry_strategy', '无')}\n"
|
|
464
|
-
f"- 改进建议:\n"
|
|
465
|
-
)
|
|
466
|
-
for s in diag.get("suggestions", []):
|
|
467
|
-
timeout_details.append(f" - {s}")
|
|
468
|
-
timeout_detail = "\n\n## ⏰ 超时诊断详情\n" + "\n".join(timeout_details)
|
|
469
|
-
|
|
470
|
-
feedback_msg = f"[执行结果]\n{result_summary}\n\n请基于以上结果继续。"
|
|
471
|
-
if timeout_detail:
|
|
472
|
-
feedback_msg += timeout_detail + "\n\n请根据以上诊断信息决定下一步操作。"
|
|
473
|
-
|
|
474
|
-
context.conversation_history.append(
|
|
475
|
-
Message(role="user", content=feedback_msg)
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
# 退出循环判断:step 模式始终继续让 LLM 决定,batch 模式全部成功则退出
|
|
479
|
-
all_success = all(r.get("success", False) for r in results)
|
|
480
|
-
if action_mode == "batch" and all_success and results:
|
|
481
|
-
final_response = action_data.get("thought", "")
|
|
482
|
-
if "plan" in action_data and action_data["plan"]:
|
|
483
|
-
final_response += "\n\n已完成: " + " → ".join(action_data["plan"])
|
|
484
|
-
break
|
|
485
|
-
|
|
486
|
-
# 如果有超时且诊断建议不重试,强制中断循环避免无效重试
|
|
487
|
-
if has_timeout:
|
|
488
|
-
should_abort = False
|
|
489
|
-
abort_reasons = []
|
|
490
|
-
for i, r in enumerate(results, 1):
|
|
491
|
-
if r.get("timed_out"):
|
|
492
|
-
diag = r.get("timeout_diagnosis", {})
|
|
493
|
-
if diag.get("should_retry") is False:
|
|
494
|
-
should_abort = True
|
|
495
|
-
abort_reasons.append(
|
|
496
|
-
f"命令{i}: {diag.get('diagnosis', '不可恢复的超时')}"
|
|
497
|
-
)
|
|
498
|
-
if should_abort:
|
|
499
|
-
logger.warning(
|
|
500
|
-
f"[{task_id}] ⛔ 超时诊断建议不重试,终止当前执行循环 "
|
|
501
|
-
f"(iteration={self._iteration_count})"
|
|
502
|
-
)
|
|
503
|
-
# 构建诊断反馈,强制LLM输出final_answer
|
|
504
|
-
abort_msg = (
|
|
505
|
-
"[系统通知] 以下命令因超时被终止,且超时诊断结果表明不应重试:\n"
|
|
506
|
-
)
|
|
507
|
-
for reason in abort_reasons:
|
|
508
|
-
abort_msg += f"- {reason}\n"
|
|
509
|
-
abort_msg += (
|
|
510
|
-
"\n请直接以纯文本或 {\"type\": \"final_answer\", \"content\": \"...\"} "
|
|
511
|
-
"格式回复,告知用户任务无法完成的原因和建议的替代方案。"
|
|
512
|
-
"不要再尝试执行相同的命令。"
|
|
513
|
-
)
|
|
514
|
-
context.conversation_history.append(
|
|
515
|
-
Message(role="user", content=abort_msg)
|
|
516
|
-
)
|
|
517
|
-
continue # 让LLM生成final_answer,而不是重试命令
|
|
518
|
-
|
|
519
|
-
continue
|
|
520
|
-
|
|
521
|
-
# 单个 action
|
|
522
|
-
if action_data.get("type") == "final_answer":
|
|
523
|
-
final_response = action_data.get("content", content)
|
|
524
|
-
break
|
|
525
|
-
|
|
526
|
-
# 纯文本回复(没有工具调用)
|
|
527
|
-
final_response = content
|
|
528
|
-
break
|
|
529
|
-
|
|
530
|
-
# 保存助手回复到短期记忆
|
|
531
|
-
if self.memory and final_response:
|
|
532
|
-
self.memory.add_short_term(
|
|
533
|
-
session_id=context.session_id,
|
|
534
|
-
role="assistant",
|
|
535
|
-
content=final_response,
|
|
536
|
-
)
|
|
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
|
-
|
|
576
|
-
# 清理工作记忆
|
|
577
|
-
context.working_memory["final_response"] = final_response
|
|
578
|
-
context.working_memory["iterations"] = self._iteration_count
|
|
579
|
-
|
|
580
|
-
logger.info(f"[{task_id}] 处理完成 (迭代 {self._iteration_count} 次)")
|
|
581
|
-
|
|
582
|
-
# 检查是否需要增量总结对话 (使用滚动摘要,不再清空短期记忆)
|
|
583
|
-
if self.memory_agent and self._iteration_count > 5:
|
|
584
|
-
try:
|
|
585
|
-
summary_context = AgentContext(
|
|
586
|
-
session_id=context.session_id,
|
|
587
|
-
metadata={"memory_action": "summarize"},
|
|
588
|
-
)
|
|
589
|
-
await self.memory_agent.process(summary_context)
|
|
590
|
-
# 将生成的摘要更新到 ContextManager 的滚动摘要缓冲
|
|
591
|
-
summary_result = summary_context.working_memory.get("summary_result", "")
|
|
592
|
-
if summary_result:
|
|
593
|
-
self.context_manager.summary_buffer.update_summary(
|
|
594
|
-
session_id=context.session_id,
|
|
595
|
-
new_summary=summary_result,
|
|
596
|
-
summarized_count=summary_context.working_memory.get("summarized_count", 0),
|
|
597
|
-
)
|
|
598
|
-
logger.info(
|
|
599
|
-
f"[{task_id}] 滚动摘要已更新 (session={context.session_id})"
|
|
600
|
-
)
|
|
601
|
-
except Exception as e:
|
|
602
|
-
logger.warning(f"对话总结失败: {e}")
|
|
603
|
-
|
|
604
|
-
return context
|
|
605
|
-
|
|
606
|
-
def _build_messages(self, context: AgentContext) -> List[Message]:
|
|
607
|
-
"""构建完整的消息列表 (使用 ContextManager 进行 Token 预算优化)"""
|
|
608
|
-
# 合并系统提示 + 组织上下文
|
|
609
|
-
system_prompt = self.SYSTEM_PROMPT
|
|
610
|
-
org_context = self._build_org_context(context)
|
|
611
|
-
if org_context:
|
|
612
|
-
system_prompt += "\n\n" + org_context
|
|
613
|
-
|
|
614
|
-
# 注入 Agent 专属 system_prompt(如果有覆盖)
|
|
615
|
-
agent_prompt = context.metadata.get("agent_override_prompt") if context.metadata else None
|
|
616
|
-
agent_path = context.metadata.get("agent_override_path") if context.metadata else None
|
|
617
|
-
if agent_prompt:
|
|
618
|
-
prefix = f"## 当前角色\n你当前正在扮演「{agent_path}」Agent。请严格遵循以下角色设定:\n\n"
|
|
619
|
-
system_prompt += "\n\n" + prefix + agent_prompt
|
|
620
|
-
|
|
621
|
-
# 通用规则:禁止重复自我介绍(避免每次对话都问好)
|
|
622
|
-
system_prompt += "\n\n## 对话规则\n- 绝对不要在回复开头进行自我介绍(如'你好,我是XXX'),直接回答用户的问题或执行任务\n- 不要重复问候,除非用户主动打招呼"
|
|
623
|
-
|
|
624
|
-
# 执行模式:强调主动执行能力
|
|
625
|
-
chat_mode = (context.metadata.get("chat_mode") or '') if context.metadata else ''
|
|
626
|
-
if chat_mode == 'exec':
|
|
627
|
-
system_prompt += "\n\n## 执行模式 (当前激活)\n你当前处于执行模式,请务必主动使用可用工具执行操作,而不是只提供建议或反问用户。\n- 默认逐步执行(step 模式),每次一个操作,等待结果再决定下一步\n- 仅当多个操作完全独立时使用 batch 模式\n- 优先使用技能系统(skill)完成任务\n- 需要执行代码时,直接使用 code action 执行\n- 遇到不确定的操作,先尝试执行,失败后再调整\n- 不要反复询问用户是否要执行,直接执行并报告结果\n- 每步操作前先用 thought 说明你的计划"
|
|
628
|
-
|
|
629
|
-
# 记忆上下文
|
|
630
|
-
memory_ctx = context.working_memory.get("memory_context_prompt", "")
|
|
631
|
-
|
|
632
|
-
# 使用 ContextManager 构建优化后的消息列表
|
|
633
|
-
raw_messages = self.context_manager.build_context(
|
|
634
|
-
system_prompt=system_prompt,
|
|
635
|
-
memory_context=memory_ctx,
|
|
636
|
-
conversation_history=context.conversation_history,
|
|
637
|
-
user_message=context.user_message,
|
|
638
|
-
session_id=context.session_id,
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
# 转换回 Message 对象 (ContextManager 返回 dict)
|
|
642
|
-
messages = []
|
|
643
|
-
for m in raw_messages:
|
|
644
|
-
if isinstance(m, dict):
|
|
645
|
-
messages.append(Message(**m))
|
|
646
|
-
elif isinstance(m, Message):
|
|
647
|
-
messages.append(m)
|
|
648
|
-
else:
|
|
649
|
-
messages.append(m)
|
|
650
|
-
|
|
651
|
-
return messages
|
|
652
|
-
|
|
653
|
-
def _get_tools(self) -> Optional[List[Dict]]:
|
|
654
|
-
"""获取可用工具列表(OpenAI function calling 格式)"""
|
|
655
|
-
if not self.skills:
|
|
656
|
-
return None
|
|
657
|
-
try:
|
|
658
|
-
return self.skills.get_all_schemas()
|
|
659
|
-
except Exception as e:
|
|
660
|
-
logger.warning(f"获取工具列表失败: {e}")
|
|
661
|
-
return None
|
|
662
|
-
|
|
663
|
-
@staticmethod
|
|
664
|
-
def _get_skill_permission(skill_name: str) -> Optional[str]:
|
|
665
|
-
"""
|
|
666
|
-
根据技能名称映射到对应的权限项。
|
|
667
|
-
|
|
668
|
-
Returns:
|
|
669
|
-
权限项名称,或 None(不需要特殊权限的技能)
|
|
670
|
-
"""
|
|
671
|
-
# 文件技能
|
|
672
|
-
if skill_name in ("file_write", "file_delete", "file_move"):
|
|
673
|
-
return "file_write"
|
|
674
|
-
if skill_name in ("file_read", "file_list", "file_search"):
|
|
675
|
-
return "file_read"
|
|
676
|
-
# 网络/搜索技能
|
|
677
|
-
if skill_name in ("web_search", "web_read", "url_read"):
|
|
678
|
-
return "network"
|
|
679
|
-
# 系统技能涉及执行
|
|
680
|
-
if skill_name in ("command_run",):
|
|
681
|
-
return "execution"
|
|
682
|
-
# 浏览器技能需要网络
|
|
683
|
-
if skill_name in ("browser_open", "browser_click", "browser_fill"):
|
|
684
|
-
return "network"
|
|
685
|
-
# 浏览器扩展技能 - 截图/JS执行/导航/关闭
|
|
686
|
-
if skill_name in ("browser_screenshot", "browser_eval", "browser_navigate", "browser_close"):
|
|
687
|
-
return "network"
|
|
688
|
-
# 桌面 GUI 自动化技能 - 涉及系统级执行权限
|
|
689
|
-
if skill_name in ("screenshot", "mouse_click", "mouse_drag", "type_text",
|
|
690
|
-
"hotkey", "window_list", "window_focus", "screen_element"):
|
|
691
|
-
return "execution"
|
|
692
|
-
# 其他技能不需要特殊权限检查
|
|
693
|
-
return None
|
|
694
|
-
|
|
695
|
-
async def _handle_tool_calls(
|
|
696
|
-
self,
|
|
697
|
-
tool_calls: List[Dict],
|
|
698
|
-
context: AgentContext,
|
|
699
|
-
task_id: str,
|
|
700
|
-
) -> List[tuple]:
|
|
701
|
-
"""处理 OpenAI function calling 工具调用"""
|
|
702
|
-
results = []
|
|
703
|
-
for tc in tool_calls:
|
|
704
|
-
name = tc["name"]
|
|
705
|
-
args = tc.get("arguments", {})
|
|
706
|
-
logger.info(f"[{task_id}] 调用工具: {name}({args})")
|
|
161
|
+
# =========================================================================
|
|
162
|
+
# 执行循环 — 结构化输出 + Context Builder + Output Parser
|
|
163
|
+
# =========================================================================
|
|
707
164
|
|
|
165
|
+
async def _emit_v2_event(self, event_type: str, data: Dict, stream_callback: Optional[Callable] = None):
|
|
166
|
+
"""发送 V2 SSE 事件。如果 stream_callback 不存在则仅记录日志。"""
|
|
167
|
+
event = {"type": event_type, **data}
|
|
168
|
+
if stream_callback is not None:
|
|
708
169
|
try:
|
|
709
|
-
if
|
|
710
|
-
|
|
711
|
-
result_dict = result.to_dict()
|
|
170
|
+
if asyncio.iscoroutinefunction(stream_callback):
|
|
171
|
+
await stream_callback(event)
|
|
712
172
|
else:
|
|
713
|
-
|
|
173
|
+
stream_callback(event)
|
|
714
174
|
except Exception as e:
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
results.append((tc, result_dict))
|
|
718
|
-
|
|
719
|
-
# 记录错误模式
|
|
720
|
-
if not result_dict.get("success") and self.memory:
|
|
721
|
-
self.memory.record_error_pattern(
|
|
722
|
-
error=f"工具 {name} 失败: {result_dict.get('error', '')}",
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
return results
|
|
726
|
-
|
|
727
|
-
async def _execute_actions(
|
|
728
|
-
self,
|
|
729
|
-
action_data: Dict,
|
|
730
|
-
context: AgentContext,
|
|
731
|
-
task_id: str,
|
|
732
|
-
) -> List[Dict]:
|
|
733
|
-
"""执行操作列表"""
|
|
734
|
-
results = []
|
|
735
|
-
actions = action_data.get("actions", [])
|
|
736
|
-
|
|
737
|
-
for action in actions:
|
|
738
|
-
action_type = action.get("type", "")
|
|
739
|
-
|
|
740
|
-
if action_type == "skill" and self.skills:
|
|
741
|
-
# 权限检查: 根据技能类别检查权限
|
|
742
|
-
skill_name = action.get("name", "")
|
|
743
|
-
skill_perm = self._get_skill_permission(skill_name)
|
|
744
|
-
if skill_perm and not self.check_permission(skill_perm):
|
|
745
|
-
from core.permissions import PermissionManager
|
|
746
|
-
label = PermissionManager.PERMISSION_LABELS.get(skill_perm, skill_perm)
|
|
747
|
-
results.append({
|
|
748
|
-
"success": False,
|
|
749
|
-
"error": f"[权限] 当前 Agent 没有'{label}'权限,操作被拒绝",
|
|
750
|
-
"metadata": {"permission_denied": skill_perm},
|
|
751
|
-
})
|
|
752
|
-
self._add_exec_event("skill_call", {
|
|
753
|
-
"title": f"技能调用被拒: {skill_name}",
|
|
754
|
-
"skill_name": skill_name,
|
|
755
|
-
"success": False,
|
|
756
|
-
"error": f"权限不足: {label}",
|
|
757
|
-
})
|
|
758
|
-
continue
|
|
759
|
-
|
|
760
|
-
# 记录技能调用事件
|
|
761
|
-
self._add_exec_event("skill_call", {
|
|
762
|
-
"title": f"调用技能: {skill_name}",
|
|
763
|
-
"skill_name": skill_name,
|
|
764
|
-
"params": action.get("params", {}),
|
|
765
|
-
})
|
|
766
|
-
result = await self.skills.execute(
|
|
767
|
-
action.get("name", ""),
|
|
768
|
-
**action.get("params", {}),
|
|
769
|
-
)
|
|
770
|
-
result_dict = result.to_dict()
|
|
771
|
-
results.append(result_dict)
|
|
772
|
-
# 记录技能结果事件
|
|
773
|
-
self._add_exec_event("skill_result", {
|
|
774
|
-
"title": f"技能结果: {skill_name}",
|
|
775
|
-
"skill_name": skill_name,
|
|
776
|
-
"success": result_dict.get("success", False),
|
|
777
|
-
"summary": truncate_str(str(result_dict.get("output", result_dict.get("error", ""))), 500),
|
|
778
|
-
})
|
|
779
|
-
|
|
780
|
-
elif action_type == "code" and self.executor:
|
|
781
|
-
# 权限检查: execution
|
|
782
|
-
if not self.check_permission("execution"):
|
|
783
|
-
results.append({
|
|
784
|
-
"success": False,
|
|
785
|
-
"error": "[权限] 当前 Agent 没有代码执行权限,操作被拒绝",
|
|
786
|
-
"metadata": {"permission_denied": "execution"},
|
|
787
|
-
})
|
|
788
|
-
self._add_exec_event("code_exec", {
|
|
789
|
-
"title": f"代码执行被拒",
|
|
790
|
-
"language": action.get("language", "unknown"),
|
|
791
|
-
"code_preview": truncate_str(action.get("code", ""), 200),
|
|
792
|
-
"success": False,
|
|
793
|
-
"error": "代码执行权限不足",
|
|
794
|
-
})
|
|
795
|
-
continue
|
|
796
|
-
|
|
797
|
-
code_lang = action.get("language", "python")
|
|
798
|
-
code_text = action.get("code", "")
|
|
799
|
-
|
|
800
|
-
# 提取 LLM 预估的超时时间
|
|
801
|
-
timeout_seconds = action.get("timeout_seconds")
|
|
802
|
-
if timeout_seconds is None:
|
|
803
|
-
# 如果 LLM 未提供 timeout_seconds,使用默认值并记录警告
|
|
804
|
-
timeout_seconds = self.config.executor.timeout if hasattr(self.config, 'executor') else 120
|
|
805
|
-
logger.warning(
|
|
806
|
-
f"[{task_id}] LLM 未指定 timeout_seconds,使用默认值 {timeout_seconds}s"
|
|
807
|
-
)
|
|
808
|
-
else:
|
|
809
|
-
try:
|
|
810
|
-
timeout_seconds = int(timeout_seconds)
|
|
811
|
-
# 限制超时范围: 最小 5s,最大 3600s
|
|
812
|
-
timeout_seconds = max(5, min(timeout_seconds, 3600))
|
|
813
|
-
except (ValueError, TypeError):
|
|
814
|
-
timeout_seconds = 120
|
|
815
|
-
logger.warning(
|
|
816
|
-
f"[{task_id}] timeout_seconds 值无效,使用默认值 120s"
|
|
817
|
-
)
|
|
818
|
-
|
|
819
|
-
# 记录代码执行开始事件
|
|
820
|
-
self._add_exec_event("code_exec", {
|
|
821
|
-
"title": f"执行 {code_lang} 代码",
|
|
822
|
-
"language": code_lang,
|
|
823
|
-
"code": code_text,
|
|
824
|
-
"code_preview": truncate_str(code_text, 200),
|
|
825
|
-
"status": "running",
|
|
826
|
-
"timeout": timeout_seconds,
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
# 注入权限检查器到 executor(用于更细粒度的检查)
|
|
830
|
-
self.executor.set_permission_checker(
|
|
831
|
-
self.check_permission, self.name
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
exec_result = await self.executor.execute(
|
|
835
|
-
language=action.get("language", "python"),
|
|
836
|
-
code=action.get("code", ""),
|
|
837
|
-
timeout=timeout_seconds,
|
|
838
|
-
)
|
|
839
|
-
|
|
840
|
-
# 设置超时追踪字段(executor 已直接设置 timed_out,这里确保 timeout_limit)
|
|
841
|
-
if not exec_result.timeout_limit:
|
|
842
|
-
exec_result.timeout_limit = timeout_seconds
|
|
843
|
-
|
|
844
|
-
result_dict = exec_result.to_dict()
|
|
845
|
-
|
|
846
|
-
# 记录代码执行结果事件
|
|
847
|
-
self._add_exec_event("code_result", {
|
|
848
|
-
"title": f"{'超时' if exec_result.timed_out else '成功' if exec_result.success else '失败'}: {code_lang}",
|
|
849
|
-
"language": code_lang,
|
|
850
|
-
"code_preview": truncate_str(action.get("code", ""), 200),
|
|
851
|
-
"success": exec_result.success,
|
|
852
|
-
"timed_out": exec_result.timed_out,
|
|
853
|
-
"exit_code": exec_result.exit_code,
|
|
854
|
-
"execution_time": round(exec_result.execution_time, 3),
|
|
855
|
-
"timeout": timeout_seconds,
|
|
856
|
-
"stdout": truncate_str(exec_result.stdout, 5000),
|
|
857
|
-
"stderr": truncate_str(exec_result.stderr, 3000),
|
|
858
|
-
"error": truncate_str(exec_result.error, 2000),
|
|
859
|
-
"result": result_dict,
|
|
860
|
-
})
|
|
861
|
-
|
|
862
|
-
# 超时后自动触发 LLM 诊断分析
|
|
863
|
-
if exec_result.timed_out:
|
|
864
|
-
logger.info(
|
|
865
|
-
f"[{task_id}] ⏰ 命令执行超时 ({timeout_seconds}s),"
|
|
866
|
-
f"自动触发 LLM 诊断分析..."
|
|
867
|
-
)
|
|
868
|
-
diagnosis = await self._diagnose_timeout(
|
|
869
|
-
action, exec_result, context, task_id
|
|
870
|
-
)
|
|
871
|
-
result_dict["timeout_diagnosis"] = diagnosis
|
|
872
|
-
|
|
873
|
-
results.append(result_dict)
|
|
874
|
-
|
|
875
|
-
elif action_type == "memory" and self.memory_agent:
|
|
876
|
-
mem_ctx = AgentContext(
|
|
877
|
-
task_id=task_id,
|
|
878
|
-
session_id=context.session_id,
|
|
879
|
-
metadata={
|
|
880
|
-
"memory_action": action.get("action", ""),
|
|
881
|
-
**action.get("data", {}),
|
|
882
|
-
},
|
|
883
|
-
)
|
|
884
|
-
await self.memory_agent.process(mem_ctx)
|
|
885
|
-
results.append({"success": True, "action": "memory"})
|
|
886
|
-
|
|
887
|
-
elif action_type == "final":
|
|
888
|
-
break
|
|
889
|
-
|
|
890
|
-
else:
|
|
891
|
-
results.append({
|
|
892
|
-
"success": False,
|
|
893
|
-
"error": f"未知操作类型: {action_type}",
|
|
894
|
-
})
|
|
895
|
-
|
|
896
|
-
return results
|
|
897
|
-
|
|
898
|
-
@staticmethod
|
|
899
|
-
def _build_timeout_diagnosis_message(action: Dict, exec_result: 'ExecResult') -> str:
|
|
900
|
-
"""构建超时诊断的用户消息(避免 f-string 内换行问题)"""
|
|
901
|
-
parts = []
|
|
902
|
-
parts.append("## 超时命令信息")
|
|
903
|
-
parts.append(f"- 语言: {action.get('language', 'unknown')}")
|
|
904
|
-
parts.append(f"- 超时上限: {exec_result.timeout_limit}s")
|
|
905
|
-
parts.append(f"- 实际执行时间: {exec_result.execution_time:.1f}s")
|
|
906
|
-
parts.append(f"- 代码:")
|
|
907
|
-
parts.append(f"```{action.get('language', '')}")
|
|
908
|
-
parts.append(f"{action.get('code', '')}")
|
|
909
|
-
parts.append("```")
|
|
910
|
-
parts.append("")
|
|
911
|
-
parts.append("## 执行输出(超时前)")
|
|
912
|
-
|
|
913
|
-
if exec_result.stdout and exec_result.stdout.strip():
|
|
914
|
-
parts.append("### 标准输出")
|
|
915
|
-
parts.append("```")
|
|
916
|
-
parts.append(exec_result.stdout[:5000])
|
|
917
|
-
parts.append("```")
|
|
918
|
-
parts.append("")
|
|
919
|
-
else:
|
|
920
|
-
parts.append("(无输出)")
|
|
921
|
-
parts.append("")
|
|
922
|
-
|
|
923
|
-
if exec_result.stderr and exec_result.stderr.strip():
|
|
924
|
-
parts.append("### 标准错误")
|
|
925
|
-
parts.append("```")
|
|
926
|
-
parts.append(exec_result.stderr[:3000])
|
|
927
|
-
parts.append("```")
|
|
175
|
+
logger.debug(f"V2 SSE 事件发送失败 ({event_type}): {e}")
|
|
928
176
|
else:
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
return "\n".join(parts)
|
|
177
|
+
logger.debug(f"[v2-event] {event_type}: {data}")
|
|
932
178
|
|
|
933
|
-
async def
|
|
179
|
+
async def _merge_duplicate_memory(
|
|
934
180
|
self,
|
|
935
|
-
|
|
936
|
-
|
|
181
|
+
old_memory,
|
|
182
|
+
new_content: str,
|
|
937
183
|
context: AgentContext,
|
|
938
184
|
task_id: str,
|
|
939
|
-
) ->
|
|
185
|
+
) -> Optional[str]:
|
|
940
186
|
"""
|
|
941
|
-
|
|
187
|
+
当发现新旧记忆高度相似时,调用 LLM API 让其判断最终记忆内容。
|
|
942
188
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
4. 是否值得重试
|
|
189
|
+
将旧记忆、新记忆、当前上下文发送给 LLM,由 LLM 决定:
|
|
190
|
+
- 合并为一条更完整的记忆
|
|
191
|
+
- 保留新记忆(旧记忆已过时)
|
|
192
|
+
- 保留旧记忆(新记忆无新增信息)
|
|
948
193
|
|
|
949
194
|
Returns:
|
|
950
|
-
|
|
195
|
+
合并后的记忆内容,或 None(合并失败)
|
|
951
196
|
"""
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
content=(
|
|
956
|
-
"你是一个执行超时诊断专家。一个命令刚刚因超时被强制终止,"
|
|
957
|
-
"请仔细分析以下信息,给出诊断结论和改进建议。\n\n"
|
|
958
|
-
"请用 JSON 格式回复:\n"
|
|
959
|
-
"{\n"
|
|
960
|
-
' "diagnosis": "超时原因诊断(简明扼要)",\n'
|
|
961
|
-
' "progress": "已完成的进度评估",\n'
|
|
962
|
-
' "suggestions": ["改进建议1", "改进建议2", ...],\n'
|
|
963
|
-
' "should_retry": true/false,\n'
|
|
964
|
-
' "retry_strategy": "如果建议重试,说明重试策略",\n'
|
|
965
|
-
' "estimated_fix_time": "预计修复后需要的时间(秒)"\n'
|
|
966
|
-
"}"
|
|
967
|
-
),
|
|
968
|
-
),
|
|
969
|
-
Message(
|
|
970
|
-
role="user",
|
|
971
|
-
content=self._build_timeout_diagnosis_message(action, exec_result),
|
|
972
|
-
),
|
|
973
|
-
]
|
|
974
|
-
|
|
975
|
-
try:
|
|
976
|
-
diagnosis = await self._call_llm_json(diagnosis_messages)
|
|
977
|
-
if "error" in diagnosis:
|
|
978
|
-
logger.warning(f"[{task_id}] 超时诊断 LLM 调用失败: {diagnosis['error']}")
|
|
979
|
-
return {
|
|
980
|
-
"diagnosis": "LLM 诊断调用失败",
|
|
981
|
-
"suggestions": ["检查命令是否有死循环", "尝试减少处理数据量"],
|
|
982
|
-
"should_retry": False,
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
# 记录诊断结果
|
|
986
|
-
logger.info(
|
|
987
|
-
f"[{task_id}] ⏰ 超时诊断完成: {diagnosis.get('diagnosis', 'N/A')}"
|
|
988
|
-
)
|
|
989
|
-
|
|
990
|
-
# 如果诊断建议重试且提供了重试策略,记录到记忆中
|
|
991
|
-
if diagnosis.get("should_retry") and self.memory:
|
|
992
|
-
self.memory.record_error_pattern(
|
|
993
|
-
error=(
|
|
994
|
-
f"命令超时 ({exec_result.timeout_limit}s): "
|
|
995
|
-
f"{action.get('code', '')[:200]}\n"
|
|
996
|
-
f"诊断: {diagnosis.get('diagnosis', '')}\n"
|
|
997
|
-
f"建议: {'; '.join(diagnosis.get('suggestions', []))}"
|
|
998
|
-
)
|
|
999
|
-
)
|
|
1000
|
-
|
|
1001
|
-
return diagnosis
|
|
1002
|
-
|
|
1003
|
-
except Exception as e:
|
|
1004
|
-
logger.error(f"[{task_id}] 超时诊断异常: {e}")
|
|
1005
|
-
return {
|
|
1006
|
-
"diagnosis": f"诊断过程异常: {str(e)}",
|
|
1007
|
-
"suggestions": ["检查命令逻辑", "手动测试命令"],
|
|
1008
|
-
"should_retry": False,
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
def _summarize_action_results(self, results: List[Dict]) -> str:
|
|
1012
|
-
"""汇总操作结果"""
|
|
1013
|
-
if not results:
|
|
1014
|
-
return "无操作结果"
|
|
1015
|
-
|
|
1016
|
-
parts = []
|
|
1017
|
-
for i, r in enumerate(results, 1):
|
|
1018
|
-
status = "✅" if r.get("success") else "❌"
|
|
1019
|
-
msg = r.get("message", r.get("error", ""))[:200]
|
|
1020
|
-
|
|
1021
|
-
# 超时特殊标记
|
|
1022
|
-
if r.get("timed_out"):
|
|
1023
|
-
status = "⏰"
|
|
1024
|
-
limit = r.get("timeout_limit", "?")
|
|
1025
|
-
actual = r.get("execution_time", "?")
|
|
1026
|
-
msg = f"执行超时 (上限{limit}s, 实际{actual}s)"
|
|
1027
|
-
|
|
1028
|
-
# 追加诊断结果
|
|
1029
|
-
diagnosis = r.get("timeout_diagnosis")
|
|
1030
|
-
if diagnosis:
|
|
1031
|
-
diag_text = diagnosis.get("diagnosis", "")
|
|
1032
|
-
suggestions = diagnosis.get("suggestions", [])
|
|
1033
|
-
msg += f"\n 诊断: {diag_text}"
|
|
1034
|
-
if suggestions:
|
|
1035
|
-
msg += f"\n 建议: {'; '.join(suggestions[:3])}"
|
|
1036
|
-
|
|
1037
|
-
parts.append(f"{i}. {status} {msg}")
|
|
1038
|
-
|
|
1039
|
-
return "\n".join(parts)
|
|
1040
|
-
|
|
1041
|
-
def _build_org_context(self, context: AgentContext) -> str:
|
|
1042
|
-
"""
|
|
1043
|
-
构建组织上下文信息。
|
|
1044
|
-
|
|
1045
|
-
如果组织功能已启用:
|
|
1046
|
-
1. 读取 organization.md 内容
|
|
1047
|
-
2. 对用户查询进行 RAG 搜索,获取相关知识片段
|
|
1048
|
-
3. 返回格式化的上下文字符串
|
|
197
|
+
if not self.llm:
|
|
198
|
+
logger.warning(f"[{task_id}] 记忆合并: 无 LLM 客户端,跳过合并")
|
|
199
|
+
return None
|
|
1049
200
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
""
|
|
1053
|
-
|
|
1054
|
-
# 检查配置中是否启用了组织功能
|
|
1055
|
-
org_enabled = False
|
|
1056
|
-
if hasattr(self.config, 'organization'):
|
|
1057
|
-
org_enabled = self.config.organization.enabled
|
|
1058
|
-
if not org_enabled:
|
|
1059
|
-
return ""
|
|
201
|
+
from datetime import datetime
|
|
202
|
+
old_time = old_memory.created_at or "未知时间"
|
|
203
|
+
new_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
204
|
+
user_msg = context.user_message or ""
|
|
1060
205
|
|
|
1061
|
-
|
|
206
|
+
merge_prompt = f"""你是一个记忆管理系统。现在系统检测到两条高度相似的记忆,请你判断如何合并它们。
|
|
1062
207
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
from config import get_config
|
|
1066
|
-
config_mgr = get_config()
|
|
1067
|
-
org_dir = config_mgr.data_dir / "organization"
|
|
1068
|
-
org_info_path = org_dir / "organization.md"
|
|
1069
|
-
if org_info_path.exists():
|
|
1070
|
-
current_mtime = org_info_path.stat().st_mtime
|
|
1071
|
-
if current_mtime != self._org_context_mtime:
|
|
1072
|
-
content = org_info_path.read_text(encoding="utf-8")
|
|
1073
|
-
# 检查是否仍是模板(包含占位注释)
|
|
1074
|
-
is_template = "<!-- 在此填写" in content
|
|
1075
|
-
if not is_template:
|
|
1076
|
-
self._org_context_cache = content
|
|
1077
|
-
else:
|
|
1078
|
-
self._org_context_cache = ""
|
|
1079
|
-
self._org_context_mtime = current_mtime
|
|
1080
|
-
except Exception as e:
|
|
1081
|
-
logger.debug(f"读取组织信息失败: {e}")
|
|
208
|
+
## 旧记忆(创建于 {old_time})
|
|
209
|
+
{old_memory.content}
|
|
1082
210
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
# 截取合理的长度,避免占用过多 token
|
|
1086
|
-
org_text = self._org_context_cache[:3000]
|
|
1087
|
-
parts.append(org_text)
|
|
1088
|
-
parts.append("")
|
|
211
|
+
## 新记忆(创建于 {new_time})
|
|
212
|
+
{new_content}
|
|
1089
213
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
from config import get_config
|
|
1093
|
-
from knowledge.rag import KnowledgeRAG
|
|
1094
|
-
|
|
1095
|
-
config_mgr = get_config()
|
|
1096
|
-
kb_dir = config_mgr.data_dir / "organization" / "knowledge"
|
|
1097
|
-
if kb_dir.exists() and any(kb_dir.iterdir()):
|
|
1098
|
-
rag = KnowledgeRAG(kb_dir=kb_dir)
|
|
1099
|
-
rag.build_index()
|
|
1100
|
-
|
|
1101
|
-
if rag.total_chunks > 0:
|
|
1102
|
-
# 使用用户消息作为查询
|
|
1103
|
-
query = context.user_message
|
|
1104
|
-
results = rag.search(query, top_k=3)
|
|
1105
|
-
if results:
|
|
1106
|
-
parts.append("## 相关知识库内容\n")
|
|
1107
|
-
for r in results:
|
|
1108
|
-
parts.append(
|
|
1109
|
-
f"### 📄 {r.file_name} (片段 {r.chunk_index + 1})\n"
|
|
1110
|
-
f"{r.content[:1000]}\n"
|
|
1111
|
-
)
|
|
1112
|
-
except Exception as e:
|
|
1113
|
-
logger.debug(f"组织知识库搜索失败: {e}")
|
|
214
|
+
## 当前用户输入
|
|
215
|
+
{user_msg}
|
|
1114
216
|
|
|
1115
|
-
|
|
1116
|
-
|
|
217
|
+
## 任务
|
|
218
|
+
请分析新旧记忆,输出一条最终的合并记忆。规则:
|
|
219
|
+
1. 如果新记忆包含了旧记忆的信息并有更新,则合并为更完整的表述
|
|
220
|
+
2. 如果新记忆只是旧记忆的重复或信息量更少,保留旧记忆中更完整的信息
|
|
221
|
+
3. 如果新记忆提供了全新的信息,以新记忆为主,补充旧记忆中的有效部分
|
|
222
|
+
4. 合并后的记忆应当简洁、准确、包含时间上下文
|
|
223
|
+
5. 直接输出合并后的记忆内容,不要输出任何解释或标记
|
|
1117
224
|
|
|
1118
|
-
|
|
225
|
+
请输出合并后的记忆:"""
|
|
1119
226
|
|
|
227
|
+
try:
|
|
228
|
+
messages = [
|
|
229
|
+
Message(role="system", content="你是一个记忆管理系统,负责合并重复或相似的记忆条目。只输出合并后的记忆内容,不要输出任何额外说明。"),
|
|
230
|
+
Message(role="user", content=merge_prompt),
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
response = await self._call_llm(messages)
|
|
234
|
+
|
|
235
|
+
if response.success and response.content:
|
|
236
|
+
merged = response.content.strip()
|
|
237
|
+
# 清理可能的引号包裹
|
|
238
|
+
if merged.startswith('"') and merged.endswith('"'):
|
|
239
|
+
merged = merged[1:-1]
|
|
240
|
+
if merged.startswith("'") and merged.endswith("'"):
|
|
241
|
+
merged = merged[1:-1]
|
|
242
|
+
logger.info(
|
|
243
|
+
f"[{task_id}] 记忆合并成功: 旧({len(old_memory.content)}字) + "
|
|
244
|
+
f"新({len(new_content)}字) → 合并({len(merged)}字)"
|
|
245
|
+
)
|
|
246
|
+
return merged
|
|
247
|
+
else:
|
|
248
|
+
logger.warning(f"[{task_id}] 记忆合并 LLM 调用失败: {response.error}")
|
|
249
|
+
return None
|
|
1120
250
|
except Exception as e:
|
|
1121
|
-
logger.
|
|
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}")
|
|
251
|
+
logger.warning(f"[{task_id}] 记忆合并异常: {e}")
|
|
252
|
+
return None
|
|
1141
253
|
|
|
1142
254
|
async def process_v2(
|
|
1143
255
|
self,
|
|
@@ -1154,7 +266,7 @@ status 取值:
|
|
|
1154
266
|
|
|
1155
267
|
核心流程:
|
|
1156
268
|
1. 使用 ContextBuilder 构建 <context> XML
|
|
1157
|
-
2. 将 context 注入
|
|
269
|
+
2. 将 context 注入 SYSTEM_PROMPT,调用 LLM
|
|
1158
270
|
3. 使用 OutputParser 解析 <output> XML
|
|
1159
271
|
4. 根据 parsed.tools_to_call 依次执行工具
|
|
1160
272
|
5. 任一工具超时 → 强制回调 LLM
|
|
@@ -1176,11 +288,7 @@ status 取值:
|
|
|
1176
288
|
self._current_task_id = task_id
|
|
1177
289
|
self.clear_execution_events()
|
|
1178
290
|
|
|
1179
|
-
|
|
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]}")
|
|
291
|
+
logger.info(f"[{task_id}] 执行循环启动: {context.user_message[:100]}")
|
|
1184
292
|
|
|
1185
293
|
try:
|
|
1186
294
|
return await self._process_v2_inner(
|
|
@@ -1210,6 +318,8 @@ status 取值:
|
|
|
1210
318
|
all_tool_outputs = ""
|
|
1211
319
|
recall_content = ""
|
|
1212
320
|
get_knowledge_content = ""
|
|
321
|
+
# 追踪流式推送的 reasoning 文本(用于构建有意义的最终回复)
|
|
322
|
+
_v2_reasoning_collected: List[str] = []
|
|
1213
323
|
|
|
1214
324
|
conversation_history = list(context.conversation_history or [])
|
|
1215
325
|
|
|
@@ -1273,8 +383,14 @@ status 取值:
|
|
|
1273
383
|
stream_callback,
|
|
1274
384
|
)
|
|
1275
385
|
|
|
1276
|
-
# Step 2: 构建系统消息
|
|
1277
|
-
|
|
386
|
+
# Step 2: 构建系统消息 — 将 context XML 插入 SYSTEM_PROMPT 的 "上下文" 占位处
|
|
387
|
+
_CONTEXT_PLACEHOLDER = "__CONTEXT_PLACEHOLDER__"
|
|
388
|
+
_prompt_with_placeholder = (
|
|
389
|
+
"你是一个强内容分析格式转化引擎,要深入分析以下上下文内容:\n\n"
|
|
390
|
+
+ _CONTEXT_PLACEHOLDER + "\n\n"
|
|
391
|
+
+ self.SYSTEM_PROMPT.split("\n", 1)[1]
|
|
392
|
+
)
|
|
393
|
+
system_content = _prompt_with_placeholder.replace(_CONTEXT_PLACEHOLDER, context_xml)
|
|
1278
394
|
|
|
1279
395
|
# Step 3: 调用 LLM
|
|
1280
396
|
messages = [Message(role="system", content=system_content)]
|
|
@@ -1347,38 +463,63 @@ status 取值:
|
|
|
1347
463
|
if parsed.usersays_correct:
|
|
1348
464
|
context.working_memory["usersays_correct"] = parsed.usersays_correct
|
|
1349
465
|
|
|
1350
|
-
# Step 6: 处理 remember —
|
|
466
|
+
# Step 6: 处理 remember — 查重+LLM合并后存入长期记忆
|
|
1351
467
|
if parsed.remember:
|
|
1352
468
|
try:
|
|
1353
|
-
# 查重:跳过与已有记忆高度重复的内容
|
|
1354
|
-
is_dup = False
|
|
1355
469
|
if self.memory:
|
|
1356
|
-
|
|
470
|
+
# 查找是否有相似记忆
|
|
471
|
+
dup_memory = self.memory.find_duplicate_memory(
|
|
1357
472
|
content=parsed.remember,
|
|
1358
473
|
session_id=context.session_id,
|
|
1359
474
|
key="conversation_insight",
|
|
1360
475
|
)
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
task_id=task_id,
|
|
1367
|
-
session_id=context.session_id,
|
|
1368
|
-
metadata={
|
|
1369
|
-
"memory_action": "save",
|
|
1370
|
-
"content": parsed.remember,
|
|
1371
|
-
},
|
|
476
|
+
if dup_memory:
|
|
477
|
+
# 发现相似记忆 → 调用 LLM API 合并新旧记忆
|
|
478
|
+
logger.info(
|
|
479
|
+
f"[{task_id}] 记忆查重: 发现相似内容,调用LLM合并 "
|
|
480
|
+
f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
|
|
1372
481
|
)
|
|
1373
|
-
await self.
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
content=parsed.remember,
|
|
1379
|
-
summary=truncate_str(parsed.remember, 200),
|
|
1380
|
-
importance=0.7,
|
|
482
|
+
merged_content = await self._merge_duplicate_memory(
|
|
483
|
+
old_memory=dup_memory,
|
|
484
|
+
new_content=parsed.remember,
|
|
485
|
+
context=context,
|
|
486
|
+
task_id=task_id,
|
|
1381
487
|
)
|
|
488
|
+
if merged_content:
|
|
489
|
+
# 用 LLM 合并后的内容替换旧记忆
|
|
490
|
+
self.memory.update_memory(
|
|
491
|
+
memory_id=dup_memory.id,
|
|
492
|
+
content=merged_content,
|
|
493
|
+
summary=truncate_str(merged_content, 200),
|
|
494
|
+
)
|
|
495
|
+
logger.info(f"[{task_id}] 记忆已合并更新: {dup_memory.id}")
|
|
496
|
+
else:
|
|
497
|
+
# LLM 合并失败,直接更新为新内容
|
|
498
|
+
self.memory.update_memory(
|
|
499
|
+
memory_id=dup_memory.id,
|
|
500
|
+
content=parsed.remember,
|
|
501
|
+
)
|
|
502
|
+
logger.info(f"[{task_id}] 记忆直接更新为新内容: {dup_memory.id}")
|
|
503
|
+
else:
|
|
504
|
+
# 无重复,直接存储新记忆
|
|
505
|
+
if self.memory_agent:
|
|
506
|
+
mem_ctx = AgentContext(
|
|
507
|
+
task_id=task_id,
|
|
508
|
+
session_id=context.session_id,
|
|
509
|
+
metadata={
|
|
510
|
+
"memory_action": "save",
|
|
511
|
+
"content": parsed.remember,
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
await self.memory_agent.process(mem_ctx)
|
|
515
|
+
else:
|
|
516
|
+
self.memory.add_long_term(
|
|
517
|
+
session_id=context.session_id,
|
|
518
|
+
key="conversation_insight",
|
|
519
|
+
content=parsed.remember,
|
|
520
|
+
summary=truncate_str(parsed.remember, 200),
|
|
521
|
+
importance=0.7,
|
|
522
|
+
)
|
|
1382
523
|
await self._emit_v2_event(
|
|
1383
524
|
"v2_memory_saved",
|
|
1384
525
|
{"content": truncate_str(parsed.remember, 200)},
|
|
@@ -1474,6 +615,7 @@ status 取值:
|
|
|
1474
615
|
|
|
1475
616
|
# 发送 beforecalltext 作为显示文本
|
|
1476
617
|
if before_call:
|
|
618
|
+
_v2_reasoning_collected.append(before_call)
|
|
1477
619
|
await self._emit_v2_event(
|
|
1478
620
|
"v2_reasoning",
|
|
1479
621
|
{"content": before_call},
|
|
@@ -1552,9 +694,15 @@ status 取值:
|
|
|
1552
694
|
# 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
|
|
1553
695
|
if parsed.finish:
|
|
1554
696
|
logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
|
|
1555
|
-
|
|
1556
|
-
if
|
|
1557
|
-
final_text
|
|
697
|
+
# 构建有意义的最终回复:使用收集到的 reasoning text + 任务计划摘要
|
|
698
|
+
if _v2_reasoning_collected:
|
|
699
|
+
final_text = "\n".join(_v2_reasoning_collected)
|
|
700
|
+
if current_task_plan:
|
|
701
|
+
final_text += f"\n\n{current_task_plan}"
|
|
702
|
+
else:
|
|
703
|
+
final_text = "已完成所有操作。"
|
|
704
|
+
if current_task_plan:
|
|
705
|
+
final_text += f"\n\n任务计划:\n{current_task_plan}"
|
|
1558
706
|
context.working_memory["final_response"] = final_text
|
|
1559
707
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
1560
708
|
if self.memory:
|
|
@@ -1568,9 +716,14 @@ status 取值:
|
|
|
1568
716
|
# finish=false: 根据工具的 callback 标志决定是否回调
|
|
1569
717
|
if not need_callback:
|
|
1570
718
|
logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
719
|
+
if _v2_reasoning_collected:
|
|
720
|
+
final_text = "\n".join(_v2_reasoning_collected)
|
|
721
|
+
if current_task_plan:
|
|
722
|
+
final_text += f"\n\n{current_task_plan}"
|
|
723
|
+
else:
|
|
724
|
+
final_text = "已完成所有操作。"
|
|
725
|
+
if current_task_plan:
|
|
726
|
+
final_text += f"\n\n任务计划:\n{current_task_plan}"
|
|
1574
727
|
context.working_memory["final_response"] = final_text
|
|
1575
728
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
1576
729
|
if self.memory:
|