myagent-ai 1.9.5 → 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.
@@ -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,38 +1424,63 @@ status 取值:
1347
1424
  if parsed.usersays_correct:
1348
1425
  context.working_memory["usersays_correct"] = parsed.usersays_correct
1349
1426
 
1350
- # Step 6: 处理 remember — 查重后存入长期记忆
1427
+ # Step 6: 处理 remember — 查重+LLM合并后存入长期记忆
1351
1428
  if parsed.remember:
1352
1429
  try:
1353
- # 查重:跳过与已有记忆高度重复的内容
1354
- is_dup = False
1355
1430
  if self.memory:
1356
- is_dup = self.memory.is_duplicate_memory(
1431
+ # 查找是否有相似记忆
1432
+ dup_memory = self.memory.find_duplicate_memory(
1357
1433
  content=parsed.remember,
1358
1434
  session_id=context.session_id,
1359
1435
  key="conversation_insight",
1360
1436
  )
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
- },
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})"
1372
1442
  )
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,
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,
1381
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
+ )
1382
1484
  await self._emit_v2_event(
1383
1485
  "v2_memory_saved",
1384
1486
  {"content": truncate_str(parsed.remember, 200)},
@@ -1474,6 +1576,7 @@ status 取值:
1474
1576
 
1475
1577
  # 发送 beforecalltext 作为显示文本
1476
1578
  if before_call:
1579
+ _v2_reasoning_collected.append(before_call)
1477
1580
  await self._emit_v2_event(
1478
1581
  "v2_reasoning",
1479
1582
  {"content": before_call},
@@ -1552,9 +1655,15 @@ status 取值:
1552
1655
  # 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
1553
1656
  if parsed.finish:
1554
1657
  logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
1555
- final_text = "已完成所有操作。"
1556
- if current_task_plan:
1557
- final_text += f"\n\n任务计划:\n{current_task_plan}"
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}"
1558
1667
  context.working_memory["final_response"] = final_text
1559
1668
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1560
1669
  if self.memory:
@@ -1568,9 +1677,14 @@ status 取值:
1568
1677
  # finish=false: 根据工具的 callback 标志决定是否回调
1569
1678
  if not need_callback:
1570
1679
  logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
1571
- final_text = "已完成所有操作。"
1572
- if current_task_plan:
1573
- final_text += f"\n\n任务计划:\n{current_task_plan}"
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}"
1574
1688
  context.working_memory["final_response"] = final_text
1575
1689
  await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
1576
1690
  if self.memory:
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,18 +479,18 @@ class MemoryManager:
479
479
  # 长期记忆 (持久知识)
480
480
  # ==========================================================================
481
481
 
482
- def is_duplicate_memory(
482
+ def find_duplicate_memory(
483
483
  self,
484
484
  content: str,
485
485
  session_id: str = "",
486
486
  key: str = "",
487
487
  similarity_threshold: float = 0.85,
488
- ) -> bool:
488
+ ) -> Optional[MemoryEntry]:
489
489
  """
490
- 检查是否已存在高度相似的记忆(查重)。
490
+ 查找与给定内容高度相似的已有记忆(查重)。
491
491
 
492
492
  使用 TF-IDF 余弦相似度判断新内容是否与已有记忆高度重复。
493
- 相似度超过阈值则视为重复。
493
+ 相似度超过阈值则返回匹配的旧记忆,否则返回 None。
494
494
 
495
495
  Args:
496
496
  content: 待检查的新记忆内容
@@ -499,10 +499,10 @@ class MemoryManager:
499
499
  similarity_threshold: 相似度阈值,超过此值视为重复(默认 0.85)
500
500
 
501
501
  Returns:
502
- True 表示存在重复记忆,False 表示可以存储
502
+ 匹配的旧 MemoryEntry,或 None(无重复)
503
503
  """
504
504
  if not content or not content.strip():
505
- return True # 空内容视为重复
505
+ return None # 空内容不查重
506
506
 
507
507
  conn = self._get_conn()
508
508
  conditions = ["category = 'long_term'"]
@@ -521,27 +521,84 @@ class MemoryManager:
521
521
 
