myagent-ai 1.9.4 → 1.9.6
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 +158 -40
- package/core/output_parser.py +1 -1
- package/main.py +5 -0
- package/memory/manager.py +134 -3
- package/package.json +1 -1
- package/web/api_server.py +13 -2
- package/web/ui/chat/flow_engine.js +38 -8
package/agents/main_agent.py
CHANGED
|
@@ -188,7 +188,7 @@ status 取值:
|
|
|
188
188
|
|
|
189
189
|
<tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。示例:接下来,要调用命令行工具,获得ip地址。</beforecalltext><toolname>工具名</toolname><parms>参数</parms><timeout>预估超时时限</timeout><callback>true/false</callback></tool>
|
|
190
190
|
</toolstocal>
|
|
191
|
-
<remember
|
|
191
|
+
<remember>仅从最新用户输入(userprint 或 usersays_correct)中提炼值得长期记忆的信息(如用户偏好、重要结论、错误经验等)。不要从历史对话中重复提炼旧记忆。如果本轮用户输入没有新信息需要记忆,则为空。</remember>
|
|
192
192
|
<recall>下一轮执行需要调取的记忆,这里要设计接上记忆库</recall>
|
|
193
193
|
<get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
|
|
194
194
|
<askuser>需要询问用户的内容,如无,则为空</askuser>
|
|
@@ -203,7 +203,7 @@ status 取值:
|
|
|
203
203
|
4. <toolstocal>: 列出所有需要执行的工具调用,每个工具包含完整的参数说明
|
|
204
204
|
5. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
|
|
205
205
|
6. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
|
|
206
|
-
7. <remember>:
|
|
206
|
+
7. <remember>: 仅从最新用户输入(userprint 或 usersays_correct)中提炼值得长期记忆的关键信息,不要重复提炼历史对话中已有的记忆。如果本轮没有新信息需要记忆,则为空
|
|
207
207
|
8. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
|
|
208
208
|
9. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
|
|
209
209
|
10. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
|
|
@@ -1139,6 +1139,81 @@ status 取值:
|
|
|
1139
1139
|
else:
|
|
1140
1140
|
logger.debug(f"[v2-event] {event_type}: {data}")
|
|
1141
1141
|
|
|
1142
|
+
async def _merge_duplicate_memory(
|
|
1143
|
+
self,
|
|
1144
|
+
old_memory,
|
|
1145
|
+
new_content: str,
|
|
1146
|
+
context: AgentContext,
|
|
1147
|
+
task_id: str,
|
|
1148
|
+
) -> Optional[str]:
|
|
1149
|
+
"""
|
|
1150
|
+
当发现新旧记忆高度相似时,调用 LLM API 让其判断最终记忆内容。
|
|
1151
|
+
|
|
1152
|
+
将旧记忆、新记忆、当前上下文发送给 LLM,由 LLM 决定:
|
|
1153
|
+
- 合并为一条更完整的记忆
|
|
1154
|
+
- 保留新记忆(旧记忆已过时)
|
|
1155
|
+
- 保留旧记忆(新记忆无新增信息)
|
|
1156
|
+
|
|
1157
|
+
Returns:
|
|
1158
|
+
合并后的记忆内容,或 None(合并失败)
|
|
1159
|
+
"""
|
|
1160
|
+
if not self.llm:
|
|
1161
|
+
logger.warning(f"[{task_id}] 记忆合并: 无 LLM 客户端,跳过合并")
|
|
1162
|
+
return None
|
|
1163
|
+
|
|
1164
|
+
from datetime import datetime
|
|
1165
|
+
old_time = old_memory.created_at or "未知时间"
|
|
1166
|
+
new_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1167
|
+
user_msg = context.user_message or ""
|
|
1168
|
+
|
|
1169
|
+
merge_prompt = f"""你是一个记忆管理系统。现在系统检测到两条高度相似的记忆,请你判断如何合并它们。
|
|
1170
|
+
|
|
1171
|
+
## 旧记忆(创建于 {old_time})
|
|
1172
|
+
{old_memory.content}
|
|
1173
|
+
|
|
1174
|
+
## 新记忆(创建于 {new_time})
|
|
1175
|
+
{new_content}
|
|
1176
|
+
|
|
1177
|
+
## 当前用户输入
|
|
1178
|
+
{user_msg}
|
|
1179
|
+
|
|
1180
|
+
## 任务
|
|
1181
|
+
请分析新旧记忆,输出一条最终的合并记忆。规则:
|
|
1182
|
+
1. 如果新记忆包含了旧记忆的信息并有更新,则合并为更完整的表述
|
|
1183
|
+
2. 如果新记忆只是旧记忆的重复或信息量更少,保留旧记忆中更完整的信息
|
|
1184
|
+
3. 如果新记忆提供了全新的信息,以新记忆为主,补充旧记忆中的有效部分
|
|
1185
|
+
4. 合并后的记忆应当简洁、准确、包含时间上下文
|
|
1186
|
+
5. 直接输出合并后的记忆内容,不要输出任何解释或标记
|
|
1187
|
+
|
|
1188
|
+
请输出合并后的记忆:"""
|
|
1189
|
+
|
|
1190
|
+
try:
|
|
1191
|
+
messages = [
|
|
1192
|
+
Message(role="system", content="你是一个记忆管理系统,负责合并重复或相似的记忆条目。只输出合并后的记忆内容,不要输出任何额外说明。"),
|
|
1193
|
+
Message(role="user", content=merge_prompt),
|
|
1194
|
+
]
|
|
1195
|
+
|
|
1196
|
+
response = await self._call_llm(messages)
|
|
1197
|
+
|
|
1198
|
+
if response.success and response.content:
|
|
1199
|
+
merged = response.content.strip()
|
|
1200
|
+
# 清理可能的引号包裹
|
|
1201
|
+
if merged.startswith('"') and merged.endswith('"'):
|
|
1202
|
+
merged = merged[1:-1]
|
|
1203
|
+
if merged.startswith("'") and merged.endswith("'"):
|
|
1204
|
+
merged = merged[1:-1]
|
|
1205
|
+
logger.info(
|
|
1206
|
+
f"[{task_id}] 记忆合并成功: 旧({len(old_memory.content)}字) + "
|
|
1207
|
+
f"新({len(new_content)}字) → 合并({len(merged)}字)"
|
|
1208
|
+
)
|
|
1209
|
+
return merged
|
|
1210
|
+
else:
|
|
1211
|
+
logger.warning(f"[{task_id}] 记忆合并 LLM 调用失败: {response.error}")
|
|
1212
|
+
return None
|
|
1213
|
+
except Exception as e:
|
|
1214
|
+
logger.warning(f"[{task_id}] 记忆合并异常: {e}")
|
|
1215
|
+
return None
|
|
1216
|
+
|
|
1142
1217
|
async def process_v2(
|
|
1143
1218
|
self,
|
|
1144
1219
|
context: AgentContext,
|
|
@@ -1210,6 +1285,8 @@ status 取值:
|
|
|
1210
1285
|
all_tool_outputs = ""
|
|
1211
1286
|
recall_content = ""
|
|
1212
1287
|
get_knowledge_content = ""
|
|
1288
|
+
# 追踪流式推送的 reasoning 文本(用于构建有意义的最终回复)
|
|
1289
|
+
_v2_reasoning_collected: List[str] = []
|
|
1213
1290
|
|
|
1214
1291
|
conversation_history = list(context.conversation_history or [])
|
|
1215
1292
|
|
|
@@ -1347,39 +1424,68 @@ status 取值:
|
|
|
1347
1424
|
if parsed.usersays_correct:
|
|
1348
1425
|
context.working_memory["usersays_correct"] = parsed.usersays_correct
|
|
1349
1426
|
|
|
1350
|
-
# Step 6: 处理 remember —
|
|
1351
|
-
if parsed.remember
|
|
1427
|
+
# Step 6: 处理 remember — 查重+LLM合并后存入长期记忆
|
|
1428
|
+
if parsed.remember:
|
|
1352
1429
|
try:
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
"
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1430
|
+
if self.memory:
|
|
1431
|
+
# 查找是否有相似记忆
|
|
1432
|
+
dup_memory = self.memory.find_duplicate_memory(
|
|
1433
|
+
content=parsed.remember,
|
|
1434
|
+
session_id=context.session_id,
|
|
1435
|
+
key="conversation_insight",
|
|
1436
|
+
)
|
|
1437
|
+
if dup_memory:
|
|
1438
|
+
# 发现相似记忆 → 调用 LLM API 合并新旧记忆
|
|
1439
|
+
logger.info(
|
|
1440
|
+
f"[{task_id}] 记忆查重: 发现相似内容,调用LLM合并 "
|
|
1441
|
+
f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
|
|
1442
|
+
)
|
|
1443
|
+
merged_content = await self._merge_duplicate_memory(
|
|
1444
|
+
old_memory=dup_memory,
|
|
1445
|
+
new_content=parsed.remember,
|
|
1446
|
+
context=context,
|
|
1447
|
+
task_id=task_id,
|
|
1448
|
+
)
|
|
1449
|
+
if merged_content:
|
|
1450
|
+
# 用 LLM 合并后的内容替换旧记忆
|
|
1451
|
+
self.memory.update_memory(
|
|
1452
|
+
memory_id=dup_memory.id,
|
|
1453
|
+
content=merged_content,
|
|
1454
|
+
summary=truncate_str(merged_content, 200),
|
|
1455
|
+
)
|
|
1456
|
+
logger.info(f"[{task_id}] 记忆已合并更新: {dup_memory.id}")
|
|
1457
|
+
else:
|
|
1458
|
+
# LLM 合并失败,直接更新为新内容
|
|
1459
|
+
self.memory.update_memory(
|
|
1460
|
+
memory_id=dup_memory.id,
|
|
1461
|
+
content=parsed.remember,
|
|
1462
|
+
)
|
|
1463
|
+
logger.info(f"[{task_id}] 记忆直接更新为新内容: {dup_memory.id}")
|
|
1464
|
+
else:
|
|
1465
|
+
# 无重复,直接存储新记忆
|
|
1466
|
+
if self.memory_agent:
|
|
1467
|
+
mem_ctx = AgentContext(
|
|
1468
|
+
task_id=task_id,
|
|
1469
|
+
session_id=context.session_id,
|
|
1470
|
+
metadata={
|
|
1471
|
+
"memory_action": "save",
|
|
1472
|
+
"content": parsed.remember,
|
|
1473
|
+
},
|
|
1474
|
+
)
|
|
1475
|
+
await self.memory_agent.process(mem_ctx)
|
|
1476
|
+
else:
|
|
1477
|
+
self.memory.add_long_term(
|
|
1478
|
+
session_id=context.session_id,
|
|
1479
|
+
key="conversation_insight",
|
|
1480
|
+
content=parsed.remember,
|
|
1481
|
+
summary=truncate_str(parsed.remember, 200),
|
|
1482
|
+
importance=0.7,
|
|
1483
|
+
)
|
|
1484
|
+
await self._emit_v2_event(
|
|
1485
|
+
"v2_memory_saved",
|
|
1486
|
+
{"content": truncate_str(parsed.remember, 200)},
|
|
1487
|
+
stream_callback,
|
|
1488
|
+
)
|
|
1383
1489
|
except Exception as e:
|
|
1384
1490
|
logger.warning(f"[{task_id}] 存入记忆失败: {e}")
|
|
1385
1491
|
|
|
@@ -1470,6 +1576,7 @@ status 取值:
|
|
|
1470
1576
|
|
|
1471
1577
|
# 发送 beforecalltext 作为显示文本
|
|
1472
1578
|
if before_call:
|
|
1579
|
+
_v2_reasoning_collected.append(before_call)
|
|
1473
1580
|
await self._emit_v2_event(
|
|
1474
1581
|
"v2_reasoning",
|
|
1475
1582
|
{"content": before_call},
|
|
@@ -1548,9 +1655,15 @@ status 取值:
|
|
|
1548
1655
|
# 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
|
|
1549
1656
|
if parsed.finish:
|
|
1550
1657
|
logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
|
|
1551
|
-
|
|
1552
|
-
if
|
|
1553
|
-
final_text
|
|
1658
|
+
# 构建有意义的最终回复:使用收集到的 reasoning text + 任务计划摘要
|
|
1659
|
+
if _v2_reasoning_collected:
|
|
1660
|
+
final_text = "\n".join(_v2_reasoning_collected)
|
|
1661
|
+
if current_task_plan:
|
|
1662
|
+
final_text += f"\n\n{current_task_plan}"
|
|
1663
|
+
else:
|
|
1664
|
+
final_text = "已完成所有操作。"
|
|
1665
|
+
if current_task_plan:
|
|
1666
|
+
final_text += f"\n\n任务计划:\n{current_task_plan}"
|
|
1554
1667
|
context.working_memory["final_response"] = final_text
|
|
1555
1668
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
1556
1669
|
if self.memory:
|
|
@@ -1564,9 +1677,14 @@ status 取值:
|
|
|
1564
1677
|
# finish=false: 根据工具的 callback 标志决定是否回调
|
|
1565
1678
|
if not need_callback:
|
|
1566
1679
|
logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1680
|
+
if _v2_reasoning_collected:
|
|
1681
|
+
final_text = "\n".join(_v2_reasoning_collected)
|
|
1682
|
+
if current_task_plan:
|
|
1683
|
+
final_text += f"\n\n{current_task_plan}"
|
|
1684
|
+
else:
|
|
1685
|
+
final_text = "已完成所有操作。"
|
|
1686
|
+
if current_task_plan:
|
|
1687
|
+
final_text += f"\n\n任务计划:\n{current_task_plan}"
|
|
1570
1688
|
context.working_memory["final_response"] = final_text
|
|
1571
1689
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
1572
1690
|
if self.memory:
|
package/core/output_parser.py
CHANGED
|
@@ -20,7 +20,7 @@ Expected XML schema produced by the LLM::
|
|
|
20
20
|
<callback>true/false</callback>
|
|
21
21
|
</tool>
|
|
22
22
|
</toolstocal>
|
|
23
|
-
<remember
|
|
23
|
+
<remember>仅从最新用户输入中提炼的记忆,无新信息则为空</remember>
|
|
24
24
|
<recall>下一轮需要调取的记忆</recall>
|
|
25
25
|
<askuser>需要询问用户的内容</askuser>
|
|
26
26
|
<get_knowledge>下一轮需要搜索获得的知识</get_knowledge>
|
package/main.py
CHANGED
|
@@ -319,6 +319,11 @@ class MyAgentApp:
|
|
|
319
319
|
memory_agent=self.memory_agent,
|
|
320
320
|
config_broadcaster=self.config_broadcaster,
|
|
321
321
|
)
|
|
322
|
+
self.main_agent.init_context_builder(
|
|
323
|
+
memory_manager=self.memory,
|
|
324
|
+
skill_registry=self.skill_registry,
|
|
325
|
+
knowledge_base_dir=str(self.config_mgr.data_dir / "organization" / "knowledge")
|
|
326
|
+
)
|
|
322
327
|
self.logger.info("Agent 集群已初始化 (主/工具/记忆)")
|
|
323
328
|
|
|
324
329
|
# 10. 自动更新管理器
|
package/memory/manager.py
CHANGED
|
@@ -479,6 +479,128 @@ class MemoryManager:
|
|
|
479
479
|
# 长期记忆 (持久知识)
|
|
480
480
|
# ==========================================================================
|
|
481
481
|
|
|
482
|
+
def find_duplicate_memory(
|
|
483
|
+
self,
|
|
484
|
+
content: str,
|
|
485
|
+
session_id: str = "",
|
|
486
|
+
key: str = "",
|
|
487
|
+
similarity_threshold: float = 0.85,
|
|
488
|
+
) -> Optional[MemoryEntry]:
|
|
489
|
+
"""
|
|
490
|
+
查找与给定内容高度相似的已有记忆(查重)。
|
|
491
|
+
|
|
492
|
+
使用 TF-IDF 余弦相似度判断新内容是否与已有记忆高度重复。
|
|
493
|
+
相似度超过阈值则返回匹配的旧记忆,否则返回 None。
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
content: 待检查的新记忆内容
|
|
497
|
+
session_id: 会话 ID(为空则跨会话查重)
|
|
498
|
+
key: 记忆分类键(为空则不限制分类)
|
|
499
|
+
similarity_threshold: 相似度阈值,超过此值视为重复(默认 0.85)
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
匹配的旧 MemoryEntry,或 None(无重复)
|
|
503
|
+
"""
|
|
504
|
+
if not content or not content.strip():
|
|
505
|
+
return None # 空内容不查重
|
|
506
|
+
|
|
507
|
+
conn = self._get_conn()
|
|
508
|
+
conditions = ["category = 'long_term'"]
|
|
509
|
+
params: list = []
|
|
510
|
+
|
|
511
|
+
if session_id:
|
|
512
|
+
conditions.append("session_id = ?")
|
|
513
|
+
params.append(session_id)
|
|
514
|
+
if key:
|
|
515
|
+
conditions.append("key = ?")
|
|
516
|
+
params.append(key)
|
|
517
|
+
|
|
518
|
+
# 过滤过期
|
|
519
|
+
conditions.append("(expires_at = '' OR expires_at > ?)")
|
|
520
|
+
params.append(timestamp())
|
|
521
|
+
|
|
522
|
+
where = " AND ".join(conditions)
|
|
523
|
+
# 取最近的记忆候选进行比对
|
|
524
|
+
sql = f"SELECT id, content, summary, created_at FROM memories WHERE {where} ORDER BY created_at DESC LIMIT 50"
|
|
525
|
+
rows = conn.execute(sql, params).fetchall()
|
|
526
|
+
|
|
527
|
+
if not rows:
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
# 使用 TF-IDF 计算新内容与每条已有记忆的相似度
|
|
531
|
+
documents = [(row["id"], row["content"]) for row in rows]
|
|
532
|
+
scores = self._compute_tfidf(content, documents)
|
|
533
|
+
|
|
534
|
+
if not scores:
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
max_id = max(scores, key=scores.get)
|
|
538
|
+
max_score = scores[max_id]
|
|
539
|
+
if max_score >= similarity_threshold:
|
|
540
|
+
logger.debug(
|
|
541
|
+
f"记忆查重: 发现相似内容 (相似度={max_score:.2f}, "
|
|
542
|
+
f"阈值={similarity_threshold}, 匹配ID={max_id})"
|
|
543
|
+
)
|
|
544
|
+
# 返回匹配的旧记忆
|
|
545
|
+
for row in rows:
|
|
546
|
+
if row["id"] == max_id:
|
|
547
|
+
return MemoryEntry.from_row(row)
|
|
548
|
+
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
def is_duplicate_memory(
|
|
552
|
+
self,
|
|
553
|
+
content: str,
|
|
554
|
+
session_id: str = "",
|
|
555
|
+
key: str = "",
|
|
556
|
+
similarity_threshold: float = 0.85,
|
|
557
|
+
) -> bool:
|
|
558
|
+
"""
|
|
559
|
+
检查是否已存在高度相似的记忆(查重)。
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
True 表示存在重复记忆,False 表示可以存储
|
|
563
|
+
"""
|
|
564
|
+
return self.find_duplicate_memory(
|
|
565
|
+
content, session_id, key, similarity_threshold
|
|
566
|
+
) is not None
|
|
567
|
+
|
|
568
|
+
def update_memory(self, memory_id: str, content: str, summary: str = "", **updates) -> bool:
|
|
569
|
+
"""
|
|
570
|
+
更新记忆内容(保留原 ID 和创建时间)。
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
memory_id: 记忆 ID
|
|
574
|
+
content: 新内容
|
|
575
|
+
summary: 新摘要(为空则自动截取)
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
True 更新成功
|
|
579
|
+
"""
|
|
580
|
+
if not summary:
|
|
581
|
+
summary = truncate_str(content, 200)
|
|
582
|
+
self._update_content(memory_id, content, summary=summary, **updates)
|
|
583
|
+
logger.info(f"记忆已更新: {memory_id}")
|
|
584
|
+
return True
|
|
585
|
+
|
|
586
|
+
def delete_memory(self, memory_id: str) -> bool:
|
|
587
|
+
"""
|
|
588
|
+
删除单条记忆。
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
memory_id: 记忆 ID
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
True 删除成功
|
|
595
|
+
"""
|
|
596
|
+
conn = self._get_conn()
|
|
597
|
+
cursor = conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
|
|
598
|
+
conn.commit()
|
|
599
|
+
if cursor.rowcount > 0:
|
|
600
|
+
logger.info(f"记忆已删除: {memory_id}")
|
|
601
|
+
return True
|
|
602
|
+
return False
|
|
603
|
+
|
|
482
604
|
def add_long_term(
|
|
483
605
|
self,
|
|
484
606
|
session_id: str = "global",
|
|
@@ -498,14 +620,23 @@ class MemoryManager:
|
|
|
498
620
|
summary: 简要摘要
|
|
499
621
|
importance: 重要性(越高越不容易被淘汰)
|
|
500
622
|
"""
|
|
623
|
+
from datetime import datetime
|
|
624
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
625
|
+
# 在 content 开头自动附加时间戳
|
|
626
|
+
timestamped_content = f"[{now_str}] {truncate_str(content, 50000)}"
|
|
627
|
+
ts_summary = summary or truncate_str(content, 200)
|
|
628
|
+
|
|
501
629
|
entry = MemoryEntry(
|
|
502
630
|
session_id=session_id,
|
|
503
631
|
category="long_term",
|
|
504
632
|
key=key,
|
|
505
|
-
content=
|
|
506
|
-
summary=
|
|
633
|
+
content=timestamped_content,
|
|
634
|
+
summary=f"[{now_str}] {ts_summary}",
|
|
507
635
|
importance=importance,
|
|
508
|
-
metadata=
|
|
636
|
+
metadata={
|
|
637
|
+
"timestamp": now_str,
|
|
638
|
+
**(metadata or {}),
|
|
639
|
+
},
|
|
509
640
|
)
|
|
510
641
|
return self._insert(entry)
|
|
511
642
|
|
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -3063,8 +3063,19 @@ class ApiServer:
|
|
|
3063
3063
|
|
|
3064
3064
|
# 返回最终响应
|
|
3065
3065
|
final_response = v2_context.working_memory.get("final_response", "")
|
|
3066
|
-
|
|
3067
|
-
|
|
3066
|
+
# 优先使用 v2_reasoning 流式推送的文本(用户实际看到的内容),
|
|
3067
|
+
# 而不是后端设定的通用占位符如"已完成所有操作。"
|
|
3068
|
+
if _v2_reasoning_parts:
|
|
3069
|
+
# Smart join: only add separator when parts don't already end with newline
|
|
3070
|
+
joined_parts = []
|
|
3071
|
+
for i, part in enumerate(_v2_reasoning_parts):
|
|
3072
|
+
if i > 0 and joined_parts and not joined_parts[-1].endswith('\n') and not part.startswith('\n'):
|
|
3073
|
+
joined_parts.append('\n')
|
|
3074
|
+
joined_parts.append(part)
|
|
3075
|
+
reasoning_text = ''.join(joined_parts)
|
|
3076
|
+
# 如果 final_response 是通用占位符或为空,使用 reasoning text
|
|
3077
|
+
if not final_response or final_response.startswith("已完成所有操作") or final_response.startswith("处理完毕"):
|
|
3078
|
+
final_response = reasoning_text
|
|
3068
3079
|
return final_response
|
|
3069
3080
|
|
|
3070
3081
|
# ── V1 路由: 标准 JSON action 格式 ──
|
|
@@ -814,6 +814,29 @@ function showToolResultModal(msgIndex, eventId) {
|
|
|
814
814
|
document.body.appendChild(overlay);
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
+
// ══════════════════════════════════════════════════════
|
|
818
|
+
// ── V2 Helpers ──
|
|
819
|
+
// ══════════════════════════════════════════════════════
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Strip XML tags from text for real-time streaming preview in V2 mode.
|
|
823
|
+
* Shows plain text between tags so the user sees progress during LLM streaming.
|
|
824
|
+
*/
|
|
825
|
+
function _stripXmlTags(xml) {
|
|
826
|
+
if (!xml) return '';
|
|
827
|
+
// Remove all XML tags, collapse excessive whitespace
|
|
828
|
+
return xml
|
|
829
|
+
.replace(/<[^>]+>/g, ' ') // Replace tags with space
|
|
830
|
+
.replace(/</g, '<')
|
|
831
|
+
.replace(/>/g, '>')
|
|
832
|
+
.replace(/&/g, '&')
|
|
833
|
+
.replace(/"/g, '"')
|
|
834
|
+
.replace(/'/g, "'")
|
|
835
|
+
.replace(/'/g, "'")
|
|
836
|
+
.replace(/\s{3,}/g, ' ') // Collapse 3+ whitespace to single space
|
|
837
|
+
.trim();
|
|
838
|
+
}
|
|
839
|
+
|
|
817
840
|
// ══════════════════════════════════════════════════════
|
|
818
841
|
// ── V2 Content Assembler (V2 内容组装) ──
|
|
819
842
|
// ══════════════════════════════════════════════════════
|
|
@@ -828,9 +851,12 @@ function _assembleV2Content(msg, msgParts) {
|
|
|
828
851
|
return msg._askUser.trim();
|
|
829
852
|
}
|
|
830
853
|
// Priority 3: V1 text parts (backward compat — non-V2 mode)
|
|
831
|
-
|
|
832
|
-
if (
|
|
833
|
-
|
|
854
|
+
// Guard: msgParts may be undefined after page refresh (only role/content/time persisted)
|
|
855
|
+
if (msgParts && Array.isArray(msgParts) && msgParts.length > 0) {
|
|
856
|
+
var textParts = msgParts.filter(function(p) { return p.type === 'text'; });
|
|
857
|
+
if (textParts.length > 0) {
|
|
858
|
+
return textParts.map(function(p) { return p.content; }).join('\n\n');
|
|
859
|
+
}
|
|
834
860
|
}
|
|
835
861
|
// Priority 4: raw content from message (server-stored response)
|
|
836
862
|
if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
|
|
@@ -983,10 +1009,11 @@ async function sendMessage() {
|
|
|
983
1009
|
// Incremental streaming token
|
|
984
1010
|
currentText += evt.content;
|
|
985
1011
|
if (_isV2Mode) {
|
|
986
|
-
// In V2 mode, text_delta contains raw XML — store separately
|
|
1012
|
+
// In V2 mode, text_delta contains raw XML — store separately
|
|
987
1013
|
_v2RawXml = currentText;
|
|
988
1014
|
// Show V2 reasoning text as streaming content (if available)
|
|
989
|
-
|
|
1015
|
+
// If no reasoning yet, show a stripped version of raw text so user sees real-time progress
|
|
1016
|
+
state.messages[msgIdx]._streamingText = _v2ReasoningText || _stripXmlTags(currentText);
|
|
990
1017
|
} else {
|
|
991
1018
|
// V1 mode: text_delta IS the user-facing content
|
|
992
1019
|
state.messages[msgIdx]._streamingText = currentText;
|
|
@@ -1083,6 +1110,8 @@ async function sendMessage() {
|
|
|
1083
1110
|
_isV2Mode = true;
|
|
1084
1111
|
} else if (evt.type === 'v2_output_parsed') {
|
|
1085
1112
|
// LLM output was parsed into structured format
|
|
1113
|
+
// IMPORTANT: Also mark V2 mode here — some server configs may skip v2_context
|
|
1114
|
+
_isV2Mode = true;
|
|
1086
1115
|
// evt.data contains: {usersays_correct, task_plan, tools_to_call, remember, recall, ask_user, finish}
|
|
1087
1116
|
// Store for rendering
|
|
1088
1117
|
state.messages[msgIdx]._v2Parsed = evt.data;
|
|
@@ -1163,9 +1192,10 @@ async function sendMessage() {
|
|
|
1163
1192
|
if (!state.messages[msgIdx]._v2Reasoning) {
|
|
1164
1193
|
state.messages[msgIdx]._v2Reasoning = '';
|
|
1165
1194
|
}
|
|
1166
|
-
//
|
|
1167
|
-
|
|
1168
|
-
|
|
1195
|
+
// Smart separator: only add newline if previous reasoning doesn't already end with one
|
|
1196
|
+
var prev = state.messages[msgIdx]._v2Reasoning;
|
|
1197
|
+
if (prev.length > 0 && evt.content && !prev.endsWith('\n') && !evt.content.startsWith('\n')) {
|
|
1198
|
+
state.messages[msgIdx]._v2Reasoning += '\n';
|
|
1169
1199
|
}
|
|
1170
1200
|
state.messages[msgIdx]._v2Reasoning += evt.content;
|
|
1171
1201
|
_v2ReasoningText = state.messages[msgIdx]._v2Reasoning;
|