Files
zhenxun_bot/zhenxun/services/ai/tools/providers/builtin/memory.py
T
80fc5b86a7 ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)
* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架

- 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片
- 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式
- 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统
- 【新增】引入基于 Docker 的安全代码执行沙箱环境
- 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务
- 【新增】引入输入输出安全合规护栏与自愈反思机制
- 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等)
- 【优化】优化日志脱敏与 Token 预估机制
- 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令

* 🔧 chore(deps): 更新项目依赖与配置

- 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt
- 在 pyright 配置中设置 reportMissingImports 为 none
- 调整 .gitignore 中 resources 目录的忽略规则

* ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出

- 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束
- 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用
- 调整指令处理器(Directive)的返回值为 `tool_res.output`
- 修复部分类型检查警告并优化联合类型判断语法

* ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制

- 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用
- 移除通过 `context.state` 传递魔术变量的隐式控制流设计
- 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult`
- 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令
- 补全 `MessageBuilder` 中部分核心方法的文档注释

* 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑

-【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值
-【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题
-【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求
-【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建
-【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常

* 📝 docs(core): 补充核心模块初始化方法的文档注释

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-03 08:53:56 +08:00

325 lines
12 KiB
Python

from typing import Any, Literal, Optional
from pydantic import Field, create_model
from zhenxun.services.ai.context.memory.manager import memory_manager
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolOptions, ToolResult
from zhenxun.services.log import logger
class MemoryManagementToolkit(BaseToolkit):
"""
主动记忆管理工具箱 (Agentic Memory Toolkit)。
赋予大模型自主存取、修改和删除用户长期设定的能力。
"""
default_prefix = ""
class Config:
"""声明式配置:该工具箱下的所有工具默认静默"""
shared_options = ToolOptions(silent=True)
default_instructions = """\
## 🧠 长期记忆管理系统 (Long-Term Memory)
该系统是你的「无限档案馆」。⚠️ **注意:系统默认不会主动向你提供所有历史信息,你必须通过主动搜索来回忆。**
### 📝 何时使用长期记忆?
- **记录离散事实与经验**:当需要记录某个独立事件、历史经验、问题解决方案或具体事实时(使用 `save_memory`)。
- **寻找历史线索**:当遇到未知情况,或用户提及过去的事情、特定设定时,必须主动检索历史库(使用 `search_memory`)。
### ⚙️ 操作规范
1. **隐式记录**:当接收到值得记忆的重要信息时,请静默记录。除非用户主动提问,否则无需向用户显式汇报"我已记住"。
2. **按需更新**:如果发现某项历史记录已过时或状态发生扭转,请先检索出它的 ID,再进行修改(使用 `update_memory`)或废弃(使用 `delete_memory`)。
3. **精准提炼**:保存记忆时请提炼核心价值,避免保存无意义的闲聊。\
""" # noqa: E501
def __init__(
self,
memory_config: MemoryConfig | None = None,
namespace: str | None = None,
**kwargs: Any,
):
"""
初始化主动记忆管理工具箱。
参数:
memory_config: 记忆系统的全局配置对象,为空则使用全局默认。
namespace: 当前隔离环境的命名空间。
kwargs: 其他透传给 BaseToolkit 的参数。
"""
super().__init__(**kwargs)
self.memory_config = memory_config
self._namespace = namespace
def _get_runtime_meta_and_scope(
self, context: RunContext, scope_name: str | None = None
) -> tuple[Any, SessionMetadata]:
"""动态获取当前运行时的数据库实例与会话元信息,实现无状态化"""
ns = self._namespace or getattr(context.session, "namespace", "global")
scope = memory_manager.get_long_term_memory(self.memory_config, namespace=ns)
scope_builder = None
if (
self.memory_config
and self.memory_config.long_term
and self.memory_config.long_term.scopes
):
scopes_dict = self.memory_config.long_term.scopes
if not scope_name:
scope_builder = next(iter(scopes_dict.values()))
else:
scope_builder = scopes_dict.get(scope_name)
if not scope_builder:
scope_builder = getattr(self.memory_config, "base_isolation", None)
if not scope_builder:
from zhenxun.services.ai.context.memory.types import Isolation
scope_builder = Isolation.AGENT_USER()
selector = scope_builder.resolve(
deps=context.deps,
prefix="",
default_namespace=ns,
default_agent=context.run.agent_name,
)
parts = selector.get_scope_parts()
all_scopes = {"/"}
current_path = ""
for part in parts:
current_path += f"/{part}"
all_scopes.add(current_path)
accessible_scopes = list(all_scopes)
accessible_scopes.sort(key=lambda x: len(x.split("/")))
scope_name_mapping = {}
if (
self.memory_config
and self.memory_config.long_term
and self.memory_config.long_term.scopes
):
for name, builder in self.memory_config.long_term.scopes.items():
sel = builder.resolve(
deps=context.deps,
prefix="",
default_namespace=ns,
default_agent=context.run.agent_name,
)
scope_name_mapping[sel.scope_prefix] = name
session_meta = SessionMetadata(
session_id=context.session_id or "default_session",
selector=selector,
scope_prefix=selector.scope_prefix,
accessible_scopes=accessible_scopes,
scope_name_mapping=scope_name_mapping,
)
return scope, session_meta
async def get_tools(self, context: RunContext | None = None) -> dict[str, BaseTool]:
tools = await super().get_tools(context)
if not self.memory_config or not self.memory_config.long_term.enable:
return tools
scopes_dict = self.memory_config.long_term.scopes
if not scopes_dict:
return tools
scope_keys = tuple(scopes_dict.keys())
if len(scope_keys) > 1:
ScopeType = Literal[scope_keys]
SaveArgs = create_model(
"SaveMemoryArgs",
content=(str, Field(..., description="要保存的记忆内容")),
importance=(float, Field(default=0.5, description="重要性(0-1)")),
scope=(
ScopeType,
Field(..., description="选择记忆存储的隔离分区"),
),
)
SearchArgs = create_model(
"SearchMemoryArgs",
query=(str, Field(..., description="搜索关键词或问题")),
filters=(
str | None,
Field(default=None, description="可选的元数据过滤JSON字符串"),
),
scope=(
Optional[ScopeType], # noqa
Field(
default=None,
description="搜索特定的分区。留空则跨所有有权分区混合检索!",
),
),
)
UpdateArgs = create_model(
"UpdateMemoryArgs",
record_id=(str, Field(..., description="要更新的记忆ID")),
new_content=(str, Field(..., description="新的记忆内容")),
importance=(float, Field(default=0.5, description="重要性(0-1)")),
scope=(
ScopeType,
Field(..., description="指定该记忆所在的隔离分区"),
),
)
DeleteArgs = create_model(
"DeleteMemoryArgs",
record_id=(str, Field(..., description="要删除的记忆ID")),
scope=(
ScopeType,
Field(..., description="指定该记忆所在的隔离分区"),
),
)
for t_name, t in tools.items():
if t_name.endswith("save_memory"):
t.args_schema = SaveArgs
elif t_name.endswith("search_memory"):
t.args_schema = SearchArgs
elif t_name.endswith("update_memory"):
t.args_schema = UpdateArgs
elif t_name.endswith("delete_memory"):
t.args_schema = DeleteArgs
return tools
@tool(
name="save_memory",
description="保存关于用户的重要设定、偏好或事实到长期记忆中。",
)
async def save_memory(
self, content: str, context: RunContext, importance: float = 0.5, **kwargs
) -> ToolResult:
scope_name = kwargs.get("scope")
scope, meta = self._get_runtime_meta_and_scope(context, scope_name)
if not scope:
return ToolResult(output="错误:未启用或未配置长期记忆后端。").as_error()
await scope.remember(session=meta, content=content, importance=importance)
logger.debug(
f"[Agentic Memory] AI 主动存入记忆: {content} (重要性: {importance})"
)
return ToolResult(output="记忆已成功排入系统后台合并存入。")
@tool(
name="search_memory",
description=(
"在长期记忆中主动检索关于用户的设定和事实。"
"支持自然语言模糊搜索。"
"可选的 filters 参数用于元数据精确匹配。如果不需要过滤,"
"请直接省略该参数,不要传入空字典或空字符串。"
),
)
async def search_memory(
self,
query: str,
context: RunContext,
**kwargs,
) -> ToolResult:
scope_name = kwargs.get("scope")
scope, meta = self._get_runtime_meta_and_scope(context, scope_name)
if not scope:
return ToolResult(output="错误:未启用或未配置长期记忆后端。").as_error()
filters = kwargs.get("filters")
parsed_filters = None
if isinstance(filters, dict):
parsed_filters = filters
elif isinstance(filters, str) and filters.strip() and filters.strip() != "{}":
import json
try:
parsed_filters = json.loads(filters)
except Exception:
pass
if scope_name is not None:
meta.accessible_scopes = [meta.scope_prefix]
matches = await scope.recall(
session=meta,
query=query,
limit=5,
metadata_filter=parsed_filters,
)
if not matches:
return ToolResult(
output="未检索到相关记忆。这可能是你们关于此话题的首次探讨,请直接根据常识回答或向用户确认。"
)
results = [
(
f"ID: {m.record.id} | 内容: {m.record.content} | "
f"重要性: {m.record.metadata.get('importance', 0.5)}"
)
for m in matches
]
return ToolResult(output="检索到的记忆如下:\n" + "\n".join(results))
@tool(
name="update_memory",
description="更新指定ID的长期记忆内容。当你发现用户的某个旧设定发生改变时,请使用此工具覆盖旧记忆。",
)
async def update_memory(
self,
record_id: str,
new_content: str,
context: RunContext,
importance: float = 0.5,
**kwargs,
) -> ToolResult:
scope_name = kwargs.get("scope")
scope, meta = self._get_runtime_meta_and_scope(context, scope_name)
if not scope:
return ToolResult(output="错误:未启用或未配置长期记忆后端。").as_error()
success = await scope.update(
session=meta,
record_id=record_id,
new_content=new_content,
importance=importance,
)
if success:
logger.debug(
f"[Agentic Memory] AI 主动更新记忆: {record_id} -> {new_content}"
)
return ToolResult(
output=f"记忆 {record_id} 更新成功。新内容:{new_content}"
)
return ToolResult(
output=f"更新失败:未找到ID为 {record_id} 的记忆。"
).as_error()
@tool(
name="delete_memory",
description="根据记忆的唯一ID删除已经作废或过期的用户记忆。",
)
async def delete_memory(
self, record_id: str, context: RunContext, **kwargs
) -> ToolResult:
scope_name = kwargs.get("scope")
scope, meta = self._get_runtime_meta_and_scope(context, scope_name)
if not scope:
return ToolResult(output="错误:未启用或未配置长期记忆后端。").as_error()
deleted_count = await scope.forget(session=meta, record_ids=[record_id])
if deleted_count > 0:
logger.info(f"[Agentic Memory] AI 主动删除记忆: {record_id}")
return ToolResult(output=f"记忆 {record_id} 删除成功。")
return ToolResult(
output=f"删除失败:未找到ID为 {record_id} 的记忆。"
).as_error()