myagent-ai 1.10.6 → 1.10.8
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/__pycache__/memory_agent.cpython-312.pyc +0 -0
- package/agents/main_agent.py +319 -84
- package/agents/memory_agent.py +22 -20
- package/config.py +2 -2
- package/core/__pycache__/context_builder.cpython-312.pyc +0 -0
- package/core/context_builder.py +14 -4
- package/core/output_parser.py +53 -5
- package/main.py +3 -4
- package/memory/__pycache__/manager.cpython-312.pyc +0 -0
- package/memory/manager.py +68 -173
- 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 +37 -35
- package/web/ui/chat/chat_main.js +1 -1
- package/web/ui/chat/middle_chat.html +1 -1
- package/web/ui/index.html +18 -22
|
Binary file
|
|
Binary file
|
package/agents/main_agent.py
CHANGED
|
@@ -48,8 +48,9 @@ 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>
|
|
@@ -67,14 +68,15 @@ class MainAgent(BaseAgent):
|
|
|
67
68
|
6. <parms>: **必须使用严格合法的JSON格式**,例如 {"query": "关键词", "num": 10},不要使用其他格式
|
|
68
69
|
7. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
|
|
69
70
|
8. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
|
|
70
|
-
9. <remember>:
|
|
71
|
+
9. <remember>: 包含 <type> 和 <content> 子标签。type 填 "global"(跨会话全局记忆)或 "session"(仅当前会话)。content 填从最新用户输入中提炼的值得记忆的关键信息。如果本轮无需记忆,content 为空且不填 type。注意:用户个人偏好、重要结论、通用经验用 global;当前任务的临时上下文、过程信息用 session
|
|
71
72
|
10. <recall>: 描述下一轮执行时需要从记忆库中检索的内容关键词
|
|
72
|
-
11. <
|
|
73
|
-
12. <
|
|
74
|
-
13. <
|
|
75
|
-
14. <
|
|
76
|
-
15. <
|
|
77
|
-
16.
|
|
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. 使用中文输出所有内容
|
|
78
80
|
|
|
79
81
|
## 工具选择指南
|
|
80
82
|
- **搜索信息**: 用 `web_search`(返回标题+URL+摘要),不要用 browser_open
|
|
@@ -261,6 +263,108 @@ class MainAgent(BaseAgent):
|
|
|
261
263
|
logger.warning(f"[{task_id}] 记忆合并异常: {e}")
|
|
262
264
|
return None
|
|
263
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
|
+
|
|
264
368
|
async def process_v2(
|
|
265
369
|
self,
|
|
266
370
|
context: AgentContext,
|
|
@@ -349,9 +453,9 @@ class MainAgent(BaseAgent):
|
|
|
349
453
|
except Exception as e:
|
|
350
454
|
logger.warning(f"[{task_id}] 加载历史对话失败: {e}")
|
|
351
455
|
|
|
352
|
-
#
|
|
456
|
+
# 保存用户消息到会话记忆
|
|
353
457
|
if self.memory:
|
|
354
|
-
self.memory.
|
|
458
|
+
self.memory.add_session(
|
|
355
459
|
session_id=context.session_id,
|
|
356
460
|
role="user",
|
|
357
461
|
content=context.user_message,
|
|
@@ -402,6 +506,7 @@ class MainAgent(BaseAgent):
|
|
|
402
506
|
task_plan=current_task_plan,
|
|
403
507
|
agent_override_prompt=agent_override_prompt,
|
|
404
508
|
get_knowledge=get_knowledge_content,
|
|
509
|
+
recall=recall_content,
|
|
405
510
|
)
|
|
406
511
|
|
|
407
512
|
await self._emit_v2_event(
|
|
@@ -446,9 +551,9 @@ class MainAgent(BaseAgent):
|
|
|
446
551
|
llm_raw = response.content
|
|
447
552
|
logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
|
|
448
553
|
|
|
449
|
-
# 保存 LLM
|
|
554
|
+
# 保存 LLM 原始输出到会话记忆(用于回溯和审计)
|
|
450
555
|
if self.memory:
|
|
451
|
-
self.memory.
|
|
556
|
+
self.memory.add_session(
|
|
452
557
|
session_id=context.session_id,
|
|
453
558
|
role="assistant",
|
|
454
559
|
content=llm_raw,
|
|
@@ -483,7 +588,7 @@ class MainAgent(BaseAgent):
|
|
|
483
588
|
context.working_memory["final_response"] = final_text
|
|
484
589
|
await self._emit_v2_event("v2_reasoning", {"content": final_text}, stream_callback)
|
|
485
590
|
if self.memory:
|
|
486
|
-
self.memory.
|
|
591
|
+
self.memory.add_session(
|
|
487
592
|
session_id=context.session_id,
|
|
488
593
|
role="assistant",
|
|
489
594
|
content=final_text,
|
|
@@ -496,7 +601,7 @@ class MainAgent(BaseAgent):
|
|
|
496
601
|
context.working_memory["final_response"] = final_text
|
|
497
602
|
await self._emit_v2_event("v2_reasoning", {"content": final_text}, stream_callback)
|
|
498
603
|
if self.memory:
|
|
499
|
-
self.memory.
|
|
604
|
+
self.memory.add_session(
|
|
500
605
|
session_id=context.session_id,
|
|
501
606
|
role="assistant",
|
|
502
607
|
content=final_text,
|
|
@@ -517,77 +622,92 @@ class MainAgent(BaseAgent):
|
|
|
517
622
|
if response_text:
|
|
518
623
|
logger.debug(f"[{task_id}] 模型回复用户: {response_text[:100]}")
|
|
519
624
|
context.working_memory["model_response"] = response_text
|
|
625
|
+
_v2_reasoning_collected.append(response_text)
|
|
520
626
|
await self._emit_v2_event(
|
|
521
627
|
"v2_reasoning",
|
|
522
628
|
{"content": response_text},
|
|
523
629
|
stream_callback,
|
|
524
630
|
)
|
|
525
631
|
|
|
526
|
-
# Step 6: 处理 remember —
|
|
632
|
+
# Step 6: 处理 remember — 按 type 分全局/会话存储
|
|
527
633
|
if parsed.remember:
|
|
528
634
|
try:
|
|
529
635
|
if self.memory:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
logger.info(
|
|
539
|
-
f"[{task_id}] 记忆查重: 发现相似内容,调用LLM合并 "
|
|
540
|
-
f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
task_id=task_id,
|
|
547
|
-
)
|
|
548
|
-
if merged_content:
|
|
549
|
-
# 用 LLM 合并后的内容替换旧记忆
|
|
550
|
-
self.memory.update_memory(
|
|
551
|
-
memory_id=dup_memory.id,
|
|
552
|
-
content=merged_content,
|
|
553
|
-
summary=truncate_str(merged_content, 200),
|
|
554
|
-
)
|
|
555
|
-
logger.info(f"[{task_id}] 记忆已合并更新: {dup_memory.id}")
|
|
556
|
-
else:
|
|
557
|
-
# LLM 合并失败,直接更新为新内容
|
|
558
|
-
self.memory.update_memory(
|
|
559
|
-
memory_id=dup_memory.id,
|
|
560
|
-
content=parsed.remember,
|
|
645
|
+
if dup_memory:
|
|
646
|
+
logger.info(
|
|
647
|
+
f"[{task_id}] 全局记忆查重: 发现相似内容,调用LLM合并 "
|
|
648
|
+
f"(旧记忆ID={dup_memory.id}, 创建于={dup_memory.created_at})"
|
|
561
649
|
)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
mem_ctx = AgentContext(
|
|
650
|
+
merged_content = await self._merge_duplicate_memory(
|
|
651
|
+
old_memory=dup_memory,
|
|
652
|
+
new_content=parsed.remember,
|
|
653
|
+
context=context,
|
|
567
654
|
task_id=task_id,
|
|
568
|
-
session_id=context.session_id,
|
|
569
|
-
metadata={
|
|
570
|
-
"memory_action": "save",
|
|
571
|
-
"content": parsed.remember,
|
|
572
|
-
},
|
|
573
655
|
)
|
|
574
|
-
|
|
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}")
|
|
575
669
|
else:
|
|
576
|
-
self.memory.
|
|
670
|
+
self.memory.add_global(
|
|
577
671
|
session_id=context.session_id,
|
|
578
672
|
key="conversation_insight",
|
|
579
673
|
content=parsed.remember,
|
|
580
674
|
summary=truncate_str(parsed.remember, 200),
|
|
581
675
|
importance=0.7,
|
|
582
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
|
+
|
|
583
686
|
await self._emit_v2_event(
|
|
584
687
|
"v2_memory_saved",
|
|
585
|
-
{"content": truncate_str(parsed.remember, 200)},
|
|
688
|
+
{"type": _rem_type, "content": truncate_str(parsed.remember, 200)},
|
|
586
689
|
stream_callback,
|
|
587
690
|
)
|
|
588
691
|
except Exception as e:
|
|
589
692
|
logger.warning(f"[{task_id}] 存入记忆失败: {e}")
|
|
590
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
|
+
|
|
591
711
|
# Step 7: 处理 recall — 记录下一轮需要检索的记忆内容
|
|
592
712
|
if parsed.recall:
|
|
593
713
|
recall_content = parsed.recall
|
|
@@ -621,7 +741,7 @@ class MainAgent(BaseAgent):
|
|
|
621
741
|
stream_callback,
|
|
622
742
|
)
|
|
623
743
|
if self.memory:
|
|
624
|
-
self.memory.
|
|
744
|
+
self.memory.add_session(
|
|
625
745
|
session_id=context.session_id,
|
|
626
746
|
role="assistant",
|
|
627
747
|
content=parsed.ask_user,
|
|
@@ -638,7 +758,7 @@ class MainAgent(BaseAgent):
|
|
|
638
758
|
context.working_memory["final_response"] = final_text
|
|
639
759
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
640
760
|
if self.memory:
|
|
641
|
-
self.memory.
|
|
761
|
+
self.memory.add_session(
|
|
642
762
|
session_id=context.session_id,
|
|
643
763
|
role="assistant",
|
|
644
764
|
content=final_text,
|
|
@@ -650,7 +770,7 @@ class MainAgent(BaseAgent):
|
|
|
650
770
|
context.working_memory["final_response"] = final_text
|
|
651
771
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
652
772
|
if self.memory:
|
|
653
|
-
self.memory.
|
|
773
|
+
self.memory.add_session(
|
|
654
774
|
session_id=context.session_id,
|
|
655
775
|
role="assistant",
|
|
656
776
|
content=final_text,
|
|
@@ -660,6 +780,7 @@ class MainAgent(BaseAgent):
|
|
|
660
780
|
# Step 11: 有工具调用 — 先执行所有工具,再根据 finish 决定回调
|
|
661
781
|
need_callback = False
|
|
662
782
|
tool_outputs_parts = []
|
|
783
|
+
_reasoning_len_before_round = len(_v2_reasoning_collected) # 记录本轮开始时的长度
|
|
663
784
|
|
|
664
785
|
for tool_info in parsed.tools_to_call:
|
|
665
786
|
tool_name = tool_info.get("toolname", "").strip()
|
|
@@ -706,38 +827,103 @@ class MainAgent(BaseAgent):
|
|
|
706
827
|
|
|
707
828
|
# 发送工具结果事件
|
|
708
829
|
# 提取实际输出:SkillResult 有 output/message/data,ExecResult 有 stdout/stderr
|
|
709
|
-
def _format_data_for_llm(data):
|
|
710
|
-
"""将结构化 data
|
|
711
|
-
|
|
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:
|
|
712
836
|
return ""
|
|
713
837
|
if isinstance(data, str):
|
|
714
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
|
+
|
|
715
878
|
if isinstance(data, dict):
|
|
716
879
|
# 搜索结果列表格式 (web_search)
|
|
717
880
|
results = data.get("results")
|
|
718
881
|
if isinstance(results, list):
|
|
719
882
|
lines = []
|
|
720
|
-
for i, r in enumerate(results, 1):
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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}")
|
|
725
893
|
return "\n".join(lines)
|
|
894
|
+
|
|
726
895
|
# 网页内容格式 (web_read)
|
|
727
896
|
if "url" in data and "content" in data:
|
|
728
897
|
title = data.get("title", "")
|
|
729
898
|
content = data.get("content", "")
|
|
730
|
-
lines = [
|
|
899
|
+
lines = [
|
|
900
|
+
f"标题: {title}",
|
|
901
|
+
f"URL: {data['url']}",
|
|
902
|
+
f"内容:\n{content}",
|
|
903
|
+
]
|
|
731
904
|
return "\n".join(lines)
|
|
732
|
-
|
|
905
|
+
|
|
906
|
+
# 通用 dict: 递归格式化嵌套结构
|
|
733
907
|
parts = []
|
|
734
908
|
for k, v in data.items():
|
|
735
909
|
if k == "results":
|
|
736
910
|
continue # 已在上面处理
|
|
737
|
-
|
|
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}")
|
|
738
925
|
return "\n".join(parts) if parts else str(data)
|
|
739
|
-
|
|
740
|
-
return "\n".join(str(item) for item in data)
|
|
926
|
+
|
|
741
927
|
return str(data)
|
|
742
928
|
|
|
743
929
|
def _extract_tool_output(tr):
|
|
@@ -790,8 +976,20 @@ class MainAgent(BaseAgent):
|
|
|
790
976
|
need_callback = True
|
|
791
977
|
|
|
792
978
|
output_str = tool_output_text
|
|
793
|
-
#
|
|
794
|
-
|
|
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
|
|
795
993
|
tool_outputs_parts.append(
|
|
796
994
|
f"### {before_call}\n"
|
|
797
995
|
f"**工具**: {tool_name}\n"
|
|
@@ -808,16 +1006,16 @@ class MainAgent(BaseAgent):
|
|
|
808
1006
|
content=f"[工具 {tool_name} 执行完成] {'成功' if tool_result.get('success') else '失败'}",
|
|
809
1007
|
))
|
|
810
1008
|
|
|
811
|
-
#
|
|
1009
|
+
# 保存工具调用到会话记忆
|
|
812
1010
|
if self.memory:
|
|
813
|
-
self.memory.
|
|
1011
|
+
self.memory.add_session(
|
|
814
1012
|
session_id=context.session_id,
|
|
815
1013
|
role="assistant",
|
|
816
1014
|
content=f"调用工具: {tool_name}\n参数: {truncate_str(parms, 1000)}",
|
|
817
1015
|
key="tool_call",
|
|
818
1016
|
importance=0.4,
|
|
819
1017
|
)
|
|
820
|
-
self.memory.
|
|
1018
|
+
self.memory.add_session(
|
|
821
1019
|
session_id=context.session_id,
|
|
822
1020
|
role="tool",
|
|
823
1021
|
content=f"[{tool_name}] {'成功' if tool_result.get('success') else '失败'}\n{truncate_str(output_str, 5000)}",
|
|
@@ -831,8 +1029,15 @@ class MainAgent(BaseAgent):
|
|
|
831
1029
|
# 核心逻辑: finish=true 表示任务已完成/不需要再调用LLM,即使工具设置了callback=true
|
|
832
1030
|
if parsed.finish:
|
|
833
1031
|
logger.info(f"[{task_id}] finish=true,任务已完成,不回调 LLM")
|
|
834
|
-
#
|
|
835
|
-
|
|
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),保存全部
|
|
836
1041
|
final_text = "\n".join(_v2_reasoning_collected)
|
|
837
1042
|
if current_task_plan:
|
|
838
1043
|
final_text += f"\n\n{current_task_plan}"
|
|
@@ -843,7 +1048,7 @@ class MainAgent(BaseAgent):
|
|
|
843
1048
|
context.working_memory["final_response"] = final_text
|
|
844
1049
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
845
1050
|
if self.memory:
|
|
846
|
-
self.memory.
|
|
1051
|
+
self.memory.add_session(
|
|
847
1052
|
session_id=context.session_id,
|
|
848
1053
|
role="assistant",
|
|
849
1054
|
content=final_text,
|
|
@@ -853,7 +1058,13 @@ class MainAgent(BaseAgent):
|
|
|
853
1058
|
# finish=false: 根据工具的 callback 标志决定是否回调
|
|
854
1059
|
if not need_callback:
|
|
855
1060
|
logger.info(f"[{task_id}] 所有工具无需回调且 finish=false,结束循环")
|
|
856
|
-
|
|
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:
|
|
857
1068
|
final_text = "\n".join(_v2_reasoning_collected)
|
|
858
1069
|
if current_task_plan:
|
|
859
1070
|
final_text += f"\n\n{current_task_plan}"
|
|
@@ -864,7 +1075,7 @@ class MainAgent(BaseAgent):
|
|
|
864
1075
|
context.working_memory["final_response"] = final_text
|
|
865
1076
|
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
866
1077
|
if self.memory:
|
|
867
|
-
self.memory.
|
|
1078
|
+
self.memory.add_session(
|
|
868
1079
|
session_id=context.session_id,
|
|
869
1080
|
role="assistant",
|
|
870
1081
|
content=final_text,
|
|
@@ -873,6 +1084,30 @@ class MainAgent(BaseAgent):
|
|
|
873
1084
|
|
|
874
1085
|
logger.info(f"[{task_id}] finish=false 且 need_callback=true,回调 LLM...")
|
|
875
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_session(
|
|
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_session(
|
|
1106
|
+
session_id=context.session_id,
|
|
1107
|
+
role="assistant",
|
|
1108
|
+
content=_fallback_text,
|
|
1109
|
+
)
|
|
1110
|
+
|
|
876
1111
|
context.working_memory["iterations"] = self._iteration_count
|
|
877
1112
|
if current_task_plan:
|
|
878
1113
|
context.working_memory["task_plan"] = current_task_plan
|