✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)

* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架

- 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片
- 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式
- 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统
- 【新增】引入基于 Docker 的安全代码执行沙箱环境
- 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务
- 【新增】引入输入输出安全合规护栏与自愈反思机制
- 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等)
- 【优化】优化日志脱敏与 Token 预估机制
- 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令

* 🔧 chore(deps): 更新项目依赖与配置

- 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt
- 在 pyright 配置中设置 reportMissingImports 为 none
- 调整 .gitignore 中 resources 目录的忽略规则

* ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出

- 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束
- 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用
- 调整指令处理器(Directive)的返回值为 `tool_res.output`
- 修复部分类型检查警告并优化联合类型判断语法

* ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制

- 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用
- 移除通过 `context.state` 传递魔术变量的隐式控制流设计
- 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult`
- 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令
- 补全 `MessageBuilder` 中部分核心方法的文档注释

* 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑

-【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值
-【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题
-【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求
-【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建
-【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常

* 📝 docs(core): 补充核心模块初始化方法的文档注释

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2026-07-03 08:53:56 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bdc1374848
commit 80fc5b86a7
223 changed files with 41885 additions and 10006 deletions
@@ -0,0 +1,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 []
+265
View File
@@ -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>"
+247
View File
@@ -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
+235
View File
@@ -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)
+641
View File
@@ -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}),任务未能在限定步数内完成。"
)
+196
View File
@@ -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)
+377
View File
@@ -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()