Files
zhenxun_bot/zhenxun/services/ai/flow/team/router.py
T
0b32d69c9c ♻️ 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>
2026-07-05 11:47:21 +08:00

249 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable, Mapping, Sequence
import inspect
import re
from typing import Any, cast
from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.ai.core.templates import PromptTemplate
from zhenxun.services.ai.flow.team.models import RouteDecision, Transition
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.log import logger
class BaseRouter(ABC):
"""团队多智能体路由器基类"""
@abstractmethod
async def route(
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
"""核心路由方法"""
pass
class FunctionRouter(BaseRouter):
"""基于纯函数的极速路由器"""
def __init__(self, selector_func: Callable[..., Any], target: str | None = None):
"""
初始化基于函数的极速路由器。
参数:
selector_func: 用于进行路由判断的选择函数,返回布尔值或字符串目标名。
target: 当选择函数返回 True 时,默认路由到的目标成员名称。
"""
self.selector_func = selector_func
self.target = target
async def route(
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
sig = inspect.signature(self.selector_func)
call_kwargs = {"prompt": prompt, "context": context, "history": history}
if isinstance(prompt, AgentTask):
call_kwargs["agent_task"] = prompt
call_kwargs["task"] = prompt
kwargs_resolved = await DependencyInjector.resolve_all(
sig, call_kwargs, context
)
filtered_kwargs = {
k: v for k, v in kwargs_resolved.items() if k in sig.parameters
}
if is_coroutine_callable(self.selector_func):
_async_func = cast(Callable[..., Awaitable[Any]], self.selector_func)
selected_target = await _async_func(**filtered_kwargs)
else:
_sync_func = cast(Callable[..., Any], self.selector_func)
selected_target = _sync_func(**filtered_kwargs)
if isinstance(selected_target, bool):
if selected_target and self.target:
logger.debug(f"命中函数极速路由 -> {self.target}")
return RouteDecision(target_name=self.target, reason="")
elif selected_target is not None and isinstance(selected_target, str):
logger.debug(f"命中函数动态路由 -> {selected_target}")
return RouteDecision(target_name=selected_target, reason="")
return None
class RegexRouter(BaseRouter):
"""基于正则表达式的极速路由器"""
def __init__(self, pattern: str, target: str):
"""
初始化基于正则表达式的极速路由器。
参数:
pattern: 正则表达式匹配规则。
target: 当正则表达式成功匹配用户输入时路由到的目标成员名称。
"""
self.pattern = re.compile(pattern)
self.target = target
async def route(
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
text_to_match = (
prompt.description
if isinstance(prompt, AgentTask)
else (prompt or context.run.user_input or "")
)
if self.pattern.search(text_to_match):
logger.debug(f"命中正则极速路由 -> {self.target}")
return RouteDecision(target_name=self.target, reason="")
return None
class ChainRouter(BaseRouter):
"""责任链路由器:按顺序执行,直到其中一个命中"""
def __init__(self, routers: list[BaseRouter]):
"""
初始化责任链路由器。
参数:
routers: 路由器实例列表,按顺序链式匹配,遇到首个命中的路由器即返回。
"""
self.routers = routers
async def route(
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
for router in self.routers:
decision = await router.route(context, history, prompt)
if decision is not None:
return decision
return None
class LLMRouter(BaseRouter):
"""基于大模型的意图路由器"""
def __init__(
self,
team_name: str,
members: list[Any],
leader_model: str | None = None,
leader_tools: list[Any] | None = None,
state_flow: Mapping[str, Sequence[Transition | str]] | Callable | None = None,
runtime_config: Any = None,
custom_prompt: str | None = None,
allowed_transitions: list[Transition] | None = None,
):
"""
初始化基于大模型的意图路由器。
参数:
team_name: 当前团队的名称标识。
members: 团队的成员列表,包含 Agent, Team 或 Workflow。
leader_model: 用于进行意图决策的路由器大模型名称,若为空则默认继承全局配置。
leader_tools: 挂载给意图决策路由器的额外可用工具列表。
state_flow: 状态流转规则字典或动态流转函数,定义智能体成员之间的转接路径。
runtime_config: 团队级别的运行时全局配置。
custom_prompt: 自定义的系统提示词模板,用以覆盖默认的路由系统指令。
allowed_transitions: 允许的状态移交规则与前置条件列表。
"""
self.team_name = team_name
self.members = members
self.leader_model = leader_model
self.leader_tools = leader_tools or []
self.state_flow = state_flow
self.runtime_config = runtime_config
self.custom_prompt = custom_prompt
self.allowed_transitions = allowed_transitions
async def route(
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
from zhenxun.services.ai.flow.agent.agent import Agent
from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.flow.team.capabilities import TeamRoutingCapability
default_system_prompt = """## 角色与目标
你是一个高级任务路由器 (所在团队: {{ team_name }})。
请根据用户的输入意图,立刻调用相应的移交工具 (transfer_to_...)
将对话物理转移给合适的专员处理。
你必须且只能选择移交,不能自己作答。"""
if self.allowed_transitions:
transitions_desc = "\n## 可用的移交目标及条件:\n"
for t in self.allowed_transitions:
desc = getattr(t, "description", "") or "无特定条件"
transitions_desc += (
f"- 移交至 [{getattr(t, 'target', 'unknown')}]:{desc}\n"
)
default_system_prompt += transitions_desc
template = self.custom_prompt or default_system_prompt
route_prompt = PromptTemplate(template).render(team_name=self.team_name)
routing_cap = TeamRoutingCapability(
team_name=self.team_name, members=self.members, state_flow=self.state_flow
)
leader_config = AgentConfig(
stateless=self.runtime_config.stateless if self.runtime_config else True,
enable_hitl=getattr(self.runtime_config, "leader_enable_hitl", False),
)
target_model = self.leader_model
if not target_model:
for m in self.members:
if m_model := getattr(m, "model_name", None) or getattr(
m, "model", None
):
target_model = m_model
break
router_agent = Agent(
name=f"{self.team_name}_Router",
instruction=route_prompt,
model=target_model,
tools=self.leader_tools,
config=leader_config,
)
sub_context = context.clone_for_member(router_agent.name)
sub_context.capabilities = list(sub_context.capabilities)
sub_context.capabilities.append(routing_cap)
logger.debug("🤖 [LLMRouter] 启动 LLM 思考路由决策...")
res = await router_agent.run(
prompt=prompt,
context=sub_context,
config=AgentConfig(message_history=history),
)
if res.handoff:
logger.debug(f"🤖 [LLMRouter] 决策完毕: 移交给 -> {res.handoff.target}")
return RouteDecision(
target_name=res.handoff.target,
reason=res.handoff.reason,
context_data=res.handoff.context_data,
)
logger.warning("🤖 [LLMRouter] LLM 没有调用移交工具,放弃路由。")
return None