♻️ 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
+44 -77
View File
@@ -6,10 +6,16 @@ from typing import Any, Generic, cast
from zhenxun.services.ai.capabilities import (
AbstractCapability,
CombinedCapability,
DynamicCapability,
)
from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.context.knowledge.base import BaseKnowledge
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
from zhenxun.services.ai.context.memory.capabilities import (
AgenticMemoryCapability,
SlotMemoryCapability,
)
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.core.exceptions import (
ConcurrencyInterruptException,
@@ -28,20 +34,15 @@ from zhenxun.services.ai.core.options import (
from zhenxun.services.ai.core.protocols.tool import ToolExecutable, ToolResolvable
from zhenxun.services.ai.core.stream_events import AgentStreamEvent, EventBus
from zhenxun.services.ai.core.templates import PromptTemplate
from zhenxun.services.ai.flow.agent.engine.builders import ToolBuilder
from zhenxun.services.ai.flow.agent.models import (
AgentConfig,
AgentRunResources,
AgentState,
Persona,
)
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.guardrails import GuardrailSource
from zhenxun.services.ai.flow.base import BaseRunnable, ConcurrencyPolicy
from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
from zhenxun.services.ai.guardrails import GuardrailSource, parse_guardrails
from zhenxun.services.ai.llm.builder import IntentBuilder
from zhenxun.services.ai.message_builder import MessageBuilder
from zhenxun.services.ai.run import (
AgentRunResult,
AgentTask,
RunContext,
Task,
)
from zhenxun.services.ai.run.context import AgentDepsT
from zhenxun.services.ai.run.di import DependencyInjector
@@ -52,10 +53,20 @@ from zhenxun.services.ai.run.models import (
OutputDataT,
StreamedRunResult,
)
from zhenxun.services.ai.tools.core.tool import BaseTool
from zhenxun.services.ai.run.subscribers import (
DefaultUISubscriber,
TelemetrySubscriber,
)
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
from zhenxun.services.ai.tools.core.tool import BaseTool, FunctionTool
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
from zhenxun.services.ai.tools.models import Query
from zhenxun.services.ai.tools.models import Query, ToolOptions
from zhenxun.services.ai.tools.providers.builtin.hitl import HITLToolkit
from zhenxun.services.ai.tools.providers.skills.capabilities import (
SkillCapability,
)
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.services.ai.utils import ContextUtils
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import (
model_construct,
@@ -65,7 +76,18 @@ from zhenxun.utils.pydantic_compat import (
)
from zhenxun.utils.utils import infer_plugin_namespace
from .engine.executor import BaseAgentExecutor
from .engine.builders import (
AgentProfileResolver,
ContextBuilder,
ToolBuilder,
)
from .engine.executor import BaseAgentExecutor, StandardAgentExecutor
from .models import (
AgentConfig,
AgentRunResources,
AgentState,
Persona,
)
ToolSource = (
Callable | BaseTool | dict[str, Any] | str | BaseToolkit | ToolResolvable | Query
@@ -413,8 +435,6 @@ class Agent(
self.tool_filters = []
self.toolset_funcs = []
self._event_listeners: dict[type[AgentStreamEvent], list[Callable]] = {}
from zhenxun.services.ai.guardrails import parse_guardrails
self._guardrails = parse_guardrails(guardrails)
self.memory_config = MemoryBuilder.resolve(memory)
@@ -428,8 +448,6 @@ class Agent(
self.engine_config = self.config
if self.config.enable_hitl is None:
from zhenxun.services.ai.config import get_llm_config
self.config.enable_hitl = get_llm_config().agent_settings.enable_hitl
self.config.stateless = not self.memory_config.short_term.enable
@@ -450,19 +468,11 @@ class Agent(
self.capabilities: list[AbstractCapability] = []
if self.memory_config.long_term.enable and self.memory_config.long_term.agentic:
from zhenxun.services.ai.context.memory.capabilities import (
AgenticMemoryCapability,
)
self.capabilities.append(
AgenticMemoryCapability(self.memory_config, self.namespace)
)
if self.memory_config.slots.enable:
from zhenxun.services.ai.context.memory.capabilities import (
SlotMemoryCapability,
)
self.capabilities.append(
SlotMemoryCapability(self.memory_config, self.namespace)
)
@@ -475,15 +485,9 @@ class Agent(
self.capabilities.append(DynamicCapability(cap))
if self.config.enable_hitl:
from zhenxun.services.ai.tools.providers.builtin.hitl import HITLToolkit
self.tool_definitions.append(HITLToolkit())
if skills:
from zhenxun.services.ai.tools.providers.skills.capabilities import (
SkillCapability,
)
self.capabilities.append(
SkillCapability(skills=skills, namespace=self.namespace)
)
@@ -502,9 +506,6 @@ class Agent(
"""
def decorator(f: Callable):
from zhenxun.services.ai.tools.core.tool import FunctionTool
from zhenxun.services.ai.tools.models import ToolOptions
tool_name = name or f.__name__
tool_desc = description or f.__doc__ or "未提供描述"
base_settings = settings or getattr(f, "__tool_settings__", ToolOptions())
@@ -566,15 +567,11 @@ class Agent(
if func is None:
def decorator(f: Callable):
from zhenxun.services.ai.guardrails import parse_guardrails
self._guardrails.extend(parse_guardrails([f]))
return f
return decorator
else:
from zhenxun.services.ai.guardrails import parse_guardrails
self._guardrails.extend(parse_guardrails([func]))
return func
@@ -594,13 +591,11 @@ class Agent(
async def __resolve_to_tools__(self) -> list[ToolExecutable]:
"""协议支持:将自身 Agent 转化为可被上级调用的工具"""
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
return [DelegateTool(self)]
async def run(
self,
prompt: PromptInput | Task | None = None,
prompt: PromptInput | AgentTask | None = None,
*,
config: AgentConfig | dict | None = None,
deps: AgentDepsT | None = None,
@@ -611,7 +606,7 @@ class Agent(
智能体单次运行阻塞核心入口,内部使用上下文管理器静默消费事件流直至执行结束。
参数:
prompt: 用户输入的消息内容或标准数据契约任务对象 (Task)。
prompt: 用户输入的消息内容或标准数据契约任务对象 (AgentTask)。
deps: 强类型的外部依赖注入对象 (例如 NoneBot 的 Bot, Event)。
context: 显式传入的运行时与会话上下文 (RunContext)。
config: 单次运行时的动态配置覆盖字典或对象。
@@ -628,7 +623,7 @@ class Agent(
@contextlib.asynccontextmanager
async def run_stream(
self,
prompt: PromptInput | Task | None = None,
prompt: PromptInput | AgentTask | None = None,
*,
config: AgentConfig | dict | None = None,
deps: AgentDepsT | None = None,
@@ -648,10 +643,6 @@ class Agent(
effective_config = self.config.merge_with(override_conf)
if effective_config.skills:
from zhenxun.services.ai.tools.providers.skills.capabilities import (
SkillCapability,
)
if effective_config.capabilities is None:
effective_config.capabilities = []
effective_config.capabilities.append(
@@ -661,11 +652,6 @@ class Agent(
)
bus = event_bus or EventBus()
from zhenxun.services.ai.run.subscribers import (
DefaultUISubscriber,
TelemetrySubscriber,
)
TelemetrySubscriber().attach(bus)
if self._event_listeners:
@@ -674,8 +660,6 @@ class Agent(
def _make_handler(callback_func: Callable) -> Callable:
async def _di_handler(event: AgentStreamEvent):
from zhenxun.services.ai.run.di import DependencyInjector
await DependencyInjector.invoke(
callback_func, {"stream_event": event}, safe_context
)
@@ -700,8 +684,6 @@ class Agent(
policy = getattr(self.config, "concurrency_policy", None)
if policy is None:
from zhenxun.services.ai.flow.base import ConcurrencyPolicy
policy = (
ConcurrencyPolicy.ALLOW
if getattr(self.config, "stateless", True)
@@ -710,8 +692,6 @@ class Agent(
intervention_policy = getattr(self.config, "intervention_policy", None)
from zhenxun.services.ai.utils import ContextUtils
lock_id = ContextUtils.extract_concurrency_lock_id(
safe_context,
getattr(self.config, "concurrency_scope", None),
@@ -719,8 +699,6 @@ class Agent(
)
async def _execution_task():
from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
cancel_token = safe_context.run.cancellation_token or CancellationToken()
safe_context.run.cancellation_token = cancel_token
@@ -767,16 +745,16 @@ class Agent(
task.cancel()
def _parse_task_prompt(
self, prompt: PromptInput | Task | None
) -> tuple[Task | None, Any | None, list[Any], Any, list[Any]]:
"""解析输入意图,提取数据契约 (Task)"""
self, prompt: PromptInput | AgentTask | None
) -> tuple[AgentTask | None, Any | None, list[Any], Any, list[Any]]:
"""解析输入意图,提取数据契约 (AgentTask)"""
task_obj = None
final_prompt_payload = None
extra_tools = []
run_output_type = self.response_model
task_guardrails = []
if isinstance(prompt, Task):
if isinstance(prompt, AgentTask):
task_obj = prompt
if task_obj.response_model:
run_output_type = task_obj.response_model
@@ -803,7 +781,7 @@ class Agent(
async def on_state_init(
self,
prompt: PromptInput | Task | None = None,
prompt: PromptInput | AgentTask | None = None,
context: RunContext[AgentDepsT] | None = None,
config: AgentConfig | None = None,
cancellation_token: Any = None,
@@ -886,11 +864,6 @@ class Agent(
self, state: AgentState, resources: AgentRunResources
) -> None:
"""装配记忆与提示词上下文、解析可用工具集"""
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.flow.agent.engine.builders import (
AgentProfileResolver,
ContextBuilder,
)
context = resources.run_context
reader = resources.memory_reader
@@ -961,8 +934,6 @@ class Agent(
messages_for_run.extend(context.run.messages)
if final_prompt_payload is not None:
from zhenxun.services.ai.message_builder import MessageBuilder
if msgs := await MessageBuilder.normalize_to_llm_messages(
final_prompt_payload, bot=context.get_bot(), event=context.get_event()
):
@@ -986,8 +957,6 @@ class Agent(
self, state: AgentState, resources: AgentRunResources
) -> AgentRunResult[OutputDataT]:
"""真正调度大模型执行器并执行记忆落盘"""
from zhenxun.services.ai.flow.agent.engine.executor import StandardAgentExecutor
context = resources.run_context
for tk in resources.toolkits:
@@ -1027,7 +996,7 @@ class Agent(
async def _run_step(
self,
prompt: PromptInput | Task | None = None,
prompt: PromptInput | AgentTask | None = None,
*,
context: RunContext[AgentDepsT],
config: AgentConfig,
@@ -1046,8 +1015,6 @@ class Agent(
)
await self.on_context_build(state, resources)
from zhenxun.services.ai.capabilities import CombinedCapability
run_scoped_cap = (
resources.run_scoped_cap
if isinstance(resources.run_scoped_cap, CombinedCapability)
@@ -10,7 +10,8 @@ from zhenxun.services.ai.core.engine.structured_parser import (
from zhenxun.services.ai.core.exceptions import UpstreamServerException
from zhenxun.services.ai.core.messages import TaskLifecycleEvent
from zhenxun.services.ai.core.options import BaseOutputDefinition, ToolOutput
from zhenxun.services.ai.run import AgentRunResult, RunContext, Task
from zhenxun.services.ai.guardrails import parse_guardrails
from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext
from zhenxun.services.log import logger
@@ -33,8 +34,6 @@ class OutputValidationCapability(AbstractCapability):
self.output_type = output_type
self.raw_schema = raw_schema
from zhenxun.services.ai.guardrails import parse_guardrails
self.guardrails = parse_guardrails(guardrails)
self.processor = None
self.submit_tool = None
@@ -114,7 +113,7 @@ class OutputValidationCapability(AbstractCapability):
class TaskTrackingCapability(AbstractCapability):
"""数据契约任务状态追踪与事件遥测组件"""
def __init__(self, task: Task, agent_name: str):
def __init__(self, task: AgentTask, agent_name: str):
self.task = task
self.agent_name = agent_name
+109 -25
View File
@@ -5,19 +5,31 @@ from typing import Any, cast
from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.capabilities import (
AbstractCapability,
CombinedCapability,
DynamicCapability,
)
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
from zhenxun.services.ai.context.memory.engine import MemoryReader, MemoryWriter
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.core.messages import LLMMessage
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.core.messages import LLMMessage, TextPart
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.core.templates import PromptTemplate
from zhenxun.services.ai.flow.agent.capabilities import (
OutputValidationCapability,
TaskTrackingCapability,
)
from zhenxun.services.ai.flow.agent.models import Persona
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.run import GLOBAL_CAPABILITIES, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.tools.engine.registry import (
ToolCollection,
tool_provider_manager,
)
from zhenxun.services.ai.tools.models import GlobalToolFilter, ResolvedToolPayload
from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.utils.pydantic_compat import model_copy
@@ -28,8 +40,17 @@ class AgentProfileResolver:
def resolve_memory(
agent_memory_config: MemoryConfig, override_memory: Any | None
) -> MemoryConfig:
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
"""
解析并合并 Memory 记忆域的配置。
支持从外部覆盖配置并重新构建。
参数:
agent_memory_config: 预置的 Agent 默认记忆域配置对象。
override_memory: 运行时覆盖的记忆域配置,可为 dict, MemoryConfig 或其他合法结构。
返回:
MemoryConfig: 合并并生成的运行时记忆域配置实例。
""" # noqa: E501
if override_memory is not None:
return MemoryBuilder.resolve(override_memory)
return model_copy(agent_memory_config, deep=True)
@@ -40,6 +61,18 @@ class AgentProfileResolver:
cap_config: GenerationConfig | None,
profile_config: GenerationConfig | None,
) -> GenerationConfig:
"""
解析并合并多层 GenerationConfig 模型生成配置。
优先级顺序由低到高为:基础配置 -> 拦截器能力配置 -> 运行时 Profile 覆盖配置。
参数:
base_config: 基础的模型生成配置对象。
cap_config: 拦截器能力中提取出的模型生成参数配置。
profile_config: 运行时传入的 Profile 覆盖参数配置。
返回:
GenerationConfig: 合并多层配置后生成的最终运行时生成配置实例。
"""
final_gen_config = model_copy(base_config, deep=True)
if cap_config:
final_gen_config = final_gen_config.merge_with(cap_config)
@@ -49,7 +82,7 @@ class AgentProfileResolver:
class CapabilityBuilder:
"""拦截器能力组装器:负责合并 Agent, Task, Profile 和全局的中间件"""
"""拦截器能力组装器:负责合并 Agent, AgentTask, Profile 和全局的中间件"""
@staticmethod
async def build_for_run(
@@ -64,15 +97,25 @@ class CapabilityBuilder:
profile_capabilities: list | None,
context: RunContext,
) -> CombinedCapability:
from zhenxun.services.ai.capabilities import (
AbstractCapability,
DynamicCapability,
)
from zhenxun.services.ai.flow.agent.capabilities import (
OutputValidationCapability,
TaskTrackingCapability,
)
"""
为当前的 Agent 运行实例组装并实例化所有能力拦截器中间件。
整合全局能力、任务追踪、格式校验以及动态注入的能力。
参数:
agent_name: 执行推理的 Agent 标识名。
namespace: 会话所归属的命名空间。
output_type: 期待大模型返回的结构化 Pydantic 模型类型(支持 None 或 str)。
raw_schema: 原始结构化 Schema 定义字典。
agent_guardrails: Agent 自身定义的业务语义安全护栏列表。
task_guardrails: 本次任务定义的业务语义安全护栏列表。
task_obj: 被追踪的任务上下文对象实例。
agent_capabilities: Agent 定义的静态拦截器列表。
profile_capabilities: 运行时动态传入的能力或中间件列表。
context: 运行上下文对象实例。
返回:
CombinedCapability: 已经过运行初始化完毕的合并能力拦截器实例。
"""
dynamic_caps = []
combined_guardrails = agent_guardrails + task_guardrails
@@ -100,10 +143,6 @@ class CapabilityBuilder:
elif callable(cap):
run_level_caps.append(DynamicCapability(cap))
from zhenxun.services.ai.run import (
GLOBAL_CAPABILITIES,
)
base_caps = GLOBAL_CAPABILITIES.get("global", []).copy()
if namespace != "global" and namespace in GLOBAL_CAPABILITIES:
base_caps.extend(GLOBAL_CAPABILITIES[namespace])
@@ -129,8 +168,20 @@ class ContextBuilder:
run_scoped_cap: CombinedCapability,
persona: Persona | None = None,
) -> tuple[str, list[Any]]:
"""解析提示词,返回 (静态系统提示词文本, 动态独立消息列表) 元组"""
"""
解析、合并并渲染 Agent 的系统提示词和上下文记忆。
包含对依赖参数的动态注入和 Jinja 模板渲染。
参数:
instruction: 任务级别的初始指令或提示词模板。
system_prompts: 系统提示词生成函数(支持依赖注入)列表。
run_context: 运行上下文对象实例。
run_scoped_cap: 运行域下的合并能力中间件。
persona: 设定的 Agent 人设配置实例。
返回:
tuple[str, list[Any]]: 包含 (渲染后的静态系统提示词文本, 渲染后的动态消息列表) 的元组。
""" # noqa: E501
static_instructions = []
dynamic_messages = []
@@ -174,7 +225,7 @@ class ContextBuilder:
static_instructions.append("\n\n".join(persona_parts))
if instruction:
static_instructions.append("## 本次任务指令 (Task)")
static_instructions.append("## 本次任务指令 (AgentTask)")
if instruction:
if isinstance(instruction, PromptTemplate):
@@ -205,7 +256,6 @@ class ContextBuilder:
render_context.update(run_context.state)
rendered_dynamic_messages = []
from zhenxun.services.ai.core.messages import TextPart
for msg in dynamic_messages:
if msg.role == "system":
@@ -252,7 +302,22 @@ class ToolBuilder:
run_context: RunContext,
run_scoped_cap: CombinedCapability,
) -> ResolvedToolPayload:
"""解析、合并并过滤工具集"""
"""
解析并合并来自静态定义、动态函数依赖以及能力的工具列表。
通过工具提供者管理器完成工具的具体实例化及参数绑定。
参数:
tool_definitions: 静态工具或工具集合的定义列表。
toolset_funcs: 待依赖注入解析的工具集生成函数列表。
system_tools: 系统默认强制集成的工具定义列表。
namespace: 会话命名空间。
tool_filter: 全局的工具过滤条件,此处为占位。
run_context: 运行上下文对象实例.
run_scoped_cap: 运行域下的合并能力中间件,用于提供特定的能力工具。
返回:
ResolvedToolPayload: 解析完毕并附带依赖绑定关系的工具负载载体。
"""
defs_to_resolve = list(tool_definitions)
for ts_func in toolset_funcs:
@@ -304,7 +369,18 @@ class ToolBuilder:
tool_filters: list[Callable],
run_scoped_cap: CombinedCapability,
) -> ToolCollection:
"""处理生命周期:在工具发往执行器前,进行最终的 Schema 拦截和清洗"""
"""
在将工具发往模型执行器之前,触发最终的过滤器与能力拦截,进行 schema 的清洗。
参数:
effective_tools: 备选的工具执行实例列表。
context: 运行上下文对象实例。
tool_filters: 运行时自定义工具过滤与清洗函数列表。
run_scoped_cap: 运行域下的合并能力中间件,提供拦截入口。
返回:
ToolCollection: 准备就绪的、可直接发往模型的最终有效工具执行集。
"""
current_tool_defs = []
for t_exec in effective_tools:
if hasattr(t_exec, "get_definition"):
@@ -355,10 +431,18 @@ class SessionBuilder:
agent_name: str,
effective_memory: MemoryConfig,
) -> tuple[Any, Any, Any]:
from zhenxun.services.ai.context.memory.engine import MemoryReader, MemoryWriter
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.utils.scope import ScopeSelector
"""
根据当前用户、群组和平台标识,动态隔离前缀,计算并构建会话与记忆存储的读写门面。
参数:
context: 运行上下文对象实例。
namespace: 当前会话的命名空间。
agent_name: 执行推理的 Agent 标识名。
effective_memory: 运行时最终生效的 MemoryConfig 配置对象。
返回:
tuple[Any, Any, Any]: 包含 (SessionMetadata 会话元数据, MemoryReader 记忆读取器, MemoryWriter 记忆写入器) 的元组。
""" # noqa: E501
bot_id = None
bot_inst = context.get_bot()
if bot_inst and hasattr(bot_inst, "self_id"):
@@ -3,6 +3,7 @@ import asyncio
import json
from typing import Any, cast
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
from zhenxun.services.ai.core.engine.token_counter import (
parse_usage_info,
@@ -36,9 +37,12 @@ from zhenxun.services.ai.core.stream_events import (
)
from zhenxun.services.ai.flow.agent.engine.directive import (
DirectiveHandlerFunc,
directive_manager,
)
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.session import session_manager
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
@@ -279,9 +283,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
extra: dict[str, Any] | None = None,
cancellation_token: Any = None,
) -> ChatResponse:
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
request = ChatRequest(
messages=messages,
config=config,
@@ -315,8 +316,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
) -> AgentRunResult[Any]:
"""覆盖基类的模板方法,实现灵活的 while 控制流和 FOLLOW_UP 合并"""
await self.on_start(state, resources)
from zhenxun.services.ai.run.session import session_manager
session_info = await session_manager.get_or_create(
resources.run_context.session_id or "default_session"
)
@@ -388,9 +387,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
self, state: AgentState, resources: AgentRunResources
) -> None:
run_context = resources.run_context
from zhenxun.services.ai.run.session import session_manager
session_info = await session_manager.get_or_create(
run_context.session_id or "default_session"
)
@@ -499,8 +495,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
None,
)
if return_part:
from zhenxun.services.ai.tools.models import ToolResult
box["result"] = ToolResult(output=return_part.output)
else:
client_tool_calls.append(call)
@@ -606,8 +600,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
if not tool_calls or not tool_results:
return
from zhenxun.services.ai.flow.agent.engine.directive import directive_manager
for i, res_or_exc in enumerate(tool_results):
original_call = tool_calls[i]
tool_res = None
+1 -1
View File
@@ -165,7 +165,7 @@ class AgentRunResources(BaseModel):
memory_writer: Any | None = None
"""用于将对话历史安全落盘的写入器"""
run_scoped_cap: Any | None = None
"""聚合了 Agent/Task/全局 的复合能力拦截器 (CombinedCapability)"""
"""聚合了 Agent/AgentTask/全局 的复合能力拦截器 (CombinedCapability)"""
task_obj: Any | None = None
"""(如有) 解析后的结构化数据任务契约"""
toolkits: list[Any] = Field(default_factory=list)
+25 -14
View File
@@ -2,9 +2,16 @@ import asyncio
from contextlib import asynccontextmanager
from typing import Any
from zhenxun.services.ai.core.exceptions import ConcurrencyRejectException
from zhenxun.services.ai.core.exceptions import (
ConcurrencyRejectException,
InterventionHandledException,
)
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.flow.base import ConcurrencyPolicy
from zhenxun.services.ai.run.models import AgentTask
from zhenxun.services.ai.run.session import LockContext, session_manager
from zhenxun.services.log import logger
from .base import ConcurrencyPolicy, InterventionPolicy
@asynccontextmanager
@@ -16,8 +23,21 @@ async def apply_concurrency_policy(
intervention_policy: Any = None,
message: Any = None,
):
"""应用并发策略的中央调度上下文管理器"""
from zhenxun.services.ai.run.session import LockContext, session_manager
"""
异步上下文管理器:对大模型执行流应用特定的并发及消息干预调度策略。
负责请求互斥锁竞争、任务中断/拒绝处理,以及运行时用户实时指令的插队控制。
参数:
session_id: 当前会话的唯一标识,用于在会话管理器中隔离上下文。
lock_id: 当前锁域标识,决定了哪些 Agent 或任务使用同一套互斥锁竞争机制。
policy: 当发生并发锁占用时执行的策略(允许、拒绝、中断、排队)。
cancel_token: 运行时用于监听取消请求的取消令牌实例。
intervention_policy: 用户消息干预策略(转向、追加)。
message: 并发竞争发生时新入站的用户请求消息或 AgentTask 载荷。
返回:
AsyncGenerator: 返回异步生成器,供 async with 消费,包裹大模型的整个执行环节。
"""
current_task = asyncio.current_task()
task_tuple = (cancel_token, current_task)
@@ -30,22 +50,15 @@ async def apply_concurrency_policy(
lock_ctx = session_manager.lock_contexts.setdefault(lock_id, LockContext())
if exec_lock.locked():
from zhenxun.services.ai.flow.base import InterventionPolicy
if intervention_policy in (
InterventionPolicy.STEER,
InterventionPolicy.FOLLOW_UP,
):
from zhenxun.services.ai.core.exceptions import (
InterventionHandledException,
)
session = await session_manager.get_or_create(session_id)
actual_msg = message
from zhenxun.services.ai.run.models import Task
if isinstance(message, Task):
if isinstance(message, AgentTask):
actual_msg = message.description
elif hasattr(message, "extract_plain_text"):
actual_msg = message.extract_plain_text()
@@ -82,8 +95,6 @@ async def apply_concurrency_policy(
elif policy == ConcurrencyPolicy.QUEUE:
if exec_lock.locked():
from zhenxun.services.log import logger
logger.info(
f"⏳ [并发控制] 锁域 {lock_id} 被占用,"
"新请求已进入后台等待队列 (QUEUE)..."
+15 -1
View File
@@ -78,6 +78,7 @@ class ConcurrentCallAction(TeamAction):
"""
actions: list[CallAction]
"""并发执行的呼叫动作列表"""
class FinishAction(TeamAction):
@@ -108,13 +109,21 @@ class SubTaskRecord(BaseModel):
"""单条子任务(工单)数据契约"""
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
"""子任务的唯一标识 ID"""
title: str = ""
"""子任务标题"""
description: str = ""
"""子任务的具体描述"""
assignee: str | None = None
"""执行该任务的指派成员 Agent 名称"""
dependencies: list[str] = Field(default_factory=list)
"""该任务依赖的前置任务 ID 列表"""
status: TaskNodeStatus = TaskNodeStatus.pending
"""任务的当前执行状态"""
result: str | None = None
"""任务执行结果或输出信息"""
notes: list[str] = Field(default_factory=list)
"""执行过程中的追加备注或错误记录"""
metadata: dict[str, Any] = Field(default_factory=dict)
"""附加元数据,供系统底层或第三方插件挂载隐式上下文,对大模型不可见"""
@@ -126,8 +135,11 @@ class TaskBoardState(BaseModel):
"""
tasks: list[SubTaskRecord] = Field(default_factory=list)
"""看板中存储的所有子任务记录列表"""
is_goal_complete: bool = False
"""团队的终极目标是否已宣告完成"""
final_summary: str | None = None
"""目标完成后的终结总结报告"""
def create_task(
self,
@@ -151,6 +163,7 @@ class TaskBoardState(BaseModel):
return task
def get_task(self, task_id: str) -> SubTaskRecord | None:
"""根据 ID 或标题获取对应的子任务记录"""
return next(
(t for t in self.tasks if t.id == task_id or t.title == task_id), None
)
@@ -158,6 +171,7 @@ class TaskBoardState(BaseModel):
def update_task_status(
self, task_id: str, status: TaskNodeStatus, result: str | None = None
) -> SubTaskRecord | None:
"""更新指定任务的状态以及执行结果"""
task = self.get_task(task_id)
if not task:
return None
@@ -221,7 +235,7 @@ class TaskBoardState(BaseModel):
return available
def all_terminal(self) -> bool:
"""判断是否所有的任务都已经进入了终结状态(完成或失败)"""
"""检查是否所有的任务均已进入终结状态(已完成或已失败)"""
if not self.tasks:
return False
return all(
+9 -8
View File
@@ -9,7 +9,7 @@ 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 RunContext, Task
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.log import logger
@@ -22,7 +22,7 @@ class BaseRouter(ABC):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | Task | None = None,
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
"""核心路由方法"""
pass
@@ -46,11 +46,12 @@ class FunctionRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | Task | None = None,
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
sig = inspect.signature(self.selector_func)
call_kwargs = {"prompt": prompt, "context": context, "history": history}
if isinstance(prompt, Task):
if isinstance(prompt, AgentTask):
call_kwargs["agent_task"] = prompt
call_kwargs["task"] = prompt
kwargs_resolved = await DependencyInjector.resolve_all(
@@ -95,11 +96,11 @@ class RegexRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | Task | None = None,
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
text_to_match = (
prompt.description
if isinstance(prompt, Task)
if isinstance(prompt, AgentTask)
else (prompt or context.run.user_input or "")
)
@@ -125,7 +126,7 @@ class ChainRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | Task | None = None,
prompt: str | AgentTask | None = None,
) -> RouteDecision | None:
for router in self.routers:
decision = await router.route(context, history, prompt)
@@ -174,7 +175,7 @@ class LLMRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | Task | None = None,
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
+4 -1
View File
@@ -18,6 +18,7 @@ from zhenxun.services.ai.flow.team.strategy import BaseTeamStrategy
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_construct
class TeamRunner:
@@ -228,7 +229,9 @@ class TeamRunner:
logger.info(f"🏁 **团队 [{self.team.name}]** 协作圆满结束!")
if not isinstance(final_result, AgentRunResult):
final_result = AgentRunResult(output=final_result, usage=cumulative_usage)
final_result = model_construct(
AgentRunResult, output=final_result, usage=cumulative_usage
)
else:
final_result.usage += cumulative_usage
+34 -10
View File
@@ -17,7 +17,7 @@ from zhenxun.services.ai.flow.team.models import (
TeamAction,
)
from zhenxun.services.ai.flow.team.router import BaseRouter
from zhenxun.services.ai.run import RunContext, Task
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
from zhenxun.services.log import logger
@@ -44,7 +44,11 @@ class BaseTeamStrategy(ABC):
return PromptTemplate(template).render(**kwargs)
async def generate_plan(
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
self,
team: "Team",
prompt: str | AgentTask | None,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
"""
核心决策生成器 (Action Yielding Pattern)。
@@ -137,7 +141,11 @@ class RouteStrategy(BaseTeamStrategy):
self.state_flow = state_flow
async def generate_plan(
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
self,
team: "Team",
prompt: str | AgentTask | None,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
router = self.router
if not router:
@@ -266,7 +274,7 @@ class RouteStrategy(BaseTeamStrategy):
)
continue
yield FinishAction(result=run_result.output)
yield FinishAction(result=run_result)
break
@@ -297,7 +305,11 @@ class CoordinateStrategy(BaseTeamStrategy):
self.leader_tools = leader_tools or []
async def generate_plan(
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
self,
team: "Team",
prompt: str | AgentTask | None,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
delegation_tools = []
for m in team.members:
@@ -338,7 +350,7 @@ class CoordinateStrategy(BaseTeamStrategy):
leader_res = yield CallAction(agent=leader_agent, task=prompt)
yield FinishAction(result=leader_res.output)
yield FinishAction(result=leader_res)
class BroadcastStrategy(BaseTeamStrategy):
@@ -367,10 +379,14 @@ class BroadcastStrategy(BaseTeamStrategy):
self.leader_tools = leader_tools or []
async def generate_plan(
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
self,
team: "Team",
prompt: str | AgentTask | None,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
task_desc_str = (
prompt.description if isinstance(prompt, Task) else (prompt or "")
prompt.description if isinstance(prompt, AgentTask) else (prompt or "")
)
if context.run.event_bus:
@@ -413,7 +429,7 @@ class BroadcastStrategy(BaseTeamStrategy):
leader_res = yield CallAction(agent=leader_agent, task=synthesize_prompt)
yield FinishAction(result=leader_res.output)
yield FinishAction(result=leader_res)
class TaskStrategy(BaseTeamStrategy):
@@ -471,7 +487,11 @@ class TaskStrategy(BaseTeamStrategy):
self.leader_tools.append(self.bb_toolkit)
async def generate_plan(
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
self,
team: "Team",
prompt: str | AgentTask | None,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
from zhenxun.services.ai.flow.team.models import TaskBoardState, TaskNodeStatus
from zhenxun.services.ai.flow.team.task_tools import TaskPlanningToolkit
@@ -542,6 +562,10 @@ class TaskStrategy(BaseTeamStrategy):
goal_str = getattr(prompt, "description", None) or (
str(prompt) if prompt else ""
)
expected_out = getattr(prompt, "expected_output", None)
if expected_out:
goal_str += f"\n\n### 🎯 [预期产出要求]\n{expected_out}"
planner_prompt = f"""### 🎯 用户的终极目标 (Original Goal)
{goal_str}
+4 -4
View File
@@ -19,7 +19,7 @@ from zhenxun.services.ai.flow.team.router import BaseRouter
from zhenxun.services.ai.flow.team.strategy import (
BaseTeamStrategy,
)
from zhenxun.services.ai.run import AgentRunResult, RunContext, Task
from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.utils.utils import infer_plugin_namespace
@@ -239,7 +239,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
async def run(
self,
prompt: PromptInput | Task | None = None,
prompt: PromptInput | AgentTask | None = None,
*,
context: "RunContext | None" = None,
capabilities: list[CapabilitySource] | None = None,
@@ -250,7 +250,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
团队级运行阻塞核心入口,内部静默分配任务给成员直至汇总结束。
参数:
prompt: 派发给多智能体团队的任务描述 or 契约对象 (Task)。
prompt: 派发给多智能体团队的任务描述 or 契约对象 (AgentTask)。
context: 显式传入的会话与运行上下文。
capabilities: 仅针对本次团队执行动态注入的临时拦截器列表。
kwargs: 透传的其他附加参数。
@@ -278,7 +278,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
@contextlib.asynccontextmanager
async def run_stream(
self,
prompt: PromptInput | Task | None = None,
prompt: PromptInput | AgentTask | None = None,
*,
context: "RunContext | None" = None,
capabilities: list[CapabilitySource] | None = None,
@@ -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)