✨ 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,27 @@
from .auto import AutoWorkflow
from .decorators import AND, OR, entry, listen, router
from .engine import Workflow
from .nodes import (
Condition,
Loop,
Parallel,
Router,
Step,
Steps,
)
__all__ = [
"AND",
"OR",
"AutoWorkflow",
"Condition",
"Loop",
"Parallel",
"Router",
"Step",
"Steps",
"Workflow",
"entry",
"listen",
"router",
]
+107
View File
@@ -0,0 +1,107 @@
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
+253
View File
@@ -0,0 +1,253 @@
from abc import ABC, abstractmethod
import asyncio
from collections.abc import AsyncIterator
from typing import Any
from zhenxun.services.ai.core.exceptions import (
AbortException,
ControlFlowExit,
ToolFatalError,
)
from zhenxun.services.ai.flow.workflow.types import (
AbortPolicy,
BaseFailurePolicy,
PolicyAction,
StepInput,
StepOutput,
StepType,
)
from zhenxun.services.ai.run import RunContext
from zhenxun.services.log import logger
class BaseNode(ABC):
"""工作流节点统一抽象基类"""
def __init__(
self,
name: str,
requires_confirmation: bool = False,
confirmation_message: str | None = None,
failure_policy: BaseFailurePolicy | None = None,
):
"""
初始化工作流节点基类。
参数:
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
@abstractmethod
def node_type(self) -> StepType:
"""节点类型标识 (供子类实现)"""
pass
async def _handle_execution_failure(
self, e: BaseException, step_input: StepInput, context: RunContext, attempt: int
) -> tuple[str, StepOutput | None, StepInput | None, Any]:
"""
解析执行异常并应用容错策略
"""
if isinstance(e, asyncio.CancelledError):
raise e
if isinstance(e, ControlFlowExit):
logger.info(
f"⏭️ [控制流拦截] Node '{self.name}' 触发中断信号: "
f"{type(e).__name__} - {e}"
)
content = str(e)
if getattr(e, "display_content", None):
content = str(getattr(e, "display_content"))
elif getattr(e, "display", None):
content = str(getattr(e, "display"))
elif getattr(e, "result_output", None):
content = str(getattr(e, "result_output"))
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content=content,
success=False,
stop=True,
error=str(e)
if isinstance(e, AbortException | ToolFatalError)
else None,
)
return "break", output, None, None
logger.warning(f"Node '{self.name}' 执行发生异常: {e}")
policy_result = await self.failure_policy.handle_failure(
self, e, step_input, context
)
if policy_result.action == PolicyAction.RETRY:
if policy_result.delay > 0:
await asyncio.sleep(policy_result.delay)
new_input = policy_result.new_input or step_input
logger.debug(f" 🔄 [节点重试] `{self.name}` 进行第 {attempt} 次重试...")
return "continue", None, new_input, None
elif policy_result.action == PolicyAction.FALLBACK:
fallback_node = policy_result.fallback_node
fallback_name = getattr(fallback_node, "name", "FallbackNode")
logger.info(
f"🔀 节点 {self.name} 执行失败,触发降级路由至: {fallback_name}"
)
return "fallback", None, None, fallback_node
elif policy_result.action == PolicyAction.CONTINUE:
logger.warning(f"Node '{self.name}' 执行异常,已被策略自动跳过: {e}")
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content=f"节点执行失败,已通过策略自动跳过: {e}",
success=False,
stop=False,
error=str(e),
)
return "break", output, None, None
else:
logger.error(f"Node '{self.name}' 执行崩溃,已被策略中断执行流: {e}")
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content=f"执行崩溃: {e}",
success=False,
stop=True,
error=str(e),
)
return "break", output, None, None
@abstractmethod
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
"""子类必须实现的核心流式执行逻辑"""
yield None
async def _forward_stream(
self, stream: AsyncIterator[Any], output_box: list[StepOutput]
) -> AsyncIterator[Any]:
"""辅助方法:转发内部流事件,并将最终的 StepOutput 拦截放入 output_box 列表中"""
async for event in stream:
if isinstance(event, StepOutput):
output_box.append(event)
else:
yield event
async def aexecute(self, step_input: StepInput, context: RunContext) -> StepOutput:
"""非流式执行(聚合流并返回最终结果),子类无需重写"""
output = None
async for event in self.aexecute_stream(step_input, context):
if isinstance(event, StepOutput):
output = event
if output is None:
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content="节点未产生有效输出",
success=False,
)
return output
async def aexecute_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
"""标准化模板方法:处理缓存快进、授权挂起、异常熔断与生命周期事件分发"""
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)
):
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
while True:
output = None
try:
async for event in self.run_stream(current_input, context):
if isinstance(event, StepOutput):
output = event
output.step_name = self.name
output.step_type = self.node_type
else:
yield event
if output is None:
output = StepOutput(
step_name=self.name,
step_type=self.node_type,
content="执行完毕,无数据返回",
success=True,
)
context.upstream_results[self.name] = output.content
break
except BaseException as e:
(
action_cmd,
output,
new_input,
fallback_node,
) = await self._handle_execution_failure(
e, current_input, context, attempt
)
if action_cmd == "continue":
current_input = (
new_input if new_input is not None else current_input
)
attempt += 1
continue
elif action_cmd == "fallback" and fallback_node:
async for evt in fallback_node.aexecute_stream(
current_input, context
):
if isinstance(evt, StepOutput):
output = evt
else:
yield evt
break
yield output
@@ -0,0 +1,91 @@
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
+329
View File
@@ -0,0 +1,329 @@
import asyncio
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, Any
import uuid
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
from zhenxun.services.ai.core.stream_events import EventBus
from zhenxun.services.ai.flow.base import BaseRunnable, BaseRuntimeConfig
from zhenxun.services.ai.flow.workflow.nodes import Steps
from zhenxun.services.ai.flow.workflow.types import (
StepInput,
StepOutput,
WorkflowRunResult,
)
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.tools.core.tool import FunctionTool
from zhenxun.services.log import logger
class Workflow(BaseRunnable[WorkflowRunResult]):
"""
工作流顶层容器 (The Workflow Facade)。
继承自 BaseRunnable,支持被作为节点嵌套在 Team 或 其他工作流中。
"""
def __init__(self, name: str, steps: list["NodeSource"], description: str = ""):
"""
静态图元工作流容器初始化。
参数:
name: 工作流的名称标识。
steps: 工作流的节点列表(按列表顺序构成串行或嵌套结构)。
description: 工作流的说明描述,用于被 Agent 调用时理解其功能。
"""
self.name = name
self.description = description
self.id = uuid.uuid4().hex
self.root_steps = Steps(steps=steps, name=f"{self.name}_Root")
self.runtime_config = BaseRuntimeConfig(stateless=True)
self.persona = None
def _build_result(
self,
initial_input: StepInput,
safe_context: RunContext,
final_output: StepOutput,
) -> WorkflowRunResult:
flat_outputs = {}
def _extract(out: StepOutput):
flat_outputs[out.step_name] = out
if out.steps:
for o in out.steps:
_extract(o)
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")
)
return WorkflowRunResult(
workflow_id=self.id,
workflow_name=self.name,
status=status,
original_input=initial_input.input,
state=safe_context.state,
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
return Depends(_dependency)
async def reply(
self, prompt: PromptInput | None = None, reply_to: bool = False, **kwargs: Any
) -> WorkflowRunResult:
"""
工作流交互执行语法糖,隐式提取上下文并自动将最终流水线产出发送回复给用户。
参数:
prompt: 传入工作流入口根节点的初始参数或指令。
reply_to: 是否将结果作为回复消息发送 (at用户或引用原消息)。
kwargs: 追加的工作流附带参数 (additional_data)。
返回:
WorkflowRunResult: 包含执行状态、断点快照、各节点产出的全量工作流结果对象。
"""
from zhenxun.utils.message import MessageUtils
ctx = RunContext()
bot = ctx.get_bot()
event = ctx.get_event()
res = await self.run(prompt=prompt, context=ctx, **kwargs)
if bot and event:
if res.status == "completed" and res.final_output:
msg = (
str(res.final_output.content)
if res.final_output.content
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(
f"❌ 工作流执行发生错误: {err_msg}"
).send(reply_to=reply_to)
return res
async def run(
self,
prompt: PromptInput | None = None,
*,
context: RunContext | None = None,
**kwargs: Any,
) -> WorkflowRunResult:
"""
工作流单次运行阻塞核心入口,遍历所有图元节点直至终止。
参数:
prompt: 传入工作流入口根节点的初始参数或指令。
context: 显式传入的会话与运行上下文。
kwargs: 追加的工作流附带参数 (additional_data)。
返回:
WorkflowRunResult: 包含执行状态、断点快照、各节点产出的全量工作流结果对象。
"""
session_id = (
context.session_id if context and context.session_id else f"wf_{self.id}"
)
safe_context = context or RunContext(session_id=session_id)
logger.debug(f"🏭 **工作流 [{self.name}] 启动**")
initial_input = StepInput(input=prompt)
if kwargs:
initial_input.additional_data.update(kwargs)
try:
final_output = await self.root_steps.aexecute(initial_input, safe_context)
logger.debug(f"🏭 **工作流 [{self.name}] 运行结束**")
return self._build_result(initial_input, safe_context, final_output)
except BaseException as e:
if isinstance(e, ControlFlowExit):
logger.debug(f"⏭️ 工作流执行被业务控制流安全中止: {e}")
dummy_output = StepOutput(content=str(e), success=False)
return self._build_result(initial_input, safe_context, dummy_output)
raise e
import contextlib
@contextlib.asynccontextmanager
async def run_stream(
self,
prompt: PromptInput | None = None,
*,
context: RunContext | None = None,
**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
async def _execution_task():
try:
async for event in self._internal_stream(prompt, context, **kwargs):
await event_bus.emit(event)
except BaseException as e:
await event_bus.emit(AgentRunError(error=e))
finally:
await event_bus.end()
task = asyncio.create_task(_execution_task())
try:
yield StreamedRunResult[Any](event_bus)
finally:
if not task.done():
task.cancel()
async def _internal_stream(
self,
prompt: PromptInput | None = None,
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}"
)
safe_context = context or RunContext(session_id=session_id)
logger.debug(f"🏭 **工作流 [{self.name}] 启动**")
initial_input = StepInput(input=prompt)
if kwargs:
initial_input.additional_data.update(kwargs)
try:
final_output = None
async for event in self.root_steps.aexecute_stream(
initial_input, safe_context
):
if isinstance(event, StepOutput):
final_output = event
else:
yield event
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
)
agent_res = AgentRunResult(
output=wf_result.last_step_content,
structured_data=wf_result,
usage=UsageInfo(),
)
yield AgentRunEnd(result=agent_res)
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:
async def _execute_workflow_tool(prompt: str, context: RunContext) -> str:
run_result = await self.run(prompt=prompt, context=context)
output = run_result.final_output
if output and output.success:
return (
f"工作流 [{self.name}] 执行完毕。最终流水线产出:\n{output.content}"
)
raise ToolRetryError(
f"工作流执行失败: {output.error if output else 'unknown'},"
"请尝试换种方式处理。"
)
final_tool_name = tool_name or f"trigger_workflow_{self.id}"
tool_desc = (
f"触发执行专属流水线: {self.name}。\n"
f"描述: {self.description}\n"
f"注意:如果你认为该工作流能完全解决用户的问题,请立刻调用此工具,"
f"并将用户的诉求提炼后作为 prompt 传入。"
)
return FunctionTool(
func=_execute_workflow_tool, name=final_tool_name, description=tool_desc
)
+628
View File
@@ -0,0 +1,628 @@
import asyncio
from collections.abc import AsyncIterator, Callable, Sequence
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.types import (
BaseFailurePolicy,
StepInput,
StepOutput,
StepType,
)
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.log import logger
NodeSource = BaseNode | BaseRunnable | Callable
"""工作流节点来源,可以是图元、可执行引擎或原生函数"""
class Step(BaseNode):
"""
工作流中的最小执行单元门面 (Facade)。
对外部隐藏了 AgentNode 和 FunctionNode 的具体实现。
当实例化 Step 时,底层会自动根据 executor 的类型返回专属的节点对象。
"""
def __new__(cls, *args, **kwargs):
if cls is Step:
executor = kwargs.get("executor")
if executor is None and len(args) > 1:
executor = args[1]
from zhenxun.services.ai.flow.base import BaseRunnable
if isinstance(executor, BaseRunnable):
return object.__new__(RunnableNode)
elif callable(executor):
return object.__new__(FunctionNode)
return object.__new__(cls)
def __init__(
self,
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,
):
"""
初始化工作流单元步骤(门面)。
参数:
name: 步骤的名称,为空则自动取执行器的名称,默认 None。
executor: 该步骤要运行的核心执行器(支持 RunnableNode 或 Callable 依赖注入)。
prompt: 该步骤的初始输入或提示词定义,默认 None。
requires_confirmation: 标记该节点在执行前是否需要人工介入授权,默认 False。
confirmation_message: 挂起等待授权时展示的提示文案,默认 None。
failure_policy: 该节点执行失败时的错误处理策略,默认使用中断策略。
""" # noqa: E501
actual_name = name or getattr(
executor, "name", getattr(executor, "__name__", "unnamed_step")
)
super().__init__(
name=actual_name,
requires_confirmation=requires_confirmation,
confirmation_message=confirmation_message,
failure_policy=failure_policy,
)
self.executor = executor
self.prompt = prompt
@property
def node_type(self) -> StepType:
return StepType.STEP
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
if False:
yield None
raise NotImplementedError(
"This is a facade. Real execution happens in subclasses."
)
class RunnableNode(Step):
"""专门处理 Agent/Team/Workflow 等 BaseRunnable 状态机执行的私有节点"""
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):
prompt_data = copy.copy(prompt_data)
if step_input.previous_step_content:
prev_content = str(step_input.previous_step_content)
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:
prompt_data = (
f"[上游节点执行输出]:\n{step_input.previous_step_content}\n\n"
f"[当前需执行的任务]:\n{prompt_data or ''}"
)
context.run.user_input = str(prompt_data) if prompt_data else ""
final_result = None
sandbox_context = context.clone_for_member(self.name)
async with executor.run_stream(
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
context.state.update(sandbox_context.state)
yield StepOutput(
content=final_result.output if final_result else "无返回",
success=True,
)
class FunctionNode(Step):
"""专门处理 Python Callable 依赖注入与执行的私有节点"""
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
context.run.user_input = str(step_input.input) if step_input.input else ""
executor = cast(Callable, self.executor)
res = await DependencyInjector.invoke(
executor, {"step_input": step_input}, context
)
if isinstance(res, StepOutput):
yield res
else:
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):
"""串行执行的工作流容器。按照列表顺序依次执行。"""
def __init__(self, steps: Sequence[NodeSource], name: str = "StepsGroup"):
"""
初始化串行工作流容器。
参数:
steps: 依次串行执行的节点/执行器列表。
name: 该串行容器 of 名称,默认 "StepsGroup"。
"""
super().__init__(name=name)
self.steps = [NodeFactory.build(step) for step in steps]
@property
def node_type(self) -> StepType:
return StepType.STEPS
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
current_input = StepInput(
input=step_input.input,
previous_step_content=step_input.previous_step_content,
additional_data=step_input.additional_data.copy(),
)
all_outputs: list[StepOutput] = []
for step_obj in self.steps:
out_box: list[StepOutput] = []
async for event in self._forward_stream(
step_obj.aexecute_stream(current_input, context), out_box
):
yield event
step_out = out_box[0] if out_box else None
if step_out:
all_outputs.append(step_out)
current_input.previous_step_content = step_out.content
if step_out.stop:
break
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,
)
class Condition(BaseNode):
"""根据条件函数的返回结果,决定走向 steps 还是 else_steps"""
def __init__(
self,
evaluator: Any,
steps: Sequence[NodeSource],
else_steps: Sequence[NodeSource] | None = None,
name: str = "ConditionGroup",
):
"""
初始化条件分支节点。
参数:
evaluator: 用于评估条件真假的布尔值、表达式或可调用函数。
steps: 当 evaluator 求值为真时,将执行的步骤序列。
else_steps: 当 evaluator 求值为假时,将执行的备用步骤序列,默认 None。
name: 该条件分支容器的名称,默认 "ConditionGroup"。
"""
super().__init__(name=name)
self.evaluator = evaluator
self.steps = [NodeFactory.build(step) for step in steps]
self.else_steps = [NodeFactory.build(step) for step in (else_steps or [])]
@property
def node_type(self) -> StepType:
return StepType.CONDITION
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
if callable(self.evaluator):
condition_result = await DependencyInjector.invoke(
self.evaluator, {"step_input": step_input}, context
)
else:
condition_result = bool(self.evaluator)
target_steps = self.steps if condition_result else self.else_steps
branch_name = "if" if condition_result else "else"
if not target_steps:
yield StepOutput(
content=f"条件求值为 {condition_result},无对应步骤需执行。",
success=True,
)
return
steps_container = Steps(
steps=target_steps, name=f"{self.name}_{branch_name}_branch"
)
out_box: list[StepOutput] = []
async for event in self._forward_stream(
steps_container.aexecute_stream(step_input, context), out_box
):
yield event
if out_box:
yield out_box[0]
class Router(BaseNode):
"""根据选择器函数的返回值(名称),从候选项中挑选步骤执行"""
def __init__(
self, choices: Sequence[NodeSource], selector: Any, name: str = "RouterGroup"
):
"""
初始化选择路由器节点。
参数:
choices: 包含所有候选执行路由分支的步骤序列。
selector: 用于决定路由流向的匹配值、或者是返回分支名称的动态选择器函数。
name: 该路由器容器的名称,默认 "RouterGroup"。
"""
super().__init__(name=name)
self.choices = [NodeFactory.build(c) for c in choices]
self.selector = selector
self._choice_map = {}
for c in self.choices:
if c.name:
self._choice_map[c.name] = c
@property
def node_type(self) -> StepType:
return StepType.ROUTER
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
if callable(self.selector):
selected = await DependencyInjector.invoke(
self.selector, {"step_input": step_input}, context
)
else:
selected = self.selector
if not isinstance(selected, list):
selected = [selected]
target_steps = []
for s in selected:
if isinstance(s, str):
if s in self._choice_map:
target_steps.append(self._choice_map[s])
else:
logger.warning(f"Router '{self.name}' 选择了未知的步骤: '{s}'")
else:
target_steps.append(NodeFactory.build(s))
if not target_steps:
yield StepOutput(content="没有命中任何有效路由分支。", success=True)
return
steps_container = Steps(steps=target_steps, name=f"{self.name}_routed_steps")
out_box: list[StepOutput] = []
async for event in self._forward_stream(
steps_container.aexecute_stream(step_input, context), out_box
):
yield event
if out_box:
yield out_box[0]
class Loop(BaseNode):
"""循环执行工作流,直至达到最大次数或满足结束条件"""
def __init__(
self,
steps: Sequence[NodeSource],
max_iterations: int = 3,
end_condition: Any = None,
name: str = "LoopGroup",
):
"""
初始化循环控制器节点。
参数:
steps: 每次循环中需要顺序运行的步骤序列。
max_iterations: 最大允许循环执行的迭代次数上限,默认 3。
end_condition: 决定是否可以提前终止循环的条件布尔值或可调用判定函数,
默认 None。
name: 该循环容器的名称,默认 "LoopGroup"。
"""
super().__init__(name=name)
self.steps = [NodeFactory.build(step) for step in steps]
self.max_iterations = max_iterations
self.end_condition = end_condition
@property
def node_type(self) -> StepType:
return StepType.LOOP
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
logger.debug(
f" 🔁 开始循环: [Loop] `{self.name}` (最大 {self.max_iterations} 次)"
)
iteration = 0
all_results: list[StepOutput] = []
current_input = StepInput(
input=step_input.input,
previous_step_content=step_input.previous_step_content,
additional_data=step_input.additional_data.copy(),
)
while iteration < self.max_iterations:
logger.debug(f" ┃ 🔄 第 {iteration + 1} 次迭代...")
steps_container = Steps(
steps=self.steps, name=f"{self.name}_iter_{iteration + 1}"
)
out_box: list[StepOutput] = []
async for event in self._forward_stream(
steps_container.aexecute_stream(current_input, context), out_box
):
yield event
iter_output = out_box[0] if out_box else None
should_stop = False
if iter_output:
all_results.append(iter_output)
if self.end_condition:
if callable(self.end_condition):
should_stop = await DependencyInjector.invoke(
self.end_condition,
{"iteration_results": iter_output.steps or [iter_output]},
context,
)
else:
should_stop = bool(self.end_condition)
iteration += 1
if should_stop or iter_output.stop:
break
current_input.previous_step_content = iter_output.content
else:
iteration += 1
break
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,
)
logger.debug(f" ✅ 循环结束: [Loop] `{self.name}` (共执行 {iteration} 次)")
class Parallel(BaseNode):
"""并发执行的工作流容器。无序地并发执行内部所有步骤,并最终聚合成一个输出。"""
def __init__(self, *args: NodeSource | str, name: str | None = None):
"""
初始化并发工作流容器。
参数:
*args: 并发执行的任务节点/执行器,支持混入字符串覆盖作为 Parallel 的名字。
name: 该并发容器的名称,默认 "ParallelGroup"。
"""
super().__init__(name=name or "ParallelGroup")
self.steps = []
for arg in args:
if isinstance(arg, str):
self.name = arg
else:
self.steps.append(NodeFactory.build(arg))
@property
def node_type(self) -> StepType:
return StepType.PARALLEL
async def run_stream(
self, step_input: StepInput, context: RunContext
) -> AsyncIterator[Any]:
logger.debug(f" 🔀 [并发] `{self.name}` 开启了 {len(self.steps)} 个并发任务")
queue = asyncio.Queue()
bg_tasks = []
async def worker(idx: int, s_obj: Any, c_ctx: RunContext):
try:
async for evt in s_obj.aexecute_stream(step_input, c_ctx):
await queue.put(("event", evt))
except asyncio.CancelledError:
pass
except Exception as e:
await queue.put(("error", e, getattr(s_obj, "name", f"step_{idx}")))
finally:
await queue.put(
("done", idx, c_ctx.state, getattr(c_ctx, "upstream_results", {}))
)
for i, step_obj in enumerate(self.steps):
child_context = context.clone_for_execution()
task = asyncio.create_task(worker(i, step_obj, child_context))
bg_tasks.append(task)
completed = 0
all_outputs: list[StepOutput] = []
aggregated_content_parts = [f"## 并发执行结果汇总 [{self.name}]\n"]
has_any_failure = False
early_stopped = False
while completed < len(self.steps):
msg_type, *data = await queue.get()
if msg_type == "event":
if isinstance(data[0], StepOutput):
out = cast(StepOutput, data[0])
all_outputs.append(out)
if not out.success:
has_any_failure = True
status_icon = "✅ 成功" if out.success else "❌ 失败"
aggregated_content_parts.append(
f"### {status_icon}: {out.step_name}\n{out.content}"
)
if out.stop and not early_stopped:
early_stopped = True
logger.info(
f"并行分支 '{out.step_name}' 请求终止,"
"正在取消其他并发任务..."
)
for t in bg_tasks:
if not t.done():
t.cancel()
else:
yield data[0]
elif msg_type == "error":
err, s_name = data
logger.error(f"并发步骤 '{s_name}' 执行崩溃: {err}")
out = StepOutput(
step_name=s_name,
step_type=StepType.STEP,
content=f"执行崩溃: {err}",
success=False,
error=str(err),
)
all_outputs.append(out)
has_any_failure = True
aggregated_content_parts.append(f"### ❌ 失败: {s_name}\n{err}")
elif msg_type == "done":
_, child_state, child_upstream_results = data
context.state.update(child_state)
context.upstream_results.update(child_upstream_results)
completed += 1
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),
)
logger.debug(f" ✅ [并发] `{self.name}` 执行完毕")
class NodeFactory:
"""统一节点装配工厂"""
@classmethod
def _create_step(
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):
return RunnableNode(**kwargs)
elif callable(executor):
return FunctionNode(**kwargs)
raise ValueError(f"执行器类型 {type(executor)} 无法转换为叶子节点(Step)。")
@staticmethod
def build(item: NodeSource, name: str | None = None) -> BaseNode:
if isinstance(item, BaseNode):
if name and item.name in (
"unnamed_step",
"StepsGroup",
"ParallelGroup",
"ConditionGroup",
"RouterGroup",
"LoopGroup",
):
item.name = name
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(
f"无法将类型 {type(item)} 装配为工作流节点。"
"支持的类型:BaseRunnable, Callable 或 BaseNode。"
)
+253
View File
@@ -0,0 +1,253 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
class StepType(str, Enum):
FUNCTION = "Function"
STEP = "Step"
STEPS = "Steps"
LOOP = "Loop"
PARALLEL = "Parallel"
CONDITION = "Condition"
ROUTER = "Router"
class StepInput(BaseModel):
"""传递给每个 Step 的标准输入结构"""
input: Any = Field(default=None)
"""继承自 Workflow 的初始输入"""
previous_step_content: Any = Field(default=None)
"""上一个执行步骤产生的直接输出内容"""
additional_data: dict[str, Any] = Field(default_factory=dict)
"""在生命周期中穿透传递的附加数据"""
class StepOutput(BaseModel):
"""每个 Step 的标准输出结构"""
step_name: str | None = None
"""步骤的名称"""
step_id: str | None = None
"""步骤的唯一标识"""
step_type: StepType | None = None
"""步骤的节点枚举类型"""
executor_type: str | None = None
"""底层执行器的类型标识"""
executor_name: str | None = None
"""底层执行器的具体名称"""
content: Any = None
"""该步骤产生的直接输出内容"""
success: bool = True
"""标记该步骤是否执行成功"""
error: str | None = None
"""执行失败时的异常详情"""
stop: bool = False
"""标记是否触发了终止信号,以阻断后续流程的执行"""
is_paused: bool = False
"""标记该步骤是否因等待外力交互 (HITL) 而处于挂起状态"""
pause_reason: str | None = None
"""导致步骤挂起的原因描述"""
steps: list["StepOutput"] | None = None
"""嵌套步骤的输出结果集合(如复合节点 Loop、Parallel 的内部产出)"""
class WorkflowRunResult(BaseModel):
"""工作流运行结果(包含断点快照状态)"""
workflow_id: str
"""工作流实例运行的唯一标识"""
workflow_name: str
"""工作流的名称"""
status: str
"""流水线的最终运行状态 (completed, paused, error 等)"""
original_input: Any
"""最初传入根节点的原始输入"""
state: dict[str, Any]
"""工作流生命周期中的全局共享上下文状态字典"""
step_outputs: dict[str, StepOutput]
"""平铺展开的所有经历过的节点步骤输出字典"""
last_step_content: Any
"""最后一个成功执行的步骤所产出的内容"""
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)