♻️ 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,5 +1,3 @@
from .auto import AutoWorkflow
from .decorators import AND, OR, entry, listen, router
from .engine import Workflow
from .nodes import (
Condition,
@@ -11,9 +9,6 @@ from .nodes import (
)
__all__ = [
"AND",
"OR",
"AutoWorkflow",
"Condition",
"Loop",
"Parallel",
@@ -21,7 +16,4 @@ __all__ = [
"Step",
"Steps",
"Workflow",
"entry",
"listen",
"router",
]
-107
View File
@@ -1,107 +0,0 @@
import graphlib
from typing import Any
from zhenxun.services.ai.flow.workflow.engine import Workflow
from zhenxun.services.ai.flow.workflow.nodes import NodeFactory, Parallel, Router
from zhenxun.services.log import logger
class AutoWorkflow(Workflow):
"""
自动化声明式工作流 (Facade)。
允许开发者通过 @entry, @listen 装饰器定义类方法,
在初始化时,底层编译器会自动分析依赖并推导为原生的 Steps 和 Parallel 节点图。
"""
def __init__(self, name: str | None = None, description: str = "", **kwargs: Any):
workflow_name = name or self.__class__.__name__
compiled_steps = self._compile_graph()
super().__init__(
name=workflow_name, steps=compiled_steps, description=description
)
self._auto_kwargs = kwargs
def _compile_graph(self) -> list[Any]:
"""核心图推导编译器:支持 Router 嵌套与 AND/OR 拓扑排序"""
methods_meta = {}
router_paths = set()
for attr_name in dir(self):
if attr_name.startswith("_"):
continue
attr = getattr(self, attr_name)
if hasattr(attr, "__workflow_meta__"):
methods_meta[attr_name] = attr.__workflow_meta__
if attr.__workflow_meta__.get("paths"):
router_paths.update(attr.__workflow_meta__["paths"])
if not methods_meta:
logger.warning(
f"AutoWorkflow '{self.__class__.__name__}' 没有检测到任何被装饰的方法!"
)
return []
top_level_methods = {
k: v
for k, v in methods_meta.items()
if not any(t in router_paths for t in v.get("triggers", []))
}
branch_methods = {
k: v
for k, v in methods_meta.items()
if any(t in router_paths for t in v.get("triggers", []))
}
ts = graphlib.TopologicalSorter()
for name, meta in top_level_methods.items():
valid_triggers = [
t for t in meta.get("triggers", []) if t in top_level_methods
]
ts.add(name, *valid_triggers)
try:
ts.prepare()
except graphlib.CycleError as e:
raise ValueError(f"AutoWorkflow 编译失败:检测到循环依赖!{e}")
workflow_steps = []
while ts.is_active():
ready_nodes = ts.get_ready()
step_nodes = []
for n in ready_nodes:
meta = top_level_methods[n]
if meta["type"] in ("router", "entry_router"):
choices = []
for path in meta.get("paths", []):
branch_name = next(
(
bk
for bk, bv in branch_methods.items()
if path in bv.get("triggers", [])
),
None,
)
if branch_name:
choices.append(
NodeFactory.build(getattr(self, branch_name), name=path)
)
step_nodes.append(
Router(name=n, selector=getattr(self, n), choices=choices)
)
else:
step_nodes.append(NodeFactory.build(getattr(self, n), name=n))
if len(step_nodes) == 1:
workflow_steps.append(step_nodes[0])
elif len(step_nodes) > 1:
workflow_steps.append(
Parallel(*step_nodes, name=f"Parallel_{'_'.join(ready_nodes)[:30]}")
)
for node in ready_nodes:
ts.done(node)
return workflow_steps
+10 -39
View File
@@ -8,10 +8,12 @@ from zhenxun.services.ai.core.exceptions import (
ControlFlowExit,
ToolFatalError,
)
from zhenxun.services.ai.flow.workflow.types import (
from zhenxun.services.ai.flow.workflow.policies import (
AbortPolicy,
BaseFailurePolicy,
PolicyAction,
)
from zhenxun.services.ai.flow.workflow.types import (
StepInput,
StepOutput,
StepType,
@@ -26,8 +28,6 @@ class BaseNode(ABC):
def __init__(
self,
name: str,
requires_confirmation: bool = False,
confirmation_message: str | None = None,
failure_policy: BaseFailurePolicy | None = None,
):
"""
@@ -35,14 +35,10 @@ class BaseNode(ABC):
参数:
name: 节点的唯一名称标识。
requires_confirmation: 标记该节点在执行前是否需要人工介入授权 (HITL),默认 False。
confirmation_message: 挂起等待授权时,向前端/群聊展示的提示文案,默认 None。
failure_policy: 该节点执行失败时的错误恢复与自愈策略,
默认使用中断策略 (AbortPolicy)。
""" # noqa: E501
"""
self.name = name
self.requires_confirmation = requires_confirmation
self.confirmation_message = confirmation_message
self.failure_policy = failure_policy or AbortPolicy()
@property
@@ -79,7 +75,7 @@ class BaseNode(ABC):
content=content,
success=False,
stop=True,
error=str(e)
error=f"{type(e).__name__}: {e}"
if isinstance(e, AbortException | ToolFatalError)
else None,
)
@@ -110,10 +106,10 @@ class BaseNode(ABC):
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content=f"节点执行失败,已通过策略自动跳过: {e}",
content=f"节点执行失败,已通过策略自动跳过: {type(e).__name__} - {e}",
success=False,
stop=False,
error=str(e),
error=f"{type(e).__name__}: {e}",
)
return "break", output, None, None
else:
@@ -121,10 +117,10 @@ class BaseNode(ABC):
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content=f"执行崩溃: {e}",
content=f"执行崩溃: {type(e).__name__} - {e}",
success=False,
stop=True,
error=str(e),
error=f"{type(e).__name__}: {e}",
)
return "break", output, None, None
@@ -168,37 +164,12 @@ class BaseNode(ABC):
logger.debug(f" ⚙️ [节点] `{self.name}` 开始执行...")
cached_out = context.state.get("__completed_steps__", {}).get(self.name)
if (
cached_out
and cached_out.success
and not getattr(cached_out, "is_paused", False)
):
if cached_out and cached_out.success:
logger.debug(f"⏭️ 快进跳过已完成节点: {self.name}")
yield cached_out
return
if self.requires_confirmation:
if not context.state.get(f"__hitl_confirmed_{self.name}"):
msg = (
self.confirmation_message
or f"⚠️ 工作流即将执行高危步骤:[{self.name}],等待授权..."
)
logger.debug(f" ⏸️ **[节点挂起]** `{self.name}`: {msg}")
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content="[任务已挂起,等待人工授权/输入]",
success=True,
stop=True,
is_paused=True,
pause_reason=msg,
)
yield output
return
current_input = step_input
attempt = 1
@@ -1,91 +0,0 @@
from collections.abc import Callable
from typing import Any
def AND(*triggers: str) -> dict[str, Any]:
"""逻辑与:所有前置方法都完成才执行"""
return {"logic": "AND", "triggers": list(triggers)}
def OR(*triggers: str) -> dict[str, Any]:
"""逻辑或:任意前置方法完成即执行"""
return {"logic": "OR", "triggers": list(triggers)}
def entry() -> Callable:
"""
标记为工作流的入口节点。
执行工作流时会自动作为第一批任务执行。
"""
def decorator(func: Callable) -> Callable:
setattr(
func,
"__workflow_meta__",
{
"type": "entry",
"triggers": [],
"logic": "OR",
"paths": [],
},
)
return func
return decorator
def listen(condition: str | dict[str, Any]) -> Callable:
"""
监听其他节点的完成状态。
用法:
@listen("step_a")
@listen(AND("step_a", "step_b"))
"""
def decorator(func: Callable) -> Callable:
if isinstance(condition, str):
meta = {
"type": "listen",
"triggers": [condition],
"logic": "OR",
"paths": [],
}
elif isinstance(condition, dict):
meta = {
"type": "listen",
"triggers": condition["triggers"],
"logic": condition.get("logic", "OR"),
"paths": [],
}
else:
raise TypeError("listen condition 必须是字符串或 AND/OR 函数的返回值")
setattr(func, "__workflow_meta__", meta)
return func
return decorator
def router(
condition: str | dict[str, Any] | None = None, paths: list[str] | None = None
) -> Callable:
"""
标记为路由节点。执行此方法后,会根据返回值走向对应的 paths。
"""
def decorator(func: Callable) -> Callable:
meta = {"type": "router", "triggers": [], "logic": "OR", "paths": paths or []}
if isinstance(condition, str):
meta["triggers"] = [condition]
elif isinstance(condition, dict):
meta["triggers"] = condition["triggers"]
meta["logic"] = condition.get("logic", "OR")
if not condition:
meta["type"] = "entry_router"
setattr(func, "__workflow_meta__", meta)
return func
return decorator
+16 -69
View File
@@ -1,12 +1,13 @@
import asyncio
from collections.abc import AsyncIterator
import contextlib
from typing import TYPE_CHECKING, Any
import uuid
from nonebot.params import Depends
if TYPE_CHECKING:
from zhenxun.services.ai.flow.workflow.nodes import NodeSource
from zhenxun.services.ai.run import StreamedRunResult
from zhenxun.services.ai.core.exceptions import ControlFlowExit, ToolRetryError
from zhenxun.services.ai.core.messages import PromptInput, UsageInfo
@@ -18,9 +19,16 @@ from zhenxun.services.ai.flow.workflow.types import (
StepOutput,
WorkflowRunResult,
)
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import (
AgentRunEnd,
AgentRunError,
AgentRunResult,
StreamedRunResult,
)
from zhenxun.services.ai.tools.core.tool import FunctionTool
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
class Workflow(BaseRunnable[WorkflowRunResult]):
@@ -52,6 +60,7 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
safe_context: RunContext,
final_output: StepOutput,
) -> WorkflowRunResult:
"""根据执行链上的全量输出构建最终的工作流执行结果对象"""
flat_outputs = {}
def _extract(out: StepOutput):
@@ -63,19 +72,7 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
if final_output:
_extract(final_output)
paused_step = next(
(
v.step_name
for v in reversed(list(flat_outputs.values()))
if getattr(v, "is_paused", False) and v.step_name
),
None,
)
status = (
"paused"
if paused_step
else ("completed" if final_output and final_output.success else "error")
)
status = "completed" if final_output and final_output.success else "error"
return WorkflowRunResult(
workflow_id=self.id,
@@ -86,12 +83,10 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
step_outputs=flat_outputs,
last_step_content=final_output.content if final_output else None,
final_output=final_output,
paused_step_name=paused_step,
)
def bind(self, **kwargs: Any) -> Any:
"""DI 注入语法糖"""
from nonebot.params import Depends
async def _dependency() -> "Workflow":
return self
@@ -112,8 +107,6 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
返回:
WorkflowRunResult: 包含执行状态、断点快照、各节点产出的全量工作流结果对象。
"""
from zhenxun.utils.message import MessageUtils
ctx = RunContext()
bot = ctx.get_bot()
event = ctx.get_event()
@@ -128,12 +121,6 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
else "执行完毕"
)
await MessageUtils.build_message(msg).send(reply_to=reply_to)
elif res.status == "paused":
pause_msg = (
f"⏸️ 工作流执行已被挂起,停在步骤: {res.paused_step_name}。"
"请提供授权或人工输入后继续。"
)
await MessageUtils.build_message(pause_msg).send(reply_to=reply_to)
elif res.status == "error":
err_msg = res.final_output.error if res.final_output else "未知异常"
await MessageUtils.build_message(
@@ -186,8 +173,6 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
raise e
import contextlib
@contextlib.asynccontextmanager
async def run_stream(
self,
@@ -197,9 +182,6 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
**kwargs: Any,
) -> AsyncIterator["StreamedRunResult[Any]"]:
"""对齐 BaseRunnable 接口的流式上下文管理器"""
from zhenxun.services.ai.run import StreamedRunResult
from zhenxun.services.ai.run.models import AgentRunError
event_bus = EventBus()
if context:
context.run.event_bus = event_bus
@@ -226,7 +208,7 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
context: RunContext | None = None,
**kwargs: Any,
) -> AsyncIterator[Any]:
"""原 arun_stream 逻辑改名,供内部 _execution_task 调用"""
"""流式执行工作流节点树的内部实现"""
session_id = (
context.session_id if context and context.session_id else f"wf_{self.id}"
)
@@ -251,9 +233,6 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
if final_output:
logger.debug(f"🏭 **工作流 [{self.name}] 运行结束**")
from zhenxun.services.ai.run import AgentRunResult
from zhenxun.services.ai.run.models import AgentRunEnd
wf_result = self._build_result(
initial_input, safe_context, final_output
)
@@ -266,41 +245,9 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
except Exception:
pass
async def acontinue_run(
self,
run_result: WorkflowRunResult,
user_auth_data: dict[str, Any] | None = None,
context: RunContext | None = None,
) -> WorkflowRunResult:
safe_context = context or RunContext(session_id=f"wf_{self.id}")
safe_context.state.update(run_result.state)
safe_context.state["__completed_steps__"] = run_result.step_outputs.copy()
for step_name, out in run_result.step_outputs.items():
safe_context.upstream_results[step_name] = out.content
if run_result.paused_step_name:
safe_context.state[f"__hitl_confirmed_{run_result.paused_step_name}"] = True
if user_auth_data:
safe_context.state[f"__hitl_input_{run_result.paused_step_name}"] = (
user_auth_data
)
resume_input = StepInput(
input=run_result.original_input,
previous_step_content=run_result.last_step_content,
)
logger.debug(
f"🚀 工作流 [{self.name}] 状态已恢复,"
f"正在快进到步骤: {run_result.paused_step_name}..."
)
final_output = await self.root_steps.aexecute(resume_input, safe_context)
return self._build_result(resume_input, safe_context, final_output)
def as_tool(self, tool_name: str | None = None) -> FunctionTool:
"""将工作流封装并导出为可供 Agent 直接调用的 FunctionTool 实例"""
async def _execute_workflow_tool(prompt: str, context: RunContext) -> str:
run_result = await self.run(prompt=prompt, context=context)
output = run_result.final_output
+17 -85
View File
@@ -1,20 +1,20 @@
import asyncio
from collections.abc import AsyncIterator, Callable, Sequence
import copy
from typing import Any, cast
from pydantic import BaseModel, Field
from zhenxun.services.ai.core.messages import PromptInput
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.flow.workflow.base import BaseNode
from zhenxun.services.ai.flow.workflow.policies import BaseFailurePolicy
from zhenxun.services.ai.flow.workflow.types import (
BaseFailurePolicy,
StepInput,
StepOutput,
StepType,
)
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.log import logger
NodeSource = BaseNode | BaseRunnable | Callable
@@ -47,8 +47,6 @@ class Step(BaseNode):
name: str | None = None,
executor: NodeSource | None = None,
prompt: PromptInput | None = None,
requires_confirmation: bool = False,
confirmation_message: str | None = None,
failure_policy: BaseFailurePolicy | None = None,
):
"""
@@ -58,8 +56,6 @@ class Step(BaseNode):
name: 步骤的名称,为空则自动取执行器的名称,默认 None。
executor: 该步骤要运行的核心执行器(支持 RunnableNode 或 Callable 依赖注入)。
prompt: 该步骤的初始输入或提示词定义,默认 None。
requires_confirmation: 标记该节点在执行前是否需要人工介入授权,默认 False。
confirmation_message: 挂起等待授权时展示的提示文案,默认 None。
failure_policy: 该节点执行失败时的错误处理策略,默认使用中断策略。
""" # noqa: E501
actual_name = name or getattr(
@@ -67,8 +63,6 @@ class Step(BaseNode):
)
super().__init__(
name=actual_name,
requires_confirmation=requires_confirmation,
confirmation_message=confirmation_message,
failure_policy=failure_policy,
)
self.executor = executor
@@ -83,9 +77,7 @@ class Step(BaseNode):
) -> AsyncIterator[Any]:
if False:
yield None
raise NotImplementedError(
"This is a facade. Real execution happens in subclasses."
)
raise NotImplementedError("这是一个外观门面,实际的执行发生在子类中。")
class RunnableNode(Step):
@@ -94,27 +86,28 @@ class RunnableNode(Step):
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
import copy
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.run import Task
executor = cast(BaseRunnable, self.executor)
prompt_data = self.prompt if self.prompt is not None else step_input.input
if isinstance(prompt_data, Task):
if isinstance(step_input.previous_step_content, AgentTask):
prompt_data = step_input.previous_step_content
prev_content_to_append = None
else:
prompt_data = self.prompt if self.prompt is not None else step_input.input
prev_content_to_append = step_input.previous_step_content
if isinstance(prompt_data, AgentTask):
prompt_data = copy.copy(prompt_data)
if step_input.previous_step_content:
prev_content = str(step_input.previous_step_content)
if prev_content_to_append:
prev_content = str(prev_content_to_append)
prompt_data.description = (
f"### 🔙 [上游节点执行输出]\n{prev_content}\n\n"
f"### 🎯 [当前需执行的任务]\n{prompt_data.description}"
)
context.run.user_input = prompt_data.description
else:
if step_input.previous_step_content:
if prev_content_to_append:
prompt_data = (
f"[上游节点执行输出]:\n{step_input.previous_step_content}\n\n"
f"[上游节点执行输出]:\n{prev_content_to_append}\n\n"
f"[当前需执行的任务]:\n{prompt_data or ''}"
)
context.run.user_input = str(prompt_data) if prompt_data else ""
@@ -126,8 +119,6 @@ class RunnableNode(Step):
prompt=prompt_data, context=sandbox_context
) as stream_result:
async for event in stream_result.stream_events():
from zhenxun.services.ai.run.models import AgentRunEnd
if isinstance(event, AgentRunEnd):
final_result = event.result
yield event
@@ -158,26 +149,6 @@ class FunctionNode(Step):
yield StepOutput(content=res, success=True)
class StepMeta(BaseModel):
"""承载工作流节点装饰器元数据的内部模型"""
name: str | None = None
requires_confirmation: bool = False
confirmation_message: str | None = None
failure_policy: Any = None
class ConditionMeta(BaseModel):
name: str | None = None
if_true: list[Any] = Field(default_factory=list)
if_false: list[Any] = Field(default_factory=list)
class RouterMeta(BaseModel):
name: str | None = None
choices: list[Any] = Field(default_factory=list)
class Steps(BaseNode):
"""串行执行的工作流容器。按照列表顺序依次执行。"""
@@ -223,7 +194,6 @@ class Steps(BaseNode):
yield StepOutput(
content=all_outputs[-1].content if all_outputs else "No steps executed",
success=all(o.success for o in all_outputs),
is_paused=any(getattr(o, "is_paused", False) for o in all_outputs),
steps=all_outputs,
)
@@ -432,7 +402,6 @@ class Loop(BaseNode):
yield StepOutput(
content=all_results[-1].content if all_results else "No iterations run",
success=all(o.success for o in all_results),
is_paused=any(getattr(o, "is_paused", False) for o in all_results),
steps=all_results,
)
@@ -539,7 +508,6 @@ class Parallel(BaseNode):
yield StepOutput(
content="\n\n".join(aggregated_content_parts),
success=not has_any_failure,
is_paused=any(getattr(o, "is_paused", False) for o in all_outputs),
steps=all_outputs,
stop=any(getattr(o, "stop", False) for o in all_outputs),
)
@@ -555,18 +523,12 @@ class NodeFactory:
cls,
executor: NodeSource,
name: str | None = None,
requires_confirmation: bool = False,
confirmation_message: str | None = None,
failure_policy: Any = None,
) -> BaseNode:
"""底层物理实例化分发"""
from zhenxun.services.ai.flow.base import BaseRunnable
kwargs = {
"name": name,
"executor": executor,
"requires_confirmation": requires_confirmation,
"confirmation_message": confirmation_message,
"failure_policy": failure_policy,
}
if isinstance(executor, BaseRunnable):
@@ -590,36 +552,6 @@ class NodeFactory:
return item
if isinstance(item, BaseRunnable) or callable(item):
cond_meta = getattr(item, "__workflow_condition_meta__", None)
if cond_meta:
final_name = name or cond_meta.name or "ConditionGroup"
return Condition(
evaluator=item,
steps=cond_meta.if_true,
else_steps=cond_meta.if_false,
name=final_name,
)
router_meta = getattr(item, "__workflow_router_meta__", None)
if router_meta:
final_name = name or router_meta.name or "RouterGroup"
return Router(
selector=item,
choices=router_meta.choices,
name=final_name,
)
step_meta = getattr(item, "__workflow_step_meta__", None)
if step_meta:
final_name = name or step_meta.name
return NodeFactory._create_step(
executor=item,
name=final_name,
requires_confirmation=step_meta.requires_confirmation,
confirmation_message=step_meta.confirmation_message,
failure_policy=step_meta.failure_policy,
)
return NodeFactory._create_step(executor=item, name=name)
raise ValueError(
@@ -0,0 +1,184 @@
import copy
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
from zhenxun.services.ai.flow.workflow.types import StepInput
from zhenxun.services.ai.llm.api import generate_structured
from zhenxun.services.log import logger
class PolicyAction(str, Enum):
RETRY = "retry"
CONTINUE = "continue"
ABORT = "abort"
FALLBACK = "fallback"
class PolicyResult(BaseModel):
"""错误策略执行结果"""
action: PolicyAction
"""采取的具体恢复策略动作"""
delay: float = 0.0
"""执行延迟或重试前需要等待的缓冲秒数"""
new_input: StepInput | None = None
"""用于动态纠错自愈时替换传入的新参数结构"""
fallback_node: Any | None = None
"""策略裁定降级时所指定的备用工作流节点"""
healer_agent_name: str | None = None
"""执行了高级自愈的大模型或修复者名称"""
class BaseFailurePolicy:
"""错误处理策略抽象基类"""
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
"""
处理节点执行失败的策略入口方法。
参数:
node: 发生异常的目标工作流节点。
exception: 捕获到的具体异常实例。
step_input: 节点执行时的原始输入数据。
context: 当前工作流运行上下文。
返回:
PolicyResult: 包含错误恢复动作、延迟时间以及备用参数等决策信息的策略结果对象。
""" # noqa: E501
raise NotImplementedError
class AbortPolicy(BaseFailurePolicy):
"""直接中断策略"""
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
return PolicyResult(action=PolicyAction.ABORT)
class SkipPolicy(BaseFailurePolicy):
"""跳过并继续策略"""
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
return PolicyResult(action=PolicyAction.CONTINUE)
class RetryPolicy(BaseFailurePolicy):
"""退避重试策略"""
def __init__(self, max_retries: int = 3, delay: float = 1.0):
"""
初始化退避重试策略。
参数:
max_retries: 最大允许重试的次数限制,默认 3。
delay: 每次重试前需要等待和睡眠的秒数,默认 1.0。
"""
self.max_retries = max_retries
self.delay = delay
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
counts = context.state.setdefault("__retry_counts__", {})
key = f"{node.name}_{id(self)}"
counts[key] = counts.get(key, 0) + 1
if counts[key] <= self.max_retries:
return PolicyResult(action=PolicyAction.RETRY, delay=self.delay)
return PolicyResult(action=PolicyAction.ABORT)
class FallbackPolicy(BaseFailurePolicy):
"""降级路由策略"""
def __init__(self, fallback_node: Any):
"""
初始化降级路由策略。
参数:
fallback_node: 当主节点发生致命故障时,直接转入执行的备用降级节点。
"""
self.fallback_node = fallback_node
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
return PolicyResult(
action=PolicyAction.FALLBACK, fallback_node=self.fallback_node
)
class SelfHealingPolicy(BaseFailurePolicy):
"""大模型高级自愈策略"""
def __init__(self, healer_model: str, max_retries: int = 2):
"""
初始化大模型高级自愈策略。
参数:
healer_model: 用于分析错误原因并智能修复入参的大模型名称。
max_retries: 最大尝试自愈修复的次数,默认 2。
"""
self.healer_model = healer_model
self.max_retries = max_retries
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
counts = context.state.setdefault("__heal_counts__", {})
key = f"{node.name}_{id(self)}"
counts[key] = counts.get(key, 0) + 1
if counts[key] > self.max_retries:
logger.warning(f"节点 '{node.name}' 自愈次数达上限,宣告失败。")
return PolicyResult(action=PolicyAction.ABORT)
class HealedInput(BaseModel):
"""自愈后输入结构"""
fixed_input: str = Field(
description="""修复后的输入参数 必须是完全合法的数据结构"""
)
prompt = f"""# Self-Healing Task
请修复节点 `{node.name}` 的参数错误。
## Original Input
{step_input.input}
## Exception
{exception}
## Requirements
- 分析错误原因
- 将输入修复为可被程序正确解析的格式
- 只输出修复后的结果,不要输出额外解释
"""
try:
logger.info(f"🩹 触发 AI 自愈分析 (节点: {node.name})...")
res = await generate_structured(
prompt, response_model=HealedInput, model=self.healer_model
)
new_input = copy.copy(step_input)
new_input.input = res.fixed_input
return PolicyResult(
action=PolicyAction.RETRY,
new_input=new_input,
healer_agent_name=self.healer_model,
)
except Exception as e:
logger.error(f"自愈过程发生大模型调用异常: {e}")
return PolicyResult(action=PolicyAction.ABORT)
+1 -179
View File
@@ -1,4 +1,3 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any
@@ -50,10 +49,6 @@ class StepOutput(BaseModel):
"""执行失败时的异常详情"""
stop: bool = False
"""标记是否触发了终止信号,以阻断后续流程的执行"""
is_paused: bool = False
"""标记该步骤是否因等待外力交互 (HITL) 而处于挂起状态"""
pause_reason: str | None = None
"""导致步骤挂起的原因描述"""
steps: list["StepOutput"] | None = None
"""嵌套步骤的输出结果集合(如复合节点 Loop、Parallel 的内部产出)"""
@@ -67,7 +62,7 @@ class WorkflowRunResult(BaseModel):
workflow_name: str
"""工作流的名称"""
status: str
"""流水线的最终运行状态 (completed, paused, error 等)"""
"""流水线的最终运行状态 (completed, error 等)"""
original_input: Any
"""最初传入根节点的原始输入"""
state: dict[str, Any]
@@ -78,176 +73,3 @@ class WorkflowRunResult(BaseModel):
"""最后一个成功执行的步骤所产出的内容"""
final_output: StepOutput | None = None
"""工作流根节点最终包装的完整产出对象"""
paused_step_name: str | None = None
"""若流水线处于挂起态,记录是哪个步骤引发了挂起"""
class PolicyAction(str, Enum):
RETRY = "retry"
CONTINUE = "continue"
ABORT = "abort"
FALLBACK = "fallback"
class PolicyResult(BaseModel):
"""错误策略执行结果"""
action: PolicyAction
"""采取的具体恢复策略动作"""
delay: float = 0.0
"""执行延迟或重试前需要等待的缓冲秒数"""
new_input: StepInput | None = None
"""用于动态纠错自愈时替换传入的新参数结构"""
fallback_node: Any | None = None
"""策略裁定降级时所指定的备用工作流节点"""
healer_agent_name: str | None = None
"""执行了高级自愈的大模型或修复者名称"""
class BaseFailurePolicy(ABC):
"""错误处理策略抽象基类"""
@abstractmethod
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
pass
class AbortPolicy(BaseFailurePolicy):
"""直接中断策略"""
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
return PolicyResult(action=PolicyAction.ABORT)
class SkipPolicy(BaseFailurePolicy):
"""跳过并继续策略"""
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
return PolicyResult(action=PolicyAction.CONTINUE)
class RetryPolicy(BaseFailurePolicy):
"""退避重试策略"""
def __init__(self, max_retries: int = 3, delay: float = 1.0):
"""
初始化退避重试策略。
参数:
max_retries: 最大允许重试的次数限制,默认 3。
delay: 每次重试前需要等待和睡眠的秒数,默认 1.0。
"""
self.max_retries = max_retries
self.delay = delay
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
counts = context.state.setdefault("__retry_counts__", {})
key = f"{node.name}_{id(self)}"
counts[key] = counts.get(key, 0) + 1
if counts[key] <= self.max_retries:
return PolicyResult(action=PolicyAction.RETRY, delay=self.delay)
return PolicyResult(action=PolicyAction.ABORT)
class FallbackPolicy(BaseFailurePolicy):
"""降级路由策略"""
def __init__(self, fallback_node: Any):
"""
初始化降级路由策略。
参数:
fallback_node: 当主节点发生致命故障时,直接转入执行的备用降级节点。
"""
self.fallback_node = fallback_node
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
return PolicyResult(
action=PolicyAction.FALLBACK, fallback_node=self.fallback_node
)
class SelfHealingPolicy(BaseFailurePolicy):
"""大模型高级自愈策略"""
def __init__(self, healer_model: str, max_retries: int = 2):
"""
初始化大模型高级自愈策略。
参数:
healer_model: 用于分析错误原因并智能修复入参的大模型名称。
max_retries: 最大尝试自愈修复的次数,默认 2。
"""
self.healer_model = healer_model
self.max_retries = max_retries
async def handle_failure(
self, node: Any, exception: BaseException, step_input: StepInput, context: Any
) -> PolicyResult:
counts = context.state.setdefault("__heal_counts__", {})
key = f"{node.name}_{id(self)}"
counts[key] = counts.get(key, 0) + 1
if counts[key] > self.max_retries:
from zhenxun.services.log import logger
logger.warning(f"节点 '{node.name}' 自愈次数达上限,宣告失败。")
return PolicyResult(action=PolicyAction.ABORT)
import copy
from zhenxun.services.ai.llm.api import generate_structured
from zhenxun.services.log import logger
class HealedInput(BaseModel):
"""自愈后输入结构"""
fixed_input: str = Field(
description="""修复后的输入参数 必须是完全合法的数据结构"""
)
prompt = f"""# Self-Healing Task
请修复节点 `{node.name}` 的参数错误。
## Original Input
{step_input.input}
## Exception
{exception}
## Requirements
- 分析错误原因
- 将输入修复为可被程序正确解析的格式
- 只输出修复后的结果,不要输出额外解释
"""
try:
logger.info(f"🩹 触发 AI 自愈分析 (节点: {node.name})...")
res = await generate_structured(
prompt, response_model=HealedInput, model=self.healer_model
)
new_input = copy.copy(step_input)
new_input.input = res.fixed_input
return PolicyResult(
action=PolicyAction.RETRY,
new_input=new_input,
healer_agent_name=self.healer_model,
)
except Exception as e:
logger.error(f"自愈过程发生大模型调用异常: {e}")
return PolicyResult(action=PolicyAction.ABORT)