✨ 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,9 @@
from .delegate import DelegateTool
from .handoff import HandoffTool
from .matcher_bridge import MatcherTool
__all__ = [
"DelegateTool",
"HandoffTool",
"MatcherTool",
]
@@ -0,0 +1,156 @@
import json
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, Field
from zhenxun.services.ai.core.exceptions import ControlFlowExit
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_dump
if TYPE_CHECKING:
from zhenxun.services.ai.flow.base import BaseRunnable
STRUCTURED_INPUT_PREAMBLE = (
"\n\n### 🛠️ [嵌套调用前置语境]\n"
"你现在正在作为一个『工具/子节点』被外部主智能体调用。\n"
"以下是外部系统传递给你的结构化输入数据:\n"
"```json\n{payload}\n```\n"
"请严格将上述内容视为你的核心数据和约束条件,专注于解决该子任务,并直接返回结果,不要说多余的废话。\n"
)
class DelegateArgs(BaseModel):
task: str = Field(
..., description="指派给该实体的具体任务描述、指令或需要回答的问题"
)
class DelegateTool(BaseTool):
"""
将任意实现了 run() 方法的实体 (Agent/Team/Workflow 等) 包装为大模型可调用的工具。
(SubRoutine 委派模式)
"""
def __init__(
self,
runnable: "BaseRunnable[Any]",
name: str | None = None,
description: str | None = None,
):
resolved_name = name or getattr(runnable, "name", "SubRunnable")
resolved_desc = description or getattr(
runnable, "description", f"将子任务委派给 {resolved_name} 执行"
)
final_name = (
f"delegate_to_{resolved_name}"
if not resolved_name.startswith("delegate_")
else resolved_name
)
super().__init__(name=final_name, description=resolved_desc)
self.runnable = runnable
self.args_schema = DelegateArgs
async def execute(
self, context: RunContext | None = None, **kwargs: Any
) -> ToolResult:
task = kwargs.get("task", "")
context = context or RunContext()
counts = context.session.shared_state.setdefault("__delegate_counts__", {})
counts[self.name] = counts.get(self.name, 0) + 1
if counts[self.name] > 3:
return ToolResult(
output=(
f"❌ 系统拦截:检测到无限委派死循环!\n"
f"你已经连续 {counts[self.name]} 次将子任务委派给下级实体 "
f"{self.name} 且未获最终成功。\n"
"请立即停止委派,"
"改变你的思考方向或直接向用户汇报失败结论!"
)
).as_error()
depth = context.run.delegate_depth
if depth >= 3:
logger.warning(
f"⚠️ [DelegateTool] 委派深度超限 ({depth}),强制阻断: {self.name}"
)
from zhenxun.services.ai.core.exceptions import AbortException
raise AbortException(
reason="嵌套层级过深,系统已强制拒绝执行委派",
display=f"⚠️ {self.name} 嵌套层级过深",
)
logger.debug(
f"🔄 [DelegateTool] 正在委派下级实体 {self.name} (Task: {task[:30]}...)"
)
sub_context = context.clone_for_member(self.name)
sub_context.run.delegate_depth = depth + 1
payload_str = json.dumps(kwargs, ensure_ascii=False, indent=2)
preamble = STRUCTURED_INPUT_PREAMBLE.format(payload=payload_str)
sub_context.run.add_system_prompt(preamble)
try:
event_bus = context.run.event_bus
async with self.runnable.run_stream(
prompt=task,
context=sub_context,
) as stream_result:
response = await stream_result.forward_to(event_bus, self.name)
if response is None:
raise RuntimeError(f"Sub-agent {self.name} did not return a response.")
if isinstance(response.output, BaseModel):
final_output = model_dump(response.output)
else:
final_output = response.output
if response.handoff:
final_output = (
f"⚠️ 子任务未完成。下级实体主动发起了工作流移交 (Handoff)。\n"
f"移交目标: {response.handoff.target}\n"
f"移交原因: {response.handoff.reason}\n"
f"附带数据: {response.handoff.context_data}"
)
is_fatal = isinstance(final_output, str) and (
"DepthLimitExceeded" in final_output or "嵌套层级过深" in final_output
)
if is_fatal:
from zhenxun.services.ai.core.exceptions import AbortException
raise AbortException(
reason="下级实体遇到深度限制异常",
display=f"⚠️ 实体 {self.name} 委派失败",
)
usage = getattr(response, "usage", None)
if context and context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name=self.name, content=f"🧠 实体 {self.name} 执行完毕"
)
)
return ToolResult(
output=final_output,
usage=usage,
)
except ControlFlowExit as e:
raise e
except Exception as e:
logger.error(f"委派实体 {self.name} 执行失败: {e}", e=e)
from zhenxun.services.ai.core.exceptions import AbortException
raise AbortException(
reason=f"Delegate Execution Error: {e}",
display=f"❌ 实体 {self.name} 执行异常",
)
@@ -0,0 +1,99 @@
from typing import Any
from pydantic import BaseModel, Field, create_model
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.models import HandoffResult, ToolResult
class HandoffTool(BaseTool):
"""
触发控制权移交 (Handoff) 信号的底层工具。
当大模型调用此工具时,当前 Agent 执行流将被立即熔断并向外抛出 ToolHandoff 信号。
"""
def __init__(
self,
target_name: str,
target_description: str,
input_schema: type[BaseModel] | Any | None = None,
):
super().__init__(
name=f"transfer_to_{target_name}",
description=(
f"将对话控制权移交给 {target_name}。专长/职责:{target_description}"
),
)
self.target_name = target_name
actual_schema = None
if input_schema:
from zhenxun.services.ai.core.options import BaseOutputDefinition
if isinstance(input_schema, BaseOutputDefinition):
actual_schema = input_schema.type_
else:
actual_schema = input_schema
if actual_schema:
fields: dict[str, Any] = {
"reason": (str, Field(..., description="移交的原因或简要状态说明")),
}
schema_fields = getattr(
actual_schema, "model_fields", getattr(actual_schema, "__fields__", {})
)
for k, v in schema_fields.items():
fields[k] = (v.annotation, v)
self.args_schema = create_model(
f"DynamicHandoffArgs_{target_name}", **fields
)
else:
class DefaultHandoffArgs(BaseModel):
reason: str = Field(..., description="移交的原因或简要状态说明")
context_data: Any = Field(
default="",
description="你需要传递给下一个负责人的所有核心数据",
)
self.args_schema = DefaultHandoffArgs
async def execute(
self, context: RunContext | None = None, **kwargs: Any
) -> ToolResult:
reason = kwargs.pop("reason", "")
if "context_data" in kwargs and len(kwargs) == 1:
context_data = kwargs["context_data"]
else:
context_data = kwargs
if context:
counts = context.session.shared_state.setdefault("__handoff_counts__", {})
counts[self.target_name] = counts.get(self.target_name, 0) + 1
if counts[self.target_name] > 3:
return ToolResult(
output=(
f"❌ 系统拦截:检测到严重的踢皮球现象!\n"
f"你所在的团队已经连续 {counts[self.target_name]} 次将任务"
f"移交给 {self.target_name},\n"
"但问题仍未解决。请立刻改变策略,"
"由你亲自处理或得出最终结论,严禁再次移交!"
)
).as_error()
from zhenxun.services.ai.core.messages import HandoffEvent
if context:
context.run.add_event(
HandoffEvent(
target=self.target_name,
reason=reason,
context_data=context_data,
)
)
return HandoffResult(
target=self.target_name, reason=reason, context_data=context_data
)
@@ -0,0 +1,464 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Callable
from typing import Any, cast
from arclet.alconna import Alconna, Subcommand
from nonebot.adapters import Bot, Event
from nonebot.exception import FinishedException, PausedException, RejectedException
from nonebot.internal.matcher import Matcher
from nonebot.typing import T_State
from nonebot_plugin_alconna import UniMessage
from nonebot_plugin_alconna.consts import (
ALCONNA_EXEC_RESULT,
ALCONNA_EXTENSION,
ALCONNA_RESULT,
)
from nonebot_plugin_alconna.model import CommandResult
from pydantic import BaseModel
from zhenxun.services.ai.core.exceptions import ToolRetryError
from zhenxun.services.ai.core.models import ToolDefinition
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.schema import build_schema_hint
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.tools.models import EndRunResult, ToolOptions, ToolResult
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_validate
class MatcherAdapter(ABC):
"""Matcher 桥接适配器基类 (Adapter Pattern)"""
def __init__(
self,
matcher: type[Matcher],
args_schema: type[BaseModel] | None,
command_formatter: Callable[[Any], list[Any] | str] | None = None,
):
self.matcher = matcher
self.args_schema = args_schema
self.command_formatter = command_formatter
def modify_tool_definition(self, tool_def: ToolDefinition) -> ToolDefinition:
"""用于清洗大模型不需要的插件内部元数据"""
return tool_def
@abstractmethod
async def build_state(
self, kwargs: dict[str, Any], bot: Bot, event: Event
) -> T_State:
"""根据传入参数构造目标 Matcher 需要的 Fake State"""
raise NotImplementedError
class AlconnaAdapter(MatcherAdapter):
"""针对 nonebot_plugin_alconna 的高级解析适配器"""
def modify_tool_definition(self, tool_def: ToolDefinition) -> ToolDefinition:
if tool_def and tool_def.parameters and "properties" in tool_def.parameters:
for prop in tool_def.parameters["properties"].values():
if isinstance(prop, dict):
prop.pop("alc_dest", None)
prop.pop("alc_name", None)
return tool_def
def _build_kwargs_mapping(
self, kwargs: dict[str, Any]
) -> tuple[dict[str, Any], dict[str, str], dict[str, str]]:
mapped_kwargs = {}
name_overrides = {}
raw_key_to_dest: dict[str, str] = {}
fields = (
getattr(
self.args_schema,
"model_fields",
getattr(self.args_schema, "__fields__", {}),
)
if self.args_schema
else {}
)
for k, v in kwargs.items():
alc_dest = k
alc_name = None
if k in fields:
field = fields[k]
extra = getattr(
field,
"json_schema_extra",
getattr(getattr(field, "field_info", None), "extra", None),
)
if isinstance(extra, dict):
alc_dest = extra.get("alc_dest", k)
alc_name = extra.get("alc_name")
mapped_kwargs[alc_dest] = v
raw_key_to_dest[k] = alc_dest
if alc_name:
name_overrides[alc_dest] = alc_name
return mapped_kwargs, name_overrides, raw_key_to_dest
def _auto_infer_argv(self, kwargs: dict[str, Any]) -> tuple[list[Any], list[str]]:
command = getattr(self.matcher, "command", lambda: None)()
if not command:
raise RuntimeError(f"Matcher {self.matcher} 并非合法的 AlconnaMatcher")
command = cast(Alconna, command)
mapped_kwargs, name_overrides, raw_key_to_dest = self._build_kwargs_mapping(
kwargs
)
consumed_dests = set()
argv = []
cmd_name = command.command or ""
prefixes = command.prefixes
if prefixes and isinstance(prefixes, list):
prefix = next((p for p in prefixes if p), "")
argv.append(f"{prefix}{cmd_name}")
else:
argv.append(cmd_name)
def _process_node(node: Any, current_argv: list):
if hasattr(node, "args"):
for arg in node.args.argument:
if arg.name in mapped_kwargs:
val = mapped_kwargs[arg.name]
consumed_dests.add(arg.name)
if val is not None and val != "":
current_argv.append(str(val))
if hasattr(node, "options"):
for opt in node.options:
if isinstance(opt, Subcommand):
continue
dest = opt.dest
if dest in mapped_kwargs:
val = mapped_kwargs[dest]
consumed_dests.add(dest)
opt_name = name_overrides.get(dest, opt.name)
if isinstance(val, bool):
if val:
current_argv.append(opt_name)
elif val is not None and val != "":
current_argv.append(opt_name)
current_argv.append(str(val))
_process_node(command, argv)
for subcmd in command.options:
if isinstance(subcmd, Subcommand):
is_active = False
dest = subcmd.dest
if (
mapped_kwargs.get(dest) is True
or mapped_kwargs.get(subcmd.name) is True
):
is_active = True
consumed_dests.add(dest)
consumed_dests.add(subcmd.name)
else:
for k, v in mapped_kwargs.items():
if str(v) == subcmd.name or str(v) == subcmd.dest:
is_active = True
consumed_dests.add(k)
break
if is_active:
subcmd_name = name_overrides.get(dest, subcmd.name)
argv.append(subcmd_name)
_process_node(subcmd, argv)
break
unconsumed_raw_keys = [
raw_k for raw_k, dst in raw_key_to_dest.items() if dst not in consumed_dests
]
return argv, unconsumed_raw_keys
async def build_state(
self, kwargs: dict[str, Any], bot: Bot, event: Event
) -> T_State:
unconsumed_keys = []
if self.command_formatter:
try:
if self.args_schema:
model_inst = model_validate(self.args_schema, kwargs)
argv = self.command_formatter(model_inst)
else:
argv = self.command_formatter(kwargs)
except Exception as e:
logger.error(f"执行 command_formatter 失败: {e}")
raise ToolRetryError(f"格式化参数失败: {e}")
else:
argv, unconsumed_keys = self._auto_infer_argv(kwargs)
msg = UniMessage()
for idx, item in enumerate(argv):
if isinstance(item, str):
msg += f" {item}" if idx > 0 else item
else:
if idx > 0:
msg += " "
msg.append(item)
command_func = getattr(self.matcher, "command", None)
if not command_func:
raise ToolRetryError("无法获取底层 Alconna 命令对象")
command = cast(Alconna, command_func())
if not command:
raise ToolRetryError("底层 Alconna 命令对象为空")
arp = command.parse(msg)
if not arp.matched:
error_info = (
str(arp.error_info) if arp.error_info else "缺失必填参数或格式不匹配"
)
logger.warning(f"Alconna 解析失败: {error_info}. Argv: {argv}")
schema_hint = (
build_schema_hint(self.args_schema) if self.args_schema else ""
)
if unconsumed_keys:
schema_hint += (
f"\n\n⚠️ [参数吸收警告] 字段 {unconsumed_keys} "
"未能被底层的 Alconna 命令树成功接收!\n"
"👉 这通常意味着 Schema 的字段名与实际命令的选项名(dest)不匹配。"
)
raise ToolRetryError(
f"Alconna 内部解析失败: {error_info}。"
f"请检查参数是否遗漏或名称对不齐。{schema_hint}"
)
cmd_result = CommandResult(result=arp, output=None)
fake_state: T_State = {
ALCONNA_RESULT: cmd_result,
ALCONNA_EXEC_RESULT: {},
**kwargs,
}
executor = getattr(self.matcher, "executor", None)
if executor:
selected = executor.select(bot, event)
fake_state[ALCONNA_EXTENSION] = selected
await selected.parse_wrapper(bot, fake_state, event, arp)
logger.debug(f"通过真实解析引擎拉起 Alconna Matcher, Argv: {argv}")
return fake_state
class NativeCommandAdapter(MatcherAdapter):
"""针对 NoneBot 原生 on_command 的适配器"""
async def build_state(
self, kwargs: dict[str, Any], bot: Bot, event: Event
) -> T_State:
if self.command_formatter:
try:
if self.args_schema:
model_inst = model_validate(self.args_schema, kwargs)
arg_str = self.command_formatter(model_inst)
else:
arg_str = self.command_formatter(kwargs)
except Exception as e:
logger.error(f"执行 command_formatter 失败: {e}")
raise ToolRetryError(f"格式化参数失败: {e}")
if isinstance(arg_str, list):
arg_str = " ".join(str(i) for i in arg_str)
else:
arg_str = " ".join(
str(v) for v in kwargs.values() if v is not None and str(v).strip()
)
try:
msg_cls = type(event.get_message())
msg_obj = msg_cls(arg_str)
except Exception:
try:
from nonebot.adapters.onebot.v11 import Message as OB11Message
msg_obj = OB11Message(arg_str)
except Exception:
msg_obj = arg_str
fake_state: T_State = {
"COMMAND": ("llm_bridge",),
"_prefix": {
"command_start": "/",
"command": ("llm_bridge",),
"command_arg": msg_obj,
},
**kwargs,
}
logger.debug(f"原生 Command 适配器注入 State _prefix.command_arg: {arg_str}")
return fake_state
class MatcherTool(BaseTool):
"""
通用状态机穿透工具。
将原有的 nonebot 指令(on_alconna, on_command 等)
无缝包装为大模型可调用的结构化工具。
"""
def __init__(
self,
matcher: type[Matcher],
adapter: MatcherAdapter,
name: str,
description: str,
args_schema: type[BaseModel] | None,
terminal: bool = True,
):
self.matcher = matcher
self.adapter = adapter
self.terminal = terminal
super().__init__(
name=name,
description=description,
settings=ToolOptions(
args_schema=args_schema,
tags=["system:matcher_bridge"],
metadata={
"source": "matcher_bridge",
"adapter_type": adapter.__class__.__name__,
},
),
)
async def get_definition(
self, context: RunContext | None = None
) -> ToolDefinition | None:
tool_def = await super().get_definition(context)
if tool_def:
tool_def = self.adapter.modify_tool_definition(tool_def)
return tool_def
async def execute(
self, context: RunContext | None = None, **kwargs: Any
) -> ToolResult:
if not context:
return ToolResult(
output="缺少 RunContext 依赖,无法执行 Matcher"
).as_error()
bot = context.get_bot()
event = context.get_event()
if not bot or not event:
return ToolResult(
output="上下文缺少 bot 或 event,无法执行 Matcher"
).as_error()
try:
fake_state = await self.adapter.build_state(kwargs, bot, event)
except ToolRetryError as e:
raise e
except Exception as e:
logger.error(f"构造状态失败: {e}", e=e)
return ToolResult(output=f"适配器构造状态失败: {e}").as_error()
logger.debug(
f"拉起 Matcher: {self.name} (Adapter: {self.adapter.__class__.__name__})"
)
matcher_inst = self.matcher()
try:
await matcher_inst.run(bot, event, fake_state)
except FinishedException:
if self.terminal:
return EndRunResult(output="任务已完成。结果已直接发送给用户。")
return ToolResult(output="执行完毕,已直接向用户发送结果。")
except PausedException:
return ToolResult(
output="执行被暂停(大模型不支持交互式Pause)。"
).as_error()
except RejectedException:
return ToolResult(output="执行被拒绝(参数缺失或错误)。").as_error()
except Exception as e:
def _unwrap_err(exc: BaseException) -> str:
exceptions = getattr(exc, "exceptions", None)
if exceptions is not None:
return " | ".join(_unwrap_err(inner) for inner in exceptions)
return f"{type(exc).__name__}: {exc}"
real_err = _unwrap_err(e)
logger.error(
f"Matcher '{self.name}' 穿透执行发生未捕获异常: {real_err}",
e=e,
)
return ToolResult(output=f"执行时发生底层错误: {real_err}").as_error()
if self.terminal:
return EndRunResult(output="执行完毕。")
return ToolResult(output="命令已在后台成功执行完成。")
def bind_matcher(
matcher: type[Matcher],
name: str,
description: str,
args_schema: type[BaseModel] | None = None,
terminal: bool = True,
auto_register: bool = True,
command_formatter: Callable[[Any], list[Any] | str] | None = None,
require_prefix: bool = True,
) -> MatcherTool:
"""
将现有的 Nonebot Matcher (on_command, on_alconna等) 绑定并转化为大模型工具。
使用策略模式(Adapter Pattern)自动推导 Matcher 的类型。
参数:
matcher: 目标 Matcher 对象 (由 on_command/on_alconna 返回的 Type[Matcher])
name: 工具的名称 (仅限英文字母、数字及下划线)
description: 工具的详细说明,大模型将根据此说明决定何时调用
args_schema: 描述 LLM 需要填写的参数 Pydantic Schema。可以为 None
terminal: 是否为"终端工具" (默认为 True,执行完毕后立即中断大模型思考循环)
auto_register: 是否自动注册到系统的全局工具库中
command_formatter: 可选的格式化器。对于原生 on_command,如果不传,
则直接使用 kwargs values 拼接为空格字符串。
"""
if require_prefix:
from zhenxun.utils.utils import infer_plugin_namespace
ns = infer_plugin_namespace(default="global")
if ns and ns not in ("global", "unknown"):
if not name.startswith(f"{ns}_"):
name = f"{ns}_{name}"
is_alconna = (
hasattr(matcher, "command")
and callable(getattr(matcher, "command"))
and hasattr(matcher, "executor")
)
if is_alconna:
adapter = AlconnaAdapter(matcher, args_schema, command_formatter)
else:
adapter = NativeCommandAdapter(matcher, args_schema, command_formatter)
tool_instance = MatcherTool(
matcher=matcher,
adapter=adapter,
name=name,
description=description,
args_schema=args_schema,
terminal=terminal,
)
if auto_register:
from zhenxun.services.ai.tools.engine.registry import tool_provider_manager
tool_provider_manager.register_tool(tool_instance)
return tool_instance