myagent-ai 1.9.3 → 1.9.5
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 +38 -34
- package/core/output_parser.py +1 -1
- package/memory/manager.py +65 -0
- package/package.json +1 -1
- package/web/api_server.py +7 -1
- package/web/ui/chat/flow_engine.js +89 -12
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>: 当信息不足需要用户补充时,在此填写要问的问题
|
|
@@ -1347,39 +1347,43 @@ status 取值:
|
|
|
1347
1347
|
if parsed.usersays_correct:
|
|
1348
1348
|
context.working_memory["usersays_correct"] = parsed.usersays_correct
|
|
1349
1349
|
|
|
1350
|
-
# Step 6: 处理 remember —
|
|
1351
|
-
if parsed.remember
|
|
1350
|
+
# Step 6: 处理 remember — 查重后存入长期记忆
|
|
1351
|
+
if parsed.remember:
|
|
1352
1352
|
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
|
-
|
|
1353
|
+
# 查重:跳过与已有记忆高度重复的内容
|
|
1354
|
+
is_dup = False
|
|
1355
|
+
if self.memory:
|
|
1356
|
+
is_dup = self.memory.is_duplicate_memory(
|
|
1357
|
+
content=parsed.remember,
|
|
1358
|
+
session_id=context.session_id,
|
|
1359
|
+
key="conversation_insight",
|
|
1360
|
+
)
|
|
1361
|
+
if is_dup:
|
|
1362
|
+
logger.debug(f"[{task_id}] 记忆查重: 跳过重复内容")
|
|
1363
|
+
else:
|
|
1364
|
+
if self.memory_agent:
|
|
1365
|
+
mem_ctx = AgentContext(
|
|
1366
|
+
task_id=task_id,
|
|
1367
|
+
session_id=context.session_id,
|
|
1368
|
+
metadata={
|
|
1369
|
+
"memory_action": "save",
|
|
1370
|
+
"content": parsed.remember,
|
|
1371
|
+
},
|
|
1372
|
+
)
|
|
1373
|
+
await self.memory_agent.process(mem_ctx)
|
|
1374
|
+
elif self.memory:
|
|
1375
|
+
self.memory.add_long_term(
|
|
1376
|
+
session_id=context.session_id,
|
|
1377
|
+
key="conversation_insight",
|
|
1378
|
+
content=parsed.remember,
|
|
1379
|
+
summary=truncate_str(parsed.remember, 200),
|
|
1380
|
+
importance=0.7,
|
|
1381
|
+
)
|
|
1382
|
+
await self._emit_v2_event(
|
|
1383
|
+
"v2_memory_saved",
|
|
1384
|
+
{"content": truncate_str(parsed.remember, 200)},
|
|
1385
|
+
stream_callback,
|
|
1386
|
+
)
|
|
1383
1387
|
except Exception as e:
|
|
1384
1388
|
logger.warning(f"[{task_id}] 存入记忆失败: {e}")
|
|
1385
1389
|
|
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/memory/manager.py
CHANGED
|
@@ -479,6 +479,71 @@ class MemoryManager:
|
|
|
479
479
|
# 长期记忆 (持久知识)
|
|
480
480
|
# ==========================================================================
|
|
481
481
|
|
|
482
|
+
def is_duplicate_memory(
|
|
483
|
+
self,
|
|
484
|
+
content: str,
|
|
485
|
+
session_id: str = "",
|
|
486
|
+
key: str = "",
|
|
487
|
+
similarity_threshold: float = 0.85,
|
|
488
|
+
) -> bool:
|
|
489
|
+
"""
|
|
490
|
+
检查是否已存在高度相似的记忆(查重)。
|
|
491
|
+
|
|
492
|
+
使用 TF-IDF 余弦相似度判断新内容是否与已有记忆高度重复。
|
|
493
|
+
相似度超过阈值则视为重复。
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
content: 待检查的新记忆内容
|
|
497
|
+
session_id: 会话 ID(为空则跨会话查重)
|
|
498
|
+
key: 记忆分类键(为空则不限制分类)
|
|
499
|
+
similarity_threshold: 相似度阈值,超过此值视为重复(默认 0.85)
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
True 表示存在重复记忆,False 表示可以存储
|
|
503
|
+
"""
|
|
504
|
+
if not content or not content.strip():
|
|
505
|
+
return True # 空内容视为重复
|
|
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 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 False
|
|
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 False
|
|
536
|
+
|
|
537
|
+
max_score = max(scores.values())
|
|
538
|
+
if max_score >= similarity_threshold:
|
|
539
|
+
logger.debug(
|
|
540
|
+
f"记忆查重: 发现重复内容 (相似度={max_score:.2f}, "
|
|
541
|
+
f"阈值={similarity_threshold}, 匹配ID={max(scores, key=scores.get)})"
|
|
542
|
+
)
|
|
543
|
+
return True
|
|
544
|
+
|
|
545
|
+
return False
|
|
546
|
+
|
|
482
547
|
def add_long_term(
|
|
483
548
|
self,
|
|
484
549
|
session_id: str = "global",
|
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -3064,7 +3064,13 @@ class ApiServer:
|
|
|
3064
3064
|
# 返回最终响应
|
|
3065
3065
|
final_response = v2_context.working_memory.get("final_response", "")
|
|
3066
3066
|
if not final_response and _v2_reasoning_parts:
|
|
3067
|
-
|
|
3067
|
+
# Smart join: only add separator when parts don't already end with newline
|
|
3068
|
+
joined_parts = []
|
|
3069
|
+
for i, part in enumerate(_v2_reasoning_parts):
|
|
3070
|
+
if i > 0 and joined_parts and not joined_parts[-1].endswith('\n') and not part.startswith('\n'):
|
|
3071
|
+
joined_parts.append('\n')
|
|
3072
|
+
joined_parts.append(part)
|
|
3073
|
+
final_response = ''.join(joined_parts)
|
|
3068
3074
|
return final_response
|
|
3069
3075
|
|
|
3070
3076
|
# ── V1 路由: 标准 JSON action 格式 ──
|
|
@@ -814,6 +814,54 @@ 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
|
+
|
|
840
|
+
// ══════════════════════════════════════════════════════
|
|
841
|
+
// ── V2 Content Assembler (V2 内容组装) ──
|
|
842
|
+
// ══════════════════════════════════════════════════════
|
|
843
|
+
|
|
844
|
+
function _assembleV2Content(msg, msgParts) {
|
|
845
|
+
// Priority 1: V2 reasoning text (user-facing response from v2_reasoning events)
|
|
846
|
+
if (msg._v2Reasoning && msg._v2Reasoning.trim()) {
|
|
847
|
+
return msg._v2Reasoning.trim();
|
|
848
|
+
}
|
|
849
|
+
// Priority 2: V2 ask user text
|
|
850
|
+
if (msg._askUser && msg._askUser.trim()) {
|
|
851
|
+
return msg._askUser.trim();
|
|
852
|
+
}
|
|
853
|
+
// Priority 3: V1 text parts (backward compat — non-V2 mode)
|
|
854
|
+
var textParts = msgParts.filter(function(p) { return p.type === 'text'; });
|
|
855
|
+
if (textParts.length > 0) {
|
|
856
|
+
return textParts.map(function(p) { return p.content; }).join('\n\n');
|
|
857
|
+
}
|
|
858
|
+
// Priority 4: raw content from message (server-stored response)
|
|
859
|
+
if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
|
|
860
|
+
return msg.content.trim();
|
|
861
|
+
}
|
|
862
|
+
return '(无回复)';
|
|
863
|
+
}
|
|
864
|
+
|
|
817
865
|
// ══════════════════════════════════════════════════════
|
|
818
866
|
// ── Send Message (核心 SSE 流式消息发送) ──
|
|
819
867
|
// ══════════════════════════════════════════════════════
|
|
@@ -904,10 +952,17 @@ async function sendMessage() {
|
|
|
904
952
|
let msgIdx = state.messages.length;
|
|
905
953
|
let sessionIdReceived = sessionId;
|
|
906
954
|
let fullThought = '';
|
|
955
|
+
let _isV2Mode = false; // Track whether V2 structured output events are received
|
|
956
|
+
let _v2RawXml = ''; // In V2 mode, accumulate raw XML from text_delta separately
|
|
957
|
+
let _v2ReasoningText = ''; // Accumulated V2 reasoning text for content
|
|
907
958
|
|
|
908
959
|
function flushCurrentText() {
|
|
909
960
|
if (currentText.trim()) {
|
|
910
|
-
|
|
961
|
+
// In V2 mode, text_delta contains raw XML — do NOT add to msgParts as user-facing text
|
|
962
|
+
if (!_isV2Mode) {
|
|
963
|
+
msgParts.push({type: 'text', content: currentText});
|
|
964
|
+
}
|
|
965
|
+
// Always accumulate for backward compat content field
|
|
911
966
|
}
|
|
912
967
|
currentText = '';
|
|
913
968
|
}
|
|
@@ -950,12 +1005,17 @@ async function sendMessage() {
|
|
|
950
1005
|
} else if (evt.type === 'text_delta') {
|
|
951
1006
|
// Incremental streaming token
|
|
952
1007
|
currentText += evt.content;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1008
|
+
if (_isV2Mode) {
|
|
1009
|
+
// In V2 mode, text_delta contains raw XML — store separately
|
|
1010
|
+
_v2RawXml = currentText;
|
|
1011
|
+
// Show V2 reasoning text as streaming content (if available)
|
|
1012
|
+
// If no reasoning yet, show a stripped version of raw text so user sees real-time progress
|
|
1013
|
+
state.messages[msgIdx]._streamingText = _v2ReasoningText || _stripXmlTags(currentText);
|
|
1014
|
+
} else {
|
|
1015
|
+
// V1 mode: text_delta IS the user-facing content
|
|
1016
|
+
state.messages[msgIdx]._streamingText = currentText;
|
|
1017
|
+
}
|
|
956
1018
|
state.messages[msgIdx].parts = [...msgParts];
|
|
957
|
-
state.messages[msgIdx]._streamingText = currentText;
|
|
958
|
-
state.messages[msgIdx].content = allText;
|
|
959
1019
|
throttledStreamUpdate(msgIdx);
|
|
960
1020
|
// ── 分段流式 TTS:推送增量文本 ──
|
|
961
1021
|
if (ttsManager.enabled && !ttsManager._streamActive) {
|
|
@@ -982,7 +1042,7 @@ async function sendMessage() {
|
|
|
982
1042
|
if (state.messages[msgIdx]) {
|
|
983
1043
|
state.messages[msgIdx].streaming = false;
|
|
984
1044
|
state.messages[msgIdx].parts = [...msgParts];
|
|
985
|
-
state.messages[msgIdx].content =
|
|
1045
|
+
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
986
1046
|
state.messages[msgIdx]._streamingText = '';
|
|
987
1047
|
if (allExecEvents.length > 0) state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
988
1048
|
}
|
|
@@ -1000,7 +1060,7 @@ async function sendMessage() {
|
|
|
1000
1060
|
flushCurrentText();
|
|
1001
1061
|
state.messages[msgIdx].parts = [...msgParts];
|
|
1002
1062
|
state.messages[msgIdx]._streamingText = '';
|
|
1003
|
-
state.messages[msgIdx].content =
|
|
1063
|
+
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
1004
1064
|
throttledStreamUpdate(msgIdx);
|
|
1005
1065
|
// 停止当前轮次的 TTS 播放,防止旧迭代语音与新迭代语音互相打断
|
|
1006
1066
|
if (ttsManager.enabled && ttsManager._streamActive) {
|
|
@@ -1041,7 +1101,10 @@ async function sendMessage() {
|
|
|
1041
1101
|
}
|
|
1042
1102
|
}
|
|
1043
1103
|
}
|
|
1044
|
-
// V2 Structured Output Events
|
|
1104
|
+
// V2 Structured Output Events — mark V2 mode
|
|
1105
|
+
} else if (evt.type === 'v2_context') {
|
|
1106
|
+
// V2 context event — confirms we're in V2 mode
|
|
1107
|
+
_isV2Mode = true;
|
|
1045
1108
|
} else if (evt.type === 'v2_output_parsed') {
|
|
1046
1109
|
// LLM output was parsed into structured format
|
|
1047
1110
|
// evt.data contains: {usersays_correct, task_plan, tools_to_call, remember, recall, ask_user, finish}
|
|
@@ -1120,11 +1183,23 @@ async function sendMessage() {
|
|
|
1120
1183
|
state.messages[msgIdx]._memorySaved = (state.messages[msgIdx]._memorySaved || '') +
|
|
1121
1184
|
(evt.content ? evt.content.substring(0, 100) : '');
|
|
1122
1185
|
} else if (evt.type === 'v2_reasoning') {
|
|
1123
|
-
// V2 reasoning text from model
|
|
1186
|
+
// V2 reasoning text from model — this IS the user-facing content in V2 mode
|
|
1124
1187
|
if (!state.messages[msgIdx]._v2Reasoning) {
|
|
1125
1188
|
state.messages[msgIdx]._v2Reasoning = '';
|
|
1126
1189
|
}
|
|
1190
|
+
// Smart separator: only add newline if previous reasoning doesn't already end with one
|
|
1191
|
+
var prev = state.messages[msgIdx]._v2Reasoning;
|
|
1192
|
+
if (prev.length > 0 && evt.content && !prev.endsWith('\n') && !evt.content.startsWith('\n')) {
|
|
1193
|
+
state.messages[msgIdx]._v2Reasoning += '\n';
|
|
1194
|
+
}
|
|
1127
1195
|
state.messages[msgIdx]._v2Reasoning += evt.content;
|
|
1196
|
+
_v2ReasoningText = state.messages[msgIdx]._v2Reasoning;
|
|
1197
|
+
// In V2 mode, update streaming text to show reasoning content
|
|
1198
|
+
if (_isV2Mode) {
|
|
1199
|
+
state.messages[msgIdx]._streamingText = _v2ReasoningText;
|
|
1200
|
+
// Also update content for real-time display
|
|
1201
|
+
state.messages[msgIdx].content = _v2ReasoningText;
|
|
1202
|
+
}
|
|
1128
1203
|
throttledStreamUpdate(msgIdx);
|
|
1129
1204
|
} else if (evt.type === 'done') {
|
|
1130
1205
|
flushCurrentText();
|
|
@@ -1135,7 +1210,8 @@ async function sendMessage() {
|
|
|
1135
1210
|
state.messages[msgIdx].parts = [...msgParts];
|
|
1136
1211
|
state.messages[msgIdx]._streamingText = '';
|
|
1137
1212
|
state.messages[msgIdx].exec_events = [...allExecEvents];
|
|
1138
|
-
|
|
1213
|
+
// Assemble final content: prefer V2 reasoning/ask text over raw XML
|
|
1214
|
+
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
1139
1215
|
} else if (evt.type === 'reasoning_delta') {
|
|
1140
1216
|
// 模型推理过程增量文本(OpenAI o1/o3/DeepSeek-R1 等推理模型)
|
|
1141
1217
|
if (!state.messages[msgIdx].reasoning) state.messages[msgIdx].reasoning = '';
|
|
@@ -1164,7 +1240,8 @@ async function sendMessage() {
|
|
|
1164
1240
|
state.messages[msgIdx].parts = [...msgParts];
|
|
1165
1241
|
state.messages[msgIdx]._streamingText = '';
|
|
1166
1242
|
state.messages[msgIdx].exec_events = allExecEvents;
|
|
1167
|
-
|
|
1243
|
+
// Assemble final content: prefer V2 reasoning/ask text over raw XML
|
|
1244
|
+
state.messages[msgIdx].content = _assembleV2Content(state.messages[msgIdx], msgParts);
|
|
1168
1245
|
}
|
|
1169
1246
|
|
|
1170
1247
|
// Task list 已通过 SSE task_list_update 事件实时推送,无需再轮询
|