myagent-ai 1.10.5 → 1.10.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/__pycache__/main_agent.cpython-312.pyc +0 -0
- package/agents/main_agent.py +309 -68
- package/agents/memory_agent.py +20 -18
- package/config.py +7 -2
- package/core/__pycache__/context_builder.cpython-312.pyc +0 -0
- package/core/__pycache__/output_parser.cpython-312.pyc +0 -0
- package/core/context_builder.py +14 -4
- package/core/output_parser.py +63 -5
- package/main.py +3 -4
- package/memory/manager.py +170 -168
- package/package.json +1 -1
- package/skills/__pycache__/browser_skill.cpython-312.pyc +0 -0
- package/skills/__pycache__/file_skill.cpython-312.pyc +0 -0
- package/skills/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/browser_skill.py +6 -1
- package/skills/file_skill.py +6 -1
- package/skills/registry.py +4 -2
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +18 -17
|
Binary file
|
package/agents/main_agent.py
CHANGED
|
@@ -48,11 +48,14 @@ class MainAgent(BaseAgent):
|
|
|
48
48
|
<toolstocal>
|
|
49
49
|
<tool><beforecalltext>连接词,介绍调用什么工具,达到什么目的。</beforecalltext><toolname>工具名</toolname><parms>JSON格式的参数对象,例如: {"query": "搜索关键词", "num": 5}</parms><timeout>预估超时时限(秒)</timeout><callback>true/false,要求解析器在该工具执行完后是否要回调llm大模型,将所有工具输出结果+新构造的"context"输入给llm</callback></tool>
|
|
50
50
|
</toolstocal>
|
|
51
|
-
<remember>仅从最新用户输入(userprint 或 usersays_correct
|
|
51
|
+
<remember><type>global或session</type><content>仅从最新用户输入(userprint 或 usersays_correct)中提炼值得记忆的信息(如用户偏好、重要结论、错误经验等)。type=global表示跨会话全局记忆,type=session表示仅当前会话可用的记忆。如果本轮没有新信息需要记忆,则<content>为空、<type>不填。</content></remember>
|
|
52
52
|
<recall>下一轮执行需要调取的记忆,这里要设计接上记忆库</recall>
|
|
53
|
+
<knowledge>从本轮对话或工具执行结果中提炼值得长期保存到知识库的专业知识、事实、经验法则、技术要点等。这些知识将被持久化存储,未来可通过 <get_knowledge> 检索复用。如果本轮没有需要保存的知识,则为空。格式要求:简洁明确,每条知识一行,用换行分隔。</knowledge>
|
|
53
54
|
<get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
|
|
54
55
|
<askuser>需要询问用户的内容,如无,则为空</askuser>
|
|
55
56
|
<finish>true/false,是否结束循环调用llm。如"askuser"为非空,则"finish"输出true。否则,根据"context"判断任务是否已完成,是否结束llm回调</finish>
|
|
57
|
+
<finish_reason>当 finish=true 时必填,详细说明为什么现在结束任务(如:任务已完成/需要用户补充信息/信息不足无法继续等)。finish=false 时为空。</finish_reason>
|
|
58
|
+
<next_step>当 finish=false 时必填,描述下一步计划做什么(简洁明了,1-2句话)。finish=true 时为空。</next_step>
|
|
56
59
|
|
|
57
60
|
</output>
|
|
58
61
|
|
|
@@ -65,12 +68,15 @@ class MainAgent(BaseAgent):
|
|
|
65
68
|
6. <parms>: **必须使用严格合法的JSON格式**,例如 {"query": "关键词", "num": 10},不要使用其他格式
|
|
66
69
|
7. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
|
|
67
70
|
8. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
|
|
68
|
-
9. <remember>:
|
|
71
|
+
9. <remember>: 包含 <type> 和 <content> 子标签。type 填 "global"(跨会话全局记忆)或 "session"(仅当前会话)。content 填从最新用户输入中提炼的值得记忆的关键信息。如果本轮无需记忆,content 为空且不填 type。注意:用户个人偏好、重要结论、通用经验用 global;当前任务的临时上下文、过程信息用 session
|
|
69
72
|
10. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
|
|
70
|
-
11. <
|
|
71
|
-
12. <
|
|
72
|
-
13. <
|
|
73
|
-
14.
|
|
73
|
+
11. <knowledge>: 从本轮对话或工具执行结果中提炼值得长期保存的专业知识、事实、经验法则、技术要点等。这些知识会被持久化到知识库文件,未来可通过 get_knowledge 检索复用。如果没有需要保存的知识,则为空。格式:简洁明确,每条知识一行
|
|
74
|
+
12. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
|
|
75
|
+
13. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
|
|
76
|
+
14. <finish>: 当任务已完成或需要等待用户回应时为 true;否则为 false 继续执行
|
|
77
|
+
15. <finish_reason>: **finish=true 时必须填写**,详细说明结束原因(任务完成/等待用户/信息不足/无法处理等)
|
|
78
|
+
16. <next_step>: **finish=false 时必须填写**,描述下一步计划做什么,要求简洁明确(1-2句话)
|
|
79
|
+
17. 使用中文输出所有内容
|
|
74
80
|
|
|
75
81
|
## 工具选择指南
|
|
76
82
|
- **搜索信息**: 用 `web_search`(返回标题+URL+摘要),不要用 browser_open
|
|
@@ -257,6 +263,108 @@ class MainAgent(BaseAgent):
|
|
|
257
263
|
logger.warning(f"[{task_id}] 记忆合并异常: {e}")
|
|
258
264
|
return None
|
|
259
265
|
|
|
266
|
+
async def _save_knowledge_to_base(
|
|
267
|
+
self,
|
|
268
|
+
content: str,
|
|
269
|
+
session_id: str,
|
|
270
|
+
task_id: str,
|
|
271
|
+
) -> bool:
|
|
272
|
+
"""
|
|
273
|
+
将 LLM 输出的 <knowledge> 内容追加到知识库文件。
|
|
274
|
+
|
|
275
|
+
存储策略:
|
|
276
|
+
- 知识按会话 (session_id) 分文件存储
|
|
277
|
+
- 文件路径: {knowledge_base_dir}/auto_knowledge/{session_id}.md
|
|
278
|
+
- 每次追加时检查重复(TF-IDF 相似度 ≥ 0.9 视为重复,跳过)
|
|
279
|
+
- 追加时带有时间戳标记
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True 表示成功存储了新知识,False 表示跳过(重复)或失败
|
|
283
|
+
"""
|
|
284
|
+
if not self.context_builder or not self.context_builder.knowledge_base_dir:
|
|
285
|
+
logger.debug(f"[{task_id}] 知识库未配置,跳过 knowledge 存储")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
from datetime import datetime
|
|
289
|
+
from pathlib import Path
|
|
290
|
+
|
|
291
|
+
kb_dir = Path(self.context_builder.knowledge_base_dir)
|
|
292
|
+
auto_kb_dir = kb_dir / "auto_knowledge"
|
|
293
|
+
auto_kb_dir.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
|
|
295
|
+
# 使用 session_id 作为文件名(取前8位避免过长)
|
|
296
|
+
safe_session = session_id.replace("-", "")[:8] if session_id else "default"
|
|
297
|
+
kb_file = auto_kb_dir / f"{safe_session}.md"
|
|
298
|
+
|
|
299
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
300
|
+
|
|
301
|
+
# 检查重复:与已有文件内容做相似度比较
|
|
302
|
+
existing_content = ""
|
|
303
|
+
if kb_file.exists():
|
|
304
|
+
try:
|
|
305
|
+
existing_content = kb_file.read_text(encoding="utf-8")
|
|
306
|
+
except Exception:
|
|
307
|
+
existing_content = ""
|
|
308
|
+
|
|
309
|
+
if existing_content and content.strip():
|
|
310
|
+
# 简单去重:检查新知识是否已存在于文件中
|
|
311
|
+
# 使用逐行比对 + 关键词匹配
|
|
312
|
+
new_lines = [line.strip() for line in content.strip().split("\n") if line.strip()]
|
|
313
|
+
existing_lines = [line.strip() for line in existing_content.split("\n") if line.strip() and not line.strip().startswith("- [")]
|
|
314
|
+
|
|
315
|
+
dup_count = 0
|
|
316
|
+
for new_line in new_lines:
|
|
317
|
+
# 精确匹配或高度相似(共现字符占比 > 85%)
|
|
318
|
+
is_dup = False
|
|
319
|
+
for ex_line in existing_lines:
|
|
320
|
+
# 计算字符重叠率
|
|
321
|
+
set_new = set(new_line)
|
|
322
|
+
set_ex = set(ex_line)
|
|
323
|
+
if not set_new or not set_ex:
|
|
324
|
+
continue
|
|
325
|
+
overlap = len(set_new & set_ex) / max(len(set_new), len(set_ex))
|
|
326
|
+
if overlap >= 0.85 or new_line == ex_line:
|
|
327
|
+
is_dup = True
|
|
328
|
+
break
|
|
329
|
+
if is_dup:
|
|
330
|
+
dup_count += 1
|
|
331
|
+
|
|
332
|
+
if dup_count == len(new_lines):
|
|
333
|
+
logger.info(f"[{task_id}] 知识全部重复,跳过存储 ({dup_count}/{len(new_lines)} 条)")
|
|
334
|
+
return False
|
|
335
|
+
elif dup_count > 0:
|
|
336
|
+
# 过滤掉重复的行
|
|
337
|
+
filtered_lines = []
|
|
338
|
+
for new_line in new_lines:
|
|
339
|
+
is_dup = False
|
|
340
|
+
for ex_line in existing_lines:
|
|
341
|
+
set_new = set(new_line)
|
|
342
|
+
set_ex = set(ex_line)
|
|
343
|
+
if not set_new or not set_ex:
|
|
344
|
+
continue
|
|
345
|
+
overlap = len(set_new & set_ex) / max(len(set_new), len(set_ex))
|
|
346
|
+
if overlap >= 0.85 or new_line == ex_line:
|
|
347
|
+
is_dup = True
|
|
348
|
+
break
|
|
349
|
+
if not is_dup:
|
|
350
|
+
filtered_lines.append(new_line)
|
|
351
|
+
content = "\n".join(filtered_lines)
|
|
352
|
+
logger.info(f"[{task_id}] 知识去重: {dup_count}/{len(new_lines)} 条重复,{len(filtered_lines)} 条新增")
|
|
353
|
+
|
|
354
|
+
# 追加写入
|
|
355
|
+
try:
|
|
356
|
+
with open(kb_file, "a", encoding="utf-8") as f:
|
|
357
|
+
f.write(f"\n## {now_str}\n")
|
|
358
|
+
f.write(content.strip() + "\n")
|
|
359
|
+
logger.info(
|
|
360
|
+
f"[{task_id}] 知识已存入知识库: {kb_file.name} "
|
|
361
|
+
f"({len(content)} 字符, {len(content.strip().split(chr(10)))} 条)"
|
|
362
|
+
)
|
|
363
|
+
return True
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.warning(f"[{task_id}] 知识写入失败: {e}")
|
|
366
|
+
return False
|
|
367
|
+
|
|
260
368
|
async def process_v2(
|
|
261
369
|
self,
|
|
262
370
|
context: AgentContext,
|
|
@@ -398,6 +506,7 @@ class MainAgent(BaseAgent):
|
|
|
398
506
|
task_plan=current_task_plan,
|
|
399
507
|
agent_override_prompt=agent_override_prompt,
|
|
400
508
|
get_knowledge=get_knowledge_content,
|
|
509
|
+
recall=recall_content,
|
|
401
510
|
)
|
|
402
511
|
|
|
403
512
|
await self._emit_v2_event(
|
|
@@ -464,6 +573,8 @@ class MainAgent(BaseAgent):
|
|
|
464
573
|
"remember": truncate_str(parsed.remember, 200),
|
|
465
574
|
"ask_user": truncate_str(parsed.ask_user, 200),
|
|
466
575
|
"finish": parsed.finish,
|
|
576
|
+
"finish_reason": truncate_str(parsed.finish_reason, 200),
|
|
577
|
+
"next_step": truncate_str(parsed.next_step, 200),
|
|
467
578
|
"parse_success": parsed.parse_success,
|
|
468
579
|
}},
|
|
469
580
|
stream_callback,
|
|
@@ -511,77 +622,92 @@ class MainAgent(BaseAgent):
|
|
|
511
622
|
if response_text:
|
|
512
623
|
logger.debug(f"[{task_id}] 模型回复用户: {response_text[:100]}")
|
|
513
624
|
context.working_memory["model_response"] = response_text
|
|
625
|
+
_v2_reasoning_collected.append(response_text)
|
|
514
626
|
await self._emit_v2_event(
|
|
515
627
|
"v2_reasoning",
|
|
516
628
|
{"content": response_text},
|
|
517
629
|
stream_callback,
|
|
518
630
|
)
|
|
519
631
|
|
|
520
|
-
# Step 6: 处理 remember —
|
|
632
|
+
# Step 6: 处理 remember — 按 type 分全局/会话存储
|
|
521
633
|
if parsed.remember:
|
|
522
634
|
try:
|
|
523
635
|
if self.memory:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
logger.info(
|
|
533
|
-
f"[{task_id}] 记忆查重: 发现相似内容,调用LLM合并 "
|
|
534
|
-
f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
|
|
535
|
-
)
|
|
536
|
-
merged_content = await self._merge_duplicate_memory(
|
|
537
|
-
old_memory=dup_memory,
|
|
538
|
-
new_content=parsed.remember,
|
|
539
|
-
context=context,
|
|
540
|
-
task_id=task_id,
|
|
636
|
+
_rem_type = parsed.remember_type or "session"
|
|
637
|
+
|
|
638
|
+
if _rem_type == "global":
|
|
639
|
+
# === 全局记忆:查重 + LLM 合并 → add_global ===
|
|
640
|
+
dup_memory = self.memory.find_duplicate_memory(
|
|
641
|
+
content=parsed.remember,
|
|
642
|
+
session_id=context.session_id,
|
|
643
|
+
key="conversation_insight",
|
|
541
644
|
)
|
|
542
|
-
if
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
content=merged_content,
|
|
547
|
-
summary=truncate_str(merged_content, 200),
|
|
645
|
+
if dup_memory:
|
|
646
|
+
logger.info(
|
|
647
|
+
f"[{task_id}] 全局记忆查重: 发现相似内容,调用LLM合并 "
|
|
648
|
+
f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
|
|
548
649
|
)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
memory_id=dup_memory.id,
|
|
554
|
-
content=parsed.remember,
|
|
555
|
-
)
|
|
556
|
-
logger.info(f"[{task_id}] 记忆直接更新为新内容: {dup_memory.id}")
|
|
557
|
-
else:
|
|
558
|
-
# 无重复,直接存储新记忆
|
|
559
|
-
if self.memory_agent:
|
|
560
|
-
mem_ctx = AgentContext(
|
|
650
|
+
merged_content = await self._merge_duplicate_memory(
|
|
651
|
+
old_memory=dup_memory,
|
|
652
|
+
new_content=parsed.remember,
|
|
653
|
+
context=context,
|
|
561
654
|
task_id=task_id,
|
|
562
|
-
session_id=context.session_id,
|
|
563
|
-
metadata={
|
|
564
|
-
"memory_action": "save",
|
|
565
|
-
"content": parsed.remember,
|
|
566
|
-
},
|
|
567
655
|
)
|
|
568
|
-
|
|
656
|
+
if merged_content:
|
|
657
|
+
self.memory.update_memory(
|
|
658
|
+
memory_id=dup_memory.id,
|
|
659
|
+
content=merged_content,
|
|
660
|
+
summary=truncate_str(merged_content, 200),
|
|
661
|
+
)
|
|
662
|
+
logger.info(f"[{task_id}] 全局记忆已合并更新: {dup_memory.id}")
|
|
663
|
+
else:
|
|
664
|
+
self.memory.update_memory(
|
|
665
|
+
memory_id=dup_memory.id,
|
|
666
|
+
content=parsed.remember,
|
|
667
|
+
)
|
|
668
|
+
logger.info(f"[{task_id}] 全局记忆直接更新: {dup_memory.id}")
|
|
569
669
|
else:
|
|
570
|
-
self.memory.
|
|
670
|
+
self.memory.add_global(
|
|
571
671
|
session_id=context.session_id,
|
|
572
672
|
key="conversation_insight",
|
|
573
673
|
content=parsed.remember,
|
|
574
674
|
summary=truncate_str(parsed.remember, 200),
|
|
575
675
|
importance=0.7,
|
|
576
676
|
)
|
|
677
|
+
else:
|
|
678
|
+
# === 会话记忆:直接存储 → add_session ===
|
|
679
|
+
self.memory.add_session(
|
|
680
|
+
session_id=context.session_id,
|
|
681
|
+
key="conversation_insight",
|
|
682
|
+
content=parsed.remember,
|
|
683
|
+
importance=0.6,
|
|
684
|
+
)
|
|
685
|
+
|
|
577
686
|
await self._emit_v2_event(
|
|
578
687
|
"v2_memory_saved",
|
|
579
|
-
{"content": truncate_str(parsed.remember, 200)},
|
|
688
|
+
{"type": _rem_type, "content": truncate_str(parsed.remember, 200)},
|
|
580
689
|
stream_callback,
|
|
581
690
|
)
|
|
582
691
|
except Exception as e:
|
|
583
692
|
logger.warning(f"[{task_id}] 存入记忆失败: {e}")
|
|
584
693
|
|
|
694
|
+
# Step 6.5: 处理 knowledge — 存入知识库文件
|
|
695
|
+
if parsed.knowledge:
|
|
696
|
+
try:
|
|
697
|
+
kb_saved = await self._save_knowledge_to_base(
|
|
698
|
+
content=parsed.knowledge,
|
|
699
|
+
session_id=context.session_id,
|
|
700
|
+
task_id=task_id,
|
|
701
|
+
)
|
|
702
|
+
if kb_saved:
|
|
703
|
+
await self._emit_v2_event(
|
|
704
|
+
"v2_knowledge_saved",
|
|
705
|
+
{"content": truncate_str(parsed.knowledge, 200)},
|
|
706
|
+
stream_callback,
|
|
707
|
+
)
|
|
708
|
+
except Exception as e:
|
|
709
|
+
logger.warning(f"[{task_id}] 存入知识库失败: {e}")
|
|
710
|
+
|
|
585
711
|
# Step 7: 处理 recall — 记录下一轮需要检索的记忆内容
|
|
586
712
|
if parsed.recall:
|
|
587
713
|
recall_content = parsed.recall
|
|
@@ -654,6 +780,7 @@ class MainAgent(BaseAgent):
|
|
|
654
780
|
# Step 11: 有工具调用 — 先执行所有工具,再根据 finish 决定回调
|
|
655
781
|
need_callback = False
|
|
656
782
|
tool_outputs_parts = []
|
|
783
|
+
_reasoning_len_before_round = len(_v2_reasoning_collected) # 记录本轮开始时的长度
|
|
657
784
|
|
|
658
785
|
for tool_info in parsed.tools_to_call:
|
|
659
786
|
tool_name = tool_info.get("toolname", "").strip()
|
|
@@ -700,38 +827,103 @@ class MainAgent(BaseAgent):
|
|
|
700
827
|
|
|
701
828
|
# 发送工具结果事件
|
|
702
829
|
# 提取实际输出:SkillResult 有 output/message/data,ExecResult 有 stdout/stderr
|
|
703
|
-
def _format_data_for_llm(data):
|
|
704
|
-
"""将结构化 data
|
|
705
|
-
|
|
830
|
+
def _format_data_for_llm(data, _depth=0):
|
|
831
|
+
"""将结构化 data 递归格式化为 LLM 可读的文本"""
|
|
832
|
+
_MAX_DEPTH = 3
|
|
833
|
+
_MAX_LIST_ITEMS = 50
|
|
834
|
+
|
|
835
|
+
if data is None or _depth > _MAX_DEPTH:
|
|
706
836
|
return ""
|
|
707
837
|
if isinstance(data, str):
|
|
708
838
|
return data
|
|
839
|
+
if isinstance(data, (int, float, bool)):
|
|
840
|
+
return str(data)
|
|
841
|
+
|
|
842
|
+
if isinstance(data, list):
|
|
843
|
+
lines = []
|
|
844
|
+
for i, item in enumerate(data[:_MAX_LIST_ITEMS], 1):
|
|
845
|
+
if isinstance(item, dict):
|
|
846
|
+
# 优先提取名称类字段作为主标题
|
|
847
|
+
name = (
|
|
848
|
+
item.get("name") or item.get("title")
|
|
849
|
+
or item.get("text") or item.get("path")
|
|
850
|
+
or item.get("file") or item.get("url") or ""
|
|
851
|
+
)
|
|
852
|
+
# 其余字段作为详细信息
|
|
853
|
+
detail_parts = []
|
|
854
|
+
for k, v in item.items():
|
|
855
|
+
if k in ("name", "title", "text") and name:
|
|
856
|
+
continue
|
|
857
|
+
if v is None or v == "" or v == []:
|
|
858
|
+
continue
|
|
859
|
+
if isinstance(v, (list, dict)):
|
|
860
|
+
sub = _format_data_for_llm(v, _depth + 1)
|
|
861
|
+
if sub:
|
|
862
|
+
detail_parts.append(f"{k}={sub}")
|
|
863
|
+
else:
|
|
864
|
+
detail_parts.append(f"{k}={v}")
|
|
865
|
+
detail = ", ".join(detail_parts)
|
|
866
|
+
lines.append(
|
|
867
|
+
f"{i}. {name}" + (f" ({detail})" if detail else "")
|
|
868
|
+
)
|
|
869
|
+
elif isinstance(item, (list, dict)):
|
|
870
|
+
sub = _format_data_for_llm(item, _depth + 1)
|
|
871
|
+
lines.append(f"{i}. {sub}" if sub else f"{i}. (空)")
|
|
872
|
+
else:
|
|
873
|
+
lines.append(f"{i}. {item}")
|
|
874
|
+
if len(data) > _MAX_LIST_ITEMS:
|
|
875
|
+
lines.append(f"... 共 {len(data)} 项,仅显示前 {_MAX_LIST_ITEMS} 项")
|
|
876
|
+
return "\n".join(lines)
|
|
877
|
+
|
|
709
878
|
if isinstance(data, dict):
|
|
710
879
|
# 搜索结果列表格式 (web_search)
|
|
711
880
|
results = data.get("results")
|
|
712
881
|
if isinstance(results, list):
|
|
713
882
|
lines = []
|
|
714
|
-
for i, r in enumerate(results, 1):
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
883
|
+
for i, r in enumerate(results[:_MAX_LIST_ITEMS], 1):
|
|
884
|
+
if isinstance(r, dict):
|
|
885
|
+
title = r.get("title", "")
|
|
886
|
+
url = r.get("url", "")
|
|
887
|
+
snippet = r.get("snippet", "")
|
|
888
|
+
lines.append(
|
|
889
|
+
f"{i}. {title}\n URL: {url}\n {snippet}"
|
|
890
|
+
)
|
|
891
|
+
else:
|
|
892
|
+
lines.append(f"{i}. {r}")
|
|
719
893
|
return "\n".join(lines)
|
|
894
|
+
|
|
720
895
|
# 网页内容格式 (web_read)
|
|
721
896
|
if "url" in data and "content" in data:
|
|
722
897
|
title = data.get("title", "")
|
|
723
898
|
content = data.get("content", "")
|
|
724
|
-
lines = [
|
|
899
|
+
lines = [
|
|
900
|
+
f"标题: {title}",
|
|
901
|
+
f"URL: {data['url']}",
|
|
902
|
+
f"内容:\n{content}",
|
|
903
|
+
]
|
|
725
904
|
return "\n".join(lines)
|
|
726
|
-
|
|
905
|
+
|
|
906
|
+
# 通用 dict: 递归格式化嵌套结构
|
|
727
907
|
parts = []
|
|
728
908
|
for k, v in data.items():
|
|
729
909
|
if k == "results":
|
|
730
910
|
continue # 已在上面处理
|
|
731
|
-
|
|
911
|
+
if isinstance(v, list):
|
|
912
|
+
if len(v) == 0:
|
|
913
|
+
parts.append(f"{k}: (空)")
|
|
914
|
+
else:
|
|
915
|
+
sub = _format_data_for_llm(v, _depth + 1)
|
|
916
|
+
parts.append(f"{k}:\n{sub}")
|
|
917
|
+
elif isinstance(v, dict):
|
|
918
|
+
if not v:
|
|
919
|
+
parts.append(f"{k}: (空)")
|
|
920
|
+
else:
|
|
921
|
+
sub = _format_data_for_llm(v, _depth + 1)
|
|
922
|
+
parts.append(f"{k}:\n{sub}")
|
|
923
|
+
else:
|
|
924
|
+
parts.append(f"{k}: {v}")
|
|
732
925
|
return "\n".join(parts) if parts else str(data)
|
|
733
|
-
|
|
734
|
-
return "\n".join(str(item) for item in data)
|
|
926
|
+
|
|
735
927
|
return str(data)
|
|
736
928
|
|
|
737
929
|
def _extract_tool_output(tr):
|
|
@@ -784,8 +976,20 @@ class MainAgent(BaseAgent):
|
|
|
784
976
|
need_callback = True
|
|
785
977
|
|
|
786
978
|
output_str = tool_output_text
|
|
787
|
-
#
|
|
788
|
-
|
|
979
|
+
# 数据密集型工具允许更长的输出
|
|
980
|
+
_HEAVY_TOOLS = ("web_search", "web_read", "url_read", "file_list",
|
|
981
|
+
"file_search", "browser_open", "process_list")
|
|
982
|
+
# OpenClaw prompt-only 技能也允许较长输出(SKILL.md 指令)
|
|
983
|
+
_is_openclaw = (
|
|
984
|
+
isinstance(tool_result.get("data"), dict)
|
|
985
|
+
and tool_result.get("data", {}).get("skill_type") == "openclaw"
|
|
986
|
+
)
|
|
987
|
+
if tool_name in _HEAVY_TOOLS:
|
|
988
|
+
_max_output = 6000
|
|
989
|
+
elif _is_openclaw:
|
|
990
|
+
_max_output = 8000
|
|
991
|
+
else:
|
|
992
|
+
_max_output = 3000
|
|
789
993
|
tool_outputs_parts.append(
|
|
790
994
|
f"### {before_call}\n"
|
|
791
995
|
f"**工具**: {tool_name}\n"
|
|
@@ -825,8 +1029,15 @@ class MainAgent(BaseAgent):
|
|
|
825
1029
|
# 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
|
|
826
1030
|
if parsed.finish:
|
|
827
1031
|
logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
|
|
828
|
-
#
|
|
829
|
-
|
|
1032
|
+
# 构建有意义的最终回复:使用当前轮次的 reasoning text + 任务计划摘要
|
|
1033
|
+
# 注意:之前回调时已保存前几轮的文本,这里只保存当前轮次新增的部分
|
|
1034
|
+
_current_round = _v2_reasoning_collected[_reasoning_len_before_round:]
|
|
1035
|
+
if _current_round:
|
|
1036
|
+
final_text = "\n".join(_current_round)
|
|
1037
|
+
if current_task_plan:
|
|
1038
|
+
final_text += f"\n\n{current_task_plan}"
|
|
1039
|
+
elif _v2_reasoning_collected:
|
|
1040
|
+
# 没有回调历史(第一轮就 finish),保存全部
|
|
830
1041
|
final_text = "\n".join(_v2_reasoning_collected)
|
|
831
1042
|
if current_task_plan:
|
|
832
1043
|
final_text += f"\n\n{current_task_plan}"
|
|
@@ -847,7 +1058,13 @@ class MainAgent(BaseAgent):
|
|
|
847
1058
|
# finish=false: 根据工具的 callback 标志决定是否回调
|
|
848
1059
|
if not need_callback:
|
|
849
1060
|
logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
|
|
850
|
-
|
|
1061
|
+
# 只保存当前轮次新增的文本(前几轮已通过回调保存)
|
|
1062
|
+
_current_round = _v2_reasoning_collected[_reasoning_len_before_round:]
|
|
1063
|
+
if _current_round:
|
|
1064
|
+
final_text = "\n".join(_current_round)
|
|
1065
|
+
if current_task_plan:
|
|
1066
|
+
final_text += f"\n\n{current_task_plan}"
|
|
1067
|
+
elif _v2_reasoning_collected:
|
|
851
1068
|
final_text = "\n".join(_v2_reasoning_collected)
|
|
852
1069
|
if current_task_plan:
|
|
853
1070
|
final_text += f"\n\n{current_task_plan}"
|
|
@@ -867,6 +1084,30 @@ class MainAgent(BaseAgent):
|
|
|
867
1084
|
|
|
868
1085
|
logger.info(f"[{task_id}] finish=false 且 need_callback=true,回调 LLM...")
|
|
869
1086
|
|
|
1087
|
+
# 回调前,保存当前轮次的 LLM 输出到短期记忆
|
|
1088
|
+
# 这样每轮工具调用都有对应的 assistant 消息记录
|
|
1089
|
+
if self.memory:
|
|
1090
|
+
_round_items = _v2_reasoning_collected[_reasoning_len_before_round:]
|
|
1091
|
+
if _round_items:
|
|
1092
|
+
_round_output = "\n".join(_round_items)
|
|
1093
|
+
if _round_output.strip():
|
|
1094
|
+
self.memory.add_short_term(
|
|
1095
|
+
session_id=context.session_id,
|
|
1096
|
+
role="assistant",
|
|
1097
|
+
content=_round_output,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
# 循环正常结束(max_iter 耗尽)时兜底保存
|
|
1101
|
+
else:
|
|
1102
|
+
if self.memory and _v2_reasoning_collected:
|
|
1103
|
+
_fallback_text = "\n".join(_v2_reasoning_collected)
|
|
1104
|
+
if _fallback_text.strip():
|
|
1105
|
+
self.memory.add_short_term(
|
|
1106
|
+
session_id=context.session_id,
|
|
1107
|
+
role="assistant",
|
|
1108
|
+
content=_fallback_text,
|
|
1109
|
+
)
|
|
1110
|
+
|
|
870
1111
|
context.working_memory["iterations"] = self._iteration_count
|
|
871
1112
|
if current_task_plan:
|
|
872
1113
|
context.working_memory["task_plan"] = current_task_plan
|
package/agents/memory_agent.py
CHANGED
|
@@ -22,9 +22,9 @@ class MemoryAgent(BaseAgent):
|
|
|
22
22
|
记忆管理 Agent。
|
|
23
23
|
|
|
24
24
|
职责:
|
|
25
|
-
- 对话上下文管理(
|
|
26
|
-
- 任务进度跟踪(
|
|
27
|
-
- 知识/经验存储(
|
|
25
|
+
- 对话上下文管理(会话记忆 session)
|
|
26
|
+
- 任务进度跟踪(会话记忆 session)
|
|
27
|
+
- 知识/经验存储(全局记忆 global)
|
|
28
28
|
- 记忆检索与总结
|
|
29
29
|
- 错误模式记录(避免重复犯错)
|
|
30
30
|
"""
|
|
@@ -101,14 +101,14 @@ class MemoryAgent(BaseAgent):
|
|
|
101
101
|
return context
|
|
102
102
|
|
|
103
103
|
async def _save_conversation(self, context: AgentContext, session_id: str):
|
|
104
|
-
"""
|
|
104
|
+
"""保存对话消息到会话记忆"""
|
|
105
105
|
if not self.memory:
|
|
106
106
|
return
|
|
107
107
|
|
|
108
108
|
for msg in context.conversation_history:
|
|
109
109
|
if msg.role in ("user", "assistant", "system", "tool"):
|
|
110
110
|
# 避免重复保存
|
|
111
|
-
self.memory.
|
|
111
|
+
self.memory.add_session(
|
|
112
112
|
session_id=session_id,
|
|
113
113
|
role=msg.role,
|
|
114
114
|
content=msg.content,
|
|
@@ -119,7 +119,7 @@ class MemoryAgent(BaseAgent):
|
|
|
119
119
|
max_msgs = self.config.memory.max_short_term
|
|
120
120
|
self.memory.prune_conversation(session_id, max_msgs)
|
|
121
121
|
|
|
122
|
-
logger.debug(f"
|
|
122
|
+
logger.debug(f"对话已保存到会话记忆 (session={session_id})")
|
|
123
123
|
|
|
124
124
|
async def _get_context(self, context: AgentContext, session_id: str):
|
|
125
125
|
"""获取对话上下文"""
|
|
@@ -133,25 +133,27 @@ class MemoryAgent(BaseAgent):
|
|
|
133
133
|
logger.debug(f"已加载对话上下文: {len(entries)} 条")
|
|
134
134
|
|
|
135
135
|
async def _save_progress(self, context: AgentContext, session_id: str):
|
|
136
|
-
"""
|
|
136
|
+
"""保存任务进度到会话记忆"""
|
|
137
137
|
if not self.memory:
|
|
138
138
|
return
|
|
139
139
|
|
|
140
140
|
progress = context.metadata.get("progress_data", {})
|
|
141
141
|
for key, value in progress.items() if isinstance(progress, dict) else []:
|
|
142
|
-
self.memory.
|
|
142
|
+
self.memory.add_session(
|
|
143
143
|
session_id=session_id,
|
|
144
144
|
key=key,
|
|
145
145
|
content=str(value) if not isinstance(value, str) else value,
|
|
146
|
+
importance=0.7,
|
|
146
147
|
)
|
|
147
148
|
|
|
148
149
|
# 也保存整体状态
|
|
149
150
|
task_status = context.working_memory.get("task_status", "进行中")
|
|
150
|
-
self.memory.
|
|
151
|
+
self.memory.add_session(
|
|
151
152
|
session_id=session_id,
|
|
152
153
|
key="task_status",
|
|
153
154
|
content=task_status,
|
|
154
155
|
metadata=context.working_memory,
|
|
156
|
+
importance=0.7,
|
|
155
157
|
)
|
|
156
158
|
logger.debug(f"工作进度已保存 (session={session_id})")
|
|
157
159
|
|
|
@@ -230,7 +232,7 @@ class MemoryAgent(BaseAgent):
|
|
|
230
232
|
# 保存偏好
|
|
231
233
|
preferences = result.get("preferences", {})
|
|
232
234
|
for key, value in preferences.items():
|
|
233
|
-
self.memory.
|
|
235
|
+
self.memory.add_global(
|
|
234
236
|
session_id="global",
|
|
235
237
|
key="user_pref",
|
|
236
238
|
content=f"{key}: {value}",
|
|
@@ -278,7 +280,7 @@ class MemoryAgent(BaseAgent):
|
|
|
278
280
|
pref_key = context.metadata.get("pref_key", "")
|
|
279
281
|
pref_value = context.metadata.get("pref_value", "")
|
|
280
282
|
if pref_key and pref_value:
|
|
281
|
-
self.memory.
|
|
283
|
+
self.memory.add_global(
|
|
282
284
|
session_id="global",
|
|
283
285
|
key="user_pref",
|
|
284
286
|
content=f"{pref_key}: {pref_value}",
|
|
@@ -293,9 +295,9 @@ class MemoryAgent(BaseAgent):
|
|
|
293
295
|
|
|
294
296
|
query = context.user_message
|
|
295
297
|
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
query, session_id="", category="
|
|
298
|
+
# 搜索全局记忆
|
|
299
|
+
global_memories = self.memory.search(
|
|
300
|
+
query, session_id="", category="global", limit=5
|
|
299
301
|
)
|
|
300
302
|
# 搜索错误模式
|
|
301
303
|
errors = self.memory.get_error_patterns(session_id="global", limit=3)
|
|
@@ -303,9 +305,9 @@ class MemoryAgent(BaseAgent):
|
|
|
303
305
|
prefs = self.memory.get_preferences(session_id="global")
|
|
304
306
|
|
|
305
307
|
context.working_memory["relevant_memories"] = {
|
|
306
|
-
"
|
|
308
|
+
"global": [
|
|
307
309
|
{"content": e.content[:500], "key": e.key, "summary": e.summary}
|
|
308
|
-
for e in
|
|
310
|
+
for e in global_memories
|
|
309
311
|
],
|
|
310
312
|
"error_patterns": [
|
|
311
313
|
{"content": e.content[:300]} for e in errors
|
|
@@ -325,9 +327,9 @@ class MemoryAgent(BaseAgent):
|
|
|
325
327
|
context_parts.append("\n## 历史错误(避免重复)")
|
|
326
328
|
context_parts.extend(f"- {e.content[:200]}" for e in errors)
|
|
327
329
|
|
|
328
|
-
if
|
|
330
|
+
if global_memories:
|
|
329
331
|
context_parts.append("\n## 相关经验")
|
|
330
|
-
context_parts.extend(f"- {e.summary or e.content[:200]}" for e in
|
|
332
|
+
context_parts.extend(f"- {e.summary or e.content[:200]}" for e in global_memories)
|
|
331
333
|
|
|
332
334
|
if context_parts:
|
|
333
335
|
context.working_memory["memory_context_prompt"] = "\n".join(context_parts)
|
package/config.py
CHANGED
|
@@ -45,11 +45,16 @@ class LLMConfig:
|
|
|
45
45
|
class MemoryConfig:
|
|
46
46
|
"""记忆系统配置"""
|
|
47
47
|
db_path: str = "" # SQLite 数据库路径,默认 ~/.myagent/memory.db
|
|
48
|
-
|
|
49
|
-
max_working: int = 100 #
|
|
48
|
+
max_session: int = 50 # 会话记忆最大对话轮数
|
|
49
|
+
max_working: int = 100 # 任务进度最大条数
|
|
50
50
|
auto_summarize: bool = True # 自动总结开关
|
|
51
51
|
summarize_threshold: int = 20 # 触发总结的对话轮数
|
|
52
52
|
|
|
53
|
+
# 兼容旧配置名
|
|
54
|
+
@property
|
|
55
|
+
def max_short_term(self) -> int:
|
|
56
|
+
return self.max_session
|
|
57
|
+
|
|
53
58
|
|
|
54
59
|
@dataclass
|
|
55
60
|
class ExecutorConfig:
|
|
Binary file
|