mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-19 11:10:10 +08:00
✨ 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -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
|
||||
Reference in New Issue
Block a user