♻️ refactor(tools): 重构工具装饰器系统并优化沙箱与文档注释 (#2147)

* ♻️ refactor(ai): 重构 AI 服务模块并完善文档注释

- 【重构】统一清理并优化所有 AI 服务模块文件的导入语句,将其移至文件顶部
- 【重构】重构 `hooks.py` 中的 `Hooks` 派发逻辑,使用通用管道函数消除重复代码,并引入 `HookPoint` 描述符
- 【重构】重构工具装饰器实现,新增 `toolkit` 类装饰器,优化 `BaseToolkit` 配置合并与前缀处理
- 【功能】Docker 沙箱容器创建时支持自动注入系统代理环境变量并配置 `ExtraHosts`
- 【功能】Jupyter 服务启动前自动清理旧进程并初始化临时目录权限
- 【修复】优化 Pydantic 结构化输出校验失败时的错误信息提取,提供更详细的字段级错误反馈
- 【修复】在 `api.py` 中避免将 `ModelRetry` 和 `ControlFlowExit` 异常错误地包装为 `LLMException`
- 【文档】为 AI 服务、沙箱、工具链、工作流等核心模块补充完整的 Docstring 和类型注释

* 📝 docs(ai): 补全核心模块文档注释并清理冗余代码

- 补全 `run/context`、`run/hooks` 和 `tools/engine/registry` 中类与方法的中文文档注释
- 清理 `tools/providers/builtin/sandbox` 中未使用的 `PythonPluginProtocol` 协议及相关导入
- 规范化部分代码的格式与尾随逗号

* ♻️ refactor!(flow): 重构 Task 为 AgentTask 并优化工作流元数据定义

- 【Breaking Change】将 `Task` 重命名为 `AgentTask` 以避免命名冲突
- 更新 Agent、Team、Workflow 等模块中的类型声明与相关逻辑
- 引入 `AutoNodeMeta` 强类型元数据,替换工作流装饰器中的裸字典定义
- 将 `StepMeta`、`ConditionMeta` 和 `RouterMeta` 统一移动至 `types.py`
- 优化 `RunnableNode` 对上游 `AgentTask` 的处理与拼接逻辑
- 调整团队协作策略中 `FinishAction` 的返回值为完整结果对象

* ♻️ refactor(workflow): 移除人工确认机制并重构错误策略

- 移除工作流节点的人工确认(HITL)与挂起继续机制
- 删除 `auto` 自动化工作流及相关装饰器文件
- 将错误处理策略类从 `types.py` 拆分并移动到新文件 `policies.py`
- 优化节点执行失败时的异常信息格式化输出
- 移除 `WorkflowRunResult` 和 `StepOutput` 中与挂起相关的状态字段

* 🚨 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-05 11:47:21 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 80fc5b86a7
commit 0b32d69c9c
61 changed files with 1659 additions and 1511 deletions
@@ -1,6 +1,7 @@
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.run.context import RunContext
from zhenxun.services.ai.run.di import Inject
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
@@ -4,7 +4,7 @@ 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.run.context 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
@@ -1,31 +1,19 @@
import asyncio
from typing import Any, Protocol
from typing import Any
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent, UserCustomEvent
from zhenxun.services.ai.run import Inject, RunContext
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.di import Inject
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.decorators import Rules, 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 = ""
@@ -383,8 +371,8 @@ class SandboxToolkit(BaseToolkit):
@tool(
name="write_sandbox_file",
description="将文本内容写入沙箱文件系统中,支持保存大块数据或配置,避免超过对话上下文。",
rules=[Rules.silent()],
)
@silent()
async def write_sandbox_file(
self, path: str, content: str, context: RunContext, sandbox: Inject.Sandbox
) -> ToolResult:
@@ -405,8 +393,8 @@ class SandboxToolkit(BaseToolkit):
@tool(
name="read_sandbox_file",
description="从沙箱文件系统中读取指定文件的文本内容。",
rules=[Rules.silent()],
)
@silent()
async def read_sandbox_file(
self, path: str, context: RunContext, sandbox: Inject.Sandbox
) -> ToolResult:
@@ -17,7 +17,7 @@ from pydantic import ValidationError
from zhenxun.services.ai.core.models import ToolDefinition
from zhenxun.services.ai.core.stream_events import UserCustomEvent
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.sandbox.addons.base import BaseMcpProxyExtension
from zhenxun.services.ai.sandbox.models import SandboxBlueprint
from zhenxun.services.ai.tools.core.tool import BaseTool
@@ -120,6 +120,16 @@ class MCPRemoteTool(BaseTool):
parameters: dict,
toolkit: "MCPToolkit",
):
"""
初始化远端 MCP 工具在本地的代理对象。
参数:
name: 工具在本地注册的唯一标识名称。
original_tool_name: 该工具在远端 MCP 服务器上的原始名称。
description: 工具的描述信息,用于指导大模型选用此工具。
parameters: 工具参数的 JSON 模式描述。
toolkit: 所属的 MCPToolkit 工具箱实例。
"""
super().__init__(name=name, description=description)
self.original_tool_name = original_tool_name
self.parameters = parameters
@@ -132,7 +142,7 @@ class MCPRemoteTool(BaseTool):
@property
def effective_ttl(self) -> float:
"""动态获取当前最新的 TTL 配置,支持无缝热重载"""
"""获取当前工具关联的有效生存期 TTL"""
try:
from zhenxun.services.ai.config import get_llm_config
@@ -272,6 +282,28 @@ class MCPToolkit(BaseToolkit):
sandbox_session_id: str | None = None,
sandbox_blueprint: SandboxBlueprint | None = None,
):
"""
初始化模型上下文协议 (MCP) 的工具箱封装。
参数:
server_name: MCP 服务器的唯一标识名称。
prefix: 工具名称的前缀,防止工具命名冲突。
transport: 连接 MCP 服务器的通信传输协议,可选 "stdio", "sse", "streamable-http", "sandbox_proxy"。
command: 用于 stdio 或 sandbox_proxy 模式启动服务器的可执行命令。
args: 启动服务器时附加的命令行参数。
url: 用于 sse 或 streamable-http 模式的服务器连接 URL。
env: 启动服务器时的进程环境变量。
cwd: 启动服务器时的进程工作目录。
install_command: 首次启动前执行的环境热装配/依赖安装命令。
isolation: 环境隔离模式,可选 "shared" (共享模式) 或 "per_session" (每个会话独立)。
timeout: 初始化和请求网络接口时的超时秒数,默认 30。
admin_level: 调用工具所需的管理权限等级,默认 0 (无限制)。
header_provider: 动态生成 HTTP 头部信息的工厂函数。
env_provider: 动态生成进程环境变量的工厂函数。
ttl: 闲置清理的生存时间 (秒),默认 600。
sandbox_session_id: 当 transport 为 sandbox_proxy 时指定的沙箱会话 ID。
sandbox_blueprint: 用于沙箱自动装配的环境蓝图配置。
""" # noqa: E501
super().__init__(
config=ToolkitConfig(prefix=prefix) if prefix is not None else None
)
@@ -304,7 +336,7 @@ class MCPToolkit(BaseToolkit):
@property
def effective_ttl(self) -> float:
"""动态获取当前最新的 TTL 配置,支持无缝热重载"""
"""获取当前工具箱的有效闲置生存期 TTL"""
try:
from zhenxun.services.ai.config import get_llm_config
@@ -314,7 +346,7 @@ class MCPToolkit(BaseToolkit):
return 31536000.0 if self.ttl <= 0 else float(self.ttl)
async def _run_install_command(self, force: bool = False):
"""执行环境预安装(热加载依赖)"""
"""在本地运行预热依赖安装命令以准备 MCP 服务器环境"""
if not self.install_command or not self.cwd:
return
@@ -366,6 +398,7 @@ class MCPToolkit(BaseToolkit):
async def get_session(
self, context: RunContext | None = None
) -> ClientSession | None:
"""获取或建立与 MCP 服务器的会话连接,并刷新其生存状态"""
await self.lifespan_manager.register(
self.server_name,
ttl=self.effective_ttl,
@@ -376,8 +409,7 @@ class MCPToolkit(BaseToolkit):
return self._shared_session
async def _spawn_session_task(self, dynamic_headers: dict, dynamic_env: dict):
"""生成后台连接任务"""
"""在后台异步拉起 MCP 传输协议通道并进行服务初始化"""
max_attempts = 2 if (self.transport == "stdio" and self.install_command) else 1
try:
@@ -546,6 +578,7 @@ class MCPToolkit(BaseToolkit):
self._ready_event.set()
async def initialize(self, context: RunContext | None = None):
"""清空当前工具集并异步执行 MCP 服务端的全套初始化流程"""
if self._is_initialized:
return
@@ -584,6 +617,7 @@ class MCPToolkit(BaseToolkit):
await self.close()
async def close(self):
"""彻底关闭 MCP 工具箱,注销生存期并释放底层 WebSocket/进程通道"""
current_task = asyncio.current_task()
if (
@@ -3,7 +3,7 @@ from pathlib import Path
from typing import Any
from zhenxun.services.ai.capabilities import AbstractCapability
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.tools.providers.skills.manager import skill_manager
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.services.ai.tools.providers.skills.toolkit import SkillMetaToolkit
@@ -18,6 +18,13 @@ class SkillCapability(AbstractCapability):
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
namespace: str | None = None,
):
"""
初始化技能库挂载能力组件。
参数:
skills: 需要挂载到该环境下的技能列表,支持名称、路径、Skill 实例或 SkillSource。
namespace: 该技能库所处的作用域命名空间,若不指定则自动推导。
""" # noqa: E501
self.skills = skills or []
self.namespace = namespace or infer_plugin_namespace()
@@ -1,12 +1,13 @@
from typing import Any, cast
from zhenxun.services.ai.run import Inject, RunContext
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.di import Inject
from zhenxun.services.ai.sandbox.models import SandboxBlueprint
from zhenxun.services.ai.sandbox.protocols import (
SupportsCommandExecution,
SupportsFileSystem,
)
from zhenxun.services.ai.tools.core.decorators import silent, tool
from zhenxun.services.ai.tools.core.decorators import Rules, tool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import ResolvedToolPayload, ToolResult
from zhenxun.services.ai.tools.providers.skills.manager import (
@@ -211,8 +212,8 @@ class SkillMetaToolkit(BaseToolkit, SkillSandboxExecutionMixin):
@tool(
name="read_skill_instructions",
description="加载指定技能的完整使用说明与可用资源清单。返回值为 XML 结构。",
rules=[Rules.silent()],
)
@silent()
async def read_skill_instructions(self, skill_name: str) -> ToolResult:
skill = await self._get_skill(skill_name)
if not skill:
@@ -261,8 +262,8 @@ class SkillMetaToolkit(BaseToolkit, SkillSandboxExecutionMixin):
"安全读取指定技能目录下的附加文件"
"(如 references/ 里的参考文档或 scripts/ 里的代码)。"
),
rules=[Rules.silent()],
)
@silent()
async def read_skill_file(
self,
skill_name: str,