✨ 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>
This commit is contained in:
Rumio
2026-07-03 08:53:56 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bdc1374848
commit 80fc5b86a7
223 changed files with 41885 additions and 10006 deletions
@@ -0,0 +1,15 @@
from .blackboard import BlackboardToolkit
from .hitl import HITLToolkit
from .memory import MemoryManagementToolkit
from .rest_api import RestApiToolkit
from .sandbox import SandboxToolkit
from .slots import MemorySlotToolkit
__all__ = [
"BlackboardToolkit",
"HITLToolkit",
"MemoryManagementToolkit",
"MemorySlotToolkit",
"RestApiToolkit",
"SandboxToolkit",
]
@@ -0,0 +1,72 @@
from typing import Any
from pydantic import Field, create_model
from zhenxun.services.ai.core.exceptions import ToolRetryError
from zhenxun.services.ai.run.blackboard import BlackboardManager
from zhenxun.services.ai.tools.core.tool import FunctionTool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolOptions, ToolResult
from zhenxun.utils.pydantic_compat import model_fields
class BlackboardToolkit(BaseToolkit):
"""
黑板工具箱,将底层的 BlackboardManager 桥接给大模型,提供动态的读写工具。
"""
default_prefix = ""
def __init__(self, manager: BlackboardManager, **kwargs: Any):
super().__init__(**kwargs)
self.manager = manager
self._injected_tools.extend(self._create_dynamic_tools())
def _create_dynamic_tools(self) -> list[FunctionTool]:
async def read_blackboard() -> ToolResult:
"""读取当前团队的全局共享黑板状态。"""
content = await self.manager.read()
return ToolResult(output=content)
read_tool = FunctionTool(
func=read_blackboard,
name="read_blackboard",
description="读取当前团队的全局共享黑板最新状态。当你需要获取最新数据或了解现状时请调用此工具。",
)
optional_fields = {}
for field in model_fields(self.manager.schema):
desc = (
getattr(field.field_info, "description", None)
if hasattr(field, "field_info")
else None
)
optional_fields[field.name] = (
field.annotation | None,
Field(default=None, description=desc),
)
UpdateSchema = create_model(
f"Update_{self.manager.schema.__name__}", **optional_fields
)
async def update_blackboard(**kwargs) -> ToolResult:
valid_kwargs = {k: v for k, v in kwargs.items() if v is not None}
if not valid_kwargs:
return ToolResult(
output="警告:没有提供任何有效的更新字段,黑板状态未发生改变。"
)
try:
res = await self.manager.update(**valid_kwargs)
return ToolResult(output=res)
except ValueError as e:
raise ToolRetryError(str(e))
update_tool = FunctionTool(
func=update_blackboard,
name="update_blackboard",
description="局部更新全局黑板状态。请仅提供你需要修改的字段即可,不需要改变的字段请留空。",
settings=ToolOptions(args_schema=UpdateSchema),
)
return [read_tool, update_tool]
@@ -0,0 +1,56 @@
from zhenxun.services.ai.core.exceptions import AbortException, ToolFatalError
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
from zhenxun.services.ai.run import Inject, RunContext
from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
class HITLToolkit(BaseToolkit):
"""
Human-in-the-Loop (HITL) 工具箱。
提供大模型主动挂起执行流并向用户提问求助的能力。
"""
default_prefix = ""
default_instructions = (
"## 人机协同求助系统\n"
"你拥有向用户发起提问的权限。当你遇到困难时(例如缺少关键信息、连续执行某个操作出错、找不到文件等),"
"可以使用 `ask_user_for_help` 工具请求用户协助。"
"用户回答后,你将收到答案并可以继续任务。"
)
@tool(
name="ask_user_for_help",
description="向当前对话的用户提出问题以获取信息或指导。当你无法独立完成任务时,请调用此工具。",
)
async def ask_user_for_help(
self,
question: str,
hitl: Inject.HITL,
context: RunContext,
) -> ToolResult:
try:
user_reply = await hitl.ask_text(
f"🤖 [AI 提问]\n{question}\n\n(请在 60 秒内回复,或回复'取消')",
timeout=60.0,
)
except ToolFatalError:
return ToolResult(
output="用户未在规定时间内回复 (超时未响应),"
"你必须尝试自行解决或中止任务。",
).as_error()
except AbortException as e:
raise e
logger.info(f"收到用户求助回复: {user_reply}")
if context and context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name=context.call.tool_name, content="🗣️ 已收到用户的回复"
)
)
return ToolResult(output=f"用户提供的回复是: {user_reply}")
@@ -0,0 +1,324 @@
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()
@@ -0,0 +1,129 @@
from zhenxun.services.ai.tools.core.tool import ServerSideTool
class WebSearchTool(ServerSideTool):
"""原生网页搜索工具"""
type_id = "web_search"
def __init__(
self,
name: str = "google_search",
description: str = "使用 Google 搜索查找实时信息。",
dynamic_threshold: float | None = None,
domain_filters: list[str] | None = None,
):
super().__init__(name=name, description=description)
self.dynamic_threshold = dynamic_threshold
self.domain_filters = domain_filters
class CodeExecutionTool(ServerSideTool):
"""原生代码沙箱执行工具"""
type_id = "code_execution"
def __init__(self, name: str = "code_execution", timeout: int | None = None):
super().__init__(
name=name, description="执行 Python 代码来解决数学或数据问题。"
)
self.timeout = timeout
class ComputerUseTool(ServerSideTool):
"""原生的桌面环境控制工具"""
type_id = "computer_use"
def __init__(self, display_width_px: int = 1024, display_height_px: int = 768):
super().__init__(name="computer_use", description="控制电脑用户界面。")
self.display_width_px = display_width_px
self.display_height_px = display_height_px
class FileSearchTool(ServerSideTool):
"""原生文件检索工具"""
type_id = "file_search"
def __init__(self, name: str = "file_search"):
super().__init__(name=name, description="搜索已索引的文件。")
class GoogleMapsTool(ServerSideTool):
"""原生谷歌地图检索工具"""
type_id = "google_map"
def __init__(self):
super().__init__(name="google_map", description="搜索 Google 地图数据。")
class UrlContextTool(ServerSideTool):
"""原生 URL 上下文拉取工具"""
type_id = "url_context"
def __init__(self):
super().__init__(name="url_context", description="从指定 URL 获取上下文信息。")
class Native:
"""
云端原生工具命名空间工厂 (Namespace Factory)。
为开发者提供统一的云端内置工具调用入口,享受顶级 IDE 补全体验。
此类工具仅会向大模型提供描述,物理执行发生在各大模型厂商的服务端。
"""
@classmethod
def web_search(
cls,
name: str = "google_search",
description: str = "使用 Google 搜索查找实时信息。",
dynamic_threshold: float | None = None,
domain_filters: list[str] | None = None,
) -> WebSearchTool:
"""
原生网页搜索引擎工具 (如 Google Search, Bing)。
"""
return WebSearchTool(name, description, dynamic_threshold, domain_filters)
@classmethod
def code_execution(
cls, name: str = "code_execution", timeout: int | None = None
) -> CodeExecutionTool:
"""
原生代码沙箱执行工具 (如 Gemini Code Execution, OpenAI Advanced Data Analysis)。
"""
return CodeExecutionTool(name, timeout)
@classmethod
def computer_use(
cls, display_width_px: int = 1024, display_height_px: int = 768
) -> ComputerUseTool:
"""
原生桌面环境控制工具 (如 Claude Computer Use)。
"""
return ComputerUseTool(display_width_px, display_height_px)
@classmethod
def file_search(cls, name: str = "file_search") -> FileSearchTool:
"""
原生文件检索工具。
"""
return FileSearchTool(name)
@classmethod
def google_map(cls) -> GoogleMapsTool:
"""
原生谷歌地图检索工具 (Gemini 独占)。
"""
return GoogleMapsTool()
@classmethod
def url_context(cls) -> UrlContextTool:
"""
原生 URL 上下文拉取工具。
"""
return UrlContextTool()
@@ -0,0 +1,132 @@
import json
from typing import Any, Literal
import httpx
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.decorators import tool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
class RestApiToolkit(BaseToolkit):
"""
通用 REST API 工具箱。
让大模型无需编写任何代码即可调用外部 HTTP 接口获取或提交数据。
"""
default_prefix = ""
default_instructions = (
"你拥有调用外部 REST API 接口的能力。\n"
"1. 使用 `make_request` 工具发起 HTTP 请求。\n"
"2. 如果请求失败,请分析返回的错误信息。可能需要调整请求参数 (params) 或请求体 (json_data)。\n" # noqa: E501
"3. 务必根据返回的 JSON 数据,提取对用户有用的信息进行回答。"
)
def __init__(
self,
base_url: str = "",
default_headers: dict[str, str] | None = None,
default_params: dict[str, Any] | None = None,
timeout: int = 30,
**kwargs: Any,
):
"""
初始化通用 REST API 工具箱。
参数:
base_url: 接口基础 URL,默认发起请求时将与其拼接。
default_headers: 发起请求时携带的默认 Headers 请求头。
default_params: 发起请求时携带的默认 URL 查询参数。
timeout: 请求超时时间,单位为秒。
kwargs: 其他透传给 BaseToolkit 的参数。
"""
super().__init__(**kwargs)
self.base_url = base_url.rstrip("/") if base_url else ""
self.default_headers = default_headers or {}
self.default_params = default_params or {}
self.timeout = timeout
@tool(name="make_request")
async def make_request(
self,
endpoint: str,
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"] = "GET",
params: dict[str, Any] | None = None,
json_data: dict[str, Any] | None = None,
context: RunContext | None = None,
) -> ToolResult:
"""
发起 HTTP 请求获取外部数据。
Args:
endpoint: API的路径(若初始化了base_url,将自动拼接)。如果是一个完整的URL(以http开头),则直接请求该URL。
method: HTTP 请求方法。
params: URL 查询参数字典。
json_data: JSON 请求体字典。
""" # noqa: E501
if endpoint.startswith("http://") or endpoint.startswith("https://"):
url = endpoint
else:
url = (
f"{self.base_url}/{endpoint.lstrip('/')}" if self.base_url else endpoint
)
merged_params = self.default_params.copy()
if params:
merged_params.update(params)
logger.info(f"🌐 [RestApiToolkit] 正在请求 {method} {url}")
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.request(
method=method,
url=url,
params=merged_params,
json=json_data,
headers=self.default_headers,
)
try:
response_data = response.json()
except json.JSONDecodeError:
response_data = response.text
result_dict = {"status_code": response.status_code, "data": response_data}
is_error = response.status_code >= 400
result = ToolResult(output=result_dict)
if context and context.run.event_bus:
msg = (
f"🌐 已调用 API: {url}"
if not is_error
else f"❌ API 调用失败 (Status: {response.status_code})"
)
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name=context.call.tool_name if context else "make_request",
content=msg,
)
)
if is_error:
result = result.as_error()
return result
except Exception as e:
logger.error(f"RestApiToolkit 请求失败: {e}")
if context and context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name=context.call.tool_name if context else "make_request",
content="❌ API 网络请求发生框架级错误",
)
)
return ToolResult(
output=f"网络请求失败: {type(e).__name__} - {e}"
).as_error()
@@ -0,0 +1,425 @@
import asyncio
from typing import Any, Protocol
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent, UserCustomEvent
from zhenxun.services.ai.run import Inject, RunContext
from zhenxun.services.ai.sandbox.models import (
SandboxBlueprint,
SandboxExecutionResult,
)
from zhenxun.services.ai.tools.core.decorators import silent, tool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_copy
class PythonPluginProtocol(Protocol):
@property
def supports_state(self) -> bool: ...
async def execute(
self,
code: str,
timeout: int = 30,
injected_code: str | None = None,
) -> SandboxExecutionResult: ...
class SandboxToolkit(BaseToolkit):
default_prefix = ""
default_instructions = (
"## 🖥️ 沙箱工作区交互规范\n"
"你拥有物理隔离的沙箱环境。请严格遵循以下调度规则:\n"
"1. **短时/非交互任务**:直接使用 `execute_code`(如数据计算、算法运行)。"
"⚠️ 该工具**严禁包含 `input()`** 等阻塞式交互。\n"
"2. **长驻/交互式任务**:若需运行 Web Server 或含 `input()` 的交互程序,"
"**必须**:\n"
" - 先用 `write_sandbox_file` 将代码保存至当前工作区。\n"
" - 再用 `execute_terminal_command(is_interactive=True)` 启动并挂起进程。\n"
" - 通过 `send_sandbox_input` / `read_sandbox_screen` 与屏幕画面交互。\n"
"3. **终端互斥锁**:虚拟终端只能单线程运行前台程序。"
"若程序报错或死循环卡死,**必须**先调用 `interrupt_sandbox` "
"打断它,方可进行后续修改。"
)
def __init__(
self,
blueprint: SandboxBlueprint | None = None,
sandbox_session_id: str | None = None,
**kwargs: Any,
):
"""
初始化沙箱工作区工具箱。
参数:
blueprint: 沙箱蓝图配置对象,包含沙箱运行时的各种参数定义(例如是否需要状态、端口映射等)。
sandbox_session_id: 显式指定的沙箱会话 ID。如果不指定,则自动与当前执行上下文绑定。
kwargs: 其他透传给 BaseToolkit 的参数。
""" # noqa: E501
super().__init__(**kwargs)
self.blueprint = blueprint or SandboxBlueprint()
self.sandbox_session_id = sandbox_session_id
async def _get_session(self, context: RunContext, sandbox: Any) -> Any:
"""获取或创建沙箱会话实例"""
session_id = (
self.sandbox_session_id or context.session_id or "default_sandbox_session"
)
return await sandbox.get_or_create_session(session_id, self.blueprint)
def _get_pty(self, context: RunContext) -> Any:
"""从上下文获取虚拟终端"""
session_id = self.sandbox_session_id or context.session_id or "default"
return context.session.shared_state.get(f"pty_{session_id}")
@tool(
name="execute_code",
description=(
"在沙箱环境中执行代码。\n"
"支持多语言执行,请在 language 参数中指定具体的编程语言"
"(如 python, bash 等)。\n"
"默认超时时间为 45 秒。如果你的代码需要更长的时间或等待用户输入,"
"它将被挂在后台运行。\n"
"你将收到目前的屏幕输出,并可以决定后续操作。"
),
)
async def execute_code(
self,
code: str,
language: str,
context: RunContext,
sandbox: Inject.Sandbox,
) -> ToolResult:
session_id = (
self.sandbox_session_id or context.session_id or "default_sandbox_session"
)
logger.info(
f"大模型请求执行 {language} 代码 (Session: {session_id}, "
f"长度: {len(code)} 字符)"
)
from zhenxun.services.ai.sandbox.runtimes import CodeExecutorRegistry
ns = getattr(context.session, "namespace", "global") if context else "global"
supported = CodeExecutorRegistry.get_supported_languages(ns)
if CodeExecutorRegistry._normalize_lang(language) not in supported:
from zhenxun.services.ai.core.exceptions import ToolRetryError
raise ToolRetryError(
f"当前沙箱不支持该语言 '{language}'。支持的语言有: {supported}。"
"请换用支持的语言重新编写代码!"
)
bp = model_copy(self.blueprint, deep=True)
if context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name="Sandbox", content="正在分析代码依赖并分配沙箱环境..."
)
)
executor = await self._get_session(context, sandbox)
state_key = f"code_exec_{session_id}_{language}"
code_executor = context.session.shared_state.get(state_key)
if code_executor and getattr(code_executor, "session", None) is not executor:
code_executor = None
if not code_executor:
code_executor = CodeExecutorRegistry.create_executor(
language, bp.needs_state, executor, namespace=ns
)
context.session.shared_state[state_key] = code_executor
if context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name="Sandbox",
content=f"沙箱已就绪,正在后台执行 {language} 代码...",
)
)
import re
clean_code = code.strip()
match = re.search(
r"^```[a-zA-Z0-9_-]*\r?\n(.*?)\r?\n```$",
clean_code,
re.IGNORECASE | re.DOTALL,
)
if match:
clean_code = match.group(1)
line_buffer = ""
async def _stream_output(stream_type: str, data: bytes):
nonlocal line_buffer
line_buffer += data.decode("utf-8", errors="replace")
try:
result = await code_executor.execute_code(
code=clean_code,
timeout=45,
on_output=_stream_output,
)
except Exception as e:
logger.error(f"沙箱执行框架异常: {e}")
from zhenxun.services.ai.core.exceptions import AbortException
raise AbortException(
reason=f"System Error: 容器执行环境不可用,异常信息: {e}",
display=f"❌ 沙箱框架异常: {e}",
)
output_parts = []
if result.exit_code != 0 or result.stderr:
output_parts.append(f"Exit Code: {result.exit_code}")
if result.stdout:
output_parts.append(f"Stdout:\n{result.stdout.strip()[:2000]}")
if result.stderr:
output_parts.append(f"Stderr:\n{result.stderr.strip()[:2000]}")
if result.error:
output_parts.append(f"System Error:\n{result.error}")
output_text = "\n".join(output_parts)
system_notice = ""
if result.is_timeout:
system_notice = (
"\n\n⚠️ 警告: 代码执行超时!进程仍在后台挂起,"
"若为死循环请立刻使用 `interrupt_sandbox`。"
)
if result.stderr and "StdinNotImplementedError" in result.stderr:
system_notice += (
"\n\n🚨 致命错误: 当前环境不支持 input()。"
"请将代码写入文件并通过 "
"execute_terminal_command(is_interactive=True) 运行!"
)
image_bytes_list = []
if getattr(result, "images", None):
import base64
for b64_str in result.images:
try:
image_bytes_list.append(base64.b64decode(b64_str))
except Exception:
pass
if getattr(result, "artifacts", None):
for filename, file_bytes in result.artifacts.items():
if filename.endswith((".png", ".jpg", ".jpeg")):
image_bytes_list.append(file_bytes)
from zhenxun.services.ai.core.messages import (
ImagePart,
LLMContentPart,
TextPart,
)
final_output: list[LLMContentPart] = [TextPart(text=output_text.strip())]
for img_bytes in image_bytes_list:
final_output.append(ImagePart(raw=img_bytes))
final_output_text = output_text.strip()
if system_notice:
final_output_text += f"\n\n{system_notice}"
final_output[0] = TextPart(text=final_output_text)
result = ToolResult(
output=final_output if len(final_output) > 1 else final_output_text
)
if len(image_bytes_list) > 0 and context and context.run.event_bus:
await context.run.event_bus.emit(UserCustomEvent(display=final_output))
return result
@tool(
name="execute_terminal_command",
description=(
"在沙箱的终端中执行 Shell 命令"
"(例如 `python3 script.py` 或 `npm start`)。\n"
"如果只是执行普通的短时非交互脚本,"
"保持 is_interactive=False 即可(执行速度极快且稳定)。\n"
"如果程序包含 `input()` 或需要长期驻留(如 Server),"
"请务必设置 is_interactive=True 开启虚拟屏幕模式!"
),
)
async def execute_terminal_command(
self,
command: str,
context: RunContext,
sandbox: Inject.Sandbox,
is_interactive: bool = False,
) -> ToolResult:
session_id = self.sandbox_session_id or context.session_id or "default"
if context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name="Sandbox", content=f"正在虚拟终端执行命令: {command} ..."
)
)
executor = await self._get_session(context, sandbox)
if not is_interactive:
res = await executor.run_process(command)
if getattr(res, "is_timeout", False):
return ToolResult(
output=(
"⚠️ 警告: 命令执行超时!若程序需常驻或等待输入,"
f"请务必设置 is_interactive=True。\n"
f"Stdout: {res.stdout}\nStderr: {res.stderr}"
)
).as_error()
return ToolResult(
output=(
f"Exit Code: {res.exit_code}\n"
f"Stdout: {res.stdout}\n"
f"Stderr: {res.stderr}"
)
)
pty = self._get_pty(context)
if pty:
await pty.close()
interactive_session = await executor.create_pty_session()
context.session.shared_state[f"pty_{session_id}"] = interactive_session
try:
await interactive_session.start(command)
await asyncio.sleep(1.5)
screen = await interactive_session.read_output()
return ToolResult(
output=f"📺 虚拟终端已启动,屏幕快照:\n```text\n{screen}\n```"
)
except Exception as e:
return ToolResult(output=f"虚拟屏幕启动异常: {e}").as_error()
@tool(
name="send_sandbox_input",
description=(
"向当前沙箱中正在挂起运行的后台进程"
"(如等待 input() 的 Python 脚本)发送输入文本。\n"
"注意:你需要自己在文本末尾加上换行符 \\n 来模拟回车键。"
),
)
async def send_sandbox_input(self, text: str, context: RunContext) -> ToolResult:
interactive_session = self._get_pty(context)
if not interactive_session:
return ToolResult(
output=(
"错误:没有运行中的交互式虚拟屏幕。"
"请先调用 execute_terminal_command(is_interactive=True)。"
)
).as_error()
text = text.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t")
await interactive_session.send_input(text)
await asyncio.sleep(1.5)
output = await interactive_session.read_output(timeout=5)
if context and context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name=context.call.tool_name, content="⌨️ 已向后台进程发送输入"
)
)
return ToolResult(
output=f"已发送按键。📺 屏幕刷新后快照如下:\n```text\n{output}\n```"
)
@tool(
name="read_sandbox_screen",
description=(
"主动窥探并读取当前虚拟屏幕的画面。当你觉得后台程序可能已经渲染出新内容时,可以使用此工具。"
),
)
async def read_sandbox_screen(self, context: RunContext) -> ToolResult:
interactive_session = self._get_pty(context)
if not interactive_session:
return ToolResult(output="没有运行中的虚拟屏幕。").as_error()
output = await interactive_session.read_output()
return ToolResult(output=f"📺 当前屏幕快照:\n```text\n{output}\n```")
@tool(
name="interrupt_sandbox",
description=(
"向当前沙箱发送 Ctrl+C (SIGINT) 信号,强制中断正在死循环或挂起的后台进程。"
),
)
async def interrupt_sandbox(self, context: RunContext) -> ToolResult:
interactive_session = self._get_pty(context)
if not interactive_session:
return ToolResult(output="没有运行中的虚拟屏幕需要中断。").as_error()
await interactive_session.interrupt()
await asyncio.sleep(1)
output = await interactive_session.read_output()
if context and context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name=context.call.tool_name, content="🛑 已强制中断后台进程"
)
)
return ToolResult(
output="✅ 成功发送 Ctrl+C 中断信号。"
f"📺 当前屏幕快照:\n```text\n{output}\n```"
)
@tool(
name="write_sandbox_file",
description="将文本内容写入沙箱文件系统中,支持保存大块数据或配置,避免超过对话上下文。",
)
@silent()
async def write_sandbox_file(
self, path: str, content: str, context: RunContext, sandbox: Inject.Sandbox
) -> ToolResult:
executor = await self._get_session(context, sandbox)
success = await executor.write(path, content.encode("utf-8"))
if success:
logger.info(f"📝 已向沙箱写入文件: {path}")
return ToolResult(output=f"成功将内容写入文件: {path}")
else:
from zhenxun.services.ai.core.exceptions import AbortException
raise AbortException(
reason="写入文件失败 (当前沙箱环境失联或不支持持久化IO)",
display="❌ 写入文件失败:沙箱失联",
)
@tool(
name="read_sandbox_file",
description="从沙箱文件系统中读取指定文件的文本内容。",
)
@silent()
async def read_sandbox_file(
self, path: str, context: RunContext, sandbox: Inject.Sandbox
) -> ToolResult:
executor = await self._get_session(context, sandbox)
try:
content_bytes = await executor.read(path)
content = content_bytes.decode("utf-8", errors="replace")
if content.startswith("Error:") or content.startswith("Failed to"):
return ToolResult(output=content).as_error()
logger.info(f"已读取沙箱文件 {path} (共 {len(content)} 字符)")
return ToolResult(output=content)
except Exception as e:
from zhenxun.services.ai.core.exceptions import AbortException
raise AbortException(reason=f"读取文件发生框架级异常: {e}")
@@ -0,0 +1,368 @@
import asyncio
from contextlib import asynccontextmanager
import time
from typing import Any, Literal
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 (
MemorySlot,
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
_SLOT_LOCKS: dict[str, asyncio.Lock] = {}
_GLOBAL_LOCK = asyncio.Lock()
@asynccontextmanager
async def _slot_lock(session_id: str, label: str):
key = f"{session_id}:{label}"
async with _GLOBAL_LOCK:
if key not in _SLOT_LOCKS:
_SLOT_LOCKS[key] = asyncio.Lock()
lock = _SLOT_LOCKS[key]
async with lock:
yield
class MemorySlotToolkit(BaseToolkit):
"""
中期记忆槽工具箱。
向大模型开放直接编辑上下文 XML 节点的能力。
"""
default_prefix = ""
class Config:
"""声明式配置:该工具箱下的所有工具默认静默"""
shared_options = ToolOptions(silent=True)
default_instructions = """\
## 📋 状态与规则面板 (Memory Slots / 中期记忆)
该系统是你的「桌面便利贴」或「共享黑板」,用于保存你当前需要随时查阅的核心状态与全局规范。
### 💡 核心机制
- 被保存在记忆槽中的内容(如果已置顶),会在每次对话时**直接注入到你的上下文提示词中**,你无需任何搜索即可看见。
- 槽位容量极其有限,仅用于维持当前最新的运行状态。
### 📝 何时使用记忆槽?
- **维护全局规则**:例如设定"用户整体偏好"、"沟通口吻"、"全局指导原则"等需要时刻遵守的规范(使用 `update_slot`)。
- **追踪当前进度**:例如记录"待办事项清单"、"当前任务进度"、"上下文摘要"(使用 `append_slot` 列表或 `update_slot` 覆盖)。
### ⚙️ 操作规范
1. **探索可用面板**:接手新任务时,可使用 `list_slots` 宏观查看当前存在哪些状态面板。
2. **保持精简**:槽位有严格的字符数限制。当内容过长时,请主动将其归档到长期记忆后,重新提炼并覆盖槽位,或直接调用 `delete_slot` 删除不再需要的槽位。\
""" # 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_ctx(
self, context: RunContext, scope_name: str | None = None
) -> tuple[Any, SessionMetadata]:
ns = self._namespace or getattr(context.session, "namespace", "global")
slot_ctx = memory_manager.get_slot_context(self.memory_config, namespace=ns)
scope_builder = None
if (
self.memory_config
and self.memory_config.slots
and self.memory_config.slots.scopes
):
scopes_dict = self.memory_config.slots.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.slots
and self.memory_config.slots.scopes
):
for name, builder in self.memory_config.slots.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 slot_ctx, 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.slots
or not self.memory_config.slots.enable
):
return tools
scopes_dict = self.memory_config.slots.scopes
if not scopes_dict:
return tools
scope_keys = tuple(scopes_dict.keys())
if len(scope_keys) > 1:
ScopeType = Literal[scope_keys]
UpdateArgs = create_model(
"UpdateSlotArgs",
label=(str, Field(..., description="槽位标签名称")),
content=(str, Field(..., description="槽位内容(全量覆写)")),
description=(str, Field(default="", description="槽位的简要说明")),
scope=(
ScopeType,
Field(..., description="选择记忆存储的隔离分区"),
),
)
AppendArgs = create_model(
"AppendSlotArgs",
label=(str, Field(..., description="槽位标签名称")),
text=(str, Field(..., description="要追加的文本内容")),
scope=(
ScopeType,
Field(..., description="选择记忆存储的隔离分区"),
),
)
ReadArgs = create_model(
"ReadSlotArgs",
label=(str, Field(..., description="槽位标签名称")),
scope=(
ScopeType,
Field(..., description="选择记忆存储的隔离分区"),
),
)
DeleteArgs = create_model(
"DeleteSlotArgs",
label=(str, Field(..., description="槽位标签名称")),
scope=(
ScopeType,
Field(..., description="选择记忆存储的隔离分区"),
),
)
ListArgs = create_model(
"ListSlotsArgs",
scope=(
ScopeType,
Field(..., description="选择记忆存储的隔离分区"),
),
)
for t_name, t in tools.items():
if t_name.endswith("update_slot"):
t.args_schema = UpdateArgs
elif t_name.endswith("append_slot"):
t.args_schema = AppendArgs
elif t_name.endswith("read_slot"):
t.args_schema = ReadArgs
elif t_name.endswith("delete_slot"):
t.args_schema = DeleteArgs
elif t_name.endswith("list_slots"):
t.args_schema = ListArgs
return tools
@tool(
description="列出当前所有可用的记忆槽(包括未置顶显示的槽位),方便你了解有哪些信息可供读取或更新。"
)
async def list_slots(self, context: RunContext, **kwargs) -> ToolResult:
scope_name = kwargs.get("scope")
slot_ctx, meta = self._get_runtime_meta_and_ctx(context, scope_name)
if not slot_ctx:
return ToolResult(output="错误:未配置记忆槽后端").as_error()
slots = await slot_ctx.list_all_slots(meta)
if not slots:
return ToolResult(output="当前没有任何记忆槽。")
res = ["已创建的记忆槽列表:"]
show_scope = False
if (
self.memory_config
and self.memory_config.slots
and self.memory_config.slots.scopes
and len(self.memory_config.slots.scopes) > 1
):
show_scope = True
for s in slots:
pin_str = "置顶" if s.pinned else "隐藏"
if show_scope:
semantic_name = meta.scope_name_mapping.get(s.scope, "未知")
res.append(
f"- [{s.label}] (分区: "
f"{semantic_name}, {pin_str}) - {s.description}"
)
else:
res.append(f"- [{s.label}] ({pin_str}) - {s.description}")
return ToolResult(output="\n".join(res))
@tool(description="读取某个尚未展示在上下文中的记忆槽完整内容。")
async def read_slot(self, label: str, context: RunContext, **kwargs) -> ToolResult:
scope_name = kwargs.get("scope")
slot_ctx, meta = self._get_runtime_meta_and_ctx(context, scope_name)
if not slot_ctx:
return ToolResult(output="错误:未配置记忆槽后端").as_error()
slot = await slot_ctx.get_slot(meta, label)
if not slot:
return ToolResult(output=f"未找到标签为 '{label}' 的槽位。").as_error()
return ToolResult(output=f"[{label}] 内容:\n{slot.content}")
@tool(description=("更新或新建记忆槽的内容(全量覆写)。"))
async def update_slot(
self,
label: str,
content: str,
context: RunContext,
description: str = "",
**kwargs,
) -> ToolResult:
scope_name = kwargs.get("scope")
slot_ctx, meta = self._get_runtime_meta_and_ctx(context, scope_name)
if not slot_ctx:
return ToolResult(output="错误:未配置记忆槽后端").as_error()
target_sid = meta.scope_prefix
async with _slot_lock(target_sid, label):
slot = await slot_ctx.get_slot(meta, label)
if not slot:
slot = MemorySlot(
label=label,
content=content,
scope=meta.scope_prefix,
description=description,
)
else:
slot.content = content
slot.scope = meta.scope_prefix
if description:
slot.description = description
slot.updated_at = time.time()
if len(content) > slot.size_limit:
return ToolResult(
output=(
f"错误:内容长度超过限制 ({len(content)} > {slot.size_limit})。"
)
).as_error()
await slot_ctx.set_slot(meta, slot)
return ToolResult(output=f"已成功将 '{label}' 更新至记忆槽中。")
@tool(description="在指定记忆槽的末尾追加文本(例如追加待办事项清单)。")
async def append_slot(
self, label: str, text: str, context: RunContext, **kwargs
) -> ToolResult:
scope_name = kwargs.get("scope")
slot_ctx, meta = self._get_runtime_meta_and_ctx(context, scope_name)
if not slot_ctx:
return ToolResult(output="错误:未配置记忆槽后端").as_error()
slot = await slot_ctx.get_slot(meta, label)
if not slot:
return ToolResult(
output=(
f"错误:标签为 '{label}' 的槽位不存在,请先使用 update_slot 创建。"
)
).as_error()
target_sid = meta.scope_prefix
async with _slot_lock(target_sid, label):
slot = await slot_ctx.get_slot(meta, label)
if not slot:
return ToolResult(
output="错误:并发写入异常,槽位已被删除。"
).as_error()
sep = "\n" if slot.content and not slot.content.endswith("\n") else ""
new_content = f"{slot.content}{sep}{text}"
if len(new_content) > slot.size_limit:
return ToolResult(
output=(
"错误:追加后总长度超过限制 "
f"({len(new_content)} > {slot.size_limit})。"
)
).as_error()
slot.content = new_content
slot.updated_at = time.time()
await slot_ctx.set_slot(meta, slot)
return ToolResult(output=f"已成功追加至 '{label}'。")
@tool(description="删除不再需要的记忆槽(全量删除)。")
async def delete_slot(
self, label: str, context: RunContext, **kwargs
) -> ToolResult:
scope_name = kwargs.get("scope")
slot_ctx, meta = self._get_runtime_meta_and_ctx(context, scope_name)
if not slot_ctx:
return ToolResult(output="错误:未配置记忆槽后端").as_error()
target_sid = meta.scope_prefix
async with _slot_lock(target_sid, label):
await slot_ctx.delete_slot(meta, label, meta.scope_prefix)
return ToolResult(output=f"已成功删除槽位 '{label}'。")