♻️ refactor(core): 重构 AI 编排框架与记忆及 RAG 子系统 (#2149)

* ♻️ refactor(core): 重构 AI 编排框架与记忆及 RAG 子系统

- 【重构】重构 `BaseRunnable` 并引入统一的 `RunIntent` 意图载体,规范 Agent、Team 和 Workflow 的执行流
- 【解耦】将中期记忆槽和长期向量记忆从 `MemoryConfig` 中解耦,转为独立的能力组件与工具箱进行管理
- 【记忆】移除 `MemoryReader` 和 `MemoryWriter`,统一封装为 `SessionMemoryContext` 会话记忆门面
- 【RAG】重构检索器与存储后端接口,统一采用 `QueryRequest` 进行多维度联合检索,并引入 `InMemoryScorer` 提升打分性能
- 【事件】优化 `EventBus` 异步事件分发机制,引入队列机制确保事件按序处理,避免并发竞态问题
- 【依赖注入】移除 `memory` 注入项,优化 `DependencyInjector` 的签名解析缓存以提升性能

* 🚨 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-14 16:48:33 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 922d092650
commit 52f7dbdedf
66 changed files with 2131 additions and 2353 deletions
+11 -20
View File
@@ -1,13 +1,11 @@
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.flow.core.base import BaseRunnable
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 zhenxun.services.ai.tools.core.tool import BaseTool
from .models import Transition
@@ -18,8 +16,8 @@ class TeamRoutingCapability(AbstractCapability):
def __init__(
self,
team_name: str,
members: list[Any],
state_flow: Mapping[str, Sequence[Any]] | Callable | None = None,
members: list[BaseRunnable],
state_flow: Mapping[str, Sequence[Transition | str]] | Callable | None = None,
max_handoffs: int = 3,
):
self.team_name = team_name
@@ -27,7 +25,9 @@ class TeamRoutingCapability(AbstractCapability):
self.state_flow = state_flow
self.max_handoffs = max_handoffs
async def _get_allowed_transitions(self, context: RunContext) -> list[Any] | None:
async def _get_allowed_transitions(
self, context: RunContext
) -> list[Transition] | None:
"""核心FSM解析:解析静态字典或动态执行函数获取允许的 Transition 列表"""
if self.state_flow is None:
return None
@@ -45,16 +45,10 @@ class TeamRoutingCapability(AbstractCapability):
]
if callable(self.state_flow):
sig = inspect.signature(self.state_flow)
kwargs = await DependencyInjector.resolve_all(
sig, call_kwargs={}, context=context
result = await DependencyInjector.invoke(
self.state_flow, 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
@@ -62,7 +56,7 @@ class TeamRoutingCapability(AbstractCapability):
return None
async def get_tools(self, context: RunContext) -> list[Any]:
async def get_tools(self, context: RunContext) -> list[BaseTool]:
tools = []
allowed_transitions = await self._get_allowed_transitions(context)
@@ -81,10 +75,7 @@ class TeamRoutingCapability(AbstractCapability):
if transition is None:
continue
if getattr(m, "persona", None):
desc = f"角色:{m.persona.role},目标:{m.persona.goal}"
else:
desc = getattr(m, "description", "") or "处理节点"
desc = m.profile_summary
if transition and getattr(transition, "description", ""):
desc += f" 【移交条件】:{transition.description}"
+4 -3
View File
@@ -7,9 +7,10 @@ import uuid
from pydantic import BaseModel, ConfigDict, Field
from zhenxun.services.ai.core.messages import AgentMessage
from zhenxun.services.ai.core.messages import AgentMessage, PromptInput
from zhenxun.services.ai.core.options import BaseOutputDefinition
from zhenxun.services.ai.flow.base import BaseRunnable, BaseRuntimeConfig
from zhenxun.services.ai.flow.core.base import BaseRunnable
from zhenxun.services.ai.flow.core.models import BaseRuntimeConfig
from zhenxun.services.ai.run import AgentTask
@@ -67,7 +68,7 @@ class CallAction(TeamAction):
agent: str | BaseRunnable[Any]
"""目标 Agent 的名称(字符串)或动态生成的 Agent 实例"""
task: str | AgentTask
task: PromptInput | AgentTask
"""派发给该 Agent 的具体任务或提示词"""
history: Sequence[AgentMessage] | None = None
"""需要传递给该 Agent 的上下文历史记录(可选)"""
+32 -52
View File
@@ -1,18 +1,18 @@
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable, Mapping, Sequence
import inspect
from collections.abc import Callable, Mapping, Sequence
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.run import AgentTask, RunContext
from zhenxun.services.ai.flow.agent.agent import Agent, ToolSource
from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.flow.core.base import BaseRunnable
from zhenxun.services.ai.run import RunContext, RunIntent
from zhenxun.services.ai.run.di import DependencyInjector
from zhenxun.services.ai.utils.logger import log_team as logger
from .models import RouteDecision, Transition
from .capabilities import TeamRoutingCapability
from .models import RouteDecision, TeamRuntimeConfig, Transition
class BaseRouter(ABC):
@@ -23,7 +23,7 @@ class BaseRouter(ABC):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
intent: RunIntent,
) -> RouteDecision | None:
"""核心路由方法"""
pass
@@ -32,7 +32,9 @@ class BaseRouter(ABC):
class FunctionRouter(BaseRouter):
"""基于纯函数的极速路由器"""
def __init__(self, selector_func: Callable[..., Any], target: str | None = None):
def __init__(
self, selector_func: Callable[..., str | bool | None], target: str | None = None
):
"""
初始化基于函数的极速路由器。
@@ -47,27 +49,21 @@ class FunctionRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
intent: RunIntent,
) -> RouteDecision | None:
sig = inspect.signature(self.selector_func)
call_kwargs = {"prompt": prompt, "context": context, "history": history}
if isinstance(prompt, AgentTask):
call_kwargs["agent_task"] = prompt
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
call_kwargs = {
"intent": intent,
"prompt": intent.original_input,
"context": context,
"history": history,
}
if intent.task_obj:
call_kwargs["agent_task"] = intent.task_obj
call_kwargs["task"] = intent.task_obj
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)
selected_target = await DependencyInjector.invoke(
self.selector_func, call_kwargs, context
)
if isinstance(selected_target, bool):
if selected_target and self.target:
@@ -97,13 +93,9 @@ class RegexRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
intent: RunIntent,
) -> RouteDecision | None:
text_to_match = (
prompt.description
if isinstance(prompt, AgentTask)
else (prompt or context.run.user_input or "")
)
text_to_match = intent.text or context.run.user_input or ""
if self.pattern.search(text_to_match):
logger.debug(f"命中正则极速路由 -> {self.target}")
@@ -127,10 +119,10 @@ class ChainRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
intent: RunIntent,
) -> RouteDecision | None:
for router in self.routers:
decision = await router.route(context, history, prompt)
decision = await router.route(context, history, intent)
if decision is not None:
return decision
return None
@@ -142,11 +134,11 @@ class LLMRouter(BaseRouter):
def __init__(
self,
team_name: str,
members: list[Any],
members: list[BaseRunnable],
leader_model: str | None = None,
leader_tools: list[Any] | None = None,
leader_tools: list[ToolSource] | None = None,
state_flow: Mapping[str, Sequence[Transition | str]] | Callable | None = None,
runtime_config: Any = None,
runtime_config: TeamRuntimeConfig | None = None,
custom_prompt: str | None = None,
allowed_transitions: list[Transition] | None = None,
max_handoffs: int = 3,
@@ -179,13 +171,8 @@ class LLMRouter(BaseRouter):
self,
context: RunContext,
history: Sequence[AgentMessage],
prompt: str | AgentTask | None = None,
intent: RunIntent,
) -> RouteDecision | None:
from zhenxun.services.ai.flow.agent.agent import Agent
from zhenxun.services.ai.flow.agent.models import AgentConfig
from .capabilities import TeamRoutingCapability
default_system_prompt = """## 角色与目标
你是一个高级任务路由器 (所在团队: {{ team_name }})。
请根据用户的输入意图,立刻调用相应的移交工具 (transfer_to_...)
@@ -217,13 +204,6 @@ class LLMRouter(BaseRouter):
)
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",
@@ -240,7 +220,7 @@ class LLMRouter(BaseRouter):
logger.debug("🤖 [LLMRouter] 启动 LLM 思考路由决策...")
res = await router_agent.run(
prompt=prompt,
prompt=intent.original_input,
context=sub_context,
config=AgentConfig(message_history=history),
)
+78 -73
View File
@@ -1,6 +1,9 @@
import asyncio
from collections.abc import AsyncGenerator
from typing import Any
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from .team import Team
from zhenxun.services.ai.core.exceptions import (
AbortException,
@@ -8,19 +11,19 @@ from zhenxun.services.ai.core.exceptions import (
LLMException,
)
from zhenxun.services.ai.core.messages import UsageInfo
from zhenxun.services.ai.core.stream_events import AgentStreamEvent
from zhenxun.services.ai.flow.agent.models import AgentConfig
from zhenxun.services.ai.run import AgentRunResult, RunContext
from zhenxun.services.ai.run import AgentRunResult, RunContext, RunIntent
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 .strategy import BaseTeamStrategy, RouteStrategy
from .strategy import BaseTeamStrategy
class TeamRunner:
@@ -28,10 +31,33 @@ class TeamRunner:
多智能体团队核心执行引擎。
"""
def __init__(self, team: Any, strategy: BaseTeamStrategy):
def __init__(self, team: "Team", strategy: BaseTeamStrategy):
self.team = team
self.strategy = strategy
async def _consume_event_queue(
self,
queue: asyncio.Queue,
tasks: list[asyncio.Task],
expected_results: int,
results_box: dict[int, tuple[str, AgentRunResult]],
) -> AsyncGenerator[AgentStreamEvent, None]:
"""辅助方法:统一消费队列中的流事件、异常和结果"""
try:
while len(results_box) < expected_results:
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
results_box[idx] = (agent_name, agent_res)
finally:
for task in tasks:
if not task.done():
task.cancel()
async def _execute_call_action_to_queue(
self,
index: int,
@@ -69,14 +95,9 @@ class TeamRunner:
sub_context = context.clone_for_member(target_agent.name)
sub_context.capabilities = list(sub_context.capabilities)
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)
sub_context.capabilities.extend(
self.strategy.get_member_capabilities(self.team, target_agent)
)
logger.debug(f"🚀 **专员 👨💼`{target_agent.name}`** 开始执行子任务...")
@@ -125,7 +146,7 @@ class TeamRunner:
target_name = agent_res.handoff.target
reason = agent_res.handoff.reason
logger.info(
logger.debug(
f"🛣️ **路由决策**: 委派给专员 👨💼`{target_name}` (理由: {reason})"
)
@@ -138,15 +159,37 @@ class TeamRunner:
await queue.put(("result", index, target_agent.name, agent_res))
async def _dispatch_actions(
self,
actions: list[CallAction],
context: RunContext,
session_id: str,
results_container: list,
) -> AsyncGenerator[AgentStreamEvent, None]:
"""统一的任务派发、流事件转译与结果回传调度器"""
queue = asyncio.Queue()
tasks = [
asyncio.create_task(
self._execute_call_action_to_queue(i, act, context, session_id, queue)
)
for i, act in enumerate(actions)
]
results_box: dict[int, tuple[str, AgentRunResult]] = {}
async for event in self._consume_event_queue(
queue, tasks, len(actions), results_box
):
yield event
results_container.extend([results_box[i] for i in range(len(actions))])
async def run_stream(
self, prompt: Any, context: RunContext, **kwargs: Any
) -> AsyncGenerator[Any, None]:
self, intent: RunIntent, context: RunContext, **kwargs: Any
) -> AsyncGenerator[AgentStreamEvent, None]:
session_id = context.session_id or "default_team_session"
task_desc = getattr(prompt, "description", str(prompt))
task_desc = intent.text
logger.info(f"🤝 **团队 [{self.team.name}] 开始协作**: `{task_desc}`")
logger.debug(f"🤝 **团队 [{self.team.name}] 开始协作**: `{task_desc}`")
plan_gen = self.strategy.generate_plan(self.team, prompt, context, **kwargs)
plan_gen = self.strategy.generate_plan(self.team, intent, context, **kwargs)
send_value = None
final_result = None
@@ -160,61 +203,23 @@ class TeamRunner:
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()
res_container = []
async for event in self._dispatch_actions(
[action], context, session_id, res_container
):
yield event
_, send_value = res_container[0]
cumulative_usage += send_value.usage
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()
res_container = []
async for event in self._dispatch_actions(
action.actions, context, session_id, res_container
):
yield event
send_value = res_container
for _, res in res_container:
cumulative_usage += res.usage
elif isinstance(action, FinishAction):
final_result = action.result
@@ -225,7 +230,7 @@ class TeamRunner:
except Exception as e:
raise e
logger.info(f"🏁 **团队 [{self.team.name}]** 协作圆满结束!")
logger.debug(f"🏁 **团队 [{self.team.name}]** 协作圆满结束!")
if not isinstance(final_result, AgentRunResult):
final_result = model_construct(
+146 -129
View File
@@ -1,16 +1,20 @@
from abc import ABC
from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
import json
import re
from typing import TYPE_CHECKING, Any, cast
from pydantic import BaseModel
from zhenxun.services.ai.capabilities import AbstractCapability
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.run import AgentTask, RunContext
from zhenxun.services.ai.flow.core.base import BaseRunnable
from zhenxun.services.ai.run import RunContext, RunIntent
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
@@ -25,7 +29,7 @@ from .models import (
TeamAction,
Transition,
)
from .router import BaseRouter
from .router import BaseRouter, ChainRouter, FunctionRouter, LLMRouter
from .task_tools import TaskPlanningToolkit
if TYPE_CHECKING:
@@ -50,10 +54,16 @@ class BaseTeamStrategy(ABC):
template = self.custom_prompt or self.default_system_prompt
return PromptTemplate(template).render(**kwargs)
def get_member_capabilities(
self, team: "Team", member: BaseRunnable
) -> list[AbstractCapability]:
"""获取派发给子成员时需要动态注入的能力组件"""
return []
async def generate_plan(
self,
team: "Team",
prompt: str | AgentTask | None,
intent: RunIntent,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
@@ -66,36 +76,39 @@ class BaseTeamStrategy(ABC):
2. 使用 `yield FinishAction(...)` 结束团队协作。
"""
yield FinishAction(
result="The Strategy has not implemented generate_plan() yet."
)
yield FinishAction(result="该策略尚未实现 generate_plan() 方法。")
def _build_leader_agent(
self, team: "Team", name: str, instruction: str, tools: list[ToolSource]
self,
team: "Team",
role_name: str,
extra_instruction: str = "",
extra_tools: list[ToolSource] | None = None,
) -> Agent:
"""
统一的团队 Leader / Planner 装配工厂。
自动处理无状态配置以及 HITL 状态继承。
统一的团队 Leader / Planner / Broadcaster 装配工厂。
自动合并默认指令、追加指令、基类工具和策略专属工具,
并处理无状态配置以及 HITL 状态继承。
"""
instruction = self.get_prompt()
if extra_instruction:
instruction += f"\n\n{extra_instruction}"
tools = getattr(self, "leader_tools", []).copy()
if extra_tools:
tools.extend(extra_tools)
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
target_model = getattr(self, "leader_model", None) or team.default_model
return Agent(
name=name,
name=f"{team.name}_{role_name}",
instruction=instruction,
persona=team.persona,
model=target_model,
tools=tools,
config=leader_config,
@@ -107,7 +120,7 @@ class RouteStrategy(BaseTeamStrategy):
def __init__(
self,
state_flow: "Mapping[str, Sequence[str | Any]] | Callable | None" = None,
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,
@@ -148,17 +161,79 @@ class RouteStrategy(BaseTeamStrategy):
else:
self.state_flow = state_flow
def get_member_capabilities(
self, team: "Team", member: BaseRunnable
) -> list[AbstractCapability]:
from .capabilities import TeamRoutingCapability
return [
TeamRoutingCapability(
team_name=team.name,
members=team.members,
state_flow=self.state_flow,
max_handoffs=self.max_handoffs,
)
]
def _build_handoff_history(
self, handoff_reason: str, context_data: Any
) -> list[AgentMessage]:
"""将移交数据格式化为系统的引导历史消息"""
handoff_history_messages = []
upstream_info = []
if handoff_reason:
upstream_info.append(f"【移交说明】\n{handoff_reason}")
if context_data:
if isinstance(context_data, dict):
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}")
if combined_info := "\n\n".join(upstream_info):
handoff_msg = LLMMessage.system(
f"### 🔄 [来自上游节点的移交数据]\n{combined_info}"
)
handoff_history_messages.append(handoff_msg)
return handoff_history_messages
def _check_fast_route(
self, current_target: str, output_str: str
) -> tuple[bool, str]:
"""检查并执行快速硬路由,返回(是否触发路由, 新目标名称)"""
if (
not isinstance(self.state_flow, dict)
or current_target not in self.state_flow
):
return False, current_target
for t in self.state_flow[current_target]:
if trigger_regex := getattr(t, "trigger_regex", None):
if re.search(trigger_regex, output_str):
return True, getattr(t, "target", current_target)
if trigger_func := getattr(t, "trigger_func", None):
try:
res = trigger_func(output_str)
if res:
if isinstance(res, str):
return True, res
return True, getattr(t, "target", current_target)
except Exception:
pass
return False, current_target
async def generate_plan(
self,
team: "Team",
prompt: str | AgentTask | None,
intent: RunIntent,
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))
@@ -166,7 +241,7 @@ class RouteStrategy(BaseTeamStrategy):
LLMRouter(
team_name=team.name,
members=team.members,
leader_model=self.leader_model,
leader_model=self.leader_model or team.default_model,
leader_tools=self.leader_tools,
state_flow=self.state_flow,
runtime_config=getattr(team, "runtime_config", None),
@@ -182,7 +257,7 @@ class RouteStrategy(BaseTeamStrategy):
logger.debug(f"🛣️ '{team.name}' 正在获取初始路由决策...")
decision = await router.route(context, [], prompt)
decision = await router.route(context, [], intent)
if not decision:
logger.warning(f"🚨 Team '{team.name}' 的所有路由策略未能命中目标。")
raise AbortException(
@@ -210,33 +285,13 @@ class RouteStrategy(BaseTeamStrategy):
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)
handoff_history_messages = self._build_handoff_history(
handoff_reason, context_data
)
run_result = yield CallAction(
agent=current_target,
task=prompt or "",
task=intent.original_input or "",
history=handoff_history_messages,
kwargs=kwargs,
)
@@ -249,32 +304,11 @@ class RouteStrategy(BaseTeamStrategy):
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
fast_routed, new_target = self._check_fast_route(current_target, output_str)
if fast_routed:
current_target = new_target
handoff_reason = ""
context_data = output_str
logger.debug(
f"🛣️ **路由决策**: 委派给专员 👨💼`{current_target}`"
"(系统拦截:正则/函数状态流发生转移)"
@@ -317,16 +351,13 @@ class CoordinateStrategy(BaseTeamStrategy):
async def generate_plan(
self,
team: "Team",
prompt: str | AgentTask | None,
intent: RunIntent,
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}"
desc = m.profile_summary
delegation_tools.append(
DelegateTool(
@@ -337,14 +368,10 @@ class CoordinateStrategy(BaseTeamStrategy):
)
)
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,
role_name="Leader",
extra_tools=delegation_tools,
)
logger.debug(f"✨ **团队 [{team.name}] Leader** 正在汇总各方报告...")
@@ -358,7 +385,9 @@ class CoordinateStrategy(BaseTeamStrategy):
logger.debug(f"👨💼 [CoordinateStrategy] '{team.name}' 正在启动协调推理循环...")
leader_res = yield CallAction(agent=leader_agent, task=prompt or "")
leader_res = yield CallAction(
agent=leader_agent, task=intent.original_input or ""
)
yield FinishAction(result=leader_res)
@@ -391,13 +420,11 @@ class BroadcastStrategy(BaseTeamStrategy):
async def generate_plan(
self,
team: "Team",
prompt: str | AgentTask | None,
intent: RunIntent,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
task_desc_str = (
prompt.description if isinstance(prompt, AgentTask) else (prompt or "")
)
task_desc_str = intent.text
await context.run.emit(
ToolStreamChunkEvent(
@@ -406,7 +433,10 @@ class BroadcastStrategy(BaseTeamStrategy):
)
)
actions = [CallAction(agent=m.name, task=task_desc_str) for m in team.members]
actions = [
CallAction(agent=m.name, task=intent.original_input or "")
for m in team.members
]
results = yield ConcurrentCallAction(actions=actions)
await context.run.emit(
@@ -430,9 +460,7 @@ class BroadcastStrategy(BaseTeamStrategy):
leader_agent = self._build_leader_agent(
team=team,
name=f"{team.name}_Leader",
instruction=self.get_prompt(),
tools=self.leader_tools,
role_name="Leader",
)
leader_res = yield CallAction(agent=leader_agent, task=synthesize_prompt)
@@ -497,33 +525,36 @@ class TaskStrategy(BaseTeamStrategy):
schema=schema, initial_state=initial_state
)
self.bb_toolkit = BlackboardToolkit(self.blackboard)
self.leader_tools.append(self.bb_toolkit)
def get_member_capabilities(
self, team: "Team", member: BaseRunnable
) -> list[AbstractCapability]:
caps = super().get_member_capabilities(team, member)
if self.bb_toolkit:
class BlackboardInjectCapability(AbstractCapability):
def __init__(self, tk):
self.toolkit = tk
async def get_tools(self, context: RunContext) -> list[Any]:
return [self.toolkit]
caps.append(BlackboardInjectCapability(self.bb_toolkit))
return caps
async def generate_plan(
self,
team: "Team",
prompt: str | AgentTask | None,
intent: RunIntent,
context: RunContext,
**kwargs,
) -> AsyncGenerator[TeamAction, Any]:
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}"
desc = m.profile_summary
member_infos.append(
f'<member id="{m.name}" name="{m.name}">\n'
f" Description: {desc}\n"
@@ -532,18 +563,11 @@ class TaskStrategy(BaseTeamStrategy):
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,
role_name="Planner",
extra_instruction=members_xml,
extra_tools=[TaskPlanningToolkit(members=team.members)],
)
logger.debug(
@@ -555,7 +579,7 @@ class TaskStrategy(BaseTeamStrategy):
board = cast(TaskBoardState, context.session.shared_state["__task_board__"])
max_iterations = self.max_iterations
planner_prompt = prompt
planner_prompt = intent.original_input
for iteration in range(max_iterations):
if board.is_goal_complete:
@@ -567,12 +591,7 @@ class TaskStrategy(BaseTeamStrategy):
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 ""
)
expected_out = getattr(prompt, "expected_output", None)
if expected_out:
goal_str += f"\n\n### 🎯 [预期产出要求]\n{expected_out}"
goal_str = intent.text
planner_prompt = f"""### 🎯 用户的终极目标 (Original Goal)
{goal_str}
@@ -637,8 +656,6 @@ class TaskStrategy(BaseTeamStrategy):
)
if task.metadata:
import json
meta_str = json.dumps(task.metadata, ensure_ascii=False)
task_prompt += f"\n\n### ⚙️ 附加系统元数据约束:\n{meta_str}"
+1 -1
View File
@@ -2,7 +2,7 @@ from typing import Annotated, Any
from pydantic import Field
from zhenxun.services.ai.flow.base import BaseRunnable
from zhenxun.services.ai.flow.core.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
+39 -77
View File
@@ -1,6 +1,4 @@
import asyncio
from collections.abc import Callable, Mapping, Sequence
import contextlib
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
from pathlib import Path
from typing import Any
from typing_extensions import Self
@@ -12,24 +10,20 @@ from zhenxun.services.ai.capabilities import (
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.core.stream_events import AgentStreamEvent, EventBus
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, ConcurrencyPolicy
from zhenxun.services.ai.flow.concurrency import apply_concurrency_policy
from zhenxun.services.ai.flow.core.base import BaseRunnable
from zhenxun.services.ai.run import (
AgentRunResult,
AgentTask,
RunContext,
StreamedRunResult,
RunIntent,
)
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
@@ -52,7 +46,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
model: str | Callable[[], str] | None = None,
strategy: BaseTeamStrategy | None = None,
description: str | None = None,
persona: Persona | dict | None = None,
persona: Persona | None = None,
runtime_config: TeamRuntimeConfig | dict | None = None,
capabilities: list[CapabilitySource] | None = None,
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
@@ -74,15 +68,17 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
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 description:
self.description = description
else:
self.description = f"一个名为 {self.name} 的协作团队,"
f"包含 {len(self.members)} 个处理节点。"
self.capabilities: list[AbstractCapability] = []
if capabilities:
for cap in capabilities:
if isinstance(cap, AbstractCapability):
@@ -107,6 +103,18 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
getattr(strategy, "selector_func", None) if strategy else None
)
@property
def default_model(self) -> str | None:
"""获取当前团队默认调用的可用大模型。优先取自身配置,其次遍历成员寻找可用模型。"""
if getattr(self, "model", None):
return str(self.model() if callable(self.model) else self.model)
for m in self.members:
m_model = getattr(m, "model_name", None) or getattr(m, "model", None)
if m_model:
return str(m_model() if callable(m_model) else m_model)
return None
def with_strategy(self, strategy: BaseTeamStrategy) -> Self:
"""
挂载自定义的团队协作策略。
@@ -122,9 +130,7 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
def with_routing(
self,
state_flow: (
Mapping[str, Sequence[Transition | str | Any]] | Callable | None
) = None,
state_flow: (Mapping[str, Sequence[Transition | str]] | Callable | None) = None,
selector_func: Callable[..., str | None] | None = None,
router: BaseRouter | None = None,
leader_model: str | None = None,
@@ -287,20 +293,18 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
prompt=prompt, context=context, capabilities=capabilities, **kwargs
)
@contextlib.asynccontextmanager
async def run_stream(
async def _execute_stream(
self,
prompt: PromptInput | AgentTask | None = None,
*,
context: "RunContext | None" = None,
capabilities: list[CapabilitySource] | None = None,
skills: Sequence[str | Path | Skill | SkillSource] | None = None,
intent: RunIntent,
context: RunContext,
cancel_token: CancellationToken,
event_bus: EventBus,
**kwargs: Any,
):
) -> AsyncIterator[AgentStreamEvent]:
self._ensure_strategy()
if context is None:
context = RunContext()
capabilities = kwargs.pop("capabilities", None)
skills = kwargs.pop("skills", None)
if not hasattr(context, "capabilities"):
context.capabilities = []
@@ -310,6 +314,10 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
if skills:
capabilities = list(capabilities) if capabilities else []
from zhenxun.services.ai.tools.providers.skills.capabilities import (
SkillCapability,
)
capabilities.append(
SkillCapability(skills=skills, namespace=self.namespace)
)
@@ -323,54 +331,8 @@ class Team(BaseRunnable[AgentRunResult[Any]]):
from .runner import TeamRunner
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:
policy = (
ConcurrencyPolicy.ALLOW
if getattr(self.runtime_config, "stateless", True)
else ConcurrencyPolicy.QUEUE
)
intervention_policy = getattr(self.runtime_config, "intervention_policy", None)
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():
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:
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()
async for event in runner.run_stream(intent, context, **kwargs):
yield event