myagent-ai 1.12.0 → 1.12.3
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 +49 -33
- package/agents/memory_agent.py +88 -0
- 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 +102 -44
- package/main.py +32 -5
- package/memory/__pycache__/manager.cpython-312.pyc +0 -0
- package/memory/manager.py +69 -4
- package/package.json +1 -1
- package/skills/__pycache__/registry.cpython-312.pyc +0 -0
- package/skills/registry.py +6 -239
- package/web/__pycache__/api_server.cpython-312.pyc +0 -0
- package/web/api_server.py +3 -52
- package/web/ui/chat/chat.css +13 -8
- package/web/ui/chat/chat_main.js +40 -10
- package/web/ui/chat/flow_engine.js +14 -3
- package/web/ui/chat/left_sessions.html +1 -1
- package/web/ui/chat/right_agents.html +1 -1
- package/web/ui/index.html +18 -49
|
Binary file
|
|
Binary file
|
package/agents/main_agent.py
CHANGED
|
@@ -49,7 +49,7 @@ class MainAgent(BaseAgent):
|
|
|
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
51
|
<remember><type>global或session</type><content>仅从最新用户输入(userprint 或 usersays_correct)中提炼值得记忆的信息(如用户偏好、重要结论、错误经验等)。type=global表示跨会话全局记忆,type=session表示仅当前会话可用的记忆。如果本轮没有新信息需要记忆,则<content>为空、<type>不填。</content></remember>
|
|
52
|
-
<recall
|
|
52
|
+
<recall>下一轮需要主动召回的记忆描述。填写需要从记忆库中检索的关键字或描述。如果不填写则为空。如果需要更多记忆支持当前任务,填写相关关键词(可包含时间参考,如"2025年1月的项目"),系统将在下一轮搜索top5相关记忆并通过<recall_memory>注入上下文。你也可以直接调用recall_memory工具即时搜索。</recall>
|
|
53
53
|
<knowledge>从本轮对话或工具执行结果中提炼值得长期保存到知识库的专业知识、事实、经验法则、技术要点等。这些知识将被持久化存储,未来可通过 <get_knowledge> 检索复用。如果本轮没有需要保存的知识,则为空。格式要求:简洁明确,每条知识一行,用换行分隔。</knowledge>
|
|
54
54
|
<get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的<knowledge>内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
|
|
55
55
|
<askuser>需要询问用户的内容,如无,则为空</askuser>
|
|
@@ -69,7 +69,7 @@ class MainAgent(BaseAgent):
|
|
|
69
69
|
7. <timeout>: 预估超时秒数(简单操作10-30s,文件操作30-60s,网络请求60-120s,数据处理120-300s)
|
|
70
70
|
8. <callback>: 如果该工具的执行结果对后续决策有影响,设为 true;否则设为 false
|
|
71
71
|
9. <remember>: 包含 <type> 和 <content> 子标签。type 填 "global"(跨会话全局记忆)或 "session"(仅当前会话)。content 填从最新用户输入中提炼的值得记忆的关键信息。如果本轮无需记忆,content 为空且不填 type。注意:用户个人偏好、重要结论、通用经验用 global;当前任务的临时上下文、过程信息用 session
|
|
72
|
-
10. <recall>:
|
|
72
|
+
10. <recall>: 填写下一轮需要从记忆库中主动召回的内容描述和关键字(可包含时间参考)。系统将根据这些信息搜索top5相关记忆,在下一轮通过 <recall_memory> 标签注入上下文。如果当前 <automemory> 中的记忆已足够完成任务,<recall> 为空;如果需要更多历史记忆支撑,则填写。你也可以直接使用 recall_memory 工具在当前轮即时搜索
|
|
73
73
|
11. <knowledge>: 从本轮对话或工具执行结果中提炼值得长期保存的专业知识、事实、经验法则、技术要点等。这些知识会被持久化到知识库文件,未来可通过 get_knowledge 检索复用。如果没有需要保存的知识,则为空。格式:简洁明确,每条知识一行
|
|
74
74
|
12. <get_knowledge>: 如果当前 <knowledge> 内容不足以完成任务,填写需要从知识库搜索的关键词;否则为空
|
|
75
75
|
13. <askuser>: 当信息不足需要用户补充时,在此填写要问的问题
|
|
@@ -78,6 +78,11 @@ class MainAgent(BaseAgent):
|
|
|
78
78
|
16. <next_step>: **finish=false 时必须填写**,描述下一步计划做什么,要求简洁明确(1-2句话)
|
|
79
79
|
17. 使用中文输出所有内容
|
|
80
80
|
|
|
81
|
+
## 上下文中的记忆系统说明
|
|
82
|
+
- <automemory>: 系统自动根据你通过 <remember> 保存的记忆和当前用户输入,搜索出的 top10 相关记忆。这些是你过去主动记住的内容(包含时间信息),可供参考。
|
|
83
|
+
- <recall_memory>: 你在上一轮通过 <recall> 指定的记忆搜索结果。系统根据你提供的关键字和时间点搜索了 top5 相关记忆。
|
|
84
|
+
- 两种记忆互补:automemory 是自动匹配的,recall_memory 是你主动指定搜索的。如果 automemory 不足,使用 <recall> 请求更多。
|
|
85
|
+
|
|
81
86
|
## 工具选择指南
|
|
82
87
|
- **搜索信息**: 用 `web_search`(返回标题+URL+摘要),不要用 browser_open
|
|
83
88
|
- **读取网页内容**: 用 `web_read`(传入URL,提取正文)
|
|
@@ -85,6 +90,7 @@ class MainAgent(BaseAgent):
|
|
|
85
90
|
- **执行代码**: 用 `code` 工具(language: python/javascript/shell)
|
|
86
91
|
- **执行命令**: 用 `command` 或 `command_run` 工具
|
|
87
92
|
- **文件操作**: 用 `file_read` / `file_write` / `file_list` 等文件工具
|
|
93
|
+
- **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
|
|
88
94
|
"""
|
|
89
95
|
|
|
90
96
|
def __init__(self, tool_agent=None, memory_agent=None, **kwargs):
|
|
@@ -553,7 +559,7 @@ class MainAgent(BaseAgent):
|
|
|
553
559
|
llm_raw = response.content
|
|
554
560
|
logger.debug(f"[{task_id}] LLM 输出 (前500字): {llm_raw[:500]}")
|
|
555
561
|
|
|
556
|
-
# 保存 LLM
|
|
562
|
+
# 保存 LLM 原始输出到会话记忆(用于回溯和审计,key=llm_output 不出现在对话历史中)
|
|
557
563
|
if self.memory:
|
|
558
564
|
self.memory.add_session(
|
|
559
565
|
session_id=context.session_id,
|
|
@@ -752,31 +758,20 @@ class MainAgent(BaseAgent):
|
|
|
752
758
|
|
|
753
759
|
# Step 10: 执行工具调用(无论 finish 值如何,先执行工具)
|
|
754
760
|
if not parsed.tools_to_call:
|
|
755
|
-
# 无工具调用:
|
|
756
|
-
if
|
|
757
|
-
|
|
758
|
-
before, after = extract_surrounding_text(llm_raw)
|
|
759
|
-
final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "任务已完成。"
|
|
760
|
-
context.working_memory["final_response"] = final_text
|
|
761
|
-
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
762
|
-
if self.memory:
|
|
763
|
-
self.memory.add_session(
|
|
764
|
-
session_id=context.session_id,
|
|
765
|
-
role="assistant",
|
|
766
|
-
content=final_text,
|
|
767
|
-
)
|
|
761
|
+
# 无工具调用: 优先使用已收集的 reasoning 文本(包含 parsed.response),避免丢失第一轮输出
|
|
762
|
+
if _v2_reasoning_collected:
|
|
763
|
+
final_text = "\n".join(_v2_reasoning_collected)
|
|
768
764
|
else:
|
|
769
|
-
logger.info(f"[{task_id}] 无工具调用且 finish=false,结束")
|
|
770
765
|
before, after = extract_surrounding_text(llm_raw)
|
|
771
|
-
final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
766
|
+
final_text = (before + "\n" + after).strip() if (before.strip() or after.strip()) else "任务已完成。"
|
|
767
|
+
context.working_memory["final_response"] = final_text
|
|
768
|
+
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(final_text, 3000)}, stream_callback)
|
|
769
|
+
if self.memory:
|
|
770
|
+
self.memory.add_session(
|
|
771
|
+
session_id=context.session_id,
|
|
772
|
+
role="assistant",
|
|
773
|
+
content=final_text,
|
|
774
|
+
)
|
|
780
775
|
break
|
|
781
776
|
|
|
782
777
|
# Step 11: 有工具调用 — 先执行所有工具,再根据 finish 决定回调
|
|
@@ -981,15 +976,8 @@ class MainAgent(BaseAgent):
|
|
|
981
976
|
# 数据密集型工具允许更长的输出
|
|
982
977
|
_HEAVY_TOOLS = ("web_search", "web_read", "url_read", "file_list",
|
|
983
978
|
"file_search", "browser_open", "process_list")
|
|
984
|
-
# OpenClaw prompt-only 技能也允许较长输出(SKILL.md 指令)
|
|
985
|
-
_is_openclaw = (
|
|
986
|
-
isinstance(tool_result.get("data"), dict)
|
|
987
|
-
and tool_result.get("data", {}).get("skill_type") == "openclaw"
|
|
988
|
-
)
|
|
989
979
|
if tool_name in _HEAVY_TOOLS:
|
|
990
980
|
_max_output = 6000
|
|
991
|
-
elif _is_openclaw:
|
|
992
|
-
_max_output = 8000
|
|
993
981
|
else:
|
|
994
982
|
_max_output = 3000
|
|
995
983
|
tool_outputs_parts.append(
|
|
@@ -1168,6 +1156,34 @@ class MainAgent(BaseAgent):
|
|
|
1168
1156
|
else:
|
|
1169
1157
|
result["error"] = "执行引擎未初始化"
|
|
1170
1158
|
|
|
1159
|
+
elif tool_name == "recall_memory":
|
|
1160
|
+
# === 主动召回记忆工具 ===
|
|
1161
|
+
# 根据 memory_agent.recall_memory() 搜索历史记忆
|
|
1162
|
+
try:
|
|
1163
|
+
if self.memory_agent:
|
|
1164
|
+
recall_results = await self.memory_agent.recall_memory(
|
|
1165
|
+
keyword=params.get("keyword", ""),
|
|
1166
|
+
time_point=params.get("time_point", ""),
|
|
1167
|
+
session_id=params.get("session_id", ""),
|
|
1168
|
+
limit=params.get("limit", 5),
|
|
1169
|
+
)
|
|
1170
|
+
if recall_results:
|
|
1171
|
+
output_lines = [f"找到 {len(recall_results)} 条相关记忆:"]
|
|
1172
|
+
for i, mem in enumerate(recall_results, 1):
|
|
1173
|
+
output_lines.append(
|
|
1174
|
+
f"{i}. [{mem.get('created_at', '')}] "
|
|
1175
|
+
f"[{mem.get('category', '')}] "
|
|
1176
|
+
f"{mem.get('content', '')}"
|
|
1177
|
+
)
|
|
1178
|
+
result = {"success": True, "output": "\n".join(output_lines), "data": recall_results}
|
|
1179
|
+
else:
|
|
1180
|
+
result = {"success": True, "output": "未找到相关记忆", "data": []}
|
|
1181
|
+
else:
|
|
1182
|
+
result = {"success": False, "error": "记忆系统未初始化"}
|
|
1183
|
+
except Exception as re_err:
|
|
1184
|
+
result = {"success": False, "error": f"记忆召回失败: {re_err}"}
|
|
1185
|
+
logger.warning(f"[{task_id}] recall_memory 工具异常: {re_err}")
|
|
1186
|
+
|
|
1171
1187
|
elif self.skills:
|
|
1172
1188
|
exec_result = await self.skills.execute(tool_name, **params)
|
|
1173
1189
|
result = exec_result.to_dict()
|
package/agents/memory_agent.py
CHANGED
|
@@ -333,3 +333,91 @@ class MemoryAgent(BaseAgent):
|
|
|
333
333
|
|
|
334
334
|
if context_parts:
|
|
335
335
|
context.working_memory["memory_context_prompt"] = "\n".join(context_parts)
|
|
336
|
+
|
|
337
|
+
async def recall_memory(
|
|
338
|
+
self,
|
|
339
|
+
keyword: str = "",
|
|
340
|
+
time_point: str = "",
|
|
341
|
+
session_id: str = "",
|
|
342
|
+
limit: int = 5,
|
|
343
|
+
) -> list:
|
|
344
|
+
"""
|
|
345
|
+
主动召回记忆 —— 根据关键字和时间点搜索历史记忆。
|
|
346
|
+
|
|
347
|
+
这是供大模型通过 <recall> 标签调用的工具方法。
|
|
348
|
+
搜索范围包括全局记忆和会话记忆。
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
keyword: 搜索关键字(必填,用于模糊匹配和语义搜索)
|
|
352
|
+
time_point: 时间参考点(可选),格式如 "2025-01" 或 "2025-01-15"
|
|
353
|
+
系统会将其转换为 start_time 进行时间范围过滤
|
|
354
|
+
session_id: 会话 ID(可选,为空则跨会话搜索)
|
|
355
|
+
limit: 返回数量(默认 5)
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
匹配的记忆列表,每项包含 content, created_at, key, category
|
|
359
|
+
"""
|
|
360
|
+
if not self.memory:
|
|
361
|
+
return []
|
|
362
|
+
|
|
363
|
+
# 解析 time_point 为 start_time
|
|
364
|
+
start_time = ""
|
|
365
|
+
if time_point:
|
|
366
|
+
# 支持多种格式: "2025-01", "2025-01-15", "2025年1月"
|
|
367
|
+
import re as _re
|
|
368
|
+
# 尝试 "YYYY年MM月" 格式
|
|
369
|
+
m = _re.match(r"(\d{4})年(\d{1,2})月", time_point)
|
|
370
|
+
if m:
|
|
371
|
+
start_time = f"{m.group(1)}-{int(m.group(2)):02d}-01 00:00:00"
|
|
372
|
+
else:
|
|
373
|
+
# 尝试 "YYYY-MM" 或 "YYYY-MM-DD"
|
|
374
|
+
m = _re.match(r"(\d{4})-(\d{1,2})(?:-(\d{1,2}))?", time_point)
|
|
375
|
+
if m:
|
|
376
|
+
year = m.group(1)
|
|
377
|
+
month = int(m.group(2))
|
|
378
|
+
day = m.group(3)
|
|
379
|
+
if day:
|
|
380
|
+
start_time = f"{year}-{month:02d}-{int(day):02d} 00:00:00"
|
|
381
|
+
else:
|
|
382
|
+
start_time = f"{year}-{month:02d}-01 00:00:00"
|
|
383
|
+
|
|
384
|
+
# 使用 search_by_time_range 进行精确搜索
|
|
385
|
+
if start_time or keyword:
|
|
386
|
+
results = self.memory.search_by_time_range(
|
|
387
|
+
session_id=session_id,
|
|
388
|
+
start_time=start_time,
|
|
389
|
+
keyword=keyword,
|
|
390
|
+
limit=limit,
|
|
391
|
+
)
|
|
392
|
+
if results:
|
|
393
|
+
return [
|
|
394
|
+
{
|
|
395
|
+
"content": e.content,
|
|
396
|
+
"created_at": e.created_at,
|
|
397
|
+
"key": e.key,
|
|
398
|
+
"category": e.category,
|
|
399
|
+
"summary": e.summary,
|
|
400
|
+
}
|
|
401
|
+
for e in results
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
# 回退到普通搜索
|
|
405
|
+
if keyword:
|
|
406
|
+
results = self.memory.search(
|
|
407
|
+
query=keyword,
|
|
408
|
+
session_id=session_id,
|
|
409
|
+
limit=limit,
|
|
410
|
+
mode="hybrid",
|
|
411
|
+
)
|
|
412
|
+
return [
|
|
413
|
+
{
|
|
414
|
+
"content": e.content,
|
|
415
|
+
"created_at": e.created_at,
|
|
416
|
+
"key": e.key,
|
|
417
|
+
"category": e.category,
|
|
418
|
+
"summary": e.summary,
|
|
419
|
+
}
|
|
420
|
+
for e in results
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
return []
|
|
Binary file
|
|
Binary file
|
package/core/context_builder.py
CHANGED
|
@@ -4,14 +4,15 @@ core/context_builder.py - 上下文构建器
|
|
|
4
4
|
为 LLM 系统提示词构建结构化的 XML <context> 块。
|
|
5
5
|
|
|
6
6
|
<context> 包含以下段落:
|
|
7
|
-
<whomi>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
<
|
|
7
|
+
<whomi> - Agent 身份信息(名称、描述)
|
|
8
|
+
<automemory> - 自动记忆检索结果(Top 10 相关记忆,由 <remember> 产生)
|
|
9
|
+
<recall_memory> - 主动召回记忆(上一轮 <recall> 触发的 Top 5 记忆)
|
|
10
|
+
<knowledge> - 知识库 RAG 检索结果(根据 get_knowledge 关键词搜索)
|
|
11
|
+
<resentdialog> - 近期对话历史(截断至 ~20K 字符)
|
|
12
|
+
<userprint> - 用户键盘输入文本(语音输入时为空)
|
|
13
|
+
<usersays> - 用户语音转文本输入(键盘输入时为空)
|
|
14
|
+
<task_plan> - 当前任务计划(Markdown 格式,上轮已完成时为空)
|
|
15
|
+
<tools> - 可用工具列表(名称、描述、参数格式)
|
|
15
16
|
|
|
16
17
|
使用示例:
|
|
17
18
|
builder = ContextBuilder(memory_manager=mm, skill_registry=sr)
|
|
@@ -51,6 +52,12 @@ class ContextBuilder:
|
|
|
51
52
|
|
|
52
53
|
该 XML 块会注入到 LLM 系统提示词中,为模型提供完整的上下文感知能力。
|
|
53
54
|
|
|
55
|
+
上下文中的记忆分为两层:
|
|
56
|
+
- <automemory>: 自动记忆 —— 由大模型 <remember> 产生并持久化的记忆,
|
|
57
|
+
根据用户当前输入自动搜索 top10 相关记忆供参考。
|
|
58
|
+
- <recall_memory>: 主动召回记忆 —— 大模型在上一轮通过 <recall> 标签
|
|
59
|
+
指定要召回的记忆,系统根据关键字+时间搜索 top5 注入。
|
|
60
|
+
|
|
54
61
|
Args:
|
|
55
62
|
memory_manager: 记忆管理器实例(可选,传入后可检索相关记忆)
|
|
56
63
|
skill_registry: 技能注册表实例(可选,传入后可列出可用工具)
|
|
@@ -200,51 +207,102 @@ class ContextBuilder:
|
|
|
200
207
|
|
|
201
208
|
def _build_memory(self, query: str, session_id: str, recall: str = "") -> str:
|
|
202
209
|
"""
|
|
203
|
-
构建 <
|
|
210
|
+
构建 <automemory> 和 <recall_memory> 段落 —— 双层记忆检索结果。
|
|
211
|
+
|
|
212
|
+
<automemory>: 根据用户当前输入自动搜索 top10 相关记忆。
|
|
213
|
+
这些记忆来自大模型通过 <remember> 标签持久化的内容(包含时间信息)。
|
|
214
|
+
搜索范围: 全局记忆(global) + 当前会话的 remember 类记忆。
|
|
204
215
|
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
<recall_memory>: 上一轮 LLM 输出的 <recall> 内容触发的主动召回。
|
|
217
|
+
根据关键字和时间点搜索 top5 历史记忆,注入到本轮上下文中。
|
|
218
|
+
如果上一轮未输出 <recall>,则此段为空。
|
|
207
219
|
|
|
208
220
|
Args:
|
|
209
221
|
query: 搜索查询文本(通常为最新用户消息)
|
|
210
222
|
session_id: 会话 ID
|
|
211
|
-
recall: LLM 上一轮输出的 <recall>
|
|
212
|
-
用于定向检索长期记忆(优先级高于 query)
|
|
223
|
+
recall: LLM 上一轮输出的 <recall> 内容(关键字+时间描述)
|
|
213
224
|
|
|
214
225
|
Returns:
|
|
215
|
-
<
|
|
226
|
+
<automemory> + <recall_memory> XML 段落字符串
|
|
216
227
|
"""
|
|
217
228
|
if not self.memory_manager:
|
|
218
|
-
return "<
|
|
219
|
-
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
229
|
+
return "<automemory>\n(记忆系统未启用)\n</automemory>\n<recall_memory>\n(记忆系统未启用)\n</recall_memory>"
|
|
230
|
+
|
|
231
|
+
# ═══════════════════════════════════════════
|
|
232
|
+
# Part 1: <automemory> — 自动记忆检索
|
|
233
|
+
# ═══════════════════════════════════════════
|
|
234
|
+
search_query = query.strip()
|
|
235
|
+
auto_lines: List[str] = []
|
|
236
|
+
|
|
237
|
+
if search_query:
|
|
238
|
+
try:
|
|
239
|
+
# 搜索全局记忆中由 remember 产生的内容
|
|
240
|
+
global_results = self.memory_manager.search(
|
|
241
|
+
query=search_query,
|
|
242
|
+
session_id="", # 跨会话搜索全局记忆
|
|
243
|
+
category="global",
|
|
244
|
+
limit=10,
|
|
245
|
+
mode="hybrid",
|
|
246
|
+
)
|
|
247
|
+
# 搜索当前会话中 conversation_insight 类记忆
|
|
248
|
+
session_results = self.memory_manager.search(
|
|
249
|
+
query=search_query,
|
|
250
|
+
session_id=session_id,
|
|
251
|
+
category="session",
|
|
252
|
+
limit=5,
|
|
253
|
+
mode="hybrid",
|
|
254
|
+
)
|
|
255
|
+
# 合并去重(全局优先,会话补充)
|
|
256
|
+
seen_ids = set()
|
|
257
|
+
combined = []
|
|
258
|
+
for entry in global_results + session_results:
|
|
259
|
+
if entry.id not in seen_ids:
|
|
260
|
+
seen_ids.add(entry.id)
|
|
261
|
+
combined.append(entry)
|
|
262
|
+
|
|
263
|
+
if combined:
|
|
264
|
+
auto_lines.append("<automemory>")
|
|
265
|
+
for i, entry in enumerate(combined[:10], 1):
|
|
266
|
+
content = entry.content.strip()
|
|
267
|
+
if content:
|
|
268
|
+
auto_lines.append(f"{i}. {_xml_escape(content)}")
|
|
269
|
+
auto_lines.append("</automemory>")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.warning(f"automemory 搜索失败: {e}")
|
|
272
|
+
|
|
273
|
+
if not auto_lines:
|
|
274
|
+
auto_lines = ["<automemory>", "(无相关自动记忆)", "</automemory>"]
|
|
275
|
+
|
|
276
|
+
# ═══════════════════════════════════════════
|
|
277
|
+
# Part 2: <recall_memory> — 主动召回记忆
|
|
278
|
+
# ═══════════════════════════════════════════
|
|
279
|
+
recall_lines: List[str] = []
|
|
280
|
+
recall_text = recall.strip() if recall else ""
|
|
281
|
+
|
|
282
|
+
if recall_text:
|
|
283
|
+
try:
|
|
284
|
+
# 解析 recall 中的关键字和时间描述
|
|
285
|
+
# 格式可能是: "关键字1 关键字2" 或 "关于XX的记忆 2025年1月" 等
|
|
286
|
+
recall_results = self.memory_manager.search(
|
|
287
|
+
query=recall_text,
|
|
288
|
+
session_id="", # 跨会话搜索
|
|
289
|
+
limit=5,
|
|
290
|
+
mode="hybrid",
|
|
291
|
+
)
|
|
292
|
+
if recall_results:
|
|
293
|
+
recall_lines.append("<recall_memory>")
|
|
294
|
+
for i, entry in enumerate(recall_results[:5], 1):
|
|
295
|
+
content = entry.content.strip()
|
|
296
|
+
if content:
|
|
297
|
+
recall_lines.append(f"{i}. {_xml_escape(content)}")
|
|
298
|
+
recall_lines.append("</recall_memory>")
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.warning(f"recall_memory 搜索失败: {e}")
|
|
301
|
+
|
|
302
|
+
if not recall_lines:
|
|
303
|
+
recall_lines = ["<recall_memory>", "(无主动召回记忆)", "</recall_memory>"]
|
|
304
|
+
|
|
305
|
+
return "\n".join(auto_lines) + "\n" + "\n".join(recall_lines)
|
|
248
306
|
|
|
249
307
|
def _build_knowledge(self, query: str) -> str:
|
|
250
308
|
"""
|
package/main.py
CHANGED
|
@@ -42,6 +42,36 @@ from core.permissions import PermissionManager
|
|
|
42
42
|
from core.deps_checker import check_and_install_deps
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _get_screen_resolution() -> tuple[int, int]:
|
|
46
|
+
"""获取当前屏幕分辨率,跨平台兼容 Windows / macOS / Linux。"""
|
|
47
|
+
try:
|
|
48
|
+
import tkinter as tk
|
|
49
|
+
root = tk.Tk()
|
|
50
|
+
root.withdraw() # 不显示窗口
|
|
51
|
+
w = root.winfo_screenwidth()
|
|
52
|
+
h = root.winfo_screenheight()
|
|
53
|
+
root.destroy()
|
|
54
|
+
return w, h
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
# Linux 回退方案
|
|
58
|
+
try:
|
|
59
|
+
import subprocess
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
["xdpyinfo"], capture_output=True, text=True, timeout=5
|
|
62
|
+
)
|
|
63
|
+
for line in result.stdout.splitlines():
|
|
64
|
+
if "dimensions:" in line:
|
|
65
|
+
parts = line.split()
|
|
66
|
+
for p in parts:
|
|
67
|
+
if "x" in p:
|
|
68
|
+
w, h = p.split("x")
|
|
69
|
+
return int(w), int(h)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return 1920, 1080
|
|
73
|
+
|
|
74
|
+
|
|
45
75
|
def _open_browser_kiosk(url: str):
|
|
46
76
|
"""打开浏览器窗口(无地址栏模式),回退到系统浏览器。
|
|
47
77
|
|
|
@@ -54,10 +84,9 @@ def _open_browser_kiosk(url: str):
|
|
|
54
84
|
|
|
55
85
|
async def _launch():
|
|
56
86
|
pw = await async_playwright().start()
|
|
57
|
-
#
|
|
87
|
+
# 获取实际屏幕分辨率,确保窗口占满屏幕
|
|
58
88
|
# --app 模式下 --start-maximized 不生效,需要手动设置窗口大小
|
|
59
|
-
screen_width =
|
|
60
|
-
screen_height = 1080
|
|
89
|
+
screen_width, screen_height = _get_screen_resolution()
|
|
61
90
|
browser = await pw.chromium.launch(
|
|
62
91
|
headless=False,
|
|
63
92
|
args=[
|
|
@@ -229,8 +258,6 @@ class MyAgentApp:
|
|
|
229
258
|
self.skill_registry = SkillRegistry()
|
|
230
259
|
# 注册内置技能
|
|
231
260
|
self._register_builtin_skills()
|
|
232
|
-
# 加载外部 OpenClaw 技能
|
|
233
|
-
self.skill_registry.load_openclaw_skills()
|
|
234
261
|
skills = self.skill_registry.list_skills()
|
|
235
262
|
self.logger.info(f"技能系统: {len(skills)} 个技能已注册 - {skills}")
|
|
236
263
|
|
|
Binary file
|
package/memory/manager.py
CHANGED
|
@@ -152,6 +152,8 @@ class MemoryManager:
|
|
|
152
152
|
CREATE INDEX IF NOT EXISTS idx_key ON memories(key);
|
|
153
153
|
CREATE INDEX IF NOT EXISTS idx_session_category ON memories(session_id, category);
|
|
154
154
|
CREATE INDEX IF NOT EXISTS idx_importance ON memories(importance DESC);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_session_category_created ON memories(session_id, category, created_at);
|
|
155
157
|
|
|
156
158
|
CREATE TABLE IF NOT EXISTS session_names (
|
|
157
159
|
session_id TEXT PRIMARY KEY,
|
|
@@ -276,19 +278,28 @@ class MemoryManager:
|
|
|
276
278
|
# ==========================================================================
|
|
277
279
|
|
|
278
280
|
def add_session(self, session_id, role="", content="", key="", importance=0.5, metadata=None) -> str:
|
|
279
|
-
"""
|
|
281
|
+
"""添加会话记忆。内容自动注入时间前缀,确保自包含时间信息。"""
|
|
282
|
+
from datetime import datetime as _dt
|
|
283
|
+
_now_str = _dt.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
284
|
+
# 对话类记忆(user/assistant/system/tool)自动加时间前缀
|
|
285
|
+
if role and content and not content.startswith("["):
|
|
286
|
+
timestamped_content = f"[{_now_str}] {content}"
|
|
287
|
+
else:
|
|
288
|
+
timestamped_content = truncate_str(content, 50000)
|
|
280
289
|
entry = MemoryEntry(
|
|
281
290
|
session_id=session_id, category="session", role=role,
|
|
282
|
-
content=
|
|
283
|
-
importance=importance, metadata=metadata or {},
|
|
291
|
+
content=timestamped_content, key=key,
|
|
292
|
+
importance=importance, metadata={"timestamp": _now_str, **(metadata or {})},
|
|
284
293
|
)
|
|
285
294
|
return self._insert(entry)
|
|
286
295
|
|
|
287
296
|
def get_conversation(self, session_id, limit=500, include_roles=None) -> List[MemoryEntry]:
|
|
288
|
-
"""获取对话历史(session 分类中 role
|
|
297
|
+
"""获取对话历史(session 分类中 role 非空的条目),按时间正序排列。"""
|
|
289
298
|
conn = self._get_conn()
|
|
299
|
+
# 排除内部系统条目(llm_output, tool_call, tool_result, conversation_insight)
|
|
290
300
|
sql = """SELECT * FROM memories
|
|
291
301
|
WHERE session_id = ? AND category = 'session' AND role != ''
|
|
302
|
+
AND key NOT IN ('llm_output', 'tool_call', 'tool_result', 'conversation_insight')
|
|
292
303
|
ORDER BY created_at ASC LIMIT ?"""
|
|
293
304
|
rows = conn.execute(sql, (session_id, limit)).fetchall()
|
|
294
305
|
entries = [MemoryEntry.from_row(row) for row in rows]
|
|
@@ -296,6 +307,60 @@ class MemoryManager:
|
|
|
296
307
|
entries = [e for e in entries if e.role in include_roles]
|
|
297
308
|
return entries
|
|
298
309
|
|
|
310
|
+
def get_conversation_all(self, session_id, limit=5000) -> List[MemoryEntry]:
|
|
311
|
+
"""获取全量对话历史(包含所有内部条目),用于完整回溯。"""
|
|
312
|
+
conn = self._get_conn()
|
|
313
|
+
sql = """SELECT * FROM memories
|
|
314
|
+
WHERE session_id = ? AND category = 'session' AND role != ''
|
|
315
|
+
ORDER BY created_at ASC LIMIT ?"""
|
|
316
|
+
rows = conn.execute(sql, (session_id, limit)).fetchall()
|
|
317
|
+
return [MemoryEntry.from_row(row) for row in rows]
|
|
318
|
+
|
|
319
|
+
def search_by_time_range(
|
|
320
|
+
self,
|
|
321
|
+
session_id: str = "",
|
|
322
|
+
start_time: str = "",
|
|
323
|
+
end_time: str = "",
|
|
324
|
+
keyword: str = "",
|
|
325
|
+
limit: int = 10,
|
|
326
|
+
) -> List[MemoryEntry]:
|
|
327
|
+
"""
|
|
328
|
+
按时间范围 + 关键词搜索记忆。
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
session_id: 会话 ID(空=跨会话)
|
|
332
|
+
start_time: 起始时间 ISO 格式(如 "2025-01-01 00:00:00"),空=不限
|
|
333
|
+
end_time: 截止时间 ISO 格式,空=不限
|
|
334
|
+
keyword: 关键词过滤(LIKE 模糊匹配),空=不限
|
|
335
|
+
limit: 返回数量
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
匹配的记忆条目列表(按时间倒序)
|
|
339
|
+
"""
|
|
340
|
+
conn = self._get_conn()
|
|
341
|
+
conditions = ["category != ''", "(expires_at = '' OR expires_at > ?)"]
|
|
342
|
+
params: list = [timestamp()]
|
|
343
|
+
|
|
344
|
+
if session_id:
|
|
345
|
+
conditions.append("session_id = ?")
|
|
346
|
+
params.append(session_id)
|
|
347
|
+
if start_time:
|
|
348
|
+
conditions.append("created_at >= ?")
|
|
349
|
+
params.append(start_time)
|
|
350
|
+
if end_time:
|
|
351
|
+
conditions.append("created_at <= ?")
|
|
352
|
+
params.append(end_time)
|
|
353
|
+
if keyword:
|
|
354
|
+
conditions.append("(content LIKE ? OR summary LIKE ? OR key LIKE ?)")
|
|
355
|
+
like_pattern = f"%{keyword}%"
|
|
356
|
+
params.extend([like_pattern, like_pattern, like_pattern])
|
|
357
|
+
|
|
358
|
+
where = " AND ".join(conditions)
|
|
359
|
+
sql = f"SELECT * FROM memories WHERE {where} ORDER BY created_at DESC LIMIT ?"
|
|
360
|
+
params.append(limit)
|
|
361
|
+
rows = conn.execute(sql, params).fetchall()
|
|
362
|
+
return [MemoryEntry.from_row(row) for row in rows]
|
|
363
|
+
|
|
299
364
|
def get_conversation_text(
|
|
300
365
|
self,
|
|
301
366
|
session_id: str,
|
package/package.json
CHANGED
|
Binary file
|