Files
zhenxun_bot/zhenxun/services/ai/tools/bridges/handoff.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

108 lines
4.0 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 typing import Any
from pydantic import BaseModel, Field, create_model
from zhenxun.services.ai.run.context 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,
):
"""
初始化移交工具,为模型赋予转移对话控制权到指定实体的能力。
参数:
target_name: 被转移的目标接收者(Agent 或负责人)的唯一标识名称。
target_description: 目标接收者的职责或专长说明,供模型决策是否移交。
input_schema: 自定义移交数据结构,指定转移时所需携带的结构化参数。
"""
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
)