mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-18 02:30:08 +08:00
✨ 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Zhenxun AI - Flow (核心编排引擎)
|
||||
|
||||
提供大模型任务编排的三大范式:
|
||||
1. Agent: 基于动态工具调用的自主推理流。
|
||||
2. Team: 多智能体协同的群体决策流。
|
||||
3. Workflow: 基于图元/状态机的静态控制流。
|
||||
"""
|
||||
|
||||
from .agent.agent import Agent
|
||||
from .team.team import Team
|
||||
from .workflow.engine import Workflow
|
||||
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"Team",
|
||||
"Workflow",
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
from .agent import Agent
|
||||
from .models import (
|
||||
AgentConfig,
|
||||
Persona,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"AgentConfig",
|
||||
"Persona",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
import asyncio
|
||||
from typing import Any, Generic, cast
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot_plugin_alconna.uniseg import UniMessage
|
||||
|
||||
from zhenxun.services.ai.core.exceptions import (
|
||||
ConcurrencyInterruptException,
|
||||
ConcurrencyRejectException,
|
||||
ControlFlowExit,
|
||||
InterventionHandledException,
|
||||
)
|
||||
from zhenxun.services.ai.core.messages import UsageInfo
|
||||
from zhenxun.services.ai.flow.base import BaseRunnable
|
||||
from zhenxun.services.ai.run import AgentRunResult, RunContext
|
||||
from zhenxun.services.ai.run.models import AgentRunEnd, AgentRunError
|
||||
from zhenxun.services.ai.run.ui import UIController
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
T_Deps = TypeVar("T_Deps", default=Any)
|
||||
T_Out = TypeVar("T_Out", default=str)
|
||||
|
||||
|
||||
class AgentRunner(Generic[T_Out]):
|
||||
"""
|
||||
智能体运行器。
|
||||
负责将大模型的纯净数据流包装为平台交互动作(发消息、UI渲染)。
|
||||
自带 ContextVars 隐式上下文提取魔法。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
runnable: BaseRunnable,
|
||||
context: RunContext | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.runnable = runnable
|
||||
self.context = context or RunContext(**kwargs)
|
||||
|
||||
is_stateless = (
|
||||
getattr(self.runnable.runtime_config, "stateless", True)
|
||||
if hasattr(self.runnable, "runtime_config")
|
||||
else True
|
||||
)
|
||||
if is_stateless and self.context.session_id:
|
||||
if not self.context.session_id.startswith("stateless_"):
|
||||
import uuid
|
||||
|
||||
self.context.session_id = (
|
||||
f"stateless_{self.context.session_id}_{uuid.uuid4().hex[:8]}"
|
||||
)
|
||||
if self.context.session:
|
||||
self.context.session.session_id = self.context.session_id
|
||||
|
||||
@property
|
||||
def _bot(self) -> Bot | None:
|
||||
return self.context.get_bot()
|
||||
|
||||
@property
|
||||
def _event(self) -> Event | None:
|
||||
return self.context.get_event()
|
||||
|
||||
async def reply(
|
||||
self, prompt: Any = None, reply_to: bool = False, **kwargs: Any
|
||||
) -> AgentRunResult[T_Out]:
|
||||
"""交互式执行:将 Agent 运行过程中的工具调用状态和最终结果自动发送给用户。"""
|
||||
final_result = None
|
||||
|
||||
profile = kwargs.pop("profile", None)
|
||||
|
||||
try:
|
||||
async with self.runnable.run_stream(
|
||||
prompt=prompt,
|
||||
context=self.context,
|
||||
profile=profile,
|
||||
**kwargs,
|
||||
) as stream_result:
|
||||
async for stream_event in stream_result.stream_events():
|
||||
if isinstance(stream_event, AgentRunEnd):
|
||||
final_result = stream_event.result
|
||||
|
||||
elif isinstance(stream_event, AgentRunError):
|
||||
raise stream_event.error
|
||||
|
||||
except ControlFlowExit as e:
|
||||
if isinstance(e, InterventionHandledException):
|
||||
logger.info(f"✨ {self.runnable.name} 触发运行时干预: {e.message}")
|
||||
if e.display_content and self._bot and self._event:
|
||||
await MessageUtils.build_message(str(e.display_content)).send(
|
||||
reply_to=reply_to
|
||||
)
|
||||
return cast(
|
||||
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
|
||||
)
|
||||
|
||||
if isinstance(e, ConcurrencyRejectException):
|
||||
logger.warning(
|
||||
f"⏳ {self.runnable.name} 触发并发拒绝 (REJECT): {e.message}"
|
||||
)
|
||||
return cast(
|
||||
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
|
||||
)
|
||||
|
||||
if isinstance(e, ConcurrencyInterruptException):
|
||||
logger.warning(
|
||||
f"🛑 {self.runnable.name} 触发并发中断 (INTERRUPT): {e.message}"
|
||||
)
|
||||
return cast(
|
||||
AgentRunResult[T_Out], AgentRunResult(output="", usage=UsageInfo())
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"{self.runnable.name} 控制流正常中断: {type(e).__name__} - {e}"
|
||||
)
|
||||
await UIController.handle_control_flow_exit_display(
|
||||
e, self.context, reply_to
|
||||
)
|
||||
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.runnable.name} 运行失败: {e}", e=e)
|
||||
if self._bot and self._event:
|
||||
await MessageUtils.build_message(f"❌ 运行发生错误: {e}").send()
|
||||
raise e
|
||||
|
||||
if final_result and final_result.output and self._bot and self._event:
|
||||
if isinstance(final_result.output, UniMessage):
|
||||
await final_result.output.send(
|
||||
self._event, bot=self._bot, reply_to=reply_to
|
||||
)
|
||||
final_result.output = final_result.output.extract_plain_text()
|
||||
else:
|
||||
final_msg = str(final_result.output)
|
||||
await MessageUtils.build_message(final_msg).send()
|
||||
|
||||
if final_result is None:
|
||||
raise RuntimeError("智能体运行流异常结束:未返回最终结果。")
|
||||
|
||||
return cast(AgentRunResult[T_Out], final_result)
|
||||
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
from typing import Any, cast
|
||||
|
||||
from zhenxun.services.ai.capabilities import AbstractCapability
|
||||
from zhenxun.services.ai.capabilities.base import CapabilityOrdering
|
||||
from zhenxun.services.ai.core.engine.structured_parser import (
|
||||
BaseOutputProcessor,
|
||||
SubmitFinalResultExecutable,
|
||||
)
|
||||
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.log import logger
|
||||
|
||||
|
||||
class OutputValidationCapability(AbstractCapability):
|
||||
"""输出拦截与校验能力组件 (支持纯文本及结构化护栏)"""
|
||||
|
||||
def get_ordering(self) -> Any:
|
||||
from zhenxun.services.ai.capabilities.builtin import (
|
||||
ReflexionCapability,
|
||||
)
|
||||
|
||||
return CapabilityOrdering(wraps=[ReflexionCapability])
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
output_type: Any | None = None,
|
||||
guardrails: list[Any] | None = None,
|
||||
raw_schema: dict[str, Any] | None = None,
|
||||
):
|
||||
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
|
||||
|
||||
if self.output_type is not None:
|
||||
if isinstance(self.output_type, BaseOutputDefinition):
|
||||
out_type = self.output_type.type_
|
||||
tool_name_override = (
|
||||
self.output_type.name
|
||||
if isinstance(self.output_type, ToolOutput)
|
||||
else None
|
||||
)
|
||||
else:
|
||||
out_type = cast(type[Any], self.output_type)
|
||||
tool_name_override = None
|
||||
|
||||
self.processor = BaseOutputProcessor(
|
||||
response_model=out_type,
|
||||
)
|
||||
self.submit_tool = SubmitFinalResultExecutable(
|
||||
self.processor, self.guardrails
|
||||
)
|
||||
if tool_name_override:
|
||||
self.submit_tool.name = tool_name_override
|
||||
elif self.raw_schema is not None:
|
||||
self.processor = BaseOutputProcessor(
|
||||
response_model=None,
|
||||
raw_schema=self.raw_schema,
|
||||
)
|
||||
self.submit_tool = SubmitFinalResultExecutable(
|
||||
self.processor, self.guardrails
|
||||
)
|
||||
|
||||
async def get_system_prompts(self, context: RunContext) -> list[str]:
|
||||
"""动态注入结构化要求提示词"""
|
||||
if self.submit_tool:
|
||||
return [
|
||||
"### ⚠️ [核心任务:结构化输出要求]\n"
|
||||
"当前任务处于严格的 **结构化输出模式**。\n"
|
||||
"当你完成所有调查和思考后,必须且只能调用 "
|
||||
f"`{self.submit_tool.name}` 工具来提交最终结果,"
|
||||
"禁止用纯文本直接作答。\n"
|
||||
"(📌 提示:最终需要返回的数据结构要求,"
|
||||
"请严格查阅并遵循 "
|
||||
f"`{self.submit_tool.name}` 工具的参数 Schema 定义,"
|
||||
"将其视为唯一的数据约束)"
|
||||
]
|
||||
return []
|
||||
|
||||
async def get_tools(self, context: RunContext) -> list[Any]:
|
||||
"""动态挂载提交最终结果的工具"""
|
||||
if self.submit_tool:
|
||||
return [self.submit_tool]
|
||||
return []
|
||||
|
||||
async def wrap_model_request(self, context, llm_context, handler):
|
||||
"""将 Processor 和 Guardrails 传给底层的 IvrCapability"""
|
||||
llm_context.request.extra["output_processor"] = self.processor
|
||||
llm_context.request.extra["guardrails"] = self.guardrails
|
||||
return await handler(llm_context)
|
||||
|
||||
async def wrap_run(self, context: RunContext, handler: Any) -> AgentRunResult[Any]:
|
||||
"""运行结束后,校验是否成功提取了结构化数据"""
|
||||
result = await handler()
|
||||
if self.output_type is not None or self.raw_schema is not None:
|
||||
if result.structured_data is not None:
|
||||
result.output = result.structured_data
|
||||
else:
|
||||
tool_name = self.submit_tool.name if self.submit_tool else "unknown"
|
||||
logger.error(f"Agent 未能调用 {tool_name} 提交结构化数据。")
|
||||
raise UpstreamServerException(
|
||||
"模型未能输出符合要求的结构化数据。",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class TaskTrackingCapability(AbstractCapability):
|
||||
"""数据契约任务状态追踪与事件遥测组件"""
|
||||
|
||||
def __init__(self, task: Task, agent_name: str):
|
||||
self.task = task
|
||||
self.agent_name = agent_name
|
||||
|
||||
async def wrap_run(self, context: RunContext, handler: Any) -> AgentRunResult[Any]:
|
||||
"""任务生命周期追踪"""
|
||||
|
||||
task_name = self.task.name or self.task.id[:8]
|
||||
logger.debug(f"📋 **开始任务**: `{task_name}` (由 {self.agent_name} 执行)")
|
||||
context.run.add_event(TaskLifecycleEvent(task_name=task_name, action="start"))
|
||||
try:
|
||||
result = await handler()
|
||||
logger.debug(f"✅ **任务完成**: `{task_name}`")
|
||||
context.run.add_event(
|
||||
TaskLifecycleEvent(task_name=task_name, action="complete")
|
||||
)
|
||||
return result
|
||||
except asyncio.CancelledError as e:
|
||||
logger.warning(f"⚠️ **任务被强制取消**: `{task_name}`")
|
||||
context.run.add_event(
|
||||
TaskLifecycleEvent(
|
||||
task_name=task_name,
|
||||
action="fail",
|
||||
error_msg="任务执行被中止或取消",
|
||||
)
|
||||
)
|
||||
raise e
|
||||
except BaseException as error:
|
||||
event_error = (
|
||||
error if isinstance(error, Exception) else Exception(str(error))
|
||||
)
|
||||
logger.error(f"❌ **任务失败**: `{task_name}` - {event_error}")
|
||||
context.run.add_event(
|
||||
TaskLifecycleEvent(
|
||||
task_name=task_name,
|
||||
action="fail",
|
||||
error_msg=str(event_error),
|
||||
)
|
||||
)
|
||||
raise error
|
||||
@@ -0,0 +1,20 @@
|
||||
from .builders import (
|
||||
AgentProfileResolver,
|
||||
CapabilityBuilder,
|
||||
ContextBuilder,
|
||||
ToolBuilder,
|
||||
)
|
||||
from .directive import DirectiveManager, directive, directive_manager
|
||||
from .executor import BaseAgentExecutor, StandardAgentExecutor
|
||||
|
||||
__all__ = [
|
||||
"AgentProfileResolver",
|
||||
"BaseAgentExecutor",
|
||||
"CapabilityBuilder",
|
||||
"ContextBuilder",
|
||||
"DirectiveManager",
|
||||
"StandardAgentExecutor",
|
||||
"ToolBuilder",
|
||||
"directive",
|
||||
"directive_manager",
|
||||
]
|
||||
@@ -0,0 +1,434 @@
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
import inspect
|
||||
from typing import Any, cast
|
||||
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
|
||||
from zhenxun.services.ai.capabilities import CombinedCapability
|
||||
from zhenxun.services.ai.context.memory.models import MemoryConfig
|
||||
from zhenxun.services.ai.core.messages import LLMMessage
|
||||
from zhenxun.services.ai.core.options import GenerationConfig
|
||||
from zhenxun.services.ai.core.templates import PromptTemplate
|
||||
from zhenxun.services.ai.flow.agent.models import Persona
|
||||
from zhenxun.services.ai.run import 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.utils.pydantic_compat import model_copy
|
||||
|
||||
|
||||
class AgentProfileResolver:
|
||||
"""Agent 配置解析器:负责提取与合并 Agent 的运行时 Profile"""
|
||||
|
||||
@staticmethod
|
||||
def resolve_memory(
|
||||
agent_memory_config: MemoryConfig, override_memory: Any | None
|
||||
) -> MemoryConfig:
|
||||
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
|
||||
|
||||
if override_memory is not None:
|
||||
return MemoryBuilder.resolve(override_memory)
|
||||
return model_copy(agent_memory_config, deep=True)
|
||||
|
||||
@staticmethod
|
||||
def resolve_generation_config(
|
||||
base_config: GenerationConfig,
|
||||
cap_config: GenerationConfig | None,
|
||||
profile_config: GenerationConfig | None,
|
||||
) -> GenerationConfig:
|
||||
final_gen_config = model_copy(base_config, deep=True)
|
||||
if cap_config:
|
||||
final_gen_config = final_gen_config.merge_with(cap_config)
|
||||
if profile_config:
|
||||
final_gen_config = final_gen_config.merge_with(profile_config)
|
||||
return final_gen_config
|
||||
|
||||
|
||||
class CapabilityBuilder:
|
||||
"""拦截器能力组装器:负责合并 Agent, Task, Profile 和全局的中间件"""
|
||||
|
||||
@staticmethod
|
||||
async def build_for_run(
|
||||
agent_name: str,
|
||||
namespace: str,
|
||||
output_type: Any | None,
|
||||
raw_schema: dict | None,
|
||||
agent_guardrails: list,
|
||||
task_guardrails: list,
|
||||
task_obj: Any | None,
|
||||
agent_capabilities: list,
|
||||
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,
|
||||
)
|
||||
|
||||
dynamic_caps = []
|
||||
combined_guardrails = agent_guardrails + task_guardrails
|
||||
|
||||
if output_type is not None and output_type is not str:
|
||||
dynamic_caps.append(
|
||||
OutputValidationCapability(output_type, combined_guardrails)
|
||||
)
|
||||
elif raw_schema is not None:
|
||||
dynamic_caps.append(
|
||||
OutputValidationCapability(
|
||||
None, combined_guardrails, raw_schema=raw_schema
|
||||
)
|
||||
)
|
||||
elif combined_guardrails:
|
||||
dynamic_caps.append(OutputValidationCapability(None, combined_guardrails))
|
||||
|
||||
if task_obj:
|
||||
dynamic_caps.append(TaskTrackingCapability(task_obj, agent_name))
|
||||
|
||||
run_level_caps = []
|
||||
if profile_capabilities:
|
||||
for cap in profile_capabilities:
|
||||
if isinstance(cap, AbstractCapability):
|
||||
run_level_caps.append(cap)
|
||||
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])
|
||||
|
||||
combined_cap = CombinedCapability(
|
||||
base_caps
|
||||
+ getattr(context, "capabilities", [])
|
||||
+ agent_capabilities
|
||||
+ run_level_caps
|
||||
+ dynamic_caps
|
||||
)
|
||||
return cast(CombinedCapability, await combined_cap.for_run(context))
|
||||
|
||||
|
||||
class ContextBuilder:
|
||||
"""系统提示词与上下文记忆构建器"""
|
||||
|
||||
@staticmethod
|
||||
async def build_prompts(
|
||||
instruction: str | PromptTemplate,
|
||||
system_prompts: list[Any],
|
||||
run_context: RunContext,
|
||||
run_scoped_cap: CombinedCapability,
|
||||
persona: Persona | None = None,
|
||||
) -> tuple[str, list[Any]]:
|
||||
"""解析提示词,返回 (静态系统提示词文本, 动态独立消息列表) 元组"""
|
||||
|
||||
static_instructions = []
|
||||
dynamic_messages = []
|
||||
|
||||
for sp_func in system_prompts:
|
||||
sig = inspect.signature(sp_func)
|
||||
if len(sig.parameters) > 0:
|
||||
injected_kwargs = await DependencyInjector.resolve_all(
|
||||
sig=sig,
|
||||
call_kwargs={},
|
||||
context=run_context,
|
||||
)
|
||||
res = (
|
||||
(await sp_func(**injected_kwargs))
|
||||
if is_coroutine_callable(sp_func)
|
||||
else sp_func(**injected_kwargs)
|
||||
)
|
||||
else:
|
||||
res = (await sp_func()) if is_coroutine_callable(sp_func) else sp_func()
|
||||
if res:
|
||||
if isinstance(res, LLMMessage):
|
||||
dynamic_messages.append(res)
|
||||
elif isinstance(res, list) and all(
|
||||
isinstance(m, LLMMessage) for m in res
|
||||
):
|
||||
dynamic_messages.extend(res)
|
||||
else:
|
||||
if isinstance(res, list):
|
||||
for item in res:
|
||||
if item:
|
||||
dynamic_messages.append(LLMMessage.system(str(item)))
|
||||
else:
|
||||
dynamic_messages.append(LLMMessage.system(str(res)))
|
||||
|
||||
if persona:
|
||||
persona_parts = [
|
||||
f"## 扮演角色 (Role)\n{persona.role}",
|
||||
f"## 核心目标 (Goal)\n{persona.goal}",
|
||||
]
|
||||
if persona.backstory:
|
||||
persona_parts.append(f"## 角色背景 (Backstory)\n{persona.backstory}")
|
||||
static_instructions.append("\n\n".join(persona_parts))
|
||||
|
||||
if instruction:
|
||||
static_instructions.append("## 本次任务指令 (Task)")
|
||||
|
||||
if instruction:
|
||||
if isinstance(instruction, PromptTemplate):
|
||||
static_instructions.append(instruction.format_with_context(run_context))
|
||||
else:
|
||||
static_instructions.append(str(instruction))
|
||||
|
||||
caps = (
|
||||
run_scoped_cap.capabilities
|
||||
if run_scoped_cap
|
||||
else getattr(run_context, "capabilities", [])
|
||||
)
|
||||
for cap in caps:
|
||||
cap_prompts = await cap.get_system_prompts(run_context)
|
||||
for prompt_text in cap_prompts:
|
||||
if prompt_text and prompt_text.strip():
|
||||
dynamic_messages.append(LLMMessage.system(prompt_text))
|
||||
|
||||
static_text = "\n\n".join(static_instructions)
|
||||
|
||||
render_context = {
|
||||
"deps": run_context.deps,
|
||||
"bot": getattr(run_context.deps, "bot", None),
|
||||
"event": getattr(run_context.deps, "event", None),
|
||||
"matcher": getattr(run_context.deps, "matcher", None),
|
||||
}
|
||||
if run_context.state:
|
||||
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":
|
||||
new_content = []
|
||||
changed = False
|
||||
for part in msg.content:
|
||||
if isinstance(part, TextPart) and part.text:
|
||||
try:
|
||||
rendered_text = PromptTemplate(part.text).render(
|
||||
**render_context
|
||||
)
|
||||
new_content.append(TextPart(text=rendered_text))
|
||||
if rendered_text != part.text:
|
||||
changed = True
|
||||
except Exception:
|
||||
new_content.append(part)
|
||||
else:
|
||||
new_content.append(part)
|
||||
if changed:
|
||||
new_msg = msg.model_copy(deep=True)
|
||||
new_msg.content = new_content
|
||||
rendered_dynamic_messages.append(new_msg)
|
||||
else:
|
||||
rendered_dynamic_messages.append(msg)
|
||||
else:
|
||||
rendered_dynamic_messages.append(msg)
|
||||
|
||||
return (
|
||||
PromptTemplate(static_text).render(**render_context),
|
||||
rendered_dynamic_messages,
|
||||
)
|
||||
|
||||
|
||||
class ToolBuilder:
|
||||
"""系统工具集合解析与构建器"""
|
||||
|
||||
@staticmethod
|
||||
async def resolve_tools(
|
||||
tool_definitions: list[Any],
|
||||
toolset_funcs: list[Any],
|
||||
system_tools: list[Any],
|
||||
namespace: str,
|
||||
tool_filter: GlobalToolFilter | None,
|
||||
run_context: RunContext,
|
||||
run_scoped_cap: CombinedCapability,
|
||||
) -> ResolvedToolPayload:
|
||||
"""解析、合并并过滤工具集"""
|
||||
defs_to_resolve = list(tool_definitions)
|
||||
|
||||
for ts_func in toolset_funcs:
|
||||
sig = inspect.signature(ts_func)
|
||||
injected_kwargs = {}
|
||||
if len(sig.parameters) > 0:
|
||||
injected_kwargs = await DependencyInjector.resolve_all(
|
||||
sig=sig,
|
||||
call_kwargs={},
|
||||
context=run_context,
|
||||
)
|
||||
|
||||
res = (
|
||||
(await ts_func(**injected_kwargs))
|
||||
if is_coroutine_callable(ts_func)
|
||||
else ts_func(**injected_kwargs)
|
||||
)
|
||||
|
||||
if res is not None:
|
||||
if isinstance(res, list):
|
||||
defs_to_resolve.extend(res)
|
||||
else:
|
||||
defs_to_resolve.append(res)
|
||||
|
||||
if system_tools:
|
||||
for st in system_tools:
|
||||
if st not in defs_to_resolve:
|
||||
defs_to_resolve.append(st)
|
||||
|
||||
caps = (
|
||||
run_scoped_cap.capabilities
|
||||
if run_scoped_cap
|
||||
else getattr(run_context, "capabilities", [])
|
||||
)
|
||||
for cap in caps:
|
||||
cap_tools = await cap.get_tools(run_context)
|
||||
defs_to_resolve.extend(cap_tools)
|
||||
|
||||
payload = await tool_provider_manager.resolve_tools(
|
||||
defs_to_resolve, namespace, context=run_context
|
||||
)
|
||||
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
async def prepare_effective_tools(
|
||||
effective_tools: list[Any],
|
||||
context: RunContext,
|
||||
tool_filters: list[Callable],
|
||||
run_scoped_cap: CombinedCapability,
|
||||
) -> ToolCollection:
|
||||
"""处理生命周期:在工具发往执行器前,进行最终的 Schema 拦截和清洗"""
|
||||
current_tool_defs = []
|
||||
for t_exec in effective_tools:
|
||||
if hasattr(t_exec, "get_definition"):
|
||||
t_def = await t_exec.get_definition(context)
|
||||
if t_def:
|
||||
current_tool_defs.append(t_def)
|
||||
|
||||
if tool_filters:
|
||||
for filter_func in tool_filters:
|
||||
sig = inspect.signature(filter_func)
|
||||
call_kwargs = {"tool_defs": current_tool_defs}
|
||||
resolved_kwargs = await DependencyInjector.resolve_all(
|
||||
sig, call_kwargs, context
|
||||
)
|
||||
filtered_kwargs = {
|
||||
k: v for k, v in resolved_kwargs.items() if k in sig.parameters
|
||||
}
|
||||
_res = (
|
||||
await filter_func(**filtered_kwargs)
|
||||
if is_coroutine_callable(filter_func)
|
||||
else filter_func(**filtered_kwargs)
|
||||
)
|
||||
if _res is not None:
|
||||
current_tool_defs = list(_res)
|
||||
|
||||
_cap_res = await run_scoped_cap.prepare_tools(context, current_tool_defs)
|
||||
if _cap_res is not None:
|
||||
current_tool_defs = list(_cap_res)
|
||||
|
||||
final_defs_map = {d.name.lower(): d for d in current_tool_defs if d}
|
||||
final_effective_tools = ToolCollection()
|
||||
for t_exec in effective_tools:
|
||||
t_name = getattr(t_exec, "name", "unknown")
|
||||
if t_name.lower() in final_defs_map:
|
||||
cloned_tool = copy.copy(t_exec)
|
||||
cloned_tool._dynamic_def = final_defs_map[t_name.lower()]
|
||||
final_effective_tools.append(cloned_tool)
|
||||
return final_effective_tools
|
||||
|
||||
|
||||
class SessionBuilder:
|
||||
"""会话与记忆域构建器:负责隔离前缀计算和读写门面装配"""
|
||||
|
||||
@staticmethod
|
||||
def build_session_and_memory(
|
||||
context: RunContext,
|
||||
namespace: str,
|
||||
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
|
||||
|
||||
bot_id = None
|
||||
bot_inst = context.get_bot()
|
||||
if bot_inst and hasattr(bot_inst, "self_id"):
|
||||
bot_id = str(bot_inst.self_id)
|
||||
|
||||
selector = ScopeSelector(
|
||||
user_id=context.get_user_id(),
|
||||
group_id=context.get_group_id(),
|
||||
platform=context.get_platform(),
|
||||
bot_id=bot_id,
|
||||
namespace=namespace,
|
||||
agent_name=agent_name,
|
||||
)
|
||||
|
||||
all_scopes = {"/"}
|
||||
scope_name_mapping = {}
|
||||
|
||||
if effective_memory.short_term and effective_memory.short_term.isolation:
|
||||
sel = effective_memory.short_term.isolation.resolve(
|
||||
deps=context.deps,
|
||||
prefix="",
|
||||
default_namespace=namespace,
|
||||
default_agent=agent_name,
|
||||
)
|
||||
all_scopes.add(sel.scope_prefix)
|
||||
|
||||
for config_part in [effective_memory.slots, effective_memory.long_term]:
|
||||
if config_part and hasattr(config_part, "scopes") and config_part.scopes:
|
||||
for name, builder in config_part.scopes.items():
|
||||
sel = builder.resolve(
|
||||
deps=context.deps,
|
||||
prefix="",
|
||||
default_namespace=namespace,
|
||||
default_agent=agent_name,
|
||||
)
|
||||
all_scopes.add(sel.scope_prefix)
|
||||
scope_name_mapping[sel.scope_prefix] = name
|
||||
|
||||
parts = selector.get_scope_parts()
|
||||
for i in range(len(parts)):
|
||||
all_scopes.add("/" + "/".join(parts[: i + 1]))
|
||||
|
||||
accessible_scopes = sorted(all_scopes, key=lambda x: len(x.split("/")))
|
||||
|
||||
short_term_builder = (
|
||||
effective_memory.short_term.isolation
|
||||
if effective_memory.short_term
|
||||
else effective_memory.base_isolation
|
||||
)
|
||||
short_term_selector = short_term_builder.resolve(
|
||||
deps=context.deps,
|
||||
prefix="",
|
||||
default_namespace=namespace,
|
||||
default_agent=agent_name,
|
||||
)
|
||||
|
||||
session_metadata = SessionMetadata(
|
||||
session_id=short_term_selector.scope_prefix,
|
||||
selector=selector,
|
||||
scope_prefix=selector.scope_prefix,
|
||||
accessible_scopes=accessible_scopes,
|
||||
scope_name_mapping=scope_name_mapping,
|
||||
)
|
||||
reader = MemoryReader(
|
||||
session_meta=session_metadata, memory_config=effective_memory
|
||||
)
|
||||
writer = MemoryWriter(
|
||||
session_meta=session_metadata,
|
||||
memory_config=effective_memory,
|
||||
context=context,
|
||||
)
|
||||
|
||||
return session_metadata, reader, writer
|
||||
@@ -0,0 +1,115 @@
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
|
||||
from zhenxun.services.ai.run.models import AgentRunResult, HandoffPayload
|
||||
from zhenxun.services.ai.tools.models import ToolResult
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.pydantic_compat import model_construct
|
||||
from zhenxun.utils.utils import infer_plugin_namespace
|
||||
|
||||
DirectiveHandlerFunc = Callable[
|
||||
[AgentState, AgentRunResources, ToolResult], Awaitable[None]
|
||||
]
|
||||
|
||||
|
||||
class DirectiveManager:
|
||||
"""工具指令路由注册中心"""
|
||||
|
||||
def __init__(self):
|
||||
self._handlers: dict[str, dict[str, DirectiveHandlerFunc]] = defaultdict(dict)
|
||||
|
||||
def register(
|
||||
self, name: str, handler: DirectiveHandlerFunc, namespace: str = "global"
|
||||
) -> None:
|
||||
self._handlers[namespace][name] = handler
|
||||
logger.debug(f"已注册工具副作用指令: '{name}' -> Namespace: '{namespace}'")
|
||||
|
||||
def get_handler(
|
||||
self, name: str, namespace: str = "global"
|
||||
) -> DirectiveHandlerFunc | None:
|
||||
"""优先从指定 namespace 找,找不到回退到 global"""
|
||||
ns_dict = self._handlers.get(namespace, {})
|
||||
if name in ns_dict:
|
||||
return ns_dict[name]
|
||||
return self._handlers.get("global", {}).get(name)
|
||||
|
||||
|
||||
directive_manager = DirectiveManager()
|
||||
|
||||
|
||||
def directive(name: str | None = None, namespace: str | None = None):
|
||||
"""
|
||||
注册一个自定义工具副作用指令处理器的装饰器。
|
||||
|
||||
参数:
|
||||
name: 指令的名称,如果不填则默认使用被装饰的函数名。
|
||||
namespace: 插件命名空间,如果不填则基于代码调用栈自动推断。
|
||||
"""
|
||||
|
||||
def decorator(func: DirectiveHandlerFunc):
|
||||
dir_name = name or func.__name__
|
||||
ns = namespace if namespace is not None else infer_plugin_namespace()
|
||||
directive_manager.register(dir_name, func, ns)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@directive("submit_structured", namespace="global")
|
||||
async def handle_submit_structured(
|
||||
state: AgentState, resources: AgentRunResources, tool_res: ToolResult
|
||||
) -> None:
|
||||
parsed_obj = (
|
||||
tool_res.directive.payload.get("parsed_obj") if tool_res.directive else None
|
||||
)
|
||||
logger.info("✅ 拦截到结构化结果提交,结束循环。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=None,
|
||||
messages=state.messages,
|
||||
structured_data=parsed_obj,
|
||||
usage=state.usage,
|
||||
)
|
||||
|
||||
|
||||
@directive("end_run", namespace="global")
|
||||
async def handle_end_run(
|
||||
state: AgentState, resources: AgentRunResources, tool_res: ToolResult
|
||||
) -> None:
|
||||
output = (
|
||||
tool_res.directive.payload.get("output", tool_res.output)
|
||||
if tool_res.directive
|
||||
else tool_res.output
|
||||
)
|
||||
logger.debug("✅ 捕获到工具发出的终止信号,提前结束推理循环。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=output,
|
||||
messages=state.messages,
|
||||
usage=state.usage,
|
||||
)
|
||||
|
||||
|
||||
@directive("handoff", namespace="global")
|
||||
async def handle_handoff(
|
||||
state: AgentState, resources: AgentRunResources, tool_res: ToolResult
|
||||
) -> None:
|
||||
payload = tool_res.directive.payload if tool_res.directive else {}
|
||||
handoff = HandoffPayload(
|
||||
target=payload.get("target", "unknown"),
|
||||
reason=payload.get("reason", ""),
|
||||
context_data=payload.get("context_data", ""),
|
||||
)
|
||||
output_text = f"已触发控制权移交 -> {handoff.target}。原因: {handoff.reason}"
|
||||
logger.info(f"✅ 拦截到移交(Handoff)信号: 移交给 -> {handoff.target}。结束循环。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=output_text,
|
||||
messages=state.messages,
|
||||
usage=state.usage,
|
||||
handoff=handoff,
|
||||
)
|
||||
@@ -0,0 +1,691 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, cast
|
||||
|
||||
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
|
||||
from zhenxun.services.ai.core.engine.token_counter import (
|
||||
parse_usage_info,
|
||||
token_counter,
|
||||
)
|
||||
from zhenxun.services.ai.core.exceptions import (
|
||||
ControlFlowExit,
|
||||
UpstreamServerException,
|
||||
)
|
||||
from zhenxun.services.ai.core.messages import (
|
||||
AgentMessage,
|
||||
AssistantContentUnion,
|
||||
AssistantMessage,
|
||||
AudioPart,
|
||||
ChatRequest,
|
||||
ChatResponse,
|
||||
FilePart,
|
||||
ImagePart,
|
||||
LLMMessage,
|
||||
TextPart,
|
||||
ToolCallPart,
|
||||
ToolReturnPart,
|
||||
VideoPart,
|
||||
)
|
||||
from zhenxun.services.ai.core.models import LLMContext
|
||||
from zhenxun.services.ai.core.options import GenerationConfig
|
||||
from zhenxun.services.ai.core.stream_events import (
|
||||
LLMEndEvent,
|
||||
LLMStartEvent,
|
||||
ToolStreamChunkEvent,
|
||||
)
|
||||
from zhenxun.services.ai.flow.agent.engine.directive import (
|
||||
DirectiveHandlerFunc,
|
||||
)
|
||||
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
|
||||
from zhenxun.services.ai.run import AgentRunResult, RunContext
|
||||
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
|
||||
from zhenxun.services.ai.tools.models import ToolResult
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
|
||||
|
||||
|
||||
class BaseAgentExecutor(ABC):
|
||||
"""
|
||||
Agent 核心执行器基类 (Template Method Pattern)。
|
||||
定义了基于生命周期的大模型控制流。第三方开发者可通过重写特定钩子,
|
||||
"""
|
||||
|
||||
async def run(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> AgentRunResult[Any]:
|
||||
"""
|
||||
核心模板方法 (Template Method)。
|
||||
组织整个大模型推导与工具调用的生命周期循环。如无必要,请勿重写此方法。
|
||||
"""
|
||||
await self.on_start(state, resources)
|
||||
|
||||
try:
|
||||
for cycle_index in range(resources.config.max_cycles):
|
||||
state.current_cycle = cycle_index
|
||||
await self.on_cycle_start(state, resources)
|
||||
|
||||
await self.build_llm_request(state, resources)
|
||||
await self.execute_llm(state, resources)
|
||||
|
||||
await self.handle_llm_response(state, resources)
|
||||
if state.is_finished:
|
||||
assert state.final_result is not None
|
||||
return state.final_result
|
||||
|
||||
await self.filter_tool_calls(state, resources)
|
||||
if state.is_finished:
|
||||
assert state.final_result is not None
|
||||
return state.final_result
|
||||
|
||||
await self.execute_tools(state, resources)
|
||||
|
||||
await self.handle_tool_results(state, resources)
|
||||
if state.is_finished:
|
||||
assert state.final_result is not None
|
||||
return state.final_result
|
||||
|
||||
return await self.on_fallback(state, resources)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@abstractmethod
|
||||
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
|
||||
"""生命周期: Agent 启动时调用,用于初始化状态或资源。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def on_cycle_start(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""生命周期: 每次推理循环开始时调用。可用于 Token 预估或防死循环检测。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def build_llm_request(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""生命周期: 构造请求大模型的 Messages 上下文和 Extra 参数。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def execute_llm(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""生命周期: 触发大模型 API 请求并返回响应。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def handle_llm_response(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""
|
||||
生命周期: 处理大模型返回的结果,解析 Token 用量,
|
||||
并将模型回复追加至对话历史。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def filter_tool_calls(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""生命周期: 从大模型的响应中提取并过滤出需要在本地客户端执行的工具调用请求。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def execute_tools(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""生命周期: 并发执行提取出的工具,并收集结果或异常。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def handle_tool_results(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""
|
||||
生命周期: 处理工具返回的结果。
|
||||
包括异常拦截、UI 渲染、Handoff 移交指令以及将结果追加至对话历史。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def on_fallback(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> AgentRunResult[Any]:
|
||||
"""生命周期: 当大模型思考循环达到 max_cycles 时触发,执行兜底策略。"""
|
||||
pass
|
||||
|
||||
|
||||
class StandardAgentExecutor(BaseAgentExecutor):
|
||||
"""
|
||||
LLM 任务执行器(核心推理引擎)。
|
||||
负责:生命周期回调触发、工具循环调用、
|
||||
错误反思(Reflexion)、Token消耗追踪。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, directive_handlers: dict[str, DirectiveHandlerFunc] | None = None
|
||||
):
|
||||
self.tool_executor = ToolExecutor()
|
||||
self._directive_handlers: dict[str, DirectiveHandlerFunc] = (
|
||||
directive_handlers or {}
|
||||
)
|
||||
|
||||
def _can_retry_via_llm(self, result: ToolResult) -> bool:
|
||||
"""通过新版的专属字段直接判断是否允许重试"""
|
||||
return result.is_retryable
|
||||
|
||||
def _check_follow_up(
|
||||
self, state: AgentState, resources: AgentRunResources, session_info: Any
|
||||
) -> bool:
|
||||
"""检查追加队列,排空并合并数据到上下文,返回是否发现新消息"""
|
||||
follow_ups = session_info.follow_up_queue.drain()
|
||||
if follow_ups:
|
||||
for fm in follow_ups:
|
||||
state.messages.append(LLMMessage.user(f"💬 [用户追加指示]:{fm}"))
|
||||
resources.run_context.session.append_only_manager.sync_messages(
|
||||
state.messages
|
||||
)
|
||||
state.is_finished = False
|
||||
state.final_result = None
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _invoke_and_record_llm(
|
||||
self,
|
||||
state: AgentState,
|
||||
resources: AgentRunResources,
|
||||
messages: list[AgentMessage],
|
||||
tools: list[Any] | None,
|
||||
tool_choice: Any = None,
|
||||
) -> ChatResponse:
|
||||
"""执行 LLM 请求,处理基础指标遥测统计,并将新对话上下文追加到状态流"""
|
||||
run_context = resources.run_context
|
||||
cancellation_token = run_context.run.cancellation_token
|
||||
|
||||
current_extra = run_context.state.copy()
|
||||
current_extra["__global_max_cycles__"] = getattr(
|
||||
resources.config, "global_max_cycles", None
|
||||
)
|
||||
current_extra["__sys_capabilities"] = getattr(run_context, "capabilities", [])
|
||||
current_extra["run_context"] = run_context
|
||||
|
||||
flattened_messages = ContextConverter.flatten_to_llm_messages(
|
||||
messages, run_context
|
||||
)
|
||||
|
||||
if run_context.run.event_bus:
|
||||
await run_context.run.event_bus.emit(
|
||||
LLMStartEvent(
|
||||
model_name=resources.model_name or "unknown",
|
||||
messages=flattened_messages,
|
||||
)
|
||||
)
|
||||
|
||||
response = await self._execute_model_request(
|
||||
model_name=resources.model_name,
|
||||
messages=flattened_messages,
|
||||
config=resources.generation_config or GenerationConfig(),
|
||||
run_context=run_context,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
extra=current_extra,
|
||||
cancellation_token=cancellation_token,
|
||||
)
|
||||
|
||||
if run_context.run.event_bus:
|
||||
await run_context.run.event_bus.emit(LLMEndEvent(response=response))
|
||||
|
||||
assistant_content = (
|
||||
response.content_parts if response.content_parts else response.text
|
||||
)
|
||||
if response.thought_signature and isinstance(assistant_content, list):
|
||||
for part in assistant_content:
|
||||
if part.type == "thought":
|
||||
if part.metadata is None:
|
||||
part.metadata = {}
|
||||
part.metadata["thought_signature"] = response.thought_signature
|
||||
break
|
||||
|
||||
assistant_message = AssistantMessage(
|
||||
content=cast(list[AssistantContentUnion], response.content_parts)
|
||||
)
|
||||
|
||||
if hasattr(response, "parsed_obj") and response.parsed_obj is not None:
|
||||
if not isinstance(response.parsed_obj, str):
|
||||
if assistant_message.metadata is None:
|
||||
assistant_message.metadata = {}
|
||||
assistant_message.metadata["parsed_obj"] = response.parsed_obj
|
||||
|
||||
usage_obj = parse_usage_info(response.usage_info)
|
||||
state.usage += usage_obj
|
||||
if usage_obj.completion_tokens > 0:
|
||||
assistant_message.token_cost = usage_obj.completion_tokens
|
||||
|
||||
state.messages.append(assistant_message)
|
||||
run_context.session.append_only_manager.sync_messages(state.messages)
|
||||
|
||||
return response
|
||||
|
||||
async def _execute_model_request(
|
||||
self,
|
||||
model_name: str | None,
|
||||
messages: list[LLMMessage],
|
||||
config: GenerationConfig,
|
||||
run_context: RunContext,
|
||||
tools: list[Any] | None = None,
|
||||
tool_choice: Any = None,
|
||||
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,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
extra=extra or {},
|
||||
)
|
||||
|
||||
sys_caps = request.extra.pop("__sys_capabilities", [])
|
||||
llm_context = LLMContext(request=request, cancellation_token=cancellation_token)
|
||||
combined_cap = CombinedCapability(sys_caps)
|
||||
|
||||
async def inner_handler(ctx: LLMContext[Any, Any]) -> ChatResponse:
|
||||
return await LLMOrchestrator.invoke(
|
||||
request=ctx.request,
|
||||
model_name=model_name,
|
||||
task="chat",
|
||||
override_config=config,
|
||||
cancellation_token=ctx.cancellation_token,
|
||||
)
|
||||
|
||||
return await combined_cap.wrap_model_request(
|
||||
run_context, llm_context, inner_handler
|
||||
)
|
||||
|
||||
async def on_start(self, state: AgentState, resources: AgentRunResources) -> None:
|
||||
resources.run_context.run.messages = state.messages
|
||||
|
||||
async def run(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> 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"
|
||||
)
|
||||
|
||||
try:
|
||||
cycle_count = 0
|
||||
while cycle_count < resources.config.max_cycles:
|
||||
state.current_cycle = cycle_count
|
||||
await self.on_cycle_start(state, resources)
|
||||
|
||||
await self.build_llm_request(state, resources)
|
||||
await self.execute_llm(state, resources)
|
||||
|
||||
await self.handle_llm_response(state, resources)
|
||||
if state.is_finished:
|
||||
if self._check_follow_up(state, resources, session_info):
|
||||
cycle_count = 0
|
||||
continue
|
||||
assert state.final_result is not None
|
||||
return state.final_result
|
||||
|
||||
await self.filter_tool_calls(state, resources)
|
||||
if state.is_finished:
|
||||
if self._check_follow_up(state, resources, session_info):
|
||||
cycle_count = 0
|
||||
continue
|
||||
assert state.final_result is not None
|
||||
return state.final_result
|
||||
|
||||
await self.execute_tools(state, resources)
|
||||
|
||||
await self.handle_tool_results(state, resources)
|
||||
if state.is_finished:
|
||||
if self._check_follow_up(state, resources, session_info):
|
||||
cycle_count = 0
|
||||
continue
|
||||
assert state.final_result is not None
|
||||
return state.final_result
|
||||
|
||||
cycle_count += 1
|
||||
|
||||
if self._check_follow_up(state, resources, session_info):
|
||||
return await self.run(state, resources)
|
||||
|
||||
return await self.on_fallback(state, resources)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def on_cycle_start(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
cancellation_token = resources.run_context.run.cancellation_token
|
||||
if cancellation_token:
|
||||
cancellation_token.raise_if_cancelled()
|
||||
|
||||
try:
|
||||
est_tokens = token_counter.count_context(
|
||||
state.messages, resources.model_name or "", base_overhead=0
|
||||
)
|
||||
logger.debug(
|
||||
f"[TokenTracker] (Iter {state.current_cycle + 1}) "
|
||||
f"预估将消耗 {est_tokens} Token "
|
||||
f"(Model: {resources.model_name or 'Unknown'})"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def build_llm_request(
|
||||
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"
|
||||
)
|
||||
steer_msgs = session_info.steer_queue.drain()
|
||||
if steer_msgs:
|
||||
for sm in steer_msgs:
|
||||
state.messages.append(LLMMessage.user(f"💬 [用户实时修正指示]:{sm}"))
|
||||
run_context.session.append_only_manager.sync_messages(state.messages)
|
||||
|
||||
messages_to_send = []
|
||||
if state.static_system_prompt:
|
||||
if isinstance(state.static_system_prompt, list):
|
||||
for sp in state.static_system_prompt:
|
||||
if sp and sp.strip():
|
||||
messages_to_send.append(LLMMessage.system(sp))
|
||||
else:
|
||||
if state.static_system_prompt and state.static_system_prompt.strip():
|
||||
messages_to_send.append(
|
||||
LLMMessage.system(state.static_system_prompt)
|
||||
)
|
||||
|
||||
if state.dynamic_system_messages:
|
||||
messages_to_send.extend(state.dynamic_system_messages)
|
||||
|
||||
if (
|
||||
hasattr(run_context.run, "dynamic_prompts")
|
||||
and run_context.run.dynamic_prompts
|
||||
):
|
||||
for prompt_text in run_context.run.dynamic_prompts.values():
|
||||
if prompt_text and prompt_text.strip():
|
||||
messages_to_send.append(LLMMessage.system(prompt_text))
|
||||
|
||||
messages_to_send.extend(state.messages)
|
||||
|
||||
state.current_request_messages = messages_to_send
|
||||
|
||||
async def execute_llm(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
tools = state.tools
|
||||
|
||||
state.current_response = await self._invoke_and_record_llm(
|
||||
state=state,
|
||||
resources=resources,
|
||||
messages=state.current_request_messages,
|
||||
tools=list(tools) if tools else None,
|
||||
tool_choice=None,
|
||||
)
|
||||
|
||||
async def handle_llm_response(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
response = state.current_response
|
||||
if not response:
|
||||
return
|
||||
|
||||
if not response.tool_calls:
|
||||
logger.debug("✅ AgentExecutor:模型未请求工具调用,推理循环结束。")
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=response.text,
|
||||
messages=state.messages,
|
||||
usage=state.usage,
|
||||
)
|
||||
|
||||
async def filter_tool_calls(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
response = state.current_response
|
||||
if not response:
|
||||
return
|
||||
tools = state.tools
|
||||
event_bus = resources.run_context.run.event_bus
|
||||
|
||||
completed_call_ids = {
|
||||
p.tool_call_id
|
||||
for p in response.content_parts
|
||||
if isinstance(p, ToolReturnPart)
|
||||
}
|
||||
client_tool_calls = []
|
||||
for call in response.tool_calls:
|
||||
tool_inst = tools.get(call.tool_name) if tools else None
|
||||
is_server_side = call.id in completed_call_ids or (
|
||||
tool_inst and getattr(tool_inst, "execution_side", "client") == "server"
|
||||
)
|
||||
|
||||
if is_server_side:
|
||||
logger.debug(
|
||||
"☁️ [AgentExecutor] 检测到云端工具调用: "
|
||||
f"{call.tool_name},已跳过本地执行。"
|
||||
)
|
||||
async with self.tool_executor._tool_stream_scope(
|
||||
event_bus,
|
||||
call.tool_name,
|
||||
call.args if isinstance(call.args, dict) else {},
|
||||
getattr(call, "intent", None),
|
||||
) as box:
|
||||
return_part = next(
|
||||
(
|
||||
p
|
||||
for p in response.content_parts
|
||||
if isinstance(p, ToolReturnPart)
|
||||
and p.tool_call_id == call.id
|
||||
),
|
||||
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)
|
||||
|
||||
if not client_tool_calls:
|
||||
logger.info("✅ AgentExecutor:无本地客户端工具需执行,推理循环平滑结束。")
|
||||
|
||||
state.is_finished = True
|
||||
state.final_result = model_construct(
|
||||
AgentRunResult,
|
||||
output=response.text,
|
||||
messages=state.messages,
|
||||
usage=state.usage,
|
||||
)
|
||||
|
||||
state.current_tool_calls = client_tool_calls
|
||||
|
||||
async def execute_tools(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
run_context = resources.run_context
|
||||
tools = state.tools
|
||||
event_bus = run_context.run.event_bus
|
||||
tool_calls = state.current_tool_calls
|
||||
|
||||
if not tool_calls:
|
||||
return
|
||||
|
||||
val_tasks = [
|
||||
self.tool_executor.validate_tool_call(
|
||||
call,
|
||||
tools,
|
||||
run_context,
|
||||
event_bus=event_bus,
|
||||
)
|
||||
for call in tool_calls
|
||||
]
|
||||
validated_calls = await asyncio.gather(*val_tasks)
|
||||
|
||||
exec_tasks = [
|
||||
self.tool_executor.execute_tool_call(
|
||||
val_call,
|
||||
tools,
|
||||
run_context,
|
||||
event_bus=event_bus,
|
||||
)
|
||||
for val_call in validated_calls
|
||||
]
|
||||
tool_results = await asyncio.gather(*exec_tasks, return_exceptions=True)
|
||||
state.current_tool_results = tool_results
|
||||
|
||||
def _assemble_tool_message(
|
||||
self,
|
||||
original_call: ToolCallPart,
|
||||
res_or_exc: Any,
|
||||
tool_res: ToolResult | None,
|
||||
state: AgentState,
|
||||
) -> LLMMessage:
|
||||
"""负责处理异常、解析多模态、序列化,并装配为最终的工具消息载体"""
|
||||
media_parts = []
|
||||
final_content = "Success"
|
||||
|
||||
if isinstance(res_or_exc, BaseException):
|
||||
if isinstance(res_or_exc, ControlFlowExit):
|
||||
raise res_or_exc
|
||||
final_content = json.dumps(
|
||||
{"error": str(res_or_exc), "status": "failed"},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
elif tool_res is not None:
|
||||
if isinstance(tool_res.output, list):
|
||||
texts = []
|
||||
for item in tool_res.output:
|
||||
if isinstance(item, ImagePart | AudioPart | VideoPart | FilePart):
|
||||
media_parts.append(item)
|
||||
elif isinstance(item, TextPart):
|
||||
texts.append(item.text)
|
||||
else:
|
||||
texts.append(str(item))
|
||||
final_content = " ".join(texts) if texts else "Success"
|
||||
elif isinstance(tool_res.output, str):
|
||||
final_content = tool_res.output
|
||||
else:
|
||||
final_content = dump_json_safely(tool_res.output, ensure_ascii=False)
|
||||
|
||||
tool_usage = getattr(tool_res, "usage", None)
|
||||
if tool_usage is not None:
|
||||
state.usage += tool_usage
|
||||
|
||||
msg = LLMMessage.tool_response(
|
||||
original_call.id, original_call.tool_name, final_content
|
||||
)
|
||||
if media_parts:
|
||||
msg.content.extend(media_parts)
|
||||
return msg
|
||||
|
||||
async def handle_tool_results(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> None:
|
||||
"""处理所有工具执行结果,调度副作用指令并装配对话回传报文。"""
|
||||
tool_calls = state.current_tool_calls
|
||||
tool_results = state.current_tool_results
|
||||
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
|
||||
|
||||
if not isinstance(res_or_exc, BaseException):
|
||||
_, raw_tool_res = res_or_exc
|
||||
tool_res = raw_tool_res
|
||||
|
||||
msg = self._assemble_tool_message(
|
||||
original_call, res_or_exc, tool_res, state
|
||||
)
|
||||
state.messages.append(msg)
|
||||
|
||||
if tool_res and getattr(tool_res, "directive", None):
|
||||
ns = getattr(resources.run_context.session, "namespace", "global")
|
||||
handler = directive_manager.get_handler(
|
||||
tool_res.directive.name, namespace=ns
|
||||
)
|
||||
|
||||
if handler:
|
||||
await handler(state, resources, tool_res)
|
||||
if state.is_finished:
|
||||
resources.run_context.session.append_only_manager.sync_messages(
|
||||
state.messages
|
||||
)
|
||||
return
|
||||
else:
|
||||
logger.warning(
|
||||
f"⚠️ 未能找到名为 '{tool_res.directive.name}' "
|
||||
f"的指令处理器 (Namespace: {ns})"
|
||||
)
|
||||
|
||||
resources.run_context.session.append_only_manager.sync_messages(state.messages)
|
||||
|
||||
async def on_fallback(
|
||||
self, state: AgentState, resources: AgentRunResources
|
||||
) -> AgentRunResult[Any]:
|
||||
run_context = resources.run_context
|
||||
event_bus = run_context.run.event_bus
|
||||
|
||||
if not resources.config.enable_fallback_summary:
|
||||
raise UpstreamServerException(
|
||||
f"超过最大工具调用循环次数 ({resources.config.max_cycles})。",
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
f"AgentExecutor 达到最大循环次数 ({resources.config.max_cycles}),"
|
||||
"触发兜底总结机制。"
|
||||
)
|
||||
|
||||
if event_bus:
|
||||
await event_bus.emit(
|
||||
ToolStreamChunkEvent(
|
||||
tool_name="System",
|
||||
content="⏳ 思考过程过于复杂,正在强制生成最终总结...",
|
||||
)
|
||||
)
|
||||
|
||||
fallback_msg = LLMMessage.user(
|
||||
"### 🚨 [系统强制指令]\n"
|
||||
"你的任务执行已达到最大工具调用循环次数上限,当前思考流已被框架强制中断。\n"
|
||||
"请**诚实地**向用户总结:你目前进行到了哪一步?遇到了什么困难导致循环耗尽?还有哪些预期步骤未能完成?\n"
|
||||
"**绝对禁止**对用户撒谎声声称你已经完成了任务。严禁再次尝试调用任何工具!请直接输出纯文本结果。"
|
||||
)
|
||||
state.messages.append(fallback_msg)
|
||||
|
||||
fallback_response = await self._invoke_and_record_llm(
|
||||
state=state,
|
||||
resources=resources,
|
||||
messages=state.messages,
|
||||
tools=[],
|
||||
tool_choice="none",
|
||||
)
|
||||
|
||||
return model_construct(
|
||||
AgentRunResult,
|
||||
output=fallback_response.text,
|
||||
messages=state.messages,
|
||||
structured_data=None,
|
||||
usage=state.usage,
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Agent 相关静态声明类型定义
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from zhenxun.services.ai.context.memory.types import SessionMetadata
|
||||
from zhenxun.services.ai.core.messages import (
|
||||
AgentMessage,
|
||||
ChatResponse,
|
||||
LLMMessage,
|
||||
ToolCallPart,
|
||||
UsageInfo,
|
||||
)
|
||||
from zhenxun.services.ai.core.options import GenerationConfig
|
||||
from zhenxun.services.ai.flow.base import BaseRuntimeConfig
|
||||
from zhenxun.services.ai.run import RunContext
|
||||
from zhenxun.services.ai.tools.engine.registry import ToolCollection
|
||||
from zhenxun.services.ai.tools.models import GlobalToolFilter
|
||||
from zhenxun.utils.pydantic_compat import model_copy
|
||||
|
||||
|
||||
class Persona(BaseModel):
|
||||
"""智能体人设与上下文背景"""
|
||||
|
||||
role: str = Field(...)
|
||||
"""扮演的角色身份"""
|
||||
|
||||
goal: str = Field(...)
|
||||
"""角色的核心目标"""
|
||||
|
||||
backstory: str | None = Field(default=None)
|
||||
"""角色背景故事或性格设定"""
|
||||
|
||||
model_config = ConfigDict(extra="ignore") # type: ignore
|
||||
|
||||
|
||||
class AgentConfig(BaseRuntimeConfig):
|
||||
"""统一的智能体全局与单次运行配置 (Unification of Settings & Profile)"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
max_cycles: int = Field(default=10)
|
||||
"""工具调用最大循环次数"""
|
||||
global_max_cycles: int | None = Field(default=None)
|
||||
"""整个会话生命周期内的绝对最大循环次数上限(覆盖全局配置)。"""
|
||||
enable_parallel_calls: bool = Field(default=True)
|
||||
"""允许并行工具调用"""
|
||||
reflexion_retries: int = Field(default=1)
|
||||
"""反思重试次数"""
|
||||
enable_fallback_summary: bool = Field(default=True)
|
||||
"""达到最大循环次数时,是否触发大模型兜底总结(而不是直接报错)"""
|
||||
enable_hitl: bool | None = Field(default=None)
|
||||
"""是否允许智能体主动挂起任务,向用户求助 (Human-in-the-Loop)。
|
||||
若为 None 则跟随全局设置。
|
||||
"""
|
||||
|
||||
message_history: Sequence[AgentMessage] | None = Field(default=None)
|
||||
"""初始化的底层对话历史记录。"""
|
||||
tool_filter: GlobalToolFilter | None = Field(default=None)
|
||||
"""全局工具过滤器,限制本次运行可用的工具池。"""
|
||||
memory: Any | None = Field(default=None)
|
||||
"""单次运行级别的记忆门面覆盖 (支持 bool, MemoryConfig, MemoryBuilder)。"""
|
||||
generation_config: GenerationConfig | None = Field(default=None)
|
||||
"""单次运行覆盖的大模型生成配置。"""
|
||||
capabilities: list[Any] | None = Field(default=None)
|
||||
"""仅针对本次运行动态注入的临时拦截器/能力组件列表。"""
|
||||
skills: Sequence[Any] | None = Field(default=None)
|
||||
"""仅针对本次运行动态注入的临时技能集合。"""
|
||||
executor: Any | None = Field(default=None)
|
||||
"""单次运行覆盖的核心执行引擎策略 (BaseAgentExecutor)。"""
|
||||
|
||||
verbose_ui: bool = Field(default=False)
|
||||
"""是否在 UI 前端展示细粒度的工具执行中间过程。
|
||||
在不支持流式更新的平台(如QQ)建议保持 False。"""
|
||||
|
||||
def merge_with(self, other: "AgentConfig | dict | None") -> "AgentConfig":
|
||||
"""深度合并另一份配置,生成一个新的覆盖实例"""
|
||||
|
||||
if not other:
|
||||
return model_copy(self, deep=True)
|
||||
|
||||
update_dict = {}
|
||||
if isinstance(other, dict):
|
||||
other_dict = {k: v for k, v in other.items() if v is not None}
|
||||
else:
|
||||
fields_set = getattr(
|
||||
other, "model_fields_set", getattr(other, "__fields_set__", set())
|
||||
)
|
||||
other_dict = {}
|
||||
for k in fields_set:
|
||||
val = getattr(other, k)
|
||||
if val is not None:
|
||||
other_dict[k] = val
|
||||
|
||||
for k, v in other_dict.items():
|
||||
if k in ("capabilities", "skills") and isinstance(v, list):
|
||||
base_list = getattr(self, k) or []
|
||||
update_dict[k] = base_list + v
|
||||
else:
|
||||
update_dict[k] = v
|
||||
|
||||
return model_copy(self, update=update_dict, deep=True)
|
||||
|
||||
|
||||
class AgentState(BaseModel):
|
||||
"""大模型思考循环的有限状态机 (FSM) 流转状态"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
static_system_prompt: str | list[str] = ""
|
||||
"""绝对不变的系统提示词(用于前缀缓存)"""
|
||||
dynamic_system_messages: list[LLMMessage] = Field(default_factory=list)
|
||||
"""包含变量与实时状态的动态独立提示消息列表(绝对头部注入)"""
|
||||
tools: ToolCollection | None = None
|
||||
"""当前轮次生效的、已完成鉴权和过滤的工具集合"""
|
||||
|
||||
messages: list[AgentMessage] = Field(default_factory=list)
|
||||
"""大模型将看到的完整历史消息列表 (执行历史)"""
|
||||
usage: UsageInfo = Field(default_factory=UsageInfo)
|
||||
"""累计的 Token 消耗"""
|
||||
structured_result: Any | None = None
|
||||
"""拦截到的结构化输出结果"""
|
||||
early_result_output: Any | None = None
|
||||
"""拦截到的早期终止输出结果"""
|
||||
should_terminate: bool = False
|
||||
"""标记是否应提前终止循环"""
|
||||
handoff_triggered: Any | None = None
|
||||
"""标记是否触发了移交"""
|
||||
is_finished: bool = False
|
||||
"""标记大模型循环是否彻底结束"""
|
||||
final_result: Any | None = None
|
||||
"""最终的运行结果 (AgentRunResult)"""
|
||||
origin_msg_len: int = 0
|
||||
"""初始进入循环时的消息历史长度 (用于增量保存记忆)"""
|
||||
current_cycle: int = 0
|
||||
"""当前思考循环的轮次索引"""
|
||||
|
||||
current_request_messages: list[AgentMessage] = Field(default_factory=list)
|
||||
"""当前即将发往大模型的实际请求消息"""
|
||||
current_request_extra: dict[str, Any] = Field(default_factory=dict)
|
||||
"""当前请求附加的Extra控制参数"""
|
||||
current_response: ChatResponse | None = None
|
||||
"""大模型最新返回的响应实体"""
|
||||
current_tool_calls: list[ToolCallPart] = Field(default_factory=list)
|
||||
"""当前轮次被提取出准备执行的客户端工具调用"""
|
||||
current_tool_results: list[Any] = Field(default_factory=list)
|
||||
"""当前轮次工具执行的结果或异常收集"""
|
||||
|
||||
|
||||
class AgentRunResources(BaseModel):
|
||||
"""大模型执行过程中的全局静态资源与配置载体"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
run_context: RunContext
|
||||
"""保留依赖注入(DI)与黑板引用的全局运行时上下文"""
|
||||
session_meta: SessionMetadata | None = None
|
||||
"""隔离会话的元信息(Session ID, 命名空间, 权限等)"""
|
||||
memory_reader: Any | None = None
|
||||
"""用于读取短/中/长期上下文记忆的读取器"""
|
||||
memory_writer: Any | None = None
|
||||
"""用于将对话历史安全落盘的写入器"""
|
||||
run_scoped_cap: Any | None = None
|
||||
"""聚合了 Agent/Task/全局 的复合能力拦截器 (CombinedCapability)"""
|
||||
task_obj: Any | None = None
|
||||
"""(如有) 解析后的结构化数据任务契约"""
|
||||
toolkits: list[Any] = Field(default_factory=list)
|
||||
"""当前轮次生效的工具箱列表 (需要执行生命周期挂载)"""
|
||||
config: AgentConfig = Field(default_factory=AgentConfig)
|
||||
"""Agent 全局与运行时的统一策略配置"""
|
||||
generation_config: GenerationConfig | None = None
|
||||
"""大模型生成配置"""
|
||||
model_name: str | None = None
|
||||
"""当前实际调用的模型名称"""
|
||||
@@ -0,0 +1,149 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator
|
||||
import contextlib
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.services.ai.run.context import RunContext
|
||||
from zhenxun.services.ai.run.ui import UIController
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from zhenxun.services.ai.flow.agent.models import Persona
|
||||
from zhenxun.services.ai.run.models import StreamedRunResult
|
||||
|
||||
from zhenxun.services.ai.core.messages import PromptInput
|
||||
|
||||
T_RunResult = TypeVar("T_RunResult")
|
||||
|
||||
|
||||
class ConcurrencyPolicy(str, Enum):
|
||||
"""并发执行策略枚举"""
|
||||
|
||||
ALLOW = "allow"
|
||||
"""允许并发:不做任何限制(适用于无状态或绝对独立任务)"""
|
||||
REJECT = "reject"
|
||||
"""拒绝新请求:当前有任务在执行时,直接丢弃新任务并提醒"""
|
||||
QUEUE = "queue"
|
||||
"""排队等待:当前有任务在执行时,新任务排队等待(先进先出)"""
|
||||
INTERRUPT = "interrupt"
|
||||
"""中断旧任务:新任务到达时,立即强制取消并覆盖正在执行的旧任务"""
|
||||
|
||||
|
||||
class ConcurrencyScope(str, Enum):
|
||||
"""并发作用域枚举(决定锁的粒度,解耦于会话隔离)"""
|
||||
|
||||
GLOBAL = "global"
|
||||
"""全局互斥:整个系统同一时间只能执行一个该任务"""
|
||||
GROUP = "group"
|
||||
"""群组互斥:同一群组内串行排队(私聊退化为用户级),防止抢话刷屏"""
|
||||
USER = "user"
|
||||
"""用户互斥:同一用户发起的任务串行排队(允许同群不同人并行)"""
|
||||
SESSION = "session"
|
||||
"""会话互斥:跟随记忆 SessionID 进行物理锁隔离"""
|
||||
|
||||
|
||||
class InterventionPolicy(str, Enum):
|
||||
"""运行时消息干预策略枚举"""
|
||||
|
||||
IGNORE = "ignore"
|
||||
"""忽略干预:丢弃在任务执行期间收到的额外消息(默认)"""
|
||||
STEER = "steer"
|
||||
"""动态转向:将额外消息立即注入到下一轮大模型推理历史中,影响其思考方向"""
|
||||
FOLLOW_UP = "follow_up"
|
||||
"""追加执行:将额外消息放入队列,在当前大模型意图(所有工具等)执行完毕后追加推理"""
|
||||
|
||||
|
||||
class BaseRuntimeConfig(BaseModel):
|
||||
"""所有可执行实体(Agent/Team/Workflow)的通用基础运行时配置"""
|
||||
|
||||
stateless: bool = Field(default=True)
|
||||
"""是否使用临时会话,不持久化历史记录"""
|
||||
concurrency_policy: ConcurrencyPolicy | None = Field(default=None)
|
||||
"""并发执行策略。如果未显式指定,无状态(stateless=True)默认为ALLOW,有状态(stateless=False)默认为QUEUE。"""
|
||||
concurrency_scope: ConcurrencyScope | None = Field(default=None)
|
||||
"""并发作用域,决定锁的粒度。如果未显式指定,默认为 GROUP 级排队。"""
|
||||
intervention_policy: InterventionPolicy | None = Field(default=None)
|
||||
"""运行时干预策略,决定在大模型执行期间接收到新消息时该如何处理数据流合并。"""
|
||||
|
||||
|
||||
class BaseRunnable(ABC, Generic[T_RunResult]):
|
||||
"""
|
||||
所有可执行 AI 编排实体的统一基类 (Composite Pattern)。
|
||||
统一了 Agent, Team, Workflow 的核心契约,支持物理上的任意嵌套。
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""可执行实体的名称标识"""
|
||||
|
||||
description: str
|
||||
"""可执行实体的详细描述。用于外部路由(Router)或上层智能体(DelegateTool)决定是否调用它"""
|
||||
|
||||
persona: "Persona | dict | None" = None
|
||||
"""(可选) 实体的角色设定 (Persona)。包含 role 和 goal,
|
||||
在多智能体路由移交时优先级最高"""
|
||||
|
||||
runtime_config: BaseRuntimeConfig
|
||||
"""运行时配置,如是否无状态、UI输出模式等"""
|
||||
|
||||
def bind(self, **kwargs: Any) -> Any:
|
||||
"""DI 注入语法糖:返回 Depends,自动绑定当前上下文"""
|
||||
from nonebot.params import Depends
|
||||
|
||||
from zhenxun.services.ai.flow.agent.bridge import AgentRunner
|
||||
|
||||
async def _dependency() -> AgentRunner[Any]:
|
||||
return AgentRunner[Any](self, **kwargs)
|
||||
|
||||
return Depends(_dependency)
|
||||
|
||||
async def reply(
|
||||
self,
|
||||
prompt: PromptInput | None = None,
|
||||
reply_to: bool = False,
|
||||
*,
|
||||
context: RunContext | None = None,
|
||||
**kwargs: Any,
|
||||
) -> T_RunResult:
|
||||
"""交互执行语法糖,自动渲染流式进度并最终将结果回复给终端用户"""
|
||||
from zhenxun.services.ai.flow.agent.bridge import AgentRunner
|
||||
|
||||
runner = AgentRunner(self, context=context, **kwargs)
|
||||
return cast(T_RunResult, await runner.reply(prompt=prompt, reply_to=reply_to))
|
||||
|
||||
async def run(
|
||||
self,
|
||||
prompt: PromptInput | None = None,
|
||||
*,
|
||||
context: RunContext | None = None,
|
||||
**kwargs: Any,
|
||||
) -> T_RunResult:
|
||||
"""阻塞式核心运行入口,安全捕获内部抛出的静默退出信号"""
|
||||
from zhenxun.services.ai.core.exceptions import ControlFlowExit
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
try:
|
||||
async with self.run_stream(
|
||||
prompt=prompt, context=context, **kwargs
|
||||
) as stream_result:
|
||||
return cast(T_RunResult, await stream_result.get_run_result())
|
||||
except ControlFlowExit as e:
|
||||
logger.info(f"[{self.name}] 触发底层控制流,已安全退出: {e}")
|
||||
|
||||
await UIController.handle_control_flow_exit_display(e, context)
|
||||
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
@abstractmethod
|
||||
@contextlib.asynccontextmanager
|
||||
async def run_stream(
|
||||
self,
|
||||
prompt: PromptInput | None = None,
|
||||
*,
|
||||
context: RunContext | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "AsyncIterator[StreamedRunResult[Any]]":
|
||||
"""流式运行入口,返回上下文管理器,用于消费底层执行流事件 (StreamedRunResult)"""
|
||||
yield cast(Any, None)
|
||||
@@ -0,0 +1,111 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.ai.core.exceptions import ConcurrencyRejectException
|
||||
from zhenxun.services.ai.core.models import CancellationToken
|
||||
from zhenxun.services.ai.flow.base import ConcurrencyPolicy
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def apply_concurrency_policy(
|
||||
session_id: str,
|
||||
lock_id: str,
|
||||
policy: ConcurrencyPolicy,
|
||||
cancel_token: CancellationToken,
|
||||
intervention_policy: Any = None,
|
||||
message: Any = None,
|
||||
):
|
||||
"""应用并发策略的中央调度上下文管理器"""
|
||||
from zhenxun.services.ai.run.session import LockContext, session_manager
|
||||
|
||||
current_task = asyncio.current_task()
|
||||
task_tuple = (cancel_token, current_task)
|
||||
if session_id not in session_manager.live_tasks:
|
||||
session_manager.live_tasks[session_id] = []
|
||||
session_manager.live_tasks[session_id].append(task_tuple)
|
||||
|
||||
try:
|
||||
exec_lock = session_manager.get_exec_lock(lock_id)
|
||||
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):
|
||||
actual_msg = message.description
|
||||
elif hasattr(message, "extract_plain_text"):
|
||||
actual_msg = message.extract_plain_text()
|
||||
|
||||
if intervention_policy == InterventionPolicy.STEER:
|
||||
session.steer_queue.enqueue(str(actual_msg))
|
||||
raise InterventionHandledException(
|
||||
"Steer successful",
|
||||
display_content="💬 已将您的补充信息传递给正在思考的 AI...",
|
||||
)
|
||||
elif intervention_policy == InterventionPolicy.FOLLOW_UP:
|
||||
session.follow_up_queue.enqueue(str(actual_msg))
|
||||
raise InterventionHandledException(
|
||||
"Follow-up successful",
|
||||
display_content="📝 已记录,AI 处理完当前任务后即刻执行...",
|
||||
)
|
||||
|
||||
if policy == ConcurrencyPolicy.ALLOW:
|
||||
yield
|
||||
return
|
||||
|
||||
if policy == ConcurrencyPolicy.REJECT:
|
||||
if exec_lock.locked():
|
||||
raise ConcurrencyRejectException(
|
||||
f"并发域 {lock_id} 正忙,新请求被拒绝。"
|
||||
)
|
||||
|
||||
elif policy == ConcurrencyPolicy.INTERRUPT:
|
||||
if exec_lock.locked():
|
||||
if lock_ctx.cancel_token:
|
||||
lock_ctx.cancel_token.cancel()
|
||||
if lock_ctx.active_task and not lock_ctx.active_task.done():
|
||||
lock_ctx.active_task.cancel()
|
||||
|
||||
elif policy == ConcurrencyPolicy.QUEUE:
|
||||
if exec_lock.locked():
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
logger.info(
|
||||
f"⏳ [并发控制] 锁域 {lock_id} 被占用,"
|
||||
"新请求已进入后台等待队列 (QUEUE)..."
|
||||
)
|
||||
|
||||
async with exec_lock:
|
||||
session = await session_manager.get_or_create(session_id)
|
||||
session.active_task = asyncio.current_task()
|
||||
session.cancel_token = cancel_token
|
||||
|
||||
lock_ctx.active_task = asyncio.current_task()
|
||||
lock_ctx.cancel_token = cancel_token
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
session.active_task = None
|
||||
session.cancel_token = None
|
||||
lock_ctx.active_task = None
|
||||
lock_ctx.cancel_token = None
|
||||
finally:
|
||||
if session_id in session_manager.live_tasks:
|
||||
if task_tuple in session_manager.live_tasks[session_id]:
|
||||
session_manager.live_tasks[session_id].remove(task_tuple)
|
||||
if not session_manager.live_tasks[session_id]:
|
||||
del session_manager.live_tasks[session_id]
|
||||
@@ -0,0 +1,7 @@
|
||||
from .models import Transition
|
||||
from .team import Team
|
||||
|
||||
__all__ = [
|
||||
"Team",
|
||||
"Transition",
|
||||
]
|
||||
@@ -0,0 +1,131 @@
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
import inspect
|
||||
from typing import Any, cast
|
||||
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
|
||||
from zhenxun.services.ai.capabilities import AbstractCapability
|
||||
from zhenxun.services.ai.run import RunContext
|
||||
from zhenxun.services.ai.tools.bridges.handoff import HandoffTool
|
||||
|
||||
|
||||
class TeamRoutingCapability(AbstractCapability):
|
||||
"""团队路由能力组件:动态向所有团队成员"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
team_name: str,
|
||||
members: list[Any],
|
||||
state_flow: Mapping[str, Sequence[Any]] | Callable | None = None,
|
||||
):
|
||||
self.team_name = team_name
|
||||
self.members = members
|
||||
self.state_flow = state_flow
|
||||
|
||||
async def _get_allowed_transitions(self, context: RunContext) -> list[Any] | None:
|
||||
"""核心FSM解析:解析静态字典或动态执行函数获取允许的 Transition 列表"""
|
||||
if self.state_flow is None:
|
||||
return None
|
||||
|
||||
current_speaker = context.run.agent_name or "unknown"
|
||||
|
||||
if isinstance(self.state_flow, dict):
|
||||
raw_targets = self.state_flow.get(
|
||||
current_speaker,
|
||||
[m.name for m in self.members if m.name != current_speaker],
|
||||
)
|
||||
from zhenxun.services.ai.flow.team.models import Transition
|
||||
|
||||
return [
|
||||
Transition(target=t) if isinstance(t, str) else t for t in raw_targets
|
||||
]
|
||||
|
||||
if callable(self.state_flow):
|
||||
from zhenxun.services.ai.run.di import DependencyInjector
|
||||
|
||||
sig = inspect.signature(self.state_flow)
|
||||
kwargs = await DependencyInjector.resolve_all(
|
||||
sig, call_kwargs={}, context=context
|
||||
)
|
||||
|
||||
if is_coroutine_callable(self.state_flow):
|
||||
result = await cast(Callable, self.state_flow)(**kwargs)
|
||||
else:
|
||||
result = cast(Callable, self.state_flow)(**kwargs)
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
from zhenxun.services.ai.flow.team.models import Transition
|
||||
|
||||
return [Transition(target=t) if isinstance(t, str) else t for t in result]
|
||||
|
||||
return None
|
||||
|
||||
async def get_tools(self, context: RunContext) -> list[Any]:
|
||||
tools = []
|
||||
allowed_transitions = await self._get_allowed_transitions(context)
|
||||
|
||||
for m in self.members:
|
||||
if context.run.agent_name != m.name:
|
||||
transition = None
|
||||
if allowed_transitions is not None:
|
||||
transition = next(
|
||||
(
|
||||
t
|
||||
for t in allowed_transitions
|
||||
if getattr(t, "target", "") == m.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if transition is None:
|
||||
continue
|
||||
|
||||
if getattr(m, "persona", None):
|
||||
desc = f"角色:{m.persona.role},目标:{m.persona.goal}"
|
||||
else:
|
||||
desc = getattr(m, "description", "") or "处理节点"
|
||||
|
||||
if transition and getattr(transition, "description", ""):
|
||||
desc += f" 【移交条件】:{transition.description}"
|
||||
|
||||
input_schema = (
|
||||
getattr(transition, "input_schema", None) if transition else None
|
||||
)
|
||||
|
||||
tools.append(
|
||||
HandoffTool(
|
||||
target_name=m.name,
|
||||
target_description=desc,
|
||||
input_schema=input_schema,
|
||||
)
|
||||
)
|
||||
return tools
|
||||
|
||||
async def get_system_prompts(self, context: RunContext) -> list[str]:
|
||||
if context.run.agent_name != f"{self.team_name}_Router":
|
||||
base_prompt = f"""### 🤝 [团队协作规范]
|
||||
你是跨域协作团队 '{self.team_name}' 的一员。如果你认为当前任务超出了你的职责范畴,
|
||||
或你目前已经完成了前置处理但需要其他专家的处理结果进行下一步推进,
|
||||
请务必使用移交工具 (transfer_to_...) 将控制权移交给合适的队友。
|
||||
移交时必须在 `reason` 参数中详细说明你的移交原因,
|
||||
并附带你已经处理好的上下文关键数据!"""
|
||||
|
||||
allowed_transitions = await self._get_allowed_transitions(context)
|
||||
if allowed_transitions is not None:
|
||||
if not allowed_transitions:
|
||||
base_prompt += """
|
||||
|
||||
⚠️ **[系统状态机规则] 当前流程已到达终点!你没有任何可移交的对象。
|
||||
请直接输出最终总结并结束当前任务,严禁尝试移交。**"""
|
||||
else:
|
||||
targets = [
|
||||
getattr(t, "target", "unknown") for t in allowed_transitions
|
||||
]
|
||||
base_prompt += f"""
|
||||
|
||||
⚠️ **[系统状态机规则] 根据当前的状态流转限制,如果你需要移交控制权,
|
||||
你必须且只能从以下对象中选择:
|
||||
[{", ".join(targets)}]。禁止移交给除此之外的任何实体!**"""
|
||||
|
||||
return [base_prompt]
|
||||
return []
|
||||
@@ -0,0 +1,265 @@
|
||||
from collections.abc import Callable, Sequence
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from zhenxun.services.ai.core.messages import AgentMessage
|
||||
from zhenxun.services.ai.core.options import BaseOutputDefinition
|
||||
from zhenxun.services.ai.flow.base import BaseRuntimeConfig
|
||||
|
||||
|
||||
class TeamRuntimeConfig(BaseRuntimeConfig):
|
||||
"""Team 专属的运行时配置"""
|
||||
|
||||
leader_enable_hitl: bool = Field(default=False)
|
||||
"""是否允许团队的隐式 Leader / Router 发起人机求助 (Human-in-the-Loop)"""
|
||||
|
||||
|
||||
class RouteDecision(BaseModel):
|
||||
"""大模型动态路由决策的数据契约"""
|
||||
|
||||
target_name: str
|
||||
"""选定的最合适的团队成员名称"""
|
||||
reason: str = ""
|
||||
"""选择该成员的详细理由"""
|
||||
context_data: Any = ""
|
||||
"""传递的上下文载荷"""
|
||||
|
||||
|
||||
class Transition(BaseModel):
|
||||
"""
|
||||
声明式移交契约。
|
||||
用于定义 Team 模式下,智能体之间转移控制权的条件和目标。
|
||||
"""
|
||||
|
||||
target: str
|
||||
"""目标智能体的名称"""
|
||||
description: str = ""
|
||||
"""自然语言描述的移交条件(提供给大模型 LLMRouter 思考时使用)"""
|
||||
input_schema: type[BaseModel] | BaseOutputDefinition | None = None
|
||||
"""(可选) 强类型的输入约束。如果设置,
|
||||
LLMRouter 决定移交时必须且只能生成符合该 Schema 的 JSON 参数,
|
||||
并作为 context_data 传递。"""
|
||||
trigger_regex: str | None = None
|
||||
"""(可选) 正则表达式。
|
||||
如果用户的输入匹配此正则,将触发极速硬路由,跳过大模型思考。"""
|
||||
trigger_func: Callable[..., Any] | None = None
|
||||
"""(可选) 自定义校验函数。返回 True 或目标名称时触发硬路由。支持依赖注入。"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class TeamAction(BaseModel):
|
||||
"""多智能体团队协作动作基类"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class CallAction(TeamAction):
|
||||
"""
|
||||
调度动作:呼叫指定的 Agent 执行任务
|
||||
"""
|
||||
|
||||
agent: str | Any
|
||||
"""目标 Agent 的名称(字符串)或动态生成的 Agent 实例"""
|
||||
task: str | Any
|
||||
"""派发给该 Agent 的具体任务或提示词"""
|
||||
history: Sequence[AgentMessage] | None = None
|
||||
"""需要传递给该 Agent 的上下文历史记录(可选)"""
|
||||
kwargs: dict[str, Any] | None = None
|
||||
"""其他透传给 Agent.run_stream 的 kwargs(可选)"""
|
||||
|
||||
|
||||
class ConcurrentCallAction(TeamAction):
|
||||
"""
|
||||
并发调度动作:同时呼叫多个 Agent 执行任务
|
||||
"""
|
||||
|
||||
actions: list[CallAction]
|
||||
|
||||
|
||||
class FinishAction(TeamAction):
|
||||
"""
|
||||
结束动作:团队协作完成,返回最终结果
|
||||
"""
|
||||
|
||||
result: Any
|
||||
"""团队协作的最终产出"""
|
||||
|
||||
|
||||
class TaskNodeStatus(str, Enum):
|
||||
"""团队自主任务节点状态枚举"""
|
||||
|
||||
pending = "pending"
|
||||
"""待处理:所有前置依赖已完成,等待分配执行"""
|
||||
in_progress = "in_progress"
|
||||
"""进行中:正在被 Member Agent 执行"""
|
||||
completed = "completed"
|
||||
"""已完成:执行成功"""
|
||||
failed = "failed"
|
||||
"""已失败:执行报错或由于前置依赖失败而自动失败"""
|
||||
blocked = "blocked"
|
||||
"""阻塞中:有前置依赖任务尚未完成"""
|
||||
|
||||
|
||||
class SubTaskRecord(BaseModel):
|
||||
"""单条子任务(工单)数据契约"""
|
||||
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
title: str = ""
|
||||
description: str = ""
|
||||
assignee: str | None = None
|
||||
dependencies: list[str] = Field(default_factory=list)
|
||||
status: TaskNodeStatus = TaskNodeStatus.pending
|
||||
result: str | None = None
|
||||
notes: list[str] = Field(default_factory=list)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
"""附加元数据,供系统底层或第三方插件挂载隐式上下文,对大模型不可见"""
|
||||
|
||||
|
||||
class TaskBoardState(BaseModel):
|
||||
"""
|
||||
Team 自主任务模式下的全局共享黑板状态 (Task Board)。
|
||||
提供任务的 CRUD、依赖拓扑计算和格式化渲染功能。
|
||||
"""
|
||||
|
||||
tasks: list[SubTaskRecord] = Field(default_factory=list)
|
||||
is_goal_complete: bool = False
|
||||
final_summary: str | None = None
|
||||
|
||||
def create_task(
|
||||
self,
|
||||
title: str,
|
||||
description: str = "",
|
||||
assignee: str | None = None,
|
||||
dependencies: list[str] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> SubTaskRecord:
|
||||
"""创建一个新任务并加入看板。Python 引擎和 Tool 均调用此方法。"""
|
||||
clean_deps = [d for d in (dependencies or []) if d.strip()]
|
||||
task = SubTaskRecord(
|
||||
title=title,
|
||||
description=description,
|
||||
assignee=assignee,
|
||||
dependencies=clean_deps,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
self.tasks.append(task)
|
||||
self._update_blocked_statuses()
|
||||
return task
|
||||
|
||||
def get_task(self, task_id: str) -> SubTaskRecord | None:
|
||||
return next(
|
||||
(t for t in self.tasks if t.id == task_id or t.title == task_id), None
|
||||
)
|
||||
|
||||
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
|
||||
task.status = status
|
||||
if result is not None:
|
||||
task.result = result
|
||||
self._update_blocked_statuses()
|
||||
return task
|
||||
|
||||
def _is_blocked(self, task: SubTaskRecord) -> bool:
|
||||
"""检查该任务是否有尚未完成的前置依赖"""
|
||||
if not task.dependencies:
|
||||
return False
|
||||
for dep_id in task.dependencies:
|
||||
dep = self.get_task(dep_id)
|
||||
if dep is None:
|
||||
return True
|
||||
if dep.status != TaskNodeStatus.completed:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_failed_dependency(self, task: SubTaskRecord) -> bool:
|
||||
"""检查该任务是否有已经失败的前置依赖"""
|
||||
if not task.dependencies:
|
||||
return False
|
||||
for dep_id in task.dependencies:
|
||||
dep = self.get_task(dep_id)
|
||||
if dep is not None and dep.status == TaskNodeStatus.failed:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _update_blocked_statuses(self) -> None:
|
||||
"""重新计算所有未终结任务的阻塞状态 (基于拓扑依赖)"""
|
||||
for task in self.tasks:
|
||||
if task.status == TaskNodeStatus.blocked:
|
||||
if self._has_failed_dependency(task):
|
||||
task.status = TaskNodeStatus.failed
|
||||
task.result = "自动标记失败: 前置依赖任务已失败。"
|
||||
elif not self._is_blocked(task):
|
||||
task.status = TaskNodeStatus.pending
|
||||
elif task.status == TaskNodeStatus.pending:
|
||||
if self._has_failed_dependency(task):
|
||||
task.status = TaskNodeStatus.failed
|
||||
task.result = "自动标记失败: 前置依赖任务已失败。"
|
||||
elif self._is_blocked(task):
|
||||
task.status = TaskNodeStatus.blocked
|
||||
|
||||
def get_available_tasks(
|
||||
self, for_assignee: str | None = None
|
||||
) -> list[SubTaskRecord]:
|
||||
"""获取所有当前无依赖阻塞、可立即执行的 Pending 任务"""
|
||||
available = []
|
||||
for task in self.tasks:
|
||||
if task.status != TaskNodeStatus.pending:
|
||||
continue
|
||||
if self._is_blocked(task):
|
||||
continue
|
||||
if for_assignee and task.assignee and task.assignee != for_assignee:
|
||||
continue
|
||||
available.append(task)
|
||||
return available
|
||||
|
||||
def all_terminal(self) -> bool:
|
||||
"""判断是否所有的任务都已经进入了终结状态(完成或失败)"""
|
||||
if not self.tasks:
|
||||
return False
|
||||
return all(
|
||||
t.status in (TaskNodeStatus.completed, TaskNodeStatus.failed)
|
||||
for t in self.tasks
|
||||
)
|
||||
|
||||
def render_board_to_string(self) -> str:
|
||||
"""渲染供 LLM 阅读的 Markdown 看板战报"""
|
||||
if not self.tasks:
|
||||
return "目前尚未创建任何任务。"
|
||||
|
||||
counts: dict[str, int] = {}
|
||||
for t in self.tasks:
|
||||
counts[t.status.value] = counts.get(t.status.value, 0) + 1
|
||||
|
||||
parts = [f"{v} {k}" for k, v in counts.items()]
|
||||
header = (
|
||||
f"### 📋 任务状态总览 (共计 {len(self.tasks)} 个任务: {', '.join(parts)}):"
|
||||
)
|
||||
|
||||
lines = [header]
|
||||
for t in self.tasks:
|
||||
status_str = t.status.value.upper()
|
||||
assignee_str = f" (指派给: {t.assignee})" if t.assignee else " (尚未指派)"
|
||||
lines.append(f" [{t.id}] {t.title} - {status_str}{assignee_str}")
|
||||
if t.dependencies:
|
||||
lines.append(f" 依赖于: {t.dependencies}")
|
||||
if t.result:
|
||||
result_preview = (
|
||||
t.result[:1000] + "..." if len(t.result) > 1000 else t.result
|
||||
)
|
||||
lines.append(f" 结果: {result_preview}")
|
||||
if t.notes:
|
||||
for note in t.notes[-3:]:
|
||||
lines.append(f" 附注: {note}")
|
||||
|
||||
if self.is_goal_complete and self.final_summary:
|
||||
lines.append(f"\n✅ 终极目标已标记完成: {self.final_summary}")
|
||||
|
||||
return "<current_task_state>\n" + "\n".join(lines) + "\n</current_task_state>"
|
||||
@@ -0,0 +1,247 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
import inspect
|
||||
import re
|
||||
from typing import Any, cast
|
||||
|
||||
from nonebot.utils import is_coroutine_callable
|
||||
|
||||
from zhenxun.services.ai.core.messages import AgentMessage
|
||||
from zhenxun.services.ai.core.templates import PromptTemplate
|
||||
from zhenxun.services.ai.flow.team.models import RouteDecision, Transition
|
||||
from zhenxun.services.ai.run import RunContext, Task
|
||||
from zhenxun.services.ai.run.di import DependencyInjector
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
|
||||
class BaseRouter(ABC):
|
||||
"""团队多智能体路由器基类"""
|
||||
|
||||
@abstractmethod
|
||||
async def route(
|
||||
self,
|
||||
context: RunContext,
|
||||
history: Sequence[AgentMessage],
|
||||
prompt: str | Task | None = None,
|
||||
) -> RouteDecision | None:
|
||||
"""核心路由方法"""
|
||||
pass
|
||||
|
||||
|
||||
class FunctionRouter(BaseRouter):
|
||||
"""基于纯函数的极速路由器"""
|
||||
|
||||
def __init__(self, selector_func: Callable[..., Any], target: str | None = None):
|
||||
"""
|
||||
初始化基于函数的极速路由器。
|
||||
|
||||
参数:
|
||||
selector_func: 用于进行路由判断的选择函数,返回布尔值或字符串目标名。
|
||||
target: 当选择函数返回 True 时,默认路由到的目标成员名称。
|
||||
"""
|
||||
self.selector_func = selector_func
|
||||
self.target = target
|
||||
|
||||
async def route(
|
||||
self,
|
||||
context: RunContext,
|
||||
history: Sequence[AgentMessage],
|
||||
prompt: str | Task | None = None,
|
||||
) -> RouteDecision | None:
|
||||
sig = inspect.signature(self.selector_func)
|
||||
call_kwargs = {"prompt": prompt, "context": context, "history": history}
|
||||
if isinstance(prompt, Task):
|
||||
call_kwargs["task"] = prompt
|
||||
|
||||
kwargs_resolved = await DependencyInjector.resolve_all(
|
||||
sig, call_kwargs, context
|
||||
)
|
||||
filtered_kwargs = {
|
||||
k: v for k, v in kwargs_resolved.items() if k in sig.parameters
|
||||
}
|
||||
|
||||
if is_coroutine_callable(self.selector_func):
|
||||
_async_func = cast(Callable[..., Awaitable[Any]], self.selector_func)
|
||||
selected_target = await _async_func(**filtered_kwargs)
|
||||
else:
|
||||
_sync_func = cast(Callable[..., Any], self.selector_func)
|
||||
selected_target = _sync_func(**filtered_kwargs)
|
||||
|
||||
if isinstance(selected_target, bool):
|
||||
if selected_target and self.target:
|
||||
logger.debug(f"命中函数极速路由 -> {self.target}")
|
||||
return RouteDecision(target_name=self.target, reason="")
|
||||
elif selected_target is not None and isinstance(selected_target, str):
|
||||
logger.debug(f"命中函数动态路由 -> {selected_target}")
|
||||
return RouteDecision(target_name=selected_target, reason="")
|
||||
return None
|
||||
|
||||
|
||||
class RegexRouter(BaseRouter):
|
||||
"""基于正则表达式的极速路由器"""
|
||||
|
||||
def __init__(self, pattern: str, target: str):
|
||||
"""
|
||||
初始化基于正则表达式的极速路由器。
|
||||
|
||||
参数:
|
||||
pattern: 正则表达式匹配规则。
|
||||
target: 当正则表达式成功匹配用户输入时路由到的目标成员名称。
|
||||
"""
|
||||
self.pattern = re.compile(pattern)
|
||||
self.target = target
|
||||
|
||||
async def route(
|
||||
self,
|
||||
context: RunContext,
|
||||
history: Sequence[AgentMessage],
|
||||
prompt: str | Task | None = None,
|
||||
) -> RouteDecision | None:
|
||||
text_to_match = (
|
||||
prompt.description
|
||||
if isinstance(prompt, Task)
|
||||
else (prompt or context.run.user_input or "")
|
||||
)
|
||||
|
||||
if self.pattern.search(text_to_match):
|
||||
logger.debug(f"命中正则极速路由 -> {self.target}")
|
||||
return RouteDecision(target_name=self.target, reason="")
|
||||
return None
|
||||
|
||||
|
||||
class ChainRouter(BaseRouter):
|
||||
"""责任链路由器:按顺序执行,直到其中一个命中"""
|
||||
|
||||
def __init__(self, routers: list[BaseRouter]):
|
||||
"""
|
||||
初始化责任链路由器。
|
||||
|
||||
参数:
|
||||
routers: 路由器实例列表,按顺序链式匹配,遇到首个命中的路由器即返回。
|
||||
"""
|
||||
self.routers = routers
|
||||
|
||||
async def route(
|
||||
self,
|
||||
context: RunContext,
|
||||
history: Sequence[AgentMessage],
|
||||
prompt: str | Task | None = None,
|
||||
) -> RouteDecision | None:
|
||||
for router in self.routers:
|
||||
decision = await router.route(context, history, prompt)
|
||||
if decision is not None:
|
||||
return decision
|
||||
return None
|
||||
|
||||
|
||||
class LLMRouter(BaseRouter):
|
||||
"""基于大模型的意图路由器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
team_name: str,
|
||||
members: list[Any],
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[Any] | None = None,
|
||||
state_flow: Mapping[str, Sequence[Transition | str]] | Callable | None = None,
|
||||
runtime_config: Any = None,
|
||||
custom_prompt: str | None = None,
|
||||
allowed_transitions: list[Transition] | None = None,
|
||||
):
|
||||
"""
|
||||
初始化基于大模型的意图路由器。
|
||||
|
||||
参数:
|
||||
team_name: 当前团队的名称标识。
|
||||
members: 团队的成员列表,包含 Agent, Team 或 Workflow。
|
||||
leader_model: 用于进行意图决策的路由器大模型名称,若为空则默认继承全局配置。
|
||||
leader_tools: 挂载给意图决策路由器的额外可用工具列表。
|
||||
state_flow: 状态流转规则字典或动态流转函数,定义智能体成员之间的转接路径。
|
||||
runtime_config: 团队级别的运行时全局配置。
|
||||
custom_prompt: 自定义的系统提示词模板,用以覆盖默认的路由系统指令。
|
||||
allowed_transitions: 允许的状态移交规则与前置条件列表。
|
||||
"""
|
||||
self.team_name = team_name
|
||||
self.members = members
|
||||
self.leader_model = leader_model
|
||||
self.leader_tools = leader_tools or []
|
||||
self.state_flow = state_flow
|
||||
self.runtime_config = runtime_config
|
||||
self.custom_prompt = custom_prompt
|
||||
self.allowed_transitions = allowed_transitions
|
||||
|
||||
async def route(
|
||||
self,
|
||||
context: RunContext,
|
||||
history: Sequence[AgentMessage],
|
||||
prompt: str | Task | None = None,
|
||||
) -> RouteDecision | None:
|
||||
from zhenxun.services.ai.flow.agent.agent import Agent
|
||||
from zhenxun.services.ai.flow.agent.models import AgentConfig
|
||||
from zhenxun.services.ai.flow.team.capabilities import TeamRoutingCapability
|
||||
|
||||
default_system_prompt = """## 角色与目标
|
||||
你是一个高级任务路由器 (所在团队: {{ team_name }})。
|
||||
请根据用户的输入意图,立刻调用相应的移交工具 (transfer_to_...)
|
||||
将对话物理转移给合适的专员处理。
|
||||
你必须且只能选择移交,不能自己作答。"""
|
||||
|
||||
if self.allowed_transitions:
|
||||
transitions_desc = "\n## 可用的移交目标及条件:\n"
|
||||
for t in self.allowed_transitions:
|
||||
desc = getattr(t, "description", "") or "无特定条件"
|
||||
transitions_desc += (
|
||||
f"- 移交至 [{getattr(t, 'target', 'unknown')}]:{desc}\n"
|
||||
)
|
||||
default_system_prompt += transitions_desc
|
||||
|
||||
template = self.custom_prompt or default_system_prompt
|
||||
route_prompt = PromptTemplate(template).render(team_name=self.team_name)
|
||||
|
||||
routing_cap = TeamRoutingCapability(
|
||||
team_name=self.team_name, members=self.members, state_flow=self.state_flow
|
||||
)
|
||||
|
||||
leader_config = AgentConfig(
|
||||
stateless=self.runtime_config.stateless if self.runtime_config else True,
|
||||
enable_hitl=getattr(self.runtime_config, "leader_enable_hitl", False),
|
||||
)
|
||||
|
||||
target_model = self.leader_model
|
||||
if not target_model:
|
||||
for m in self.members:
|
||||
if m_model := getattr(m, "model_name", None) or getattr(
|
||||
m, "model", None
|
||||
):
|
||||
target_model = m_model
|
||||
break
|
||||
|
||||
router_agent = Agent(
|
||||
name=f"{self.team_name}_Router",
|
||||
instruction=route_prompt,
|
||||
model=target_model,
|
||||
tools=self.leader_tools,
|
||||
config=leader_config,
|
||||
)
|
||||
|
||||
sub_context = context.clone_for_member(router_agent.name)
|
||||
sub_context.capabilities = list(sub_context.capabilities)
|
||||
sub_context.capabilities.append(routing_cap)
|
||||
|
||||
logger.debug("🤖 [LLMRouter] 启动 LLM 思考路由决策...")
|
||||
|
||||
res = await router_agent.run(
|
||||
prompt=prompt,
|
||||
context=sub_context,
|
||||
config=AgentConfig(message_history=history),
|
||||
)
|
||||
if res.handoff:
|
||||
logger.debug(f"🤖 [LLMRouter] 决策完毕: 移交给 -> {res.handoff.target}")
|
||||
return RouteDecision(
|
||||
target_name=res.handoff.target,
|
||||
reason=res.handoff.reason,
|
||||
context_data=res.handoff.context_data,
|
||||
)
|
||||
|
||||
logger.warning("🤖 [LLMRouter] LLM 没有调用移交工具,放弃路由。")
|
||||
return None
|
||||
@@ -0,0 +1,235 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.ai.core.exceptions import (
|
||||
AbortException,
|
||||
ControlFlowExit,
|
||||
LLMException,
|
||||
)
|
||||
from zhenxun.services.ai.core.messages import UsageInfo
|
||||
from zhenxun.services.ai.flow.team.capabilities import TeamRoutingCapability
|
||||
from zhenxun.services.ai.flow.team.models import (
|
||||
CallAction,
|
||||
ConcurrentCallAction,
|
||||
FinishAction,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class TeamRunner:
|
||||
"""
|
||||
多智能体团队核心执行引擎。
|
||||
"""
|
||||
|
||||
def __init__(self, team: Any, strategy: BaseTeamStrategy):
|
||||
self.team = team
|
||||
self.strategy = strategy
|
||||
|
||||
async def _execute_call_action_to_queue(
|
||||
self,
|
||||
index: int,
|
||||
action: CallAction,
|
||||
context: RunContext,
|
||||
session_id: str,
|
||||
queue: asyncio.Queue,
|
||||
):
|
||||
"""
|
||||
辅助方法:执行单一 Agent 任务,
|
||||
并将内部产生的 UI 事件与最终结果通过队列透传回主线程
|
||||
"""
|
||||
if isinstance(action.agent, str):
|
||||
target_agent = next(
|
||||
(m for m in self.team.members if m.name == action.agent), None
|
||||
)
|
||||
if not target_agent:
|
||||
logger.error(f"❌ [TeamRunner] 找不到团队成员: {action.agent}")
|
||||
await queue.put(
|
||||
(
|
||||
"result",
|
||||
(
|
||||
action.agent,
|
||||
AgentRunResult(
|
||||
output=f"Error: {action.agent} not found",
|
||||
usage=UsageInfo(),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return
|
||||
else:
|
||||
target_agent = action.agent
|
||||
|
||||
sub_context = context.clone_for_member(target_agent.name)
|
||||
sub_context.capabilities = list(sub_context.capabilities)
|
||||
|
||||
from zhenxun.services.ai.flow.team.strategy import RouteStrategy
|
||||
|
||||
if isinstance(self.strategy, RouteStrategy):
|
||||
routing_cap = TeamRoutingCapability(
|
||||
team_name=self.team.name,
|
||||
members=self.team.members,
|
||||
state_flow=getattr(self.strategy, "state_flow", None),
|
||||
)
|
||||
sub_context.capabilities.append(routing_cap)
|
||||
|
||||
logger.debug(f"🚀 **专员 👨💼`{target_agent.name}`** 开始执行子任务...")
|
||||
|
||||
agent_res = None
|
||||
|
||||
try:
|
||||
from zhenxun.services.ai.flow.agent.models import AgentConfig
|
||||
|
||||
async with target_agent.run_stream(
|
||||
prompt=action.task,
|
||||
context=sub_context,
|
||||
config=AgentConfig(message_history=action.history),
|
||||
**(action.kwargs or {}),
|
||||
) as stream_result:
|
||||
async for event in stream_result.stream_events():
|
||||
if isinstance(event, AgentRunEnd):
|
||||
agent_res = event.result
|
||||
else:
|
||||
await queue.put(("yield_event", event))
|
||||
except ControlFlowExit as cfe:
|
||||
if isinstance(cfe, AbortException):
|
||||
await queue.put(("control_flow_error", cfe))
|
||||
return
|
||||
else:
|
||||
logger.debug(
|
||||
f"Agent {target_agent.name} 触发局部控制流: "
|
||||
f"{type(cfe).__name__} - {cfe}"
|
||||
)
|
||||
agent_res = AgentRunResult(output=str(cfe), usage=UsageInfo())
|
||||
except Exception as e:
|
||||
logger.error(f"Agent {target_agent.name} 执行崩溃: {e}")
|
||||
|
||||
if isinstance(e, LLMException):
|
||||
abort_msg = getattr(e, "user_friendly_message", str(e))
|
||||
display_msg = (
|
||||
f"❌ 智能体 {target_agent.name} 执行发生致命故障: {abort_msg}"
|
||||
)
|
||||
abort_err = AbortException(
|
||||
reason=str(e),
|
||||
display=display_msg,
|
||||
)
|
||||
await queue.put(("control_flow_error", abort_err))
|
||||
return
|
||||
|
||||
agent_res = AgentRunResult(output=f"Error: {e}", usage=UsageInfo())
|
||||
|
||||
if agent_res and agent_res.handoff:
|
||||
target_name = agent_res.handoff.target
|
||||
reason = agent_res.handoff.reason
|
||||
|
||||
logger.info(
|
||||
f"🛣️ **路由决策**: 委派给专员 👨💼`{target_name}` (理由: {reason})"
|
||||
)
|
||||
|
||||
if not agent_res:
|
||||
agent_res = AgentRunResult(
|
||||
output="Error: No result returned", usage=UsageInfo()
|
||||
)
|
||||
|
||||
logger.debug(f"✅ **专员 👨💼`{target_agent.name}`** 完成任务!")
|
||||
|
||||
await queue.put(("result", index, target_agent.name, agent_res))
|
||||
|
||||
async def run_stream(
|
||||
self, prompt: Any, context: RunContext, **kwargs: Any
|
||||
) -> AsyncGenerator[Any, None]:
|
||||
session_id = context.session_id or "default_team_session"
|
||||
task_desc = getattr(prompt, "description", str(prompt))
|
||||
|
||||
logger.info(f"🤝 **团队 [{self.team.name}] 开始协作**: `{task_desc}`")
|
||||
|
||||
plan_gen = self.strategy.generate_plan(self.team, prompt, context, **kwargs)
|
||||
|
||||
send_value = None
|
||||
final_result = None
|
||||
cumulative_usage = UsageInfo()
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
action = await plan_gen.asend(send_value)
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
if isinstance(action, CallAction):
|
||||
queue = asyncio.Queue()
|
||||
task = asyncio.create_task(
|
||||
self._execute_call_action_to_queue(
|
||||
0, action, context, session_id, queue
|
||||
)
|
||||
)
|
||||
try:
|
||||
while True:
|
||||
msg_type, *payload = await queue.get()
|
||||
if msg_type == "yield_event":
|
||||
yield payload[0]
|
||||
elif msg_type == "control_flow_error":
|
||||
raise payload[0]
|
||||
elif msg_type == "result":
|
||||
idx, agent_name, agent_res = payload
|
||||
send_value = agent_res
|
||||
cumulative_usage += agent_res.usage
|
||||
break
|
||||
finally:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
||||
elif isinstance(action, ConcurrentCallAction):
|
||||
queue = asyncio.Queue()
|
||||
tasks = []
|
||||
for i, act in enumerate(action.actions):
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
self._execute_call_action_to_queue(
|
||||
i, act, context, session_id, queue
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
results_dict = {}
|
||||
try:
|
||||
while len(results_dict) < len(action.actions):
|
||||
msg_type, *payload = await queue.get()
|
||||
if msg_type == "yield_event":
|
||||
yield payload[0]
|
||||
elif msg_type == "control_flow_error":
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
raise payload[0]
|
||||
elif msg_type == "result":
|
||||
idx, agent_name, agent_res = payload
|
||||
results_dict[idx] = (agent_name, agent_res)
|
||||
cumulative_usage += agent_res.usage
|
||||
send_value = [
|
||||
results_dict[i] for i in range(len(action.actions))
|
||||
]
|
||||
finally:
|
||||
for task in tasks:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
||||
elif isinstance(action, FinishAction):
|
||||
final_result = action.result
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"TeamRunner 遇到了未知的动作类型: {type(action)}")
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
logger.info(f"🏁 **团队 [{self.team.name}]** 协作圆满结束!")
|
||||
|
||||
if not isinstance(final_result, AgentRunResult):
|
||||
final_result = AgentRunResult(output=final_result, usage=cumulative_usage)
|
||||
else:
|
||||
final_result.usage += cumulative_usage
|
||||
|
||||
yield AgentRunEnd(result=final_result)
|
||||
@@ -0,0 +1,641 @@
|
||||
from abc import ABC
|
||||
from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.services.ai.core.exceptions import AbortException
|
||||
from zhenxun.services.ai.core.messages import AgentMessage, LLMMessage
|
||||
from zhenxun.services.ai.core.stream_events import ToolStreamChunkEvent
|
||||
from zhenxun.services.ai.core.templates import PromptTemplate
|
||||
from zhenxun.services.ai.flow.agent.agent import Agent, ToolSource
|
||||
from zhenxun.services.ai.flow.agent.models import AgentConfig
|
||||
from zhenxun.services.ai.flow.team.models import (
|
||||
CallAction,
|
||||
ConcurrentCallAction,
|
||||
FinishAction,
|
||||
TeamAction,
|
||||
)
|
||||
from zhenxun.services.ai.flow.team.router import BaseRouter
|
||||
from zhenxun.services.ai.run import RunContext, Task
|
||||
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from zhenxun.services.ai.flow.team.team import Team
|
||||
|
||||
|
||||
class BaseTeamStrategy(ABC):
|
||||
"""多智能体团队协作策略基类"""
|
||||
|
||||
default_system_prompt: str = ""
|
||||
|
||||
def __init__(self, custom_prompt: str | None = None):
|
||||
"""
|
||||
多智能体团队协作策略基类初始化。
|
||||
|
||||
参数:
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的团队系统提示词模板。
|
||||
"""
|
||||
self.custom_prompt = custom_prompt
|
||||
|
||||
def get_prompt(self, **kwargs) -> str:
|
||||
template = self.custom_prompt or self.default_system_prompt
|
||||
return PromptTemplate(template).render(**kwargs)
|
||||
|
||||
async def generate_plan(
|
||||
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
|
||||
) -> AsyncGenerator[TeamAction, Any]:
|
||||
"""
|
||||
核心决策生成器 (Action Yielding Pattern)。
|
||||
|
||||
第三方开发者只需重写此方法:
|
||||
1. 使用 `yield CallAction(...)` 派发任务,系统会自动拦截并执行,
|
||||
然后将 `AgentRunResult` 通过 .asend() 传回。
|
||||
2. 使用 `yield FinishAction(...)` 结束团队协作。
|
||||
|
||||
"""
|
||||
yield FinishAction(
|
||||
result="The Strategy has not implemented generate_plan() yet."
|
||||
)
|
||||
|
||||
def _build_leader_agent(
|
||||
self, team: "Team", name: str, instruction: str, tools: list[ToolSource]
|
||||
) -> Agent:
|
||||
"""
|
||||
统一的团队 Leader / Planner 装配工厂。
|
||||
自动处理无状态配置以及 HITL 状态继承。
|
||||
"""
|
||||
leader_config = AgentConfig(
|
||||
stateless=team.runtime_config.stateless if team.runtime_config else True,
|
||||
enable_hitl=getattr(team.runtime_config, "leader_enable_hitl", False),
|
||||
)
|
||||
|
||||
target_model = getattr(self, "leader_model", None) or getattr(
|
||||
team, "model", None
|
||||
)
|
||||
if not target_model:
|
||||
for m in team.members:
|
||||
if m_model := getattr(m, "model_name", None) or getattr(
|
||||
m, "model", None
|
||||
):
|
||||
target_model = m_model
|
||||
break
|
||||
|
||||
return Agent(
|
||||
name=name,
|
||||
instruction=instruction,
|
||||
model=target_model,
|
||||
tools=tools,
|
||||
config=leader_config,
|
||||
)
|
||||
|
||||
|
||||
class RouteStrategy(BaseTeamStrategy):
|
||||
"""路由策略:基于挂载的 Router 进行最合适的专家分发"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state_flow: "Mapping[str, Sequence[str | Any]] | Callable | None" = None,
|
||||
selector_func: Callable[..., str | None] | None = None,
|
||||
router: BaseRouter | None = None,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
):
|
||||
"""
|
||||
路由策略初始化,基于挂载的 Router 进行最合适的专家分发。
|
||||
|
||||
参数:
|
||||
state_flow: 状态流转规则字典或动态函数,定义成员之间控制流的物理走向。
|
||||
selector_func: 极速硬路由的静态选择函数,返回目标智能体名称。
|
||||
router: 自定义的动态路由器实例 (如 LLMRouter, RegexRouter 等)。
|
||||
leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。
|
||||
"""
|
||||
super().__init__(custom_prompt=custom_prompt)
|
||||
self.selector_func = selector_func
|
||||
self.router = router
|
||||
self.leader_model = leader_model
|
||||
self.leader_tools = leader_tools or []
|
||||
|
||||
if isinstance(state_flow, dict):
|
||||
from zhenxun.services.ai.flow.team.models import Transition
|
||||
|
||||
normalized_flow = {}
|
||||
for k, targets in state_flow.items():
|
||||
normalized_targets = []
|
||||
for t in targets:
|
||||
if isinstance(t, str):
|
||||
normalized_targets.append(Transition(target=t))
|
||||
else:
|
||||
normalized_targets.append(t)
|
||||
normalized_flow[k] = normalized_targets
|
||||
self.state_flow = normalized_flow
|
||||
else:
|
||||
self.state_flow = state_flow
|
||||
|
||||
async def generate_plan(
|
||||
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
|
||||
) -> AsyncGenerator[TeamAction, Any]:
|
||||
router = self.router
|
||||
if not router:
|
||||
from .router import ChainRouter, FunctionRouter, LLMRouter
|
||||
|
||||
routers = []
|
||||
if self.selector_func:
|
||||
routers.append(FunctionRouter(self.selector_func))
|
||||
routers.append(
|
||||
LLMRouter(
|
||||
team_name=team.name,
|
||||
members=team.members,
|
||||
leader_model=self.leader_model,
|
||||
leader_tools=self.leader_tools,
|
||||
state_flow=self.state_flow,
|
||||
runtime_config=getattr(team, "runtime_config", None),
|
||||
custom_prompt=self.custom_prompt,
|
||||
)
|
||||
)
|
||||
router = ChainRouter(routers)
|
||||
|
||||
cycle_count = 0
|
||||
exec_config = kwargs.get("config")
|
||||
max_cycles = getattr(exec_config, "max_cycles", 15) if exec_config else 15
|
||||
|
||||
logger.info(f"🛣️ [RouteStrategy] '{team.name}' 正在获取初始路由决策...")
|
||||
|
||||
decision = await router.route(context, [], prompt)
|
||||
if not decision:
|
||||
logger.warning(
|
||||
f"🚨 [RouteStrategy] Team '{team.name}' 的所有路由策略未能命中目标。"
|
||||
)
|
||||
raise AbortException(
|
||||
reason=f"Team '{team.name}' 无法找到合适的路由节点处理该任务",
|
||||
display="🚨 团队协作失败,无法分配任务。",
|
||||
)
|
||||
|
||||
current_target = decision.target_name
|
||||
handoff_reason = decision.reason
|
||||
context_data = decision.context_data
|
||||
|
||||
while True:
|
||||
cycle_count += 1
|
||||
if cycle_count > max_cycles:
|
||||
logger.error(
|
||||
f"🚨 [RouteStrategy] Team '{team.name}' 路由陷入死循环!"
|
||||
f"已达到最大限制 {max_cycles} 次。"
|
||||
)
|
||||
raise AbortException(
|
||||
reason=(
|
||||
f"Team '{team.name}' 路由流转超过最大次数限制"
|
||||
f" ({max_cycles}次),"
|
||||
"已强制熔断。"
|
||||
),
|
||||
display="🚨 团队协作陷入死循环,已被系统强制中断。",
|
||||
)
|
||||
|
||||
handoff_history_messages: list[AgentMessage] = []
|
||||
upstream_info = []
|
||||
if handoff_reason:
|
||||
upstream_info.append(f"【移交说明】\n{handoff_reason}")
|
||||
if context_data:
|
||||
if isinstance(context_data, dict):
|
||||
import json
|
||||
|
||||
formatted_data = json.dumps(
|
||||
context_data, ensure_ascii=False, indent=2
|
||||
)
|
||||
upstream_info.append(
|
||||
f"【结构化上下文载荷】\n```json\n{formatted_data}\n```"
|
||||
)
|
||||
else:
|
||||
upstream_info.append(f"【核心上下文数据】\n{context_data}")
|
||||
combined_info = "\n\n".join(upstream_info)
|
||||
|
||||
if combined_info:
|
||||
handoff_msg = LLMMessage.system(
|
||||
f"### 🔄 [来自上游节点的移交数据]\n{combined_info}"
|
||||
)
|
||||
handoff_history_messages.append(handoff_msg)
|
||||
|
||||
run_result = yield CallAction(
|
||||
agent=current_target,
|
||||
task=prompt,
|
||||
history=handoff_history_messages,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
if run_result.handoff:
|
||||
current_target = run_result.handoff.target
|
||||
handoff_reason = run_result.handoff.reason
|
||||
context_data = run_result.handoff.context_data
|
||||
continue
|
||||
|
||||
output_str = str(run_result.output)
|
||||
|
||||
fast_routed = False
|
||||
if isinstance(self.state_flow, dict) and current_target in self.state_flow:
|
||||
for t in self.state_flow[current_target]:
|
||||
trigger_regex = getattr(t, "trigger_regex", None)
|
||||
if trigger_regex:
|
||||
import re
|
||||
|
||||
if re.search(trigger_regex, output_str):
|
||||
current_target = getattr(t, "target", current_target)
|
||||
handoff_reason = ""
|
||||
context_data = output_str
|
||||
fast_routed = True
|
||||
break
|
||||
trigger_func = getattr(t, "trigger_func", None)
|
||||
if trigger_func:
|
||||
try:
|
||||
if trigger_func(output_str):
|
||||
current_target = getattr(t, "target", current_target)
|
||||
handoff_reason = ""
|
||||
context_data = output_str
|
||||
fast_routed = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if fast_routed:
|
||||
logger.info(
|
||||
f"🛣️ **路由决策**: 委派给专员 👨💼`{current_target}`"
|
||||
"(系统拦截:正则/函数状态流发生转移)"
|
||||
)
|
||||
continue
|
||||
|
||||
yield FinishAction(result=run_result.output)
|
||||
break
|
||||
|
||||
|
||||
class CoordinateStrategy(BaseTeamStrategy):
|
||||
"""协作策略:Leader 自主规划,委派任务给 Sub-Agents 并汇总结果"""
|
||||
|
||||
default_system_prompt = """## 角色与目标
|
||||
你是一个多智能体团队的协调者(Leader)。
|
||||
你可以使用你自身携带的工具先查阅、收集资料;也可以分析用户的目标将其拆解为子任务,并委派给合适的下属专员。
|
||||
当你收集齐所有需要的信息或专员报告后,请汇总生成最终回复向用户汇报。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
):
|
||||
"""
|
||||
协作策略初始化,Leader 主动拆解任务,委派给 Sub-Agents 并汇总结果。
|
||||
|
||||
参数:
|
||||
leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给协调节点 (Leader) 的专属工具列表。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。
|
||||
"""
|
||||
super().__init__(custom_prompt=custom_prompt)
|
||||
self.leader_model = leader_model
|
||||
self.leader_tools = leader_tools or []
|
||||
|
||||
async def generate_plan(
|
||||
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
|
||||
) -> AsyncGenerator[TeamAction, Any]:
|
||||
delegation_tools = []
|
||||
for m in team.members:
|
||||
persona = getattr(m, "persona", None)
|
||||
desc = getattr(m, "description", "") or "处理节点"
|
||||
if persona and not isinstance(persona, dict):
|
||||
desc = f"角色:{persona.role},目标:{persona.goal}"
|
||||
|
||||
delegation_tools.append(
|
||||
DelegateTool(
|
||||
runnable=m,
|
||||
name=f"delegate_to_{m.name}",
|
||||
description=f"将子任务委派给专员 [{m.name}] 处理。专长:{desc}",
|
||||
)
|
||||
)
|
||||
|
||||
leader_tools = self.leader_tools.copy()
|
||||
leader_tools.extend(delegation_tools)
|
||||
|
||||
leader_agent = self._build_leader_agent(
|
||||
team=team,
|
||||
name=f"{team.name}_Leader",
|
||||
instruction=self.get_prompt(),
|
||||
tools=leader_tools,
|
||||
)
|
||||
|
||||
logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...")
|
||||
|
||||
if context.run.event_bus:
|
||||
await context.run.event_bus.emit(
|
||||
ToolStreamChunkEvent(
|
||||
tool_name="Team Leader",
|
||||
content="✨ 团队 Leader 正在汇总各方报告...",
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(f"👨💼 [CoordinateStrategy] '{team.name}' 正在启动协调推理循环...")
|
||||
|
||||
leader_res = yield CallAction(agent=leader_agent, task=prompt)
|
||||
|
||||
yield FinishAction(result=leader_res.output)
|
||||
|
||||
|
||||
class BroadcastStrategy(BaseTeamStrategy):
|
||||
"""广播策略:并发让所有成员处理同一个任务,最后由 Leader 总结"""
|
||||
|
||||
default_system_prompt = """## 角色与目标
|
||||
你是一个多智能体团队的总结者(Leader)。
|
||||
以下是各位专家的独立处理结果,请融合各方观点,取长补短,给出一份最终的总结报告。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
):
|
||||
"""
|
||||
广播策略初始化,并发让所有成员处理同一个任务,最后由 Leader 总结。
|
||||
|
||||
参数:
|
||||
leader_model: 总结节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给总结节点 (Leader) 的专属工具列表。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的广播总结系统提示词。
|
||||
"""
|
||||
super().__init__(custom_prompt=custom_prompt)
|
||||
self.leader_model = leader_model
|
||||
self.leader_tools = leader_tools or []
|
||||
|
||||
async def generate_plan(
|
||||
self, team: "Team", prompt: str | Task | None, context: RunContext, **kwargs
|
||||
) -> AsyncGenerator[TeamAction, Any]:
|
||||
task_desc_str = (
|
||||
prompt.description if isinstance(prompt, Task) else (prompt or "")
|
||||
)
|
||||
|
||||
if context.run.event_bus:
|
||||
await context.run.event_bus.emit(
|
||||
ToolStreamChunkEvent(
|
||||
tool_name="Team Broadcaster",
|
||||
content=f"🚀 正在并发广播任务给 {len(team.members)} 位专家...",
|
||||
)
|
||||
)
|
||||
|
||||
actions = [CallAction(agent=m.name, task=task_desc_str) for m in team.members]
|
||||
results = yield ConcurrentCallAction(actions=actions)
|
||||
|
||||
if context.run.event_bus:
|
||||
await context.run.event_bus.emit(
|
||||
ToolStreamChunkEvent(
|
||||
tool_name="Team Leader",
|
||||
content="✨ 所有专家汇报完毕,Leader 正在融合各方观点...",
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...")
|
||||
|
||||
summary_text = "\n\n".join(
|
||||
[f"### 【{name} 的意见】:\n{res.output}" for name, res in results]
|
||||
)
|
||||
|
||||
synthesize_prompt = (
|
||||
f"**用户原始任务**: {task_desc_str}\n\n"
|
||||
f"以下是各位专家的独立处理结果,请融合各方观点,给出一份最终的总结报告:\n\n"
|
||||
f"{summary_text}"
|
||||
)
|
||||
|
||||
leader_agent = self._build_leader_agent(
|
||||
team=team,
|
||||
name=f"{team.name}_Leader",
|
||||
instruction=self.get_prompt(),
|
||||
tools=self.leader_tools,
|
||||
)
|
||||
|
||||
leader_res = yield CallAction(agent=leader_agent, task=synthesize_prompt)
|
||||
|
||||
yield FinishAction(result=leader_res.output)
|
||||
|
||||
|
||||
class TaskStrategy(BaseTeamStrategy):
|
||||
"""任务规划策略:Leader 利用工具箱在黑板上拆解任务、管理依赖并驱动 Member 执行"""
|
||||
|
||||
default_system_prompt = """<how_to_respond>
|
||||
你是一个多智能体团队的项目经理(Planner)。
|
||||
请仔细阅读用户的请求,将其拆解为一个个具体的子任务,
|
||||
并利用 `create_task` 建立所有任务和依赖关系(注意 `assignee` 必须严格从下方的团队成员中选择)。
|
||||
【⚠️核心执行流规范】
|
||||
1. 分配完毕后,**必须立刻停止调用任何工具,并直接输出纯文本回复**
|
||||
(如:'任务已分配,等待执行'),从而结束你的当前回合。
|
||||
2. 当底层自动执行完毕后,系统会再次唤醒你并提供最新的看板结果。
|
||||
请根据结果决定是下发新任务、要求重做,还是调用 `mark_all_complete` 汇报总结。
|
||||
</how_to_respond>""" # noqa: E501
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
max_iterations: int = 15,
|
||||
blackboard_schema: type[BaseModel] | None = None,
|
||||
initial_blackboard_state: BaseModel | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
):
|
||||
"""
|
||||
任务规划策略初始化,Leader 利用工具箱在黑板上拆解任务、管理依赖并
|
||||
驱动 Member 执行。
|
||||
|
||||
参数:
|
||||
leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。
|
||||
max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。
|
||||
blackboard_schema: 团队共享黑板的数据结构类型 (Pydantic Model 类)。
|
||||
initial_blackboard_state: 共享黑板的初始数据状态实例。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。
|
||||
"""
|
||||
super().__init__(custom_prompt=custom_prompt)
|
||||
self.leader_model = leader_model
|
||||
self.leader_tools = leader_tools or []
|
||||
self.max_iterations = max_iterations
|
||||
|
||||
self.blackboard = None
|
||||
self.bb_toolkit = None
|
||||
if blackboard_schema is not None:
|
||||
from zhenxun.services.ai.run.blackboard import BlackboardManager
|
||||
from zhenxun.services.ai.tools.providers.builtin.blackboard import (
|
||||
BlackboardToolkit,
|
||||
)
|
||||
|
||||
self.blackboard = BlackboardManager(
|
||||
schema=blackboard_schema, initial_state=initial_blackboard_state
|
||||
)
|
||||
self.bb_toolkit = BlackboardToolkit(self.blackboard)
|
||||
self.leader_tools.append(self.bb_toolkit)
|
||||
|
||||
async def generate_plan(
|
||||
self, team: "Team", prompt: str | Task | 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
|
||||
|
||||
if self.blackboard is not None:
|
||||
context.session.blackboard = self.blackboard
|
||||
|
||||
if self.bb_toolkit:
|
||||
for m in team.members:
|
||||
if not hasattr(m, "tool_definitions"):
|
||||
setattr(m, "tool_definitions", [])
|
||||
|
||||
m_tools = getattr(m, "tool_definitions")
|
||||
if self.bb_toolkit not in m_tools:
|
||||
m_tools.append(self.bb_toolkit)
|
||||
|
||||
member_infos = []
|
||||
for m in team.members:
|
||||
desc = getattr(m, "description", "") or "处理节点"
|
||||
persona = getattr(m, "persona", None)
|
||||
if persona and not isinstance(persona, dict):
|
||||
desc = f"角色:{persona.role},目标:{persona.goal}"
|
||||
member_infos.append(
|
||||
f'<member id="{m.name}" name="{m.name}">\n'
|
||||
f" Description: {desc}\n"
|
||||
f"</member>"
|
||||
)
|
||||
|
||||
members_xml = "<team_members>\n" + "\n".join(member_infos) + "\n</team_members>"
|
||||
|
||||
final_instruction = self.get_prompt() + "\n\n" + members_xml
|
||||
|
||||
task_toolkit = TaskPlanningToolkit(members=team.members)
|
||||
|
||||
leader_tools = self.leader_tools.copy()
|
||||
leader_tools.append(task_toolkit)
|
||||
|
||||
leader_agent = self._build_leader_agent(
|
||||
team=team,
|
||||
name=f"{team.name}_Planner",
|
||||
instruction=final_instruction,
|
||||
tools=leader_tools,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"📋 [TaskStrategy] '{team.name}' 正在启动 Engine-Driven 状态机循环..."
|
||||
)
|
||||
|
||||
if "__task_board__" not in context.session.shared_state:
|
||||
context.session.shared_state["__task_board__"] = (
|
||||
self.blackboard._state if self.blackboard else TaskBoardState()
|
||||
)
|
||||
board = cast(TaskBoardState, context.session.shared_state["__task_board__"])
|
||||
|
||||
max_iterations = self.max_iterations
|
||||
planner_prompt = prompt
|
||||
|
||||
for iteration in range(max_iterations):
|
||||
if board.is_goal_complete:
|
||||
yield FinishAction(result=board.final_summary or "目标已标记完成。")
|
||||
return
|
||||
|
||||
available_tasks = board.get_available_tasks()
|
||||
|
||||
if not available_tasks:
|
||||
if iteration > 0:
|
||||
board_str = board.render_board_to_string()
|
||||
goal_str = getattr(prompt, "description", None) or (
|
||||
str(prompt) if prompt else ""
|
||||
)
|
||||
planner_prompt = f"""### 🎯 用户的终极目标 (Original Goal)
|
||||
{goal_str}
|
||||
|
||||
### 📋 当前看板最新状态
|
||||
{board_str}
|
||||
|
||||
**系统指令**:底层执行引擎的回合已结束。当前没有可立即执行的 pending 任务。
|
||||
请检查是否有 failed 的任务需要修复重新指派?或者如果所有任务均已 completed,
|
||||
请立刻调用 `mark_all_complete` 汇报总结。"""
|
||||
|
||||
logger.info(f"🧠 [TaskStrategy] 唤醒 Planner (Iter: {iteration})")
|
||||
leader_res = yield CallAction(agent=leader_agent, task=planner_prompt)
|
||||
|
||||
if board.is_goal_complete:
|
||||
yield FinishAction(result=board.final_summary or leader_res.output)
|
||||
return
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"🚀 [TaskStrategy] 引擎接管:并发执行 {len(available_tasks)} 个任务..."
|
||||
)
|
||||
actions = []
|
||||
valid_tasks = []
|
||||
|
||||
for task in available_tasks:
|
||||
member_agent = next(
|
||||
(m for m in team.members if m.name == task.assignee), None
|
||||
)
|
||||
if not member_agent:
|
||||
board.update_task_status(
|
||||
task.id,
|
||||
TaskNodeStatus.failed,
|
||||
f"执行异常: 找不到名为 '{task.assignee}' 的专家。",
|
||||
)
|
||||
continue
|
||||
|
||||
board.update_task_status(task.id, TaskNodeStatus.in_progress)
|
||||
logger.debug(f" 🔄 [任务状态变更] `{task.title}` -> in_progress")
|
||||
|
||||
task_prompt = f"### 🎯 你被指派的任务目标:\n{task.description}"
|
||||
|
||||
if task.result:
|
||||
task_prompt += (
|
||||
f"\n\n### 💡 项目经理的补充建议/历史反馈:\n{task.result}"
|
||||
)
|
||||
if task.dependencies:
|
||||
dep_results = []
|
||||
for dep_id in task.dependencies:
|
||||
dep_task = board.get_task(dep_id)
|
||||
if dep_task and dep_task.result:
|
||||
dep_results.append(
|
||||
f"【前置任务 [{dep_task.title}] 的产出】:\n"
|
||||
f"{dep_task.result}"
|
||||
)
|
||||
if dep_results:
|
||||
task_prompt += (
|
||||
"\n\n### 📦 你的任务依赖以下前置结果,请基于此进行处理:\n"
|
||||
+ "\n\n".join(dep_results)
|
||||
)
|
||||
|
||||
if task.metadata:
|
||||
import json
|
||||
|
||||
meta_str = json.dumps(task.metadata, ensure_ascii=False)
|
||||
task_prompt += f"\n\n### ⚙️ 附加系统元数据约束:\n{meta_str}"
|
||||
|
||||
actions.append(CallAction(agent=member_agent.name, task=task_prompt))
|
||||
valid_tasks.append(task)
|
||||
|
||||
if not actions:
|
||||
continue
|
||||
|
||||
results = yield ConcurrentCallAction(actions=actions)
|
||||
|
||||
for task, (agent_name, agent_res) in zip(valid_tasks, results):
|
||||
if isinstance(agent_res, BaseException):
|
||||
output_str = f"❌ 专家框架级崩溃: {agent_res}"
|
||||
board.update_task_status(task.id, TaskNodeStatus.failed, output_str)
|
||||
final_status = "failed"
|
||||
else:
|
||||
output_str = str(agent_res.output)
|
||||
if output_str.startswith("Error:") or output_str.startswith("❌"):
|
||||
board.update_task_status(
|
||||
task.id, TaskNodeStatus.failed, output_str
|
||||
)
|
||||
final_status = "failed"
|
||||
else:
|
||||
board.update_task_status(
|
||||
task.id, TaskNodeStatus.completed, output_str
|
||||
)
|
||||
final_status = "completed"
|
||||
|
||||
logger.debug(f" 🔄 [任务状态变更] `{task.title}` -> {final_status}")
|
||||
|
||||
yield FinishAction(
|
||||
result=f"达到最大迭代次数 ({max_iterations}),任务未能在限定步数内完成。"
|
||||
)
|
||||
@@ -0,0 +1,196 @@
|
||||
from typing import Annotated, Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from zhenxun.services.ai.flow.base import BaseRunnable
|
||||
from zhenxun.services.ai.run.context import RunContext
|
||||
from zhenxun.services.ai.tools.core.decorators import tool
|
||||
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
|
||||
from zhenxun.services.ai.tools.models import ToolResult
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .models import TaskBoardState, TaskNodeStatus
|
||||
|
||||
|
||||
class TaskPlanningToolkit(BaseToolkit):
|
||||
"""
|
||||
任务规划工具箱 (Planner Toolkit)。
|
||||
大模型专用的黑板操作工具。大模型被剥夺了执行权,仅能拆解、指派和总结任务。
|
||||
"""
|
||||
|
||||
default_prefix = ""
|
||||
|
||||
default_instructions = """<instructions>
|
||||
## 🛠️ 任务规划工作流指南
|
||||
你现在的角色是**项目经理 (Planner)**。你的唯一职责是拆解任务、分配人员并监控看板状态,**系统底层会自动拉起专家执行任务**。
|
||||
1. **规划**:使用 `create_task` 拆解任务,设定 `assignee`(专家名称)和 `depends_on`(依赖的其它任务ID)。
|
||||
2. **等待与监控**:每次你创建或更新任务后,请立刻停止工具调用,系统引擎会自动并发执行 pending 任务并再次唤醒你。
|
||||
3. **🩹 智能自愈与重试**:如果你被唤醒后,看到看板上有任务处于 `failed` 状态,
|
||||
请仔细阅读失败结果 (result)。你可以通过 `update_task_status` 将该任务的状态重新修改为 `pending`
|
||||
以触发重新执行(可以附带修改建议在 result 里),或者创建新任务替代它。
|
||||
4. **终结**:当你确认所有目标已达成时,调用 `mark_all_complete` 附上最终总结,正式结束整个流水线。
|
||||
⚠️ 警告:你没有任何执行具体业务代码或查询的工具,你只能操作任务看板!
|
||||
</instructions>""" # noqa: E501
|
||||
|
||||
def __init__(self, members: list[BaseRunnable], **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.members = members
|
||||
|
||||
def _get_board(self, context: RunContext) -> TaskBoardState:
|
||||
"""从运行上下文中安全的获取或初始化任务看板状态"""
|
||||
if "__task_board__" not in context.session.shared_state:
|
||||
context.session.shared_state["__task_board__"] = TaskBoardState()
|
||||
return context.session.shared_state["__task_board__"]
|
||||
|
||||
@tool(description="创建一个新任务并加入看板。")
|
||||
async def create_task(
|
||||
self,
|
||||
title: Annotated[str, Field(description="任务的简短、可行动的标题")],
|
||||
description: Annotated[
|
||||
str, Field(description="详细的任务说明,告诉执行者需要做什么以及期望的产出")
|
||||
],
|
||||
assignee: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description=(
|
||||
"负责执行此任务的专家名称,必须完全匹配 <team_members> "
|
||||
"中提供的 id,严禁捏造"
|
||||
)
|
||||
),
|
||||
],
|
||||
context: RunContext,
|
||||
depends_on: Annotated[
|
||||
list[str],
|
||||
Field(
|
||||
description=(
|
||||
"该任务依赖的前置任务的【标题(title)】列表"
|
||||
"(因同一回合创建时未知ID,请务必使用前置任务的 title 作为依赖)。"
|
||||
"无依赖则必须传入空数组 []"
|
||||
),
|
||||
),
|
||||
],
|
||||
metadata: Annotated[
|
||||
dict[str, Any],
|
||||
Field(
|
||||
description="可选的附加字典,用于向执行专家传递额外的结构化约束或参数"
|
||||
),
|
||||
] = {},
|
||||
) -> ToolResult:
|
||||
board = self._get_board(context)
|
||||
|
||||
valid_member_names = [m.name for m in self.members]
|
||||
if assignee not in valid_member_names:
|
||||
return ToolResult(
|
||||
output=(
|
||||
f"❌ 创建失败:未找到名为 '{assignee}' 的专家。"
|
||||
f"可用专家: {valid_member_names}"
|
||||
)
|
||||
).as_error()
|
||||
|
||||
task = board.create_task(
|
||||
title=title,
|
||||
description=description,
|
||||
assignee=assignee,
|
||||
dependencies=depends_on,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
logger.debug(f" 🆕 [新建任务] `{task.title}` -> 👨💼{task.assignee}")
|
||||
|
||||
board_str = board.render_board_to_string()
|
||||
return ToolResult(
|
||||
output=(
|
||||
f"✅ 任务创建成功!任务 ID: [{task.id}],"
|
||||
f"状态: {task.status.value}\n\n{board_str}"
|
||||
)
|
||||
)
|
||||
|
||||
@tool(
|
||||
description=(
|
||||
"手动强制更新任务的状态(仅在特殊情况下使用,"
|
||||
"因为 execute_task 会自动更新状态)。"
|
||||
)
|
||||
)
|
||||
async def update_task_status(
|
||||
self,
|
||||
task_id: Annotated[str, Field(description="要更新的任务的唯一 ID")],
|
||||
status: Annotated[
|
||||
TaskNodeStatus,
|
||||
Field(
|
||||
description=(
|
||||
"新的任务状态,支持: pending(用于重试), completed, failed 等"
|
||||
)
|
||||
),
|
||||
],
|
||||
context: RunContext,
|
||||
result: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description=(
|
||||
"提供结果、失败原因,或在设为 pending 重试时给执行专家的建议"
|
||||
)
|
||||
),
|
||||
] = "",
|
||||
) -> ToolResult:
|
||||
board = self._get_board(context)
|
||||
|
||||
if status == TaskNodeStatus.in_progress:
|
||||
return ToolResult(
|
||||
output=(
|
||||
"❌ 权限拒绝:你不能手动将任务状态设置为 in_progress。"
|
||||
"该状态由底层执行引擎自动管理。如果你想让任务重新执行,"
|
||||
"请将其设置为 pending。"
|
||||
)
|
||||
).as_error()
|
||||
|
||||
updated = board.update_task_status(task_id, status, result if result else None)
|
||||
if not updated:
|
||||
return ToolResult(output=f"❌ 找不到 ID 为 '{task_id}' 的任务。").as_error()
|
||||
|
||||
task_obj = board.get_task(task_id)
|
||||
task_title = task_obj.title if task_obj else "Unknown"
|
||||
logger.debug(f" 🔄 [任务状态变更] `{task_title}` -> {status.value}")
|
||||
|
||||
if task_obj and task_obj.status != status:
|
||||
board_str = board.render_board_to_string()
|
||||
return ToolResult(
|
||||
output=(
|
||||
"❌ 状态更新失败(触发底层状态机防呆回滚)!\n"
|
||||
f"你尝试将 [{task_id}] 强制设置为 {status.value},"
|
||||
f"但系统计算依赖图后将其重置为了 {task_obj.status.value}。\n"
|
||||
"💡 原因分析:它的前置依赖(depends_on)可能尚未 COMPLETED,"
|
||||
"或者你填错了依赖项的名称/ID导致系统无法追踪。\n\n"
|
||||
f"{board_str}"
|
||||
)
|
||||
).as_error()
|
||||
|
||||
board_str = board.render_board_to_string()
|
||||
return ToolResult(
|
||||
output=f"✅ 任务 [{task_id}] 已更新为 {status.value}。\n\n{board_str}"
|
||||
)
|
||||
|
||||
@tool(
|
||||
description=(
|
||||
"声明整体目标已完成。在调用此工具后,"
|
||||
"大模型将被立刻中断并直接将 summary 返回给用户。"
|
||||
)
|
||||
)
|
||||
async def mark_all_complete(
|
||||
self,
|
||||
summary: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description=(
|
||||
"流程的最终战报。⚠️ 必须在总结中完整包含各专家产出的"
|
||||
"核心交付物原文(如生成的故事、最终翻译内容等),绝对不能只说“已完成”!"
|
||||
)
|
||||
),
|
||||
],
|
||||
context: RunContext,
|
||||
) -> ToolResult:
|
||||
from zhenxun.services.ai.tools.models import EndRunResult
|
||||
|
||||
board = self._get_board(context)
|
||||
board.is_goal_complete = True
|
||||
board.final_summary = summary
|
||||
return EndRunResult(output=summary)
|
||||
@@ -0,0 +1,377 @@
|
||||
import asyncio
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing_extensions import Self
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.services.ai.capabilities import AbstractCapability, DynamicCapability
|
||||
from zhenxun.services.ai.core.exceptions import ConcurrencyInterruptException
|
||||
from zhenxun.services.ai.core.messages import PromptInput
|
||||
from zhenxun.services.ai.core.models import CancellationToken
|
||||
from zhenxun.services.ai.core.stream_events import EventBus
|
||||
from zhenxun.services.ai.flow.agent.agent import CapabilitySource, ToolSource
|
||||
from zhenxun.services.ai.flow.agent.models import Persona
|
||||
from zhenxun.services.ai.flow.base import BaseRunnable
|
||||
from zhenxun.services.ai.flow.team.models import TeamRuntimeConfig, Transition
|
||||
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.tools.providers.skills.models import Skill, SkillSource
|
||||
from zhenxun.utils.utils import infer_plugin_namespace
|
||||
|
||||
|
||||
class Team(BaseRunnable[AgentRunResult[Any]]):
|
||||
"""
|
||||
多智能体动态编排与路由控制器 (Facade)。
|
||||
继承自 BaseRunnable,支持被嵌套在其他 Team 或 Workflow 中。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
members: list[BaseRunnable[Any]],
|
||||
model: str | Callable[[], str] | None = None,
|
||||
strategy: BaseTeamStrategy | None = None,
|
||||
description: str | None = None,
|
||||
persona: Persona | dict | None = None,
|
||||
runtime_config: TeamRuntimeConfig | dict | None = None,
|
||||
capabilities: list[CapabilitySource] | None = None,
|
||||
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
|
||||
):
|
||||
"""
|
||||
多智能体协作团队初始化。
|
||||
|
||||
参数:
|
||||
name: 团队的名称标识。
|
||||
members: 团队成员列表,可以包含 Agent、Workflow 或其他 Team。
|
||||
model: (可选) 团队的统一默认模型,将自动被内部的 Leader/Router 继承。
|
||||
strategy: (可选) 团队协作策略实例。
|
||||
若不传入,必须随后使用 `.with_xxx()` 链式方法配置。
|
||||
description: 团队的职能描述,用于上层节点路由。
|
||||
persona: 团队的整体人设或宏观设定。
|
||||
runtime_config: 团队级别的运行时宏观配置.
|
||||
"""
|
||||
self.name = name
|
||||
self.members = members
|
||||
self.model = model
|
||||
self.strategy = strategy
|
||||
self.description = (
|
||||
description
|
||||
or f"一个名为 {self.name} 的协作团队,包含 {len(self.members)} 个处理节点。"
|
||||
)
|
||||
self.persona = persona
|
||||
|
||||
self.namespace = infer_plugin_namespace() or "unknown"
|
||||
|
||||
self.capabilities: list[Any] = []
|
||||
if capabilities:
|
||||
for cap in capabilities:
|
||||
if isinstance(cap, AbstractCapability):
|
||||
self.capabilities.append(cap)
|
||||
elif callable(cap):
|
||||
self.capabilities.append(DynamicCapability(cap))
|
||||
|
||||
if isinstance(runtime_config, dict):
|
||||
runtime_config = TeamRuntimeConfig(**runtime_config)
|
||||
self.runtime_config = runtime_config or TeamRuntimeConfig(stateless=True)
|
||||
|
||||
if skills:
|
||||
from zhenxun.services.ai.tools.providers.skills.capabilities import (
|
||||
SkillCapability,
|
||||
)
|
||||
|
||||
self.capabilities.append(
|
||||
SkillCapability(skills=skills, namespace=self.namespace)
|
||||
)
|
||||
|
||||
self.selector_func = (
|
||||
getattr(strategy, "selector_func", None) if strategy else None
|
||||
)
|
||||
|
||||
def with_strategy(self, strategy: BaseTeamStrategy) -> Self:
|
||||
"""
|
||||
挂载自定义的团队协作策略。
|
||||
|
||||
该方法为第三方扩展策略提供了通用注入通道。
|
||||
|
||||
参数:
|
||||
strategy: 自定义的、继承自 BaseTeamStrategy 的团队协作策略实例。
|
||||
"""
|
||||
self.strategy = strategy
|
||||
self.selector_func = getattr(strategy, "selector_func", None)
|
||||
return self
|
||||
|
||||
def with_routing(
|
||||
self,
|
||||
state_flow: (
|
||||
Mapping[str, Sequence[Transition | str | Any]] | Callable | None
|
||||
) = None,
|
||||
selector_func: Callable[..., str | None] | None = None,
|
||||
router: BaseRouter | None = None,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
应用路由策略,基于挂载的 Router 进行最合适的专家动态分发。
|
||||
|
||||
路由策略初始化,通过决策大脑动态路由,将不同的输入重定向至对应的下级智能体。
|
||||
|
||||
参数:
|
||||
state_flow: 状态流转规则字典或动态函数,定义成员之间控制流的物理走向。
|
||||
selector_func: 极速硬路由的静态选择函数,返回目标智能体名称。
|
||||
router: 自定义的动态路由器实例 (如 LLMRouter, RegexRouter 等)。
|
||||
leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。
|
||||
"""
|
||||
from zhenxun.services.ai.flow.team.strategy import RouteStrategy
|
||||
|
||||
self.strategy = RouteStrategy(
|
||||
state_flow=state_flow,
|
||||
selector_func=selector_func,
|
||||
router=router,
|
||||
leader_model=leader_model,
|
||||
leader_tools=leader_tools,
|
||||
custom_prompt=custom_prompt,
|
||||
)
|
||||
self.selector_func = selector_func
|
||||
return self
|
||||
|
||||
def with_coordination(
|
||||
self,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
应用协作策略,Leader 自主规划并主动将子任务委派给 Sub-Agents,最后汇总结果。
|
||||
|
||||
协作策略初始化,Leader 主动拆解任务并挂载委托工具,
|
||||
委派给 Sub-Agents 并汇总结果。
|
||||
|
||||
参数:
|
||||
leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给协调节点 (Leader) 的专属附加工具列表。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。
|
||||
"""
|
||||
from zhenxun.services.ai.flow.team.strategy import CoordinateStrategy
|
||||
|
||||
self.strategy = CoordinateStrategy(
|
||||
leader_model=leader_model,
|
||||
leader_tools=leader_tools,
|
||||
custom_prompt=custom_prompt,
|
||||
)
|
||||
return self
|
||||
|
||||
def with_broadcast(
|
||||
self,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
应用广播策略,并发让所有成员处理同一个任务,最后由 Leader 总结。
|
||||
|
||||
广播策略初始化,并发让所有成员处理同一个任务,汇总多方报告,最后由 Leader 总结。
|
||||
|
||||
参数:
|
||||
leader_model: 总结节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给总结节点 (Leader) 的专属附加工具列表。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的广播总结系统提示词。
|
||||
"""
|
||||
from zhenxun.services.ai.flow.team.strategy import BroadcastStrategy
|
||||
|
||||
self.strategy = BroadcastStrategy(
|
||||
leader_model=leader_model,
|
||||
leader_tools=leader_tools,
|
||||
custom_prompt=custom_prompt,
|
||||
)
|
||||
return self
|
||||
|
||||
def with_task(
|
||||
self,
|
||||
leader_model: str | None = None,
|
||||
leader_tools: list[ToolSource] | None = None,
|
||||
max_iterations: int = 15,
|
||||
blackboard_schema: type[BaseModel] | None = None,
|
||||
initial_blackboard_state: BaseModel | None = None,
|
||||
custom_prompt: str | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
应用任务规划策略,Leader 利用工具箱在黑板上拆解任务、
|
||||
管理依赖并驱动 Member 执行。
|
||||
|
||||
任务规划策略初始化,Leader 利用看板在黑板上拆解任务、
|
||||
管理依赖并驱动 Member 异步推进。
|
||||
|
||||
参数:
|
||||
leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
|
||||
leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。
|
||||
max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。
|
||||
blackboard_schema: 团队共享黑板的数据结构类型 (Pydantic Model 类)。
|
||||
initial_blackboard_state: 共享黑板的初始数据状态实例。
|
||||
custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。
|
||||
"""
|
||||
from zhenxun.services.ai.flow.team.strategy import TaskStrategy
|
||||
|
||||
self.strategy = TaskStrategy(
|
||||
leader_model=leader_model,
|
||||
leader_tools=leader_tools,
|
||||
max_iterations=max_iterations,
|
||||
blackboard_schema=blackboard_schema,
|
||||
initial_blackboard_state=initial_blackboard_state,
|
||||
custom_prompt=custom_prompt,
|
||||
)
|
||||
return self
|
||||
|
||||
def _ensure_strategy(self):
|
||||
if self.strategy is None:
|
||||
raise RuntimeError(
|
||||
f"Team '{self.name}' 尚未绑定任何协作策略!"
|
||||
"请先调用 .with_routing() 等链式方法进行配置,"
|
||||
"或在初始化时传入 strategy 参数。"
|
||||
)
|
||||
|
||||
async def run(
|
||||
self,
|
||||
prompt: PromptInput | Task | None = None,
|
||||
*,
|
||||
context: "RunContext | None" = None,
|
||||
capabilities: list[CapabilitySource] | None = None,
|
||||
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentRunResult[Any]:
|
||||
"""
|
||||
团队级运行阻塞核心入口,内部静默分配任务给成员直至汇总结束。
|
||||
|
||||
参数:
|
||||
prompt: 派发给多智能体团队的任务描述 or 契约对象 (Task)。
|
||||
context: 显式传入的会话与运行上下文。
|
||||
capabilities: 仅针对本次团队执行动态注入的临时拦截器列表。
|
||||
kwargs: 透传的其他附加参数。
|
||||
|
||||
返回:
|
||||
AgentRunResult[Any]: 包含最终融合输出、消息历史和用量统计的运行结果对象。
|
||||
"""
|
||||
self._ensure_strategy()
|
||||
if skills:
|
||||
from zhenxun.services.ai.tools.providers.skills.capabilities import (
|
||||
SkillCapability,
|
||||
)
|
||||
|
||||
capabilities = list(capabilities) if capabilities else []
|
||||
capabilities.append(
|
||||
SkillCapability(skills=skills, namespace=self.namespace)
|
||||
)
|
||||
|
||||
return await super().run(
|
||||
prompt=prompt, context=context, capabilities=capabilities, **kwargs
|
||||
)
|
||||
|
||||
import contextlib
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def run_stream(
|
||||
self,
|
||||
prompt: PromptInput | Task | None = None,
|
||||
*,
|
||||
context: "RunContext | None" = None,
|
||||
capabilities: list[CapabilitySource] | None = None,
|
||||
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self._ensure_strategy()
|
||||
|
||||
if context is None:
|
||||
context = RunContext()
|
||||
|
||||
if not hasattr(context, "capabilities"):
|
||||
context.capabilities = []
|
||||
|
||||
if hasattr(self, "capabilities") and self.capabilities:
|
||||
context.capabilities.extend(self.capabilities)
|
||||
|
||||
if skills:
|
||||
from zhenxun.services.ai.tools.providers.skills.capabilities import (
|
||||
SkillCapability,
|
||||
)
|
||||
|
||||
capabilities = list(capabilities) if capabilities else []
|
||||
capabilities.append(
|
||||
SkillCapability(skills=skills, namespace=self.namespace)
|
||||
)
|
||||
|
||||
if capabilities:
|
||||
for cap in capabilities:
|
||||
if isinstance(cap, AbstractCapability):
|
||||
context.capabilities.append(cap)
|
||||
elif callable(cap):
|
||||
context.capabilities.append(DynamicCapability(cap))
|
||||
|
||||
from zhenxun.services.ai.flow.team.runner import TeamRunner
|
||||
from zhenxun.services.ai.run import StreamedRunResult
|
||||
|
||||
event_bus = EventBus()
|
||||
context.run.event_bus = event_bus
|
||||
assert self.strategy is not None
|
||||
runner = TeamRunner(self, self.strategy)
|
||||
|
||||
policy = getattr(self.runtime_config, "concurrency_policy", None)
|
||||
if policy is None:
|
||||
from zhenxun.services.ai.flow.base import ConcurrencyPolicy
|
||||
|
||||
policy = (
|
||||
ConcurrencyPolicy.ALLOW
|
||||
if getattr(self.runtime_config, "stateless", True)
|
||||
else ConcurrencyPolicy.QUEUE
|
||||
)
|
||||
|
||||
intervention_policy = getattr(self.runtime_config, "intervention_policy", None)
|
||||
|
||||
from zhenxun.services.ai.utils import ContextUtils
|
||||
|
||||
lock_id = ContextUtils.extract_concurrency_lock_id(
|
||||
context,
|
||||
getattr(self.runtime_config, "concurrency_scope", None),
|
||||
context.session_id or "default_session",
|
||||
)
|
||||
|
||||
async def _execution_task():
|
||||
from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
|
||||
|
||||
cancel_token = context.run.cancellation_token or CancellationToken()
|
||||
context.run.cancellation_token = cancel_token
|
||||
|
||||
try:
|
||||
async with apply_concurrency_policy(
|
||||
session_id=context.session_id or "default_session",
|
||||
lock_id=lock_id,
|
||||
policy=policy,
|
||||
cancel_token=cancel_token,
|
||||
intervention_policy=intervention_policy,
|
||||
message=prompt,
|
||||
):
|
||||
async for event in runner.run_stream(prompt, context, **kwargs):
|
||||
await event_bus.emit(event)
|
||||
except BaseException as e:
|
||||
from zhenxun.services.ai.run.models import AgentRunError
|
||||
|
||||
if isinstance(e, asyncio.CancelledError):
|
||||
e = ConcurrencyInterruptException("团队执行已被新请求打断并接管")
|
||||
await event_bus.emit(AgentRunError(error=e))
|
||||
finally:
|
||||
await event_bus.end()
|
||||
|
||||
task = asyncio.create_task(_execution_task())
|
||||
result_obj = StreamedRunResult[Any](event_bus)
|
||||
|
||||
try:
|
||||
yield result_obj
|
||||
finally:
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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。"
|
||||
)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user