522
522
  where = " AND ".join(conditions)
523
523
  # 取最近的记忆候选进行比对
524
- sql = f"SELECT id, content, summary FROM memories WHERE {where} ORDER BY created_at DESC LIMIT 50"
524
+ sql = f"SELECT id, content, summary, created_at FROM memories WHERE {where} ORDER BY created_at DESC LIMIT 50"
525
525
  rows = conn.execute(sql, params).fetchall()
526
526
 
527
527
  if not rows:
528
- return False
528
+ return None
529
529
 
530
530
  # 使用 TF-IDF 计算新内容与每条已有记忆的相似度
531
531
  documents = [(row["id"], row["content"]) for row in rows]
532
532
  scores = self._compute_tfidf(content, documents)
533
533
 
534
534
  if not scores:
535
- return False
535
+ return None
536
536
 
537
- max_score = max(scores.values())
537
+ max_id = max(scores, key=scores.get)
538
+ max_score = scores[max_id]
538
539
  if max_score >= similarity_threshold:
539
540
  logger.debug(
540
- f"记忆查重: 发现重复内容 (相似度={max_score:.2f}, "
541
- f"阈值={similarity_threshold}, 匹配ID={max(scores, key=scores.get)})"
541
+ f"记忆查重: 发现相似内容 (相似度={max_score:.2f}, "
542
+ f"阈值={similarity_threshold}, 匹配ID={max_id})"
542
543
  )
543
- return True
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
544
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
545
602
  return False
546
603
 
547
604
  def add_long_term(
@@ -563,14 +620,23 @@ class MemoryManager:
563
620
  summary: 简要摘要
564
621
  importance: 重要性(越高越不容易被淘汰)
565
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
+
566
629
  entry = MemoryEntry(
567
630
  session_id=session_id,
568
631
  category="long_term",
569
632
  key=key,
570
- content=truncate_str(content, 50000),
571
- summary=summary,
633
+ content=timestamped_content,
634
+ summary=f"[{now_str}] {ts_summary}",
572
635
  importance=importance,
573
- metadata=metadata or {},
636
+ metadata={
637
+ "timestamp": now_str,
638
+ **(metadata or {}),
639
+ },
574
640
  )
575
641
  return self._insert(entry)
576
642
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.9.5",
3
+ "version": "1.9.6",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -3063,14 +3063,19 @@ class ApiServer:
3063
3063
 
3064
3064
  # 返回最终响应
3065
3065
  final_response = v2_context.working_memory.get("final_response", "")
3066
- if not final_response and _v2_reasoning_parts:
3066
+ # 优先使用 v2_reasoning 流式推送的文本(用户实际看到的内容),
3067
+ # 而不是后端设定的通用占位符如"已完成所有操作。"
3068
+ if _v2_reasoning_parts:
3067
3069
  # Smart join: only add separator when parts don't already end with newline
3068
3070
  joined_parts = []
3069
3071
  for i, part in enumerate(_v2_reasoning_parts):
3070
3072
  if i > 0 and joined_parts and not joined_parts[-1].endswith('\n') and not part.startswith('\n'):
3071
3073
  joined_parts.append('\n')
3072
3074
  joined_parts.append(part)
3073
- final_response = ''.join(joined_parts)
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
3074
3079
  return final_response
3075
3080
 
3076
3081
  # ── V1 路由: 标准 JSON action 格式 ──
@@ -851,9 +851,12 @@ function _assembleV2Content(msg, msgParts) {
851
851
  return msg._askUser.trim();
852
852
  }
853
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');
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
+ }
857
860
  }
858
861
  // Priority 4: raw content from message (server-stored response)
859
862
  if (msg.content && msg.content.trim() && msg.content !== '(无回复)') {
@@ -1107,6 +1110,8 @@ async function sendMessage() {
1107
1110
  _isV2Mode = true;
1108
1111
  } else if (evt.type === 'v2_output_parsed') {
1109
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;
1110
1115
  // evt.data contains: {usersays_correct, task_plan, tools_to_call, remember, recall, ask_user, finish}
1111
1116
  // Store for rendering
1112
1117
  state.messages[msgIdx]._v2Parsed = evt.data;