♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)

* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统

- 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理
- 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册
- 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器
- 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签
- 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据
- 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度
- 重构执行分发器,支持并发限制、串行间隔和随机延迟打散
- 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入
- 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法
- 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出
- 将各模块的全局 logger 替换为对应的模块专属日志代理
- 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题
- 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token

* ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性

- 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构
- 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型
- 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义
- 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑
- 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构

* ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入

- 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性
- 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费
- 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2026-07-10 09:14:06 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 0b32d69c9c
commit 922d092650
132 changed files with 2372 additions and 1972 deletions
+10 -22
View File
@@ -5,9 +5,8 @@ from pathlib import Path
from typing import Any, Generic, cast
from zhenxun.services.ai.capabilities import (
AbstractCapability,
CapabilitySource,
CombinedCapability,
DynamicCapability,
)
from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.context.knowledge.base import BaseKnowledge
@@ -67,7 +66,7 @@ from zhenxun.services.ai.tools.providers.skills.capabilities import (
)
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.services.ai.utils import ContextUtils
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import (
model_construct,
model_copy,
@@ -78,7 +77,9 @@ from zhenxun.utils.utils import infer_plugin_namespace
from .engine.builders import (
AgentProfileResolver,
CapabilityBuilder,
ContextBuilder,
SessionBuilder,
ToolBuilder,
)
from .engine.executor import BaseAgentExecutor, StandardAgentExecutor
@@ -94,9 +95,6 @@ ToolSource = (
)
"""任何可以作为工具提供给大模型的实体对象(函数、基础工具类、字典定义、工具名、工具箱、声明式查询对象)"""
CapabilitySource = Callable | AbstractCapability
"""能力/拦截器来源(函数或 AbstractCapability 实例)"""
class AgentBuilder(Generic[AgentDepsT, OutputDataT]):
"""
@@ -148,7 +146,7 @@ class AgentBuilder(Generic[AgentDepsT, OutputDataT]):
return self
def with_tools(
self, *tools: ToolSource | list[ToolSource]
self, *tools: ToolSource | Sequence[ToolSource]
) -> "AgentBuilder[AgentDepsT, OutputDataT]":
"""
配置可供智能体调用的工具列表。
@@ -158,7 +156,7 @@ class AgentBuilder(Generic[AgentDepsT, OutputDataT]):
"""
current_tools = self._kwargs.setdefault("tools", [])
for t in tools:
if isinstance(t, list):
if isinstance(t, Sequence) and not isinstance(t, str):
current_tools.extend(t)
else:
current_tools.append(t)
@@ -353,7 +351,7 @@ class Agent(
description: str | None = None,
persona: Persona | dict | None = None,
model: str | Callable[[], str] | None = None,
tools: list[ToolSource] | None = None,
tools: Sequence[ToolSource] | None = None,
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
generation_config: GenerationConfig | IntentBuilder | dict | None = None,
response_model: BaseOutputDefinition | type[OutputDataT] | None = None,
@@ -458,14 +456,14 @@ class Agent(
def _assemble_plugins(self, tools, knowledge, capabilities, skills):
"""私有方法:集中处理各类能力、知识与技能的挂载,消解冗余样板代码"""
self.tool_definitions = tools or []
self.tool_definitions = list(tools) if tools else []
if knowledge:
if not isinstance(knowledge, list):
knowledge = [knowledge]
self.tool_definitions.extend(knowledge)
self.capabilities: list[AbstractCapability] = []
self.capabilities: list[CapabilitySource] = []
if self.memory_config.long_term.enable and self.memory_config.long_term.agentic:
self.capabilities.append(
@@ -478,11 +476,7 @@ class Agent(
)
if capabilities:
for cap in capabilities:
if isinstance(cap, AbstractCapability):
self.capabilities.append(cap)
elif callable(cap):
self.capabilities.append(DynamicCapability(cap))
self.capabilities.extend(capabilities)
if self.config.enable_hitl:
self.tool_definitions.append(HITLToolkit())
@@ -789,11 +783,6 @@ class Agent(
**kwargs: Any,
) -> tuple[AgentState, AgentRunResources]:
"""解析任务意图,初始化隔离域与基础状态载体"""
from zhenxun.services.ai.flow.agent.engine.builders import (
AgentProfileResolver,
CapabilityBuilder,
SessionBuilder,
)
if context is None:
raise ValueError("RunContext 不能为空")
@@ -900,7 +889,6 @@ class Agent(
toolset_funcs=getattr(self, "toolset_funcs", []),
system_tools=[],
namespace=self.namespace or "unknown",
tool_filter=resources.config.tool_filter,
run_context=context,
run_scoped_cap=run_scoped_cap,
)
+20 -11
View File
@@ -1,6 +1,7 @@
import asyncio
from typing import Any, Generic, cast
from typing_extensions import TypeVar
import uuid
from nonebot.adapters import Bot, Event
from nonebot_plugin_alconna.uniseg import UniMessage
@@ -16,8 +17,9 @@ 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.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
T_Deps = TypeVar("T_Deps", default=Any)
T_Out = TypeVar("T_Out", default=str)
@@ -46,8 +48,6 @@ class AgentRunner(Generic[T_Out]):
)
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]}"
)
@@ -126,15 +126,24 @@ class AgentRunner(Generic[T_Out]):
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()
if final_result and final_result.output and self._bot:
msg_to_send = (
final_result.output
if isinstance(final_result.output, UniMessage)
else MessageUtils.build_message(str(final_result.output))
)
if self._event:
await msg_to_send.send(self._event, bot=self._bot, reply_to=reply_to)
else:
final_msg = str(final_result.output)
await MessageUtils.build_message(final_msg).send()
target = PlatformUtils.get_target(
user_id=self.context.get_user_id(),
group_id=self.context.get_group_id(),
)
if target:
await msg_to_send.send(target=target, bot=self._bot)
if isinstance(final_result.output, UniMessage):
final_result.output = final_result.output.extract_plain_text()
if final_result is None:
raise RuntimeError("智能体运行流异常结束:未返回最终结果。")
@@ -12,7 +12,7 @@ from zhenxun.services.ai.core.messages import TaskLifecycleEvent
from zhenxun.services.ai.core.options import BaseOutputDefinition, ToolOutput
from zhenxun.services.ai.guardrails import parse_guardrails
from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_agent as logger
class OutputValidationCapability(AbstractCapability):
@@ -6,9 +6,7 @@ from typing import Any, cast
from nonebot.utils import is_coroutine_callable
from zhenxun.services.ai.capabilities import (
AbstractCapability,
CombinedCapability,
DynamicCapability,
)
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
from zhenxun.services.ai.context.memory.engine import MemoryReader, MemoryWriter
@@ -22,13 +20,13 @@ from zhenxun.services.ai.flow.agent.capabilities import (
TaskTrackingCapability,
)
from zhenxun.services.ai.flow.agent.models import Persona
from zhenxun.services.ai.run import GLOBAL_CAPABILITIES, RunContext
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.services.ai.tools.models import ResolvedToolPayload
from zhenxun.services.ai.utils.scope import ScopeSelector
from zhenxun.utils.pydantic_compat import model_copy
@@ -135,22 +133,21 @@ class CapabilityBuilder:
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.capabilities.manager import capability_manager
base_caps = GLOBAL_CAPABILITIES.get("global", []).copy()
if namespace != "global" and namespace in GLOBAL_CAPABILITIES:
base_caps.extend(GLOBAL_CAPABILITIES[namespace])
run_level_caps = capability_manager.resolve_capabilities(
profile_capabilities or [], namespace
)
agent_level_caps = capability_manager.resolve_capabilities(
agent_capabilities or [], namespace
)
auto_caps = capability_manager.get_auto_apply_capabilities(namespace)
combined_cap = CombinedCapability(
base_caps
auto_caps
+ getattr(context, "capabilities", [])
+ agent_capabilities
+ agent_level_caps
+ run_level_caps
+ dynamic_caps
)
@@ -298,7 +295,6 @@ class ToolBuilder:
toolset_funcs: list[Any],
system_tools: list[Any],
namespace: str,
tool_filter: GlobalToolFilter | None,
run_context: RunContext,
run_scoped_cap: CombinedCapability,
) -> ResolvedToolPayload:
@@ -309,9 +305,8 @@ class ToolBuilder:
参数:
tool_definitions: 静态工具或工具集合的定义列表。
toolset_funcs: 待依赖注入解析的工具集生成函数列表。
system_tools: 系统默认强制集成的工具定义列表。
system_tools: 系统默认强制集成的工具定义列表.
namespace: 会话命名空间。
tool_filter: 全局的工具过滤条件,此处为占位。
run_context: 运行上下文对象实例.
run_scoped_cap: 运行域下的合并能力中间件,用于提供特定的能力工具。
@@ -411,14 +406,14 @@ class ToolBuilder:
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()
final_effective_tools_list = []
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
final_effective_tools_list.append(cloned_tool)
return ToolCollection(final_effective_tools_list)
class SessionBuilder:
@@ -4,7 +4,7 @@ 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.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import model_construct
from zhenxun.utils.utils import infer_plugin_namespace
@@ -63,7 +63,7 @@ async def handle_submit_structured(
parsed_obj = (
tool_res.directive.payload.get("parsed_obj") if tool_res.directive else None
)
logger.info("✅ 拦截到结构化结果提交,结束循环。")
logger.debug("✅ 拦截到结构化结果提交,结束循环。")
state.is_finished = True
state.final_result = model_construct(
AgentRunResult,
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
import asyncio
import json
from typing import Any, cast
from typing import Any
from zhenxun.services.ai.capabilities import CombinedCapability
from zhenxun.services.ai.core.engine.context_renderer import ContextConverter
@@ -15,7 +15,6 @@ from zhenxun.services.ai.core.exceptions import (
)
from zhenxun.services.ai.core.messages import (
AgentMessage,
AssistantContentUnion,
AssistantMessage,
AudioPart,
ChatRequest,
@@ -35,19 +34,20 @@ from zhenxun.services.ai.core.stream_events import (
LLMStartEvent,
ToolStreamChunkEvent,
)
from zhenxun.services.ai.flow.agent.engine.directive import (
DirectiveHandlerFunc,
directive_manager,
)
from zhenxun.services.ai.flow.agent.models import AgentRunResources, AgentState
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.session import session_manager
from zhenxun.services.ai.tools.engine.executor import ToolExecutor
from zhenxun.services.ai.tools.models import ToolResult
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_agent as logger
from zhenxun.utils.pydantic_compat import dump_json_safely, model_construct
from .directive import (
DirectiveHandlerFunc,
directive_manager,
)
class BaseAgentExecutor(ABC):
"""
@@ -219,13 +219,12 @@ class StandardAgentExecutor(BaseAgentExecutor):
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,
)
await run_context.run.emit(
LLMStartEvent(
model_name=resources.model_name or "unknown",
messages=flattened_messages,
)
)
response = await self._execute_model_request(
model_name=resources.model_name,
@@ -238,8 +237,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
cancellation_token=cancellation_token,
)
if run_context.run.event_bus:
await run_context.run.event_bus.emit(LLMEndEvent(response=response))
await run_context.run.emit(LLMEndEvent(response=response))
assistant_content = (
response.content_parts if response.content_parts else response.text
@@ -252,9 +250,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
part.metadata["thought_signature"] = response.thought_signature
break
assistant_message = AssistantMessage(
content=cast(list[AssistantContentUnion], response.content_parts)
)
assistant_message = AssistantMessage(content=response.content_parts)
if hasattr(response, "parsed_obj") and response.parsed_obj is not None:
if not isinstance(response.parsed_obj, str):
@@ -376,7 +372,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
state.messages, resources.model_name or "", base_overhead=0
)
logger.debug(
f"[TokenTracker] (Iter {state.current_cycle + 1}) "
f"(Iter {state.current_cycle + 1}) "
f"预估将消耗 {est_tokens} Token "
f"(Model: {resources.model_name or 'Unknown'})"
)
@@ -444,7 +440,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
return
if not response.tool_calls:
logger.debug("✅ AgentExecutor:模型未请求工具调用,推理循环结束。")
logger.debug("✅ 模型未请求工具调用,推理循环结束。")
state.is_finished = True
state.final_result = model_construct(
AgentRunResult,
@@ -476,8 +472,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
if is_server_side:
logger.debug(
"☁️ [AgentExecutor] 检测到云端工具调用: "
f"{call.tool_name},已跳过本地执行。"
f"☁️ 检测到云端工具调用: {call.tool_name},已跳过本地执行。"
)
async with self.tool_executor._tool_stream_scope(
event_bus,
@@ -500,7 +495,7 @@ class StandardAgentExecutor(BaseAgentExecutor):
client_tool_calls.append(call)
if not client_tool_calls:
logger.info("✅ AgentExecutor:无本地客户端工具需执行,推理循环平滑结束。")
logger.info("✅ 无本地客户端工具需执行,推理循环平滑结束。")
state.is_finished = True
state.final_result = model_construct(
@@ -638,7 +633,6 @@ class StandardAgentExecutor(BaseAgentExecutor):
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(
@@ -646,17 +640,15 @@ class StandardAgentExecutor(BaseAgentExecutor):
)
logger.warning(
f"AgentExecutor 达到最大循环次数 ({resources.config.max_cycles}),"
"触发兜底总结机制。"
f"达到最大循环次数 ({resources.config.max_cycles}),触发兜底总结机制。"
)
if event_bus:
await event_bus.emit(
ToolStreamChunkEvent(
tool_name="System",
content="⏳ 思考过程过于复杂,正在强制生成最终总结...",
)
await run_context.run.emit(
ToolStreamChunkEvent(
tool_name="System",
content="⏳ 思考过程过于复杂,正在强制生成最终总结...",
)
)
fallback_msg = LLMMessage.user(
"### 🚨 [系统强制指令]\n"
+19 -16
View File
@@ -1,12 +1,15 @@
"""
Agent 相关静态声明类型定义
"""
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.capabilities import CapabilitySource, CombinedCapability
from zhenxun.services.ai.context.memory.builder import MemoryBuilder
from zhenxun.services.ai.context.memory.engine import MemoryReader, MemoryWriter
from zhenxun.services.ai.context.memory.models import MemoryConfig
from zhenxun.services.ai.context.memory.types import SessionMetadata
from zhenxun.services.ai.core.messages import (
AgentMessage,
@@ -18,8 +21,9 @@ from zhenxun.services.ai.core.messages import (
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.run.models import AgentRunResult, AgentTask, HandoffPayload
from zhenxun.services.ai.tools.core.toolkit import BaseToolkit
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
@@ -60,15 +64,14 @@ class AgentConfig(BaseRuntimeConfig):
message_history: Sequence[AgentMessage] | None = Field(default=None)
"""初始化的底层对话历史记录。"""
tool_filter: GlobalToolFilter | None = Field(default=None)
"""全局工具过滤器,限制本次运行可用的工具池。"""
memory: Any | None = Field(default=None)
memory: MemoryConfig | MemoryBuilder | bool | None = Field(default=None)
"""单次运行级别的记忆门面覆盖 (支持 bool, MemoryConfig, MemoryBuilder)。"""
generation_config: GenerationConfig | None = Field(default=None)
"""单次运行覆盖的大模型生成配置。"""
capabilities: list[Any] | None = Field(default=None)
capabilities: list[CapabilitySource] | None = Field(default=None)
"""仅针对本次运行动态注入的临时拦截器/能力组件列表。"""
skills: Sequence[Any] | None = Field(default=None)
skills: Sequence[str | Path | Any] | None = Field(default=None)
"""仅针对本次运行动态注入的临时技能集合。"""
executor: Any | None = Field(default=None)
"""单次运行覆盖的核心执行引擎策略 (BaseAgentExecutor)。"""
@@ -128,11 +131,11 @@ class AgentState(BaseModel):
"""拦截到的早期终止输出结果"""
should_terminate: bool = False
"""标记是否应提前终止循环"""
handoff_triggered: Any | None = None
handoff_triggered: HandoffPayload | None = None
"""标记是否触发了移交"""
is_finished: bool = False
"""标记大模型循环是否彻底结束"""
final_result: Any | None = None
final_result: AgentRunResult[Any] | None = None
"""最终的运行结果 (AgentRunResult)"""
origin_msg_len: int = 0
"""初始进入循环时的消息历史长度 (用于增量保存记忆)"""
@@ -160,15 +163,15 @@ class AgentRunResources(BaseModel):
"""保留依赖注入(DI)与黑板引用的全局运行时上下文"""
session_meta: SessionMetadata | None = None
"""隔离会话的元信息(Session ID, 命名空间, 权限等)"""
memory_reader: Any | None = None
memory_reader: MemoryReader | None = None
"""用于读取短/中/长期上下文记忆的读取器"""
memory_writer: Any | None = None
memory_writer: MemoryWriter | None = None
"""用于将对话历史安全落盘的写入器"""
run_scoped_cap: Any | None = None
run_scoped_cap: CombinedCapability | None = None
"""聚合了 Agent/AgentTask/全局 的复合能力拦截器 (CombinedCapability)"""
task_obj: Any | None = None
task_obj: AgentTask | None = None
"""(如有) 解析后的结构化数据任务契约"""
toolkits: list[Any] = Field(default_factory=list)
toolkits: list[BaseToolkit] = Field(default_factory=list)
"""当前轮次生效的工具箱列表 (需要执行生命周期挂载)"""
config: AgentConfig = Field(default_factory=AgentConfig)
"""Agent 全局与运行时的统一策略配置"""
+9 -7
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from collections.abc import AsyncIterator
@@ -7,12 +9,14 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
from pydantic import BaseModel, Field
from zhenxun.services.ai.core.exceptions import ControlFlowExit
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import StreamedRunResult
from zhenxun.services.ai.run.ui import UIController
from zhenxun.services.ai.utils.logger import log_flow as logger
if TYPE_CHECKING:
from zhenxun.services.ai.flow.agent.models import Persona
from zhenxun.services.ai.run.models import StreamedRunResult
from .agent.models import Persona
from zhenxun.services.ai.core.messages import PromptInput
@@ -92,7 +96,7 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
"""DI 注入语法糖:返回 Depends,自动绑定当前上下文"""
from nonebot.params import Depends
from zhenxun.services.ai.flow.agent.bridge import AgentRunner
from .agent.bridge import AgentRunner
async def _dependency() -> AgentRunner[Any]:
return AgentRunner[Any](self, **kwargs)
@@ -108,7 +112,7 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
**kwargs: Any,
) -> T_RunResult:
"""交互执行语法糖,自动渲染流式进度并最终将结果回复给终端用户"""
from zhenxun.services.ai.flow.agent.bridge import AgentRunner
from .agent.bridge import AgentRunner
runner = AgentRunner(self, context=context, **kwargs)
return cast(T_RunResult, await runner.reply(prompt=prompt, reply_to=reply_to))
@@ -121,8 +125,6 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
**kwargs: Any,
) -> T_RunResult:
"""阻塞式核心运行入口,安全捕获内部抛出的静默退出信号"""
from zhenxun.services.ai.core.exceptions import ControlFlowExit
from zhenxun.services.log import logger
try:
async with self.run_stream(
@@ -144,6 +146,6 @@ class BaseRunnable(ABC, Generic[T_RunResult]):
*,
context: RunContext | None = None,
**kwargs: Any,
) -> "AsyncIterator[StreamedRunResult[Any]]":
) -> AsyncIterator[StreamedRunResult[Any]]:
"""流式运行入口,返回上下文管理器,用于消费底层执行流事件 (StreamedRunResult)"""
yield cast(Any, None)
+1 -1
View File
@@ -9,7 +9,7 @@ from zhenxun.services.ai.core.exceptions import (
from zhenxun.services.ai.core.models import CancellationToken
from zhenxun.services.ai.run.models import AgentTask
from zhenxun.services.ai.run.session import LockContext, session_manager
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_flow as logger
from .base import ConcurrencyPolicy, InterventionPolicy
@@ -6,8 +6,11 @@ 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.run.di import DependencyInjector
from zhenxun.services.ai.tools.bridges.handoff import HandoffTool
from .models import Transition
class TeamRoutingCapability(AbstractCapability):
"""团队路由能力组件:动态向所有团队成员"""
@@ -17,10 +20,12 @@ class TeamRoutingCapability(AbstractCapability):
team_name: str,
members: list[Any],
state_flow: Mapping[str, Sequence[Any]] | Callable | None = None,
max_handoffs: int = 3,
):
self.team_name = team_name
self.members = members
self.state_flow = state_flow
self.max_handoffs = max_handoffs
async def _get_allowed_transitions(self, context: RunContext) -> list[Any] | None:
"""核心FSM解析:解析静态字典或动态执行函数获取允许的 Transition 列表"""
@@ -34,15 +39,12 @@ class TeamRoutingCapability(AbstractCapability):
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
@@ -55,7 +57,6 @@ class TeamRoutingCapability(AbstractCapability):
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]
@@ -97,6 +98,7 @@ class TeamRoutingCapability(AbstractCapability):
target_name=m.name,
target_description=desc,
input_schema=input_schema,
max_handoffs=self.max_handoffs,
)
)
return tools
+7 -4
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from collections.abc import Callable, Sequence
from enum import Enum
from typing import Any
@@ -7,7 +9,8 @@ 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
from zhenxun.services.ai.flow.base import BaseRunnable, BaseRuntimeConfig
from zhenxun.services.ai.run import AgentTask
class TeamRuntimeConfig(BaseRuntimeConfig):
@@ -45,7 +48,7 @@ class Transition(BaseModel):
trigger_regex: str | None = None
"""(可选) 正则表达式。
如果用户的输入匹配此正则,将触发极速硬路由,跳过大模型思考。"""
trigger_func: Callable[..., Any] | None = None
trigger_func: Callable[..., bool | str | None] | None = None
"""(可选) 自定义校验函数。返回 True 或目标名称时触发硬路由。支持依赖注入。"""
model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -62,9 +65,9 @@ class CallAction(TeamAction):
调度动作:呼叫指定的 Agent 执行任务
"""
agent: str | Any
agent: str | BaseRunnable[Any]
"""目标 Agent 的名称(字符串)或动态生成的 Agent 实例"""
task: str | Any
task: str | AgentTask
"""派发给该 Agent 的具体任务或提示词"""
history: Sequence[AgentMessage] | None = None
"""需要传递给该 Agent 的上下文历史记录(可选)"""
+12 -4
View File
@@ -8,10 +8,11 @@ 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 AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_team as logger
from .models import RouteDecision, Transition
class BaseRouter(ABC):
@@ -148,6 +149,7 @@ class LLMRouter(BaseRouter):
runtime_config: Any = None,
custom_prompt: str | None = None,
allowed_transitions: list[Transition] | None = None,
max_handoffs: int = 3,
):
"""
初始化基于大模型的意图路由器。
@@ -161,6 +163,7 @@ class LLMRouter(BaseRouter):
runtime_config: 团队级别的运行时全局配置。
custom_prompt: 自定义的系统提示词模板,用以覆盖默认的路由系统指令。
allowed_transitions: 允许的状态移交规则与前置条件列表。
max_handoffs: 同一会话中允许连续移交的最大次数。
"""
self.team_name = team_name
self.members = members
@@ -170,6 +173,7 @@ class LLMRouter(BaseRouter):
self.runtime_config = runtime_config
self.custom_prompt = custom_prompt
self.allowed_transitions = allowed_transitions
self.max_handoffs = max_handoffs
async def route(
self,
@@ -179,7 +183,8 @@ class LLMRouter(BaseRouter):
) -> 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
from .capabilities import TeamRoutingCapability
default_system_prompt = """## 角色与目标
你是一个高级任务路由器 (所在团队: {{ team_name }})。
@@ -200,7 +205,10 @@ class LLMRouter(BaseRouter):
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
team_name=self.team_name,
members=self.members,
state_flow=self.state_flow,
max_handoffs=self.max_handoffs,
)
leader_config = AgentConfig(
+11 -12
View File
@@ -8,17 +8,19 @@ from zhenxun.services.ai.core.exceptions import (
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 (
from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.ai.utils.logger import log_team as logger
from zhenxun.utils.pydantic_compat import model_construct
from .capabilities import TeamRoutingCapability
from .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
from zhenxun.utils.pydantic_compat import model_construct
from .strategy import BaseTeamStrategy, RouteStrategy
class TeamRunner:
@@ -47,7 +49,7 @@ class TeamRunner:
(m for m in self.team.members if m.name == action.agent), None
)
if not target_agent:
logger.error(f"❌ [TeamRunner] 找不到团队成员: {action.agent}")
logger.error(f"❌ 找不到团队成员: {action.agent}")
await queue.put(
(
"result",
@@ -67,13 +69,12 @@ class TeamRunner:
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),
max_handoffs=getattr(self.strategy, "max_handoffs", 3),
)
sub_context.capabilities.append(routing_cap)
@@ -82,8 +83,6 @@ class TeamRunner:
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,
+65 -55
View File
@@ -10,19 +10,26 @@ 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 (
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.blackboard import BlackboardManager
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
from zhenxun.services.ai.tools.providers.builtin.blackboard import BlackboardToolkit
from zhenxun.services.ai.utils.logger import log_team as logger
from .models import (
CallAction,
ConcurrentCallAction,
FinishAction,
TaskBoardState,
TaskNodeStatus,
TeamAction,
Transition,
)
from zhenxun.services.ai.flow.team.router import BaseRouter
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.tools.bridges.delegate import DelegateTool
from zhenxun.services.log import logger
from .router import BaseRouter
from .task_tools import TaskPlanningToolkit
if TYPE_CHECKING:
from zhenxun.services.ai.flow.team.team import Team
from .team import Team
class BaseTeamStrategy(ABC):
@@ -106,6 +113,7 @@ class RouteStrategy(BaseTeamStrategy):
leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None,
max_handoffs: int = 3,
):
"""
路由策略初始化,基于挂载的 Router 进行最合适的专家分发。
@@ -117,16 +125,16 @@ class RouteStrategy(BaseTeamStrategy):
leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。
max_handoffs: 同一会话中允许连续移交的最大次数。
"""
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 []
self.max_handoffs = max_handoffs
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 = []
@@ -163,6 +171,7 @@ class RouteStrategy(BaseTeamStrategy):
state_flow=self.state_flow,
runtime_config=getattr(team, "runtime_config", None),
custom_prompt=self.custom_prompt,
max_handoffs=self.max_handoffs,
)
)
router = ChainRouter(routers)
@@ -171,13 +180,11 @@ class RouteStrategy(BaseTeamStrategy):
exec_config = kwargs.get("config")
max_cycles = getattr(exec_config, "max_cycles", 15) if exec_config else 15
logger.info(f"🛣️ [RouteStrategy] '{team.name}' 正在获取初始路由决策...")
logger.debug(f"🛣️ '{team.name}' 正在获取初始路由决策...")
decision = await router.route(context, [], prompt)
if not decision:
logger.warning(
f"🚨 [RouteStrategy] Team '{team.name}' 的所有路由策略未能命中目标。"
)
logger.warning(f"🚨 Team '{team.name}' 的所有路由策略未能命中目标。")
raise AbortException(
reason=f"Team '{team.name}' 无法找到合适的路由节点处理该任务",
display="🚨 团队协作失败,无法分配任务。",
@@ -191,7 +198,7 @@ class RouteStrategy(BaseTeamStrategy):
cycle_count += 1
if cycle_count > max_cycles:
logger.error(
f"🚨 [RouteStrategy] Team '{team.name}' 路由陷入死循环!"
f"🚨 Team '{team.name}' 路由陷入死循环!"
f"已达到最大限制 {max_cycles} 次。"
)
raise AbortException(
@@ -229,7 +236,7 @@ class RouteStrategy(BaseTeamStrategy):
run_result = yield CallAction(
agent=current_target,
task=prompt,
task=prompt or "",
history=handoff_history_messages,
kwargs=kwargs,
)
@@ -268,7 +275,7 @@ class RouteStrategy(BaseTeamStrategy):
pass
if fast_routed:
logger.info(
logger.debug(
f"🛣️ **路由决策**: 委派给专员 👨💼`{current_target}`"
"(系统拦截:正则/函数状态流发生转移)"
)
@@ -291,18 +298,21 @@ class CoordinateStrategy(BaseTeamStrategy):
leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None,
max_delegations: int = 3,
):
"""
协作策略初始化,Leader 主动拆解任务,委派给 Sub-Agents 并汇总结果。
参数:
leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给协调节点 (Leader) 的专属工具列表。
leader_tools: 挂载给协调节点 (Leader) 的专属工具列表.
custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。
max_delegations: 允许向同一个专员连续委派失败的最大重试次数。
"""
super().__init__(custom_prompt=custom_prompt)
self.leader_model = leader_model
self.leader_tools = leader_tools or []
self.max_delegations = max_delegations
async def generate_plan(
self,
@@ -323,6 +333,7 @@ class CoordinateStrategy(BaseTeamStrategy):
runnable=m,
name=f"delegate_to_{m.name}",
description=f"将子任务委派给专员 [{m.name}] 处理。专长:{desc}",
max_delegations=self.max_delegations,
)
)
@@ -338,17 +349,16 @@ class CoordinateStrategy(BaseTeamStrategy):
logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...")
if context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name="Team Leader",
content="✨ 团队 Leader 正在汇总各方报告...",
)
await context.run.emit(
ToolStreamChunkEvent(
tool_name="Team Leader",
content="✨ 团队 Leader 正在汇总各方报告...",
)
)
logger.debug(f"👨💼 [CoordinateStrategy] '{team.name}' 正在启动协调推理循环...")
leader_res = yield CallAction(agent=leader_agent, task=prompt)
leader_res = yield CallAction(agent=leader_agent, task=prompt or "")
yield FinishAction(result=leader_res)
@@ -389,24 +399,22 @@ class BroadcastStrategy(BaseTeamStrategy):
prompt.description if isinstance(prompt, AgentTask) else (prompt or "")
)
if context.run.event_bus:
await context.run.event_bus.emit(
ToolStreamChunkEvent(
tool_name="Team Broadcaster",
content=f"🚀 正在并发广播任务给 {len(team.members)} 位专家...",
)
await context.run.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 正在融合各方观点...",
)
await context.run.emit(
ToolStreamChunkEvent(
tool_name="Team Leader",
content="✨ 所有专家汇报完毕,Leader 正在融合各方观点...",
)
)
logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...")
@@ -451,8 +459,7 @@ class TaskStrategy(BaseTeamStrategy):
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,
blackboard: type[BaseModel] | BaseModel | None = None,
custom_prompt: str | None = None,
):
"""
@@ -463,10 +470,9 @@ class TaskStrategy(BaseTeamStrategy):
leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。
max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。
blackboard_schema: 团队共享黑板的数据结构类型 (Pydantic Model 类)。
initial_blackboard_state: 共享黑板的初始数据状态实例。
blackboard: (可选) 团队共享黑板。可传入 Schema 类型类,或直接传入带有初始数据的 Schema 实例对象。
custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。
"""
""" # noqa: E501
super().__init__(custom_prompt=custom_prompt)
self.leader_model = leader_model
self.leader_tools = leader_tools or []
@@ -474,14 +480,21 @@ class TaskStrategy(BaseTeamStrategy):
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,
)
if blackboard is not None:
schema = None
initial_state = None
if isinstance(blackboard, type) and issubclass(blackboard, BaseModel):
schema = blackboard
elif isinstance(blackboard, BaseModel):
schema = type(blackboard)
initial_state = blackboard
else:
raise ValueError(
"blackboard 参数必须是 Pydantic BaseModel 的子类(类型)或其实例"
)
self.blackboard = BlackboardManager(
schema=blackboard_schema, initial_state=initial_blackboard_state
schema=schema, initial_state=initial_state
)
self.bb_toolkit = BlackboardToolkit(self.blackboard)
self.leader_tools.append(self.bb_toolkit)
@@ -493,9 +506,6 @@ class TaskStrategy(BaseTeamStrategy):
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
@@ -541,9 +551,7 @@ class TaskStrategy(BaseTeamStrategy):
)
if "__task_board__" not in context.session.shared_state:
context.session.shared_state["__task_board__"] = (
self.blackboard._state if self.blackboard else TaskBoardState()
)
context.session.shared_state["__task_board__"] = TaskBoardState()
board = cast(TaskBoardState, context.session.shared_state["__task_board__"])
max_iterations = self.max_iterations
@@ -576,15 +584,17 @@ class TaskStrategy(BaseTeamStrategy):
请检查是否有 failed 的任务需要修复重新指派?或者如果所有任务均已 completed,
请立刻调用 `mark_all_complete` 汇报总结。"""
logger.info(f"🧠 [TaskStrategy] 唤醒 Planner (Iter: {iteration})")
leader_res = yield CallAction(agent=leader_agent, task=planner_prompt)
logger.debug(f"🧠 [TaskStrategy] 唤醒 Planner (Iter: {iteration})")
leader_res = yield CallAction(
agent=leader_agent, task=planner_prompt or ""
)
if board.is_goal_complete:
yield FinishAction(result=board.final_summary or leader_res.output)
return
continue
logger.info(
logger.debug(
f"🚀 [TaskStrategy] 引擎接管:并发执行 {len(available_tasks)} 个任务..."
)
actions = []
+2 -4
View File
@@ -6,8 +6,8 @@ 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 zhenxun.services.ai.tools.models import EndRunResult, ToolResult
from zhenxun.services.ai.utils.logger import log_team as logger
from .models import TaskBoardState, TaskNodeStatus
@@ -188,8 +188,6 @@ class TaskPlanningToolkit(BaseToolkit):
],
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
+39 -40
View File
@@ -1,27 +1,42 @@
import asyncio
from collections.abc import Callable, Mapping, Sequence
import contextlib
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.capabilities import (
AbstractCapability,
CapabilitySource,
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.agent import 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 (
from zhenxun.services.ai.flow.base import BaseRunnable, ConcurrencyPolicy
from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
from zhenxun.services.ai.run import (
AgentRunResult,
AgentTask,
RunContext,
StreamedRunResult,
)
from zhenxun.services.ai.run.models import AgentRunError
from zhenxun.services.ai.tools.providers.skills.capabilities import SkillCapability
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.services.ai.utils import ContextUtils
from zhenxun.utils.utils import infer_plugin_namespace
from .models import TeamRuntimeConfig, Transition
from .router import BaseRouter
from .strategy import (
BaseTeamStrategy,
)
from zhenxun.services.ai.run import AgentRunResult, AgentTask, RunContext
from zhenxun.services.ai.tools.providers.skills.models import Skill, SkillSource
from zhenxun.utils.utils import infer_plugin_namespace
class Team(BaseRunnable[AgentRunResult[Any]]):
@@ -115,6 +130,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None,
max_handoffs: int = 3,
) -> Self:
"""
应用路由策略,基于挂载的 Router 进行最合适的专家动态分发。
@@ -128,8 +144,9 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: 路由节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给路由节点 (Leader) 的专属工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的路由系统提示词。
max_handoffs: 同一会话中允许连续移交的最大次数,防止无限踢皮球。
"""
from zhenxun.services.ai.flow.team.strategy import RouteStrategy
from .strategy import RouteStrategy
self.strategy = RouteStrategy(
state_flow=state_flow,
@@ -138,6 +155,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model=leader_model,
leader_tools=leader_tools,
custom_prompt=custom_prompt,
max_handoffs=max_handoffs,
)
self.selector_func = selector_func
return self
@@ -147,6 +165,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: str | None = None,
leader_tools: list[ToolSource] | None = None,
custom_prompt: str | None = None,
max_delegations: int = 3,
) -> Self:
"""
应用协作策略,Leader 自主规划并主动将子任务委派给 Sub-Agents,最后汇总结果。
@@ -158,13 +177,15 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: 协调节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给协调节点 (Leader) 的专属附加工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的协调系统提示词。
max_delegations: 允许向同一个专员连续委派失败的最大重试次数。
"""
from zhenxun.services.ai.flow.team.strategy import CoordinateStrategy
from .strategy import CoordinateStrategy
self.strategy = CoordinateStrategy(
leader_model=leader_model,
leader_tools=leader_tools,
custom_prompt=custom_prompt,
max_delegations=max_delegations,
)
return self
@@ -184,7 +205,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_tools: 挂载给总结节点 (Leader) 的专属附加工具列表。
custom_prompt: 自定义系统提示词,用于覆盖默认的广播总结系统提示词。
"""
from zhenxun.services.ai.flow.team.strategy import BroadcastStrategy
from .strategy import BroadcastStrategy
self.strategy = BroadcastStrategy(
leader_model=leader_model,
@@ -198,8 +219,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
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,
blackboard: type[BaseModel] | BaseModel | None = None,
custom_prompt: str | None = None,
) -> Self:
"""
@@ -213,18 +233,16 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
leader_model: 规划节点 (Leader) 使用的大模型名称,若为空则默认继承全局。
leader_tools: 挂载给规划节点 (Leader) 的专属附加工具列表。
max_iterations: 引擎驱动的状态机最大迭代/循环次数,防止死循环。
blackboard_schema: 团队共享黑板的数据结构类型 (Pydantic Model 类)。
initial_blackboard_state: 共享黑板的初始数据状态实例。
blackboard: (可选) 团队共享黑板。可传入 Schema 类型类,或直接传入带有初始数据的 Schema 实例对象。
custom_prompt: 自定义系统提示词,用于覆盖默认的规划系统提示词。
"""
from zhenxun.services.ai.flow.team.strategy import TaskStrategy
""" # noqa: E501
from .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,
blackboard=blackboard,
custom_prompt=custom_prompt,
)
return self
@@ -260,10 +278,6 @@ class Team(BaseRunnable[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)
@@ -273,8 +287,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
prompt=prompt, context=context, capabilities=capabilities, **kwargs
)
import contextlib
@contextlib.asynccontextmanager
async def run_stream(
self,
@@ -297,10 +309,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
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)
@@ -313,8 +321,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
elif callable(cap):
context.capabilities.append(DynamicCapability(cap))
from zhenxun.services.ai.flow.team.runner import TeamRunner
from zhenxun.services.ai.run import StreamedRunResult
from .runner import TeamRunner
event_bus = EventBus()
context.run.event_bus = event_bus
@@ -323,8 +330,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
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)
@@ -333,8 +338,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
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),
@@ -342,8 +345,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
)
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
@@ -359,8 +360,6 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
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))
+5 -4
View File
@@ -8,18 +8,19 @@ from zhenxun.services.ai.core.exceptions import (
ControlFlowExit,
ToolFatalError,
)
from zhenxun.services.ai.flow.workflow.policies import (
from zhenxun.services.ai.run import RunContext
from zhenxun.services.ai.utils.logger import log_flow as logger
from .policies import (
AbortPolicy,
BaseFailurePolicy,
PolicyAction,
)
from zhenxun.services.ai.flow.workflow.types import (
from .types import (
StepInput,
StepOutput,
StepType,
)
from zhenxun.services.ai.run import RunContext
from zhenxun.services.log import logger
class BaseNode(ABC):
+53 -12
View File
@@ -5,20 +5,16 @@ from typing import TYPE_CHECKING, Any
import uuid
from nonebot.params import Depends
from pydantic import BaseModel
if TYPE_CHECKING:
from zhenxun.services.ai.flow.workflow.nodes import NodeSource
from .nodes import NodeSource
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.blackboard import BlackboardManager
from zhenxun.services.ai.run.context import RunContext
from zhenxun.services.ai.run.models import (
AgentRunEnd,
@@ -27,9 +23,16 @@ from zhenxun.services.ai.run.models import (
StreamedRunResult,
)
from zhenxun.services.ai.tools.core.tool import FunctionTool
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_flow as logger
from zhenxun.utils.message import MessageUtils
from .nodes import Steps
from .types import (
StepInput,
StepOutput,
WorkflowRunResult,
)
class Workflow(BaseRunnable[WorkflowRunResult]):
"""
@@ -37,7 +40,13 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
继承自 BaseRunnable,支持被作为节点嵌套在 Team 或 其他工作流中。
"""
def __init__(self, name: str, steps: list["NodeSource"], description: str = ""):
def __init__(
self,
name: str,
steps: list["NodeSource"],
description: str = "",
blackboard: type[BaseModel] | BaseModel | None = None,
):
"""
静态图元工作流容器初始化。
@@ -45,7 +54,8 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
name: 工作流的名称标识。
steps: 工作流的节点列表(按列表顺序构成串行或嵌套结构)。
description: 工作流的说明描述,用于被 Agent 调用时理解其功能。
"""
blackboard: (可选) 结构化黑板。可传入 Schema 类型类,或直接传入带有初始数据的 Schema 实例对象。
""" # noqa: E501
self.name = name
self.description = description
self.id = uuid.uuid4().hex
@@ -54,6 +64,19 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
self.runtime_config = BaseRuntimeConfig(stateless=True)
self.persona = None
self.blackboard_schema = None
self.initial_blackboard_state = None
if blackboard is not None:
if isinstance(blackboard, type) and issubclass(blackboard, BaseModel):
self.blackboard_schema = blackboard
elif isinstance(blackboard, BaseModel):
self.blackboard_schema = type(blackboard)
self.initial_blackboard_state = blackboard
else:
raise ValueError(
"blackboard 参数必须是 Pydantic BaseModel 的子类(类型)或其实例"
)
def _build_result(
self,
initial_input: StepInput,
@@ -94,7 +117,12 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
return Depends(_dependency)
async def reply(
self, prompt: PromptInput | None = None, reply_to: bool = False, **kwargs: Any
self,
prompt: PromptInput | None = None,
reply_to: bool = False,
*,
context: RunContext | None = None,
**kwargs: Any,
) -> WorkflowRunResult:
"""
工作流交互执行语法糖,隐式提取上下文并自动将最终流水线产出发送回复给用户。
@@ -102,12 +130,13 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
参数:
prompt: 传入工作流入口根节点的初始参数或指令。
reply_to: 是否将结果作为回复消息发送 (at用户或引用原消息)。
context: 显式传入的会话与运行上下文。
kwargs: 追加的工作流附带参数 (additional_data)。
返回:
WorkflowRunResult: 包含执行状态、断点快照、各节点产出的全量工作流结果对象。
"""
ctx = RunContext()
ctx = context or RunContext()
bot = ctx.get_bot()
event = ctx.get_event()
@@ -152,6 +181,12 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
)
safe_context = context or RunContext(session_id=session_id)
if self.blackboard_schema and not safe_context.session.blackboard:
safe_context.session.blackboard = BlackboardManager(
schema=self.blackboard_schema,
initial_state=self.initial_blackboard_state,
)
logger.debug(f"🏭 **工作流 [{self.name}] 启动**")
initial_input = StepInput(input=prompt)
@@ -214,6 +249,12 @@ class Workflow(BaseRunnable[WorkflowRunResult]):
)
safe_context = context or RunContext(session_id=session_id)
if self.blackboard_schema and not safe_context.session.blackboard:
safe_context.session.blackboard = BlackboardManager(
schema=self.blackboard_schema,
initial_state=self.initial_blackboard_state,
)
logger.debug(f"🏭 **工作流 [{self.name}] 启动**")
initial_input = StepInput(input=prompt)
+8 -7
View File
@@ -5,17 +5,18 @@ from typing import Any, cast
from zhenxun.services.ai.core.messages import PromptInput
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.flow.workflow.base import BaseNode
from zhenxun.services.ai.flow.workflow.policies import BaseFailurePolicy
from zhenxun.services.ai.flow.workflow.types import (
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.ai.utils.logger import log_flow as logger
from .base import BaseNode
from .policies import BaseFailurePolicy
from .types import (
StepInput,
StepOutput,
StepType,
)
from zhenxun.services.ai.run import AgentTask, RunContext
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.run.models import AgentRunEnd
from zhenxun.services.log import logger
NodeSource = BaseNode | BaseRunnable | Callable
"""工作流节点来源,可以是图元、可执行引擎或原生函数"""
@@ -4,9 +4,10 @@ from typing import Any
from pydantic import BaseModel, Field
from zhenxun.services.ai.flow.workflow.types import StepInput
from zhenxun.services.ai.llm.api import generate_structured
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_flow as logger
from .types import StepInput
class PolicyAction(str, Enum):