[add] multi agent handoff

This commit is contained in:
Mark
2025-12-29 12:17:32 +08:00
parent 667c3393bc
commit de714d0422
8 changed files with 3555 additions and 1401 deletions

View File

@@ -2,16 +2,18 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Query, Path
from fastapi import APIRouter, Depends, Path
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from app.db import get_db
from app.dependencies import get_current_user
from app.core.response_utils import success
from app.core.logging_config import get_business_logger
from app.schemas import multi_agent_schema, MultiAgentConfigUpdate, MultiAgentConfigSchema
from app.schemas.response_schema import PageData, PageMeta
from app.schemas import MultiAgentConfigUpdate, MultiAgentConfigSchema
from app.schemas.multi_agent_schema import MultiAgentRunRequest
from app.services.multi_agent_service import MultiAgentService, get_multi_agent_service
from app.services.multi_agent_handoffs_integration import MultiAgentHandoffsService
from app.models import User
router = APIRouter(prefix="/apps", tags=["Multi-Agent"])
@@ -558,3 +560,177 @@ def update_multi_agent_config(
# data=response_data,
# msg=f"批量测试完成,准确率: {accuracy:.1f}%" if accuracy else "批量测试完成"
# )
# ==================== Agent Handoffs 协作 ====================
@router.post(
"/{app_id}/chat/handoffs",
summary="支持 Agent Handoffs 的聊天接口"
)
async def chat_with_handoffs(
app_id: uuid.UUID = Path(..., description="应用 ID"),
request: MultiAgentRunRequest = ...,
current_user: User = Depends(get_current_user),
multi_agent_service: Annotated[MultiAgentService, Depends(get_multi_agent_service)] = None,
db: Session = Depends(get_db),
):
"""支持 Agent Handoffs 的聊天接口
基于 LangChain handoffs 模式,支持:
- Agent 之间的动态切换
- 工具驱动的状态转换
- 会话上下文的保持
- 协作历史的追踪
配置要求:
在 execution_config 中设置 "enable_handoffs": true
返回信息包括:
- message: 最终回复
- conversation_id: 会话 ID
- final_agent_id: 最终处理的 Agent
- handoff_count: 切换次数
- handoff_history: 切换历史记录
"""
# 创建 handoffs 服务
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 执行协作
result = await handoffs_service.run_with_handoffs(app_id, request)
return success(
data=result,
msg="Agent Handoffs 执行成功"
)
@router.post(
"/{app_id}/chat/handoffs/stream",
summary="流式 Agent Handoffs 聊天接口"
)
async def chat_with_handoffs_stream(
app_id: uuid.UUID = Path(..., description="应用 ID"),
request: MultiAgentRunRequest = ...,
current_user: User = Depends(get_current_user),
multi_agent_service: Annotated[MultiAgentService, Depends(get_multi_agent_service)] = None,
db: Session = Depends(get_db),
):
"""流式 Agent Handoffs 聊天接口
以 SSE (Server-Sent Events) 格式返回流式响应
事件类型:
- start: 开始执行
- message: Agent 消息
- handoff: Agent 切换事件
- end: 执行结束
- error: 错误信息
"""
# 创建 handoffs 服务
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 流式执行
return StreamingResponse(
handoffs_service.run_stream_with_handoffs(app_id, request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
@router.get(
"/conversations/{conversation_id}/handoffs/history",
summary="获取会话的 Handoff 历史"
)
def get_handoff_history(
conversation_id: str = Path(..., description="会话 ID"),
current_user: User = Depends(get_current_user),
multi_agent_service: Annotated[MultiAgentService, Depends(get_multi_agent_service)] = None,
db: Session = Depends(get_db),
):
"""获取指定会话的 Agent Handoff 历史记录
返回信息包括:
- conversation_id: 会话 ID
- current_agent_id: 当前活跃的 Agent
- handoff_count: 总切换次数
- handoff_history: 详细的切换历史
"""
# 创建 handoffs 服务
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 获取历史
history = handoffs_service.get_handoff_history(conversation_id)
if not history:
return success(
data=None,
msg="该会话没有 Handoff 历史记录"
)
return success(
data=history,
msg="获取 Handoff 历史成功"
)
@router.delete(
"/conversations/{conversation_id}/handoffs",
summary="清除会话的 Handoff 状态"
)
def clear_handoff_state(
conversation_id: str = Path(..., description="会话 ID"),
current_user: User = Depends(get_current_user),
multi_agent_service: Annotated[MultiAgentService, Depends(get_multi_agent_service)] = None,
db: Session = Depends(get_db),
):
"""清除指定会话的 Handoff 状态
用于:
- 重置会话状态
- 清理测试数据
- 释放内存
"""
# 创建 handoffs 服务
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 清除状态
handoffs_service.clear_handoff_state(conversation_id)
return success(msg="Handoff 状态已清除")
@router.post(
"/{app_id}/handoffs/test-routing",
summary="测试 Handoff 路由决策"
)
async def test_handoff_routing(
app_id: uuid.UUID = Path(..., description="应用 ID"),
message: str = ...,
current_user: User = Depends(get_current_user),
multi_agent_service: Annotated[MultiAgentService, Depends(get_multi_agent_service)] = None,
db: Session = Depends(get_db),
):
"""测试 Handoff 路由决策(不实际执行)
用于调试和测试路由逻辑,返回:
- initial_agent_id: 初始选择的 Agent
- available_handoff_tools: 可用的切换工具列表
- handoff_suggestion: 自动切换建议
不会实际执行 Agent只返回路由决策信息
"""
# 创建 handoffs 服务
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 测试路由
result = await handoffs_service.test_handoff_routing(app_id, message)
return success(
data=result,
msg="路由测试完成"
)

View File

@@ -202,3 +202,68 @@ class BatchRoutingTestRequest(BaseModel):
le=1.0,
description="关键词置信度阈值"
)
# ==================== Agent Handoffs ====================
class HandoffHistoryItem(BaseModel):
"""Handoff 历史记录项"""
from_agent: str = Field(..., description="源 Agent ID")
to_agent: str = Field(..., description="目标 Agent ID")
reason: str = Field(..., description="切换原因")
timestamp: Optional[str] = Field(None, description="切换时间")
user_message: Optional[str] = Field(None, description="触发切换的用户消息")
context_summary: Optional[str] = Field(None, description="上下文摘要")
class HandoffChatResponse(BaseModel):
"""Handoff 聊天响应"""
message: str = Field(..., description="最终回复")
conversation_id: str = Field(..., description="会话 ID")
final_agent_id: str = Field(..., description="最终处理的 Agent ID")
handoff_count: int = Field(..., description="切换次数")
handoff_history: List[HandoffHistoryItem] = Field(
default_factory=list,
description="切换历史"
)
elapsed_time: float = Field(..., description="总耗时(秒)")
usage: Optional[Dict[str, Any]] = Field(None, description="资源使用情况")
error: Optional[str] = Field(None, description="错误信息")
class HandoffStateResponse(BaseModel):
"""Handoff 状态响应"""
conversation_id: str = Field(..., description="会话 ID")
current_agent_id: str = Field(..., description="当前活跃的 Agent ID")
handoff_count: int = Field(..., description="总切换次数")
handoff_history: List[HandoffHistoryItem] = Field(
default_factory=list,
description="切换历史"
)
created_at: str = Field(..., description="创建时间")
updated_at: str = Field(..., description="更新时间")
class HandoffToolInfo(BaseModel):
"""Handoff 工具信息"""
name: str = Field(..., description="工具名称")
target_agent_id: str = Field(..., description="目标 Agent ID")
target_agent_name: str = Field(..., description="目标 Agent 名称")
description: str = Field(..., description="工具描述")
class HandoffRoutingTestResponse(BaseModel):
"""Handoff 路由测试响应"""
message: str = Field(..., description="测试消息")
initial_agent_id: str = Field(..., description="初始 Agent ID")
initial_agent_name: str = Field(..., description="初始 Agent 名称")
available_handoff_tools: List[HandoffToolInfo] = Field(
default_factory=list,
description="可用的 handoff 工具"
)
handoff_suggestion: Optional[Dict[str, Any]] = Field(
None,
description="自动切换建议"
)
total_agents: int = Field(..., description="总 Agent 数量")

View File

@@ -0,0 +1,375 @@
"""Agent Handoff 机制 - 实现 Agent 之间的动态切换和协作
基于 LangChain 的 handoffs 模式,支持:
1. Agent 之间的动态切换transfer
2. 工具驱动的状态转换
3. 会话上下文的保持
4. 协作历史的追踪
"""
import uuid
from typing import Dict, Any, Optional, List
from datetime import datetime
from pydantic import BaseModel, Field
from app.core.logging_config import get_business_logger
logger = get_business_logger()
class HandoffContext(BaseModel):
"""Handoff 上下文信息"""
from_agent_id: str = Field(..., description="源 Agent ID")
to_agent_id: str = Field(..., description="目标 Agent ID")
reason: str = Field(..., description="切换原因")
timestamp: datetime = Field(default_factory=datetime.now)
user_message: Optional[str] = Field(None, description="触发切换的用户消息")
context_summary: Optional[str] = Field(None, description="上下文摘要")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class AgentHandoffTool(BaseModel):
"""Agent Handoff 工具定义"""
name: str = Field(..., description="工具名称,如 transfer_to_math_agent")
target_agent_id: str = Field(..., description="目标 Agent ID")
target_agent_name: str = Field(..., description="目标 Agent 名称")
description: str = Field(..., description="工具描述")
trigger_keywords: List[str] = Field(default_factory=list, description="触发关键词")
def to_tool_schema(self) -> Dict[str, Any]:
"""转换为 LLM 工具 schema"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "切换到该 Agent 的原因"
},
"context_summary": {
"type": "string",
"description": "需要传递给目标 Agent 的上下文摘要(可选)"
}
},
"required": ["reason"]
}
}
}
class HandoffState(BaseModel):
"""Handoff 状态管理"""
conversation_id: str = Field(..., description="会话 ID")
current_agent_id: str = Field(..., description="当前活跃的 Agent ID")
handoff_history: List[HandoffContext] = Field(default_factory=list, description="切换历史")
context_data: Dict[str, Any] = Field(default_factory=dict, description="共享上下文数据")
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
def add_handoff(self, context: HandoffContext):
"""添加 handoff 记录"""
self.handoff_history.append(context)
self.current_agent_id = context.to_agent_id
self.updated_at = datetime.now()
logger.info(
"Agent handoff 记录",
extra={
"conversation_id": self.conversation_id,
"from_agent": context.from_agent_id,
"to_agent": context.to_agent_id,
"reason": context.reason
}
)
def get_recent_handoffs(self, limit: int = 5) -> List[HandoffContext]:
"""获取最近的 handoff 记录"""
return self.handoff_history[-limit:] if self.handoff_history else []
def get_handoff_count(self) -> int:
"""获取 handoff 次数"""
return len(self.handoff_history)
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class HandoffManager:
"""Handoff 管理器 - 管理 Agent 之间的切换"""
def __init__(self):
"""初始化 Handoff 管理器"""
self._states: Dict[str, HandoffState] = {}
logger.info("Handoff 管理器初始化完成")
def create_state(
self,
conversation_id: str,
initial_agent_id: str
) -> HandoffState:
"""创建新的 handoff 状态
Args:
conversation_id: 会话 ID
initial_agent_id: 初始 Agent ID
Returns:
HandoffState
"""
state = HandoffState(
conversation_id=conversation_id,
current_agent_id=initial_agent_id
)
self._states[conversation_id] = state
logger.info(
"创建 handoff 状态",
extra={
"conversation_id": conversation_id,
"initial_agent": initial_agent_id
}
)
return state
def get_state(self, conversation_id: str) -> Optional[HandoffState]:
"""获取 handoff 状态
Args:
conversation_id: 会话 ID
Returns:
HandoffState 或 None
"""
return self._states.get(conversation_id)
def execute_handoff(
self,
conversation_id: str,
from_agent_id: str,
to_agent_id: str,
reason: str,
user_message: Optional[str] = None,
context_summary: Optional[str] = None
) -> HandoffState:
"""执行 Agent 切换
Args:
conversation_id: 会话 ID
from_agent_id: 源 Agent ID
to_agent_id: 目标 Agent ID
reason: 切换原因
user_message: 用户消息
context_summary: 上下文摘要
Returns:
更新后的 HandoffState
"""
state = self.get_state(conversation_id)
if not state:
# 如果状态不存在,创建新状态
state = self.create_state(conversation_id, from_agent_id)
# 创建 handoff 上下文
context = HandoffContext(
from_agent_id=from_agent_id,
to_agent_id=to_agent_id,
reason=reason,
user_message=user_message,
context_summary=context_summary
)
# 添加到状态
state.add_handoff(context)
logger.info(
"执行 Agent handoff",
extra={
"conversation_id": conversation_id,
"from_agent": from_agent_id,
"to_agent": to_agent_id,
"handoff_count": state.get_handoff_count()
}
)
return state
def should_handoff(
self,
conversation_id: str,
current_agent_id: str,
message: str,
available_agents: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""判断是否需要 handoff
Args:
conversation_id: 会话 ID
current_agent_id: 当前 Agent ID
message: 用户消息
available_agents: 可用的 Agent 字典
Returns:
如果需要 handoff返回目标 Agent 信息,否则返回 None
"""
state = self.get_state(conversation_id)
# 简单的关键词匹配策略
message_lower = message.lower()
for agent_id, agent_info in available_agents.items():
if agent_id == current_agent_id:
continue
# 检查 Agent 的能力关键词
capabilities = agent_info.get("info", {}).get("capabilities", [])
role = agent_info.get("info", {}).get("role", "")
# 关键词匹配
keywords = capabilities + ([role] if role else [])
for keyword in keywords:
if keyword.lower() in message_lower:
return {
"target_agent_id": agent_id,
"target_agent_name": agent_info.get("info", {}).get("name", ""),
"reason": f"检测到关键词: {keyword}",
"confidence": 0.8
}
# 检查是否频繁切换到同一个 Agent可能需要固定使用
if state:
recent_handoffs = state.get_recent_handoffs(3)
if len(recent_handoffs) >= 2:
# 检查是否有重复的目标 Agent
target_agents = [h.to_agent_id for h in recent_handoffs]
from collections import Counter
most_common = Counter(target_agents).most_common(1)
if most_common and most_common[0][1] >= 2:
# 频繁切换到同一个 Agent建议继续使用
return None
return None
def generate_handoff_tools(
self,
current_agent_id: str,
available_agents: Dict[str, Any]
) -> List[AgentHandoffTool]:
"""为当前 Agent 生成可用的 handoff 工具
Args:
current_agent_id: 当前 Agent ID
available_agents: 可用的 Agent 字典
Returns:
AgentHandoffTool 列表
"""
tools = []
for agent_id, agent_data in available_agents.items():
if agent_id == current_agent_id:
continue
agent_info = agent_data.get("info", {})
name = agent_info.get("name", "未命名")
role = agent_info.get("role", "")
capabilities = agent_info.get("capabilities", [])
# 生成工具名称
tool_name = f"transfer_to_{agent_id.replace('-', '_')}"
# 生成工具描述
description = f"切换到 {name}"
if role:
description += f"{role}"
if capabilities:
description += f"。擅长: {', '.join(capabilities[:3])}"
description += "。当用户的问题更适合该 Agent 处理时使用此工具。"
tool = AgentHandoffTool(
name=tool_name,
target_agent_id=agent_id,
target_agent_name=name,
description=description,
trigger_keywords=capabilities + ([role] if role else [])
)
tools.append(tool)
logger.info(
"生成 handoff 工具",
extra={
"current_agent": current_agent_id,
"tool_count": len(tools)
}
)
return tools
def get_handoff_context_for_agent(
self,
conversation_id: str,
agent_id: str
) -> Optional[str]:
"""获取传递给目标 Agent 的上下文信息
Args:
conversation_id: 会话 ID
agent_id: 目标 Agent ID
Returns:
上下文字符串
"""
state = self.get_state(conversation_id)
if not state:
return None
recent_handoffs = state.get_recent_handoffs(3)
if not recent_handoffs:
return None
# 构建上下文摘要
context_parts = []
for handoff in recent_handoffs:
if handoff.to_agent_id == agent_id:
context_parts.append(
f"{handoff.from_agent_id} 切换而来,原因: {handoff.reason}"
)
if handoff.context_summary:
context_parts.append(f"上下文: {handoff.context_summary}")
if context_parts:
return "\n".join(context_parts)
return None
def clear_state(self, conversation_id: str):
"""清除会话状态
Args:
conversation_id: 会话 ID
"""
if conversation_id in self._states:
del self._states[conversation_id]
logger.info(f"清除 handoff 状态: {conversation_id}")
# 全局单例
_handoff_manager = None
def get_handoff_manager() -> HandoffManager:
"""获取全局 Handoff 管理器单例"""
global _handoff_manager
if _handoff_manager is None:
_handoff_manager = HandoffManager()
return _handoff_manager

View File

@@ -0,0 +1,686 @@
"""协作编排器 - 支持 Agent 之间的动态切换和协作
基于 LangChain handoffs 模式,实现:
1. Agent 之间的动态切换tool-based handoffs
2. 会话上下文的保持
3. 智能路由决策
4. 协作历史追踪
"""
import json
import uuid
import time
from typing import Dict, Any, Optional, List, AsyncGenerator
from sqlalchemy.orm import Session
from app.services.agent_handoff import (
get_handoff_manager,
HandoffManager,
AgentHandoffTool
)
from app.services.dynamic_handoff_tools import DynamicHandoffToolCreator
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.core.models import RedBearLLM
from app.core.models.base import RedBearModelConfig
from app.models import ModelType
logger = get_business_logger()
class CollaborativeOrchestrator:
"""协作编排器 - 管理多 Agent 协作和切换"""
def __init__(
self,
db: Session,
config: Any,
handoff_manager: Optional[HandoffManager] = None
):
"""初始化协作编排器
Args:
db: 数据库会话
config: 多 Agent 配置
handoff_manager: Handoff 管理器(可选)
"""
self.db = db
self.config = config
self.handoff_manager = handoff_manager or get_handoff_manager()
# 解析配置
self.sub_agents = self._parse_sub_agents(config.sub_agents)
self.execution_config = config.execution_config or {}
# 协作模式
self.enable_handoffs = self.execution_config.get("enable_handoffs", True)
self.max_handoffs = self.execution_config.get("max_handoffs", 5)
logger.info(
"协作编排器初始化",
extra={
"sub_agent_count": len(self.sub_agents),
"enable_handoffs": self.enable_handoffs,
"max_handoffs": self.max_handoffs
}
)
def _parse_sub_agents(self, sub_agents_data: List[Dict]) -> Dict[str, Any]:
"""解析子 Agent 配置
Args:
sub_agents_data: 子 Agent 配置列表
Returns:
Agent ID 到配置的映射
"""
agents = {}
for agent_data in sub_agents_data:
agent_id = agent_data.get("agent_id")
if agent_id:
agents[str(agent_id)] = {
"info": agent_data,
"config": None # 稍后加载
}
return agents
async def execute_with_handoffs(
self,
message: str,
conversation_id: Optional[str] = None,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
initial_agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""执行支持 handoffs 的多 Agent 协作
Args:
message: 用户消息
conversation_id: 会话 ID
user_id: 用户 ID
variables: 变量参数
initial_agent_id: 初始 Agent ID可选
Returns:
执行结果
"""
if not conversation_id:
conversation_id = str(uuid.uuid4())
# 1. 确定初始 Agent
if not initial_agent_id:
initial_agent_id = await self._select_initial_agent(message, conversation_id)
# 2. 创建或获取 handoff 状态
state = self.handoff_manager.get_state(conversation_id)
if not state:
state = self.handoff_manager.create_state(conversation_id, initial_agent_id)
current_agent_id = state.current_agent_id
handoff_count = 0
conversation_history = []
# 3. 执行循环(支持多次 handoff
while handoff_count < self.max_handoffs:
logger.info(
f"执行 Agent: {current_agent_id}",
extra={
"conversation_id": conversation_id,
"handoff_count": handoff_count,
"message_length": len(message)
}
)
# 3.1 生成当前 Agent 的 handoff 工具
handoff_tools = self.handoff_manager.generate_handoff_tools(
current_agent_id,
self.sub_agents
)
# 3.2 执行当前 Agent
result = await self._execute_agent(
agent_id=current_agent_id,
message=message,
conversation_id=conversation_id,
user_id=user_id,
variables=variables,
handoff_tools=handoff_tools,
conversation_history=conversation_history
)
# 3.3 检查是否有 handoff 请求
handoff_request = result.get("handoff_request")
if not handoff_request:
# 没有 handoff返回结果
return {
"message": result.get("message", ""),
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"handoff_history": [
{
"from_agent": h.from_agent_id,
"to_agent": h.to_agent_id,
"reason": h.reason
}
for h in state.handoff_history
],
"elapsed_time": result.get("elapsed_time", 0),
"usage": result.get("usage")
}
# 3.4 执行 handoff
target_agent_id = handoff_request.get("target_agent_id")
reason = handoff_request.get("reason", "Agent 请求切换")
context_summary = handoff_request.get("context_summary")
if target_agent_id not in self.sub_agents:
logger.warning(f"目标 Agent 不存在: {target_agent_id}")
# 返回当前结果
return {
"message": result.get("message", ""),
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"error": f"目标 Agent 不存在: {target_agent_id}",
"elapsed_time": result.get("elapsed_time", 0)
}
# 执行 handoff
state = self.handoff_manager.execute_handoff(
conversation_id=conversation_id,
from_agent_id=current_agent_id,
to_agent_id=target_agent_id,
reason=reason,
user_message=message,
context_summary=context_summary
)
# 更新当前 Agent
current_agent_id = target_agent_id
handoff_count += 1
# 添加到会话历史
conversation_history.append({
"agent_id": state.handoff_history[-1].from_agent_id,
"message": result.get("message", ""),
"handoff_to": target_agent_id,
"reason": reason
})
# 如果 Agent 返回了最终答案,结束循环
if result.get("is_final_answer"):
return {
"message": result.get("message", ""),
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"handoff_history": [
{
"from_agent": h.from_agent_id,
"to_agent": h.to_agent_id,
"reason": h.reason
}
for h in state.handoff_history
],
"elapsed_time": result.get("elapsed_time", 0),
"usage": result.get("usage")
}
# 达到最大 handoff 次数
logger.warning(
f"达到最大 handoff 次数: {self.max_handoffs}",
extra={"conversation_id": conversation_id}
)
return {
"message": "已达到最大协作次数限制,请重新提问。",
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"error": "达到最大 handoff 次数",
"elapsed_time": 0
}
async def _select_initial_agent(
self,
message: str,
conversation_id: str
) -> str:
"""选择初始 Agent
Args:
message: 用户消息
conversation_id: 会话 ID
Returns:
Agent ID
"""
# 检查是否有历史状态
state = self.handoff_manager.get_state(conversation_id)
if state:
# 继续使用当前 Agent
return state.current_agent_id
# 简单的关键词匹配
message_lower = message.lower()
for agent_id, agent_data in self.sub_agents.items():
agent_info = agent_data.get("info", {})
capabilities = agent_info.get("capabilities", [])
role = agent_info.get("role", "")
# 检查关键词
keywords = capabilities + ([role] if role else [])
for keyword in keywords:
if keyword.lower() in message_lower:
logger.info(
f"根据关键词选择初始 Agent: {agent_id}",
extra={"keyword": keyword}
)
return agent_id
# 默认使用第一个 Agent
default_agent_id = next(iter(self.sub_agents.keys()))
logger.info(f"使用默认初始 Agent: {default_agent_id}")
return default_agent_id
async def _execute_agent(
self,
agent_id: str,
message: str,
conversation_id: str,
user_id: Optional[str],
variables: Optional[Dict[str, Any]],
handoff_tools: List[AgentHandoffTool],
conversation_history: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""执行单个 Agent
Args:
agent_id: Agent ID
message: 用户消息
conversation_id: 会话 ID
user_id: 用户 ID
variables: 变量参数
handoff_tools: Handoff 工具列表
conversation_history: 会话历史
Returns:
执行结果
"""
start_time = time.time()
try:
# 获取 Agent 配置
agent_data = self.sub_agents.get(agent_id)
if not agent_data:
raise BusinessException(
f"Agent 不存在: {agent_id}",
BizCode.RESOURCE_NOT_FOUND
)
# 加载 Agent 的完整配置
agent_config = await self._load_agent_config(agent_id, agent_data)
# 构建增强的 prompt包含 handoff 上下文)
enhanced_message = self._build_enhanced_message(
message,
conversation_id,
agent_id,
conversation_history
)
# 创建动态工具创建器
tool_creator = DynamicHandoffToolCreator(agent_id, self.sub_agents)
# 获取动态创建的工具
dynamic_tools = tool_creator.get_tools_for_llm()
logger.info(
f"为 Agent {agent_id} 创建了 {len(dynamic_tools)} 个 handoff 工具",
extra={"tool_names": tool_creator.get_tool_names()}
)
# 调用 Agent 的 LLM注入动态工具
response = await self._call_agent_llm(
agent_config=agent_config,
message=enhanced_message,
tools=dynamic_tools,
conversation_history=conversation_history
)
# 构建结果
result = {
"message": response.get("content", ""),
"elapsed_time": time.time() - start_time,
"usage": response.get("usage", {"total_tokens": 0}),
"is_final_answer": True
}
# 检查是否有工具调用handoff
tool_calls = response.get("tool_calls", [])
if tool_calls:
for tool_call in tool_calls:
tool_name = tool_call.get("name")
tool_args = tool_call.get("arguments", {})
# 检查是否是 handoff 工具
if tool_name in tool_creator.get_tool_names():
# 处理 handoff
handoff_request = tool_creator.handle_tool_call(tool_name, tool_args)
if handoff_request:
result["handoff_request"] = handoff_request
result["is_final_answer"] = False
logger.info(
f"检测到 handoff 请求: {agent_id}{handoff_request['target_agent_id']}",
extra={"reason": handoff_request.get("reason")}
)
break
return result
except Exception as e:
logger.error(f"Agent 执行失败: {str(e)}", exc_info=True)
return {
"message": f"Agent 执行出错: {str(e)}",
"elapsed_time": time.time() - start_time,
"error": str(e),
"is_final_answer": True
}
async def _load_agent_config(self, agent_id: str, agent_data: Dict[str, Any]) -> Dict[str, Any]:
"""加载 Agent 的完整配置
Args:
agent_id: Agent ID
agent_data: Agent 数据
Returns:
Agent 配置
"""
from app.models import AppRelease
from app.services.model_service import ModelApiKeyService
# 从数据库加载 Agent Release
try:
agent_uuid = uuid.UUID(agent_id)
release = self.db.get(AppRelease, agent_uuid)
if not release:
raise BusinessException(
f"Agent Release 不存在: {agent_id}",
BizCode.RESOURCE_NOT_FOUND
)
# 获取配置
config_data = release.config or {}
# 获取模型配置
model_config_id = release.default_model_config_id
if not model_config_id:
raise BusinessException(
f"Agent 未配置模型: {agent_id}",
BizCode.AGENT_CONFIG_MISSING
)
# 获取 API Key
api_key_config = ModelApiKeyService.get_a_api_key(self.db, model_config_id)
if not api_key_config:
raise BusinessException(
f"Agent 模型没有可用的 API Key: {agent_id}",
BizCode.API_KEY_NOT_FOUND
)
return {
"agent_id": agent_id,
"name": release.name,
"system_prompt": config_data.get("system_prompt", ""),
"model_name": api_key_config.model_name,
"provider": api_key_config.provider,
"api_key": api_key_config.api_key,
"api_base": api_key_config.api_base,
"model_parameters": config_data.get("model_parameters", {})
}
except ValueError:
raise BusinessException(
f"无效的 Agent ID: {agent_id}",
BizCode.INVALID_PARAMETER
)
async def _call_agent_llm(
self,
agent_config: Dict[str, Any],
message: str,
tools: List[Dict[str, Any]],
conversation_history: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""调用 Agent 的 LLM
Args:
agent_config: Agent 配置
message: 消息
tools: 工具列表(包含 handoff 工具)
conversation_history: 会话历史
Returns:
LLM 响应
"""
try:
# 构建系统提示(包含工具说明)
system_prompt = self._build_system_prompt_with_tools(
agent_config.get("system_prompt", ""),
tools
)
# 构建消息列表
messages = [{"role": "system", "content": system_prompt}]
# 添加历史消息(最近 5 轮)
if conversation_history:
for item in conversation_history[-5:]:
messages.append({
"role": "assistant",
"content": f"[Agent {item['agent_id']}] {item['message']}"
})
# 添加当前消息
messages.append({"role": "user", "content": message})
# 配置 LLM
model_params = agent_config.get("model_parameters", {})
extra_params = {
"temperature": model_params.get("temperature", 0.7),
"max_tokens": model_params.get("max_tokens", 2000)
}
# 如果有工具,添加到配置中
if tools:
extra_params["tools"] = tools
extra_params["tool_choice"] = "auto"
model_config = RedBearModelConfig(
model_name=agent_config["model_name"],
provider=agent_config["provider"],
api_key=agent_config["api_key"],
base_url=agent_config.get("api_base"),
extra_params=extra_params
)
# 创建 LLM 实例
llm = RedBearLLM(model_config, type=ModelType.CHAT)
# 调用 LLM
response = await llm.ainvoke(messages)
# 解析响应
result = {
"content": "",
"tool_calls": [],
"usage": {}
}
if hasattr(response, 'content'):
result["content"] = response.content
else:
result["content"] = str(response)
# 提取工具调用
if hasattr(response, 'tool_calls') and response.tool_calls:
for tool_call in response.tool_calls:
result["tool_calls"].append({
"name": tool_call.function.name if hasattr(tool_call, 'function') else tool_call.name,
"arguments": json.loads(tool_call.function.arguments) if hasattr(tool_call, 'function') else tool_call.arguments
})
# 提取 usage
if hasattr(response, 'usage_metadata'):
result["usage"] = {
"prompt_tokens": response.usage_metadata.get("input_tokens", 0),
"completion_tokens": response.usage_metadata.get("output_tokens", 0),
"total_tokens": response.usage_metadata.get("total_tokens", 0)
}
return result
except Exception as e:
logger.error(f"LLM 调用失败: {str(e)}", exc_info=True)
raise
def _build_system_prompt_with_tools(self, base_prompt: str, tools: List[Dict[str, Any]]) -> str:
"""构建包含工具说明的系统提示
Args:
base_prompt: 基础提示词
tools: 工具列表
Returns:
增强的系统提示
"""
if not tools:
return base_prompt
tools_desc = "\n\n## 可用的协作工具\n\n"
tools_desc += "当你发现用户的问题超出你的专业领域时,可以使用以下工具切换到专业的 Agent\n\n"
for tool in tools:
func = tool.get("function", {})
tools_desc += f"- **{func.get('name')}**: {func.get('description')}\n"
tools_desc += "\n请根据用户问题的性质,判断是否需要切换到其他专业 Agent。"
tools_desc += "如果需要切换,请调用相应的工具并说明原因。"
return base_prompt + tools_desc
def _build_enhanced_message(
self,
message: str,
conversation_id: str,
agent_id: str,
conversation_history: List[Dict[str, Any]]
) -> str:
"""构建增强的消息(包含 handoff 上下文)
Args:
message: 原始消息
conversation_id: 会话 ID
agent_id: 当前 Agent ID
conversation_history: 会话历史
Returns:
增强后的消息
"""
# 获取 handoff 上下文
handoff_context = self.handoff_manager.get_handoff_context_for_agent(
conversation_id,
agent_id
)
if not handoff_context and not conversation_history:
return message
# 构建上下文前缀
context_parts = []
if handoff_context:
context_parts.append(f"[协作上下文] {handoff_context}")
if conversation_history:
context_parts.append("[之前的对话]")
for item in conversation_history[-3:]: # 只保留最近3轮
context_parts.append(
f"- Agent {item['agent_id']}: {item['message'][:100]}"
)
context_parts.append(f"\n[当前问题] {message}")
return "\n".join(context_parts)
def _build_tools_with_handoffs(
self,
handoff_tools: List[AgentHandoffTool]
) -> List[Dict[str, Any]]:
"""构建包含 handoff 工具的工具列表(已废弃,使用动态工具创建)
Args:
handoff_tools: Handoff 工具列表
Returns:
工具 schema 列表
"""
# 这个方法已被 DynamicHandoffToolCreator 替代
# 保留用于向后兼容
tools = []
for tool in handoff_tools:
tools.append(tool.to_tool_schema())
return tools
async def execute_stream_with_handoffs(
self,
message: str,
conversation_id: Optional[str] = None,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
initial_agent_id: Optional[str] = None
) -> AsyncGenerator[str, None]:
"""流式执行支持 handoffs 的多 Agent 协作
Args:
message: 用户消息
conversation_id: 会话 ID
user_id: 用户 ID
variables: 变量参数
initial_agent_id: 初始 Agent ID
Yields:
SSE 格式的事件流
"""
if not conversation_id:
conversation_id = str(uuid.uuid4())
# 发送开始事件
yield f"data: {json.dumps({'event': 'start', 'conversation_id': conversation_id})}\n\n"
try:
# 执行协作
result = await self.execute_with_handoffs(
message=message,
conversation_id=conversation_id,
user_id=user_id,
variables=variables,
initial_agent_id=initial_agent_id
)
# 发送结果事件
yield f"data: {json.dumps({'event': 'message', 'data': result})}\n\n"
# 发送结束事件
yield f"data: {json.dumps({'event': 'end'})}\n\n"
except Exception as e:
logger.error(f"流式执行失败: {str(e)}")
yield f"data: {json.dumps({'event': 'error', 'error': str(e)})}\n\n"

View File

@@ -0,0 +1,481 @@
"""动态 Handoff 工具创建器
展示如何在运行时动态创建 Agent 切换工具,并将其注入到 LLM 的工具列表中
"""
import json
from typing import Dict, Any, List, Optional, Callable
from pydantic import BaseModel, Field
from app.core.logging_config import get_business_logger
logger = get_business_logger()
class DynamicHandoffToolCreator:
"""动态 Handoff 工具创建器
核心功能:
1. 根据可用 Agent 动态生成工具定义
2. 将工具转换为 LLM 可理解的 schema
3. 处理工具调用并执行 handoff
"""
def __init__(self, current_agent_id: str, available_agents: Dict[str, Any]):
"""初始化工具创建器
Args:
current_agent_id: 当前 Agent ID
available_agents: 可用的 Agent 字典
"""
self.current_agent_id = current_agent_id
self.available_agents = available_agents
self.tools = []
self.tool_handlers = {}
# 动态创建工具
self._create_handoff_tools()
def _create_handoff_tools(self):
"""动态创建所有 handoff 工具"""
for agent_id, agent_data in self.available_agents.items():
if agent_id == self.current_agent_id:
continue # 不创建切换到自己的工具
# 创建工具
tool_def = self._create_single_tool(agent_id, agent_data)
self.tools.append(tool_def)
# 创建工具处理器
handler = self._create_tool_handler(agent_id, agent_data)
self.tool_handlers[tool_def["function"]["name"]] = handler
logger.info(
f"为 Agent {self.current_agent_id} 创建了 {len(self.tools)} 个 handoff 工具"
)
def _create_single_tool(self, target_agent_id: str, agent_data: Dict[str, Any]) -> Dict[str, Any]:
"""创建单个 handoff 工具定义
Args:
target_agent_id: 目标 Agent ID
agent_data: Agent 数据
Returns:
工具定义OpenAI function calling 格式)
"""
agent_info = agent_data.get("info", {})
name = agent_info.get("name", "未命名")
role = agent_info.get("role", "")
capabilities = agent_info.get("capabilities", [])
# 生成工具名称(符合函数命名规范)
tool_name = f"transfer_to_{self._sanitize_name(target_agent_id)}"
# 生成描述
description = f"切换到 {name}"
if role:
description += f"{role}"
if capabilities:
cap_str = "".join(capabilities[:3])
description += f"。擅长: {cap_str}"
description += "。当用户的问题更适合该 Agent 处理时调用此工具。"
# 构建工具定义OpenAI function calling 格式)
tool_def = {
"type": "function",
"function": {
"name": tool_name,
"description": description,
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "为什么要切换到该 Agent请简要说明原因。"
},
"context_summary": {
"type": "string",
"description": "需要传递给目标 Agent 的上下文摘要(可选)。例如:之前的计算结果、用户的具体需求等。"
}
},
"required": ["reason"]
}
}
}
return tool_def
def _create_tool_handler(
self,
target_agent_id: str,
agent_data: Dict[str, Any]
) -> Callable:
"""创建工具处理器函数
Args:
target_agent_id: 目标 Agent ID
agent_data: Agent 数据
Returns:
工具处理器函数
"""
def handler(reason: str, context_summary: Optional[str] = None) -> Dict[str, Any]:
"""处理 handoff 工具调用
Args:
reason: 切换原因
context_summary: 上下文摘要
Returns:
Handoff 请求
"""
agent_info = agent_data.get("info", {})
logger.info(
f"Handoff 工具被调用: {self.current_agent_id}{target_agent_id}",
extra={
"reason": reason,
"has_context": bool(context_summary)
}
)
return {
"type": "handoff",
"target_agent_id": target_agent_id,
"target_agent_name": agent_info.get("name", ""),
"reason": reason,
"context_summary": context_summary,
"from_agent_id": self.current_agent_id
}
return handler
def _sanitize_name(self, name: str) -> str:
"""清理名称,使其符合函数命名规范
Args:
name: 原始名称
Returns:
清理后的名称
"""
# 替换特殊字符为下划线
sanitized = name.replace("-", "_").replace(" ", "_")
# 移除其他非法字符
sanitized = "".join(c for c in sanitized if c.isalnum() or c == "_")
return sanitized.lower()
def get_tools_for_llm(self) -> List[Dict[str, Any]]:
"""获取用于 LLM 的工具列表
Returns:
工具定义列表OpenAI function calling 格式)
"""
return self.tools
def handle_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""处理 LLM 的工具调用
Args:
tool_name: 工具名称
arguments: 工具参数
Returns:
Handoff 请求或 None
"""
handler = self.tool_handlers.get(tool_name)
if not handler:
logger.warning(f"未找到工具处理器: {tool_name}")
return None
try:
return handler(**arguments)
except Exception as e:
logger.error(f"工具调用失败: {tool_name}, 错误: {str(e)}")
return None
def get_tool_names(self) -> List[str]:
"""获取所有工具名称
Returns:
工具名称列表
"""
return [tool["function"]["name"] for tool in self.tools]
# ==================== 使用示例 ====================
def example_usage():
"""展示如何使用动态工具创建器"""
# 1. 准备可用的 Agent 信息
available_agents = {
"math-agent-uuid": {
"info": {
"name": "数学助手",
"role": "数学专家",
"capabilities": ["数学计算", "方程求解", "几何问题"]
}
},
"creative-agent-uuid": {
"info": {
"name": "创意助手",
"role": "创意专家",
"capabilities": ["写作", "诗歌", "故事创作"]
}
},
"code-agent-uuid": {
"info": {
"name": "代码助手",
"role": "编程专家",
"capabilities": ["代码编写", "调试", "代码审查"]
}
}
}
# 2. 为当前 Agent 创建工具
current_agent_id = "general-agent-uuid"
tool_creator = DynamicHandoffToolCreator(current_agent_id, available_agents)
# 3. 获取工具定义(用于 LLM
tools = tool_creator.get_tools_for_llm()
print("=" * 60)
print("动态创建的 Handoff 工具:")
print("=" * 60)
for tool in tools:
print(f"\n工具名称: {tool['function']['name']}")
print(f"描述: {tool['function']['description']}")
print(f"参数: {json.dumps(tool['function']['parameters'], indent=2, ensure_ascii=False)}")
# 4. 模拟 LLM 调用工具
print("\n" + "=" * 60)
print("模拟 LLM 工具调用:")
print("=" * 60)
# LLM 决定切换到数学 Agent
tool_call = {
"name": "transfer_to_math_agent_uuid",
"arguments": {
"reason": "用户问题涉及数学计算",
"context_summary": "用户想解方程 x^2 + 5x + 6 = 0"
}
}
print(f"\nLLM 调用: {tool_call['name']}")
print(f"参数: {json.dumps(tool_call['arguments'], indent=2, ensure_ascii=False)}")
# 5. 处理工具调用
handoff_request = tool_creator.handle_tool_call(
tool_call["name"],
tool_call["arguments"]
)
if handoff_request:
print(f"\n✓ Handoff 请求已创建:")
print(f" 类型: {handoff_request['type']}")
print(f" 从: {handoff_request['from_agent_id']}")
print(f" 到: {handoff_request['target_agent_id']} ({handoff_request['target_agent_name']})")
print(f" 原因: {handoff_request['reason']}")
print(f" 上下文: {handoff_request['context_summary']}")
# ==================== 与 LLM 集成示例 ====================
async def integrate_with_llm_example():
"""展示如何将动态工具集成到 LLM 调用中"""
from app.core.models import RedBearLLM
from app.core.models.base import RedBearModelConfig
# 1. 准备 Agent 信息
available_agents = {
"math-agent": {
"info": {
"name": "数学助手",
"role": "数学专家",
"capabilities": ["计算", "方程"]
}
}
}
# 2. 创建工具
tool_creator = DynamicHandoffToolCreator("general-agent", available_agents)
tools = tool_creator.get_tools_for_llm()
# 3. 构建 LLM 配置
model_config = RedBearModelConfig(
model_name="gpt-4",
provider="openai",
api_key="your-api-key",
extra_params={
"temperature": 0.7,
"tools": tools, # 注入动态创建的工具
"tool_choice": "auto" # 让 LLM 自动决定是否调用工具
}
)
# 4. 创建 LLM 实例
llm = RedBearLLM(model_config)
# 5. 构建消息(包含系统提示)
system_prompt = """你是一个智能助手。当用户的问题超出你的能力范围时,你可以使用工具切换到专业的 Agent。
可用的切换工具:
- transfer_to_math_agent: 当遇到数学问题时使用
请根据用户问题判断是否需要切换。"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "帮我计算 3*6+15 的结果"}
]
# 6. 调用 LLM
response = await llm.ainvoke(messages)
# 7. 检查是否有工具调用
if hasattr(response, 'tool_calls') and response.tool_calls:
for tool_call in response.tool_calls:
print(f"\n✓ LLM 调用了工具: {tool_call.name}")
print(f" 参数: {tool_call.arguments}")
# 处理工具调用
handoff_request = tool_creator.handle_tool_call(
tool_call.name,
tool_call.arguments
)
if handoff_request:
print(f"\n✓ 执行 Handoff:")
print(f" 切换到: {handoff_request['target_agent_name']}")
print(f" 原因: {handoff_request['reason']}")
# 这里可以执行实际的 Agent 切换逻辑
# await execute_handoff(handoff_request)
else:
print(f"\n✓ LLM 直接回复: {response.content}")
# ==================== 完整的 Agent 执行流程 ====================
class AgentExecutorWithHandoffs:
"""支持 Handoffs 的 Agent 执行器"""
def __init__(self, agent_id: str, agent_config: Any, available_agents: Dict[str, Any]):
"""初始化执行器
Args:
agent_id: 当前 Agent ID
agent_config: Agent 配置
available_agents: 可用的其他 Agent
"""
self.agent_id = agent_id
self.agent_config = agent_config
self.available_agents = available_agents
# 创建工具创建器
self.tool_creator = DynamicHandoffToolCreator(agent_id, available_agents)
async def execute(
self,
message: str,
conversation_history: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""执行 Agent支持 handoff
Args:
message: 用户消息
conversation_history: 会话历史
Returns:
执行结果,可能包含 handoff_request
"""
from app.core.models import RedBearLLM
from app.core.models.base import RedBearModelConfig
# 1. 获取动态工具
tools = self.tool_creator.get_tools_for_llm()
# 2. 构建系统提示(包含工具说明)
system_prompt = self._build_system_prompt_with_tools()
# 3. 构建消息
messages = [{"role": "system", "content": system_prompt}]
# 添加历史消息
if conversation_history:
messages.extend(conversation_history)
# 添加当前消息
messages.append({"role": "user", "content": message})
# 4. 配置 LLM注入工具
model_config = RedBearModelConfig(
model_name=self.agent_config.model_name,
provider=self.agent_config.provider,
api_key=self.agent_config.api_key,
extra_params={
"temperature": 0.7,
"tools": tools, # 动态工具
"tool_choice": "auto"
}
)
llm = RedBearLLM(model_config)
# 5. 调用 LLM
response = await llm.ainvoke(messages)
# 6. 处理响应
result = {
"message": response.content if hasattr(response, 'content') else str(response),
"agent_id": self.agent_id
}
# 7. 检查工具调用
if hasattr(response, 'tool_calls') and response.tool_calls:
for tool_call in response.tool_calls:
# 检查是否是 handoff 工具
if tool_call.name in self.tool_creator.get_tool_names():
# 处理 handoff
handoff_request = self.tool_creator.handle_tool_call(
tool_call.name,
tool_call.arguments
)
if handoff_request:
result["handoff_request"] = handoff_request
result["is_final_answer"] = False
break
else:
# 处理其他业务工具
pass
return result
def _build_system_prompt_with_tools(self) -> str:
"""构建包含工具说明的系统提示"""
base_prompt = self.agent_config.system_prompt or "你是一个智能助手。"
# 添加工具说明
tool_names = self.tool_creator.get_tool_names()
if tool_names:
tools_desc = "\n\n你可以使用以下工具切换到专业的 Agent\n"
for tool in self.tool_creator.get_tools_for_llm():
tools_desc += f"- {tool['function']['name']}: {tool['function']['description']}\n"
tools_desc += "\n当用户的问题超出你的能力范围时,请使用相应的工具切换到专业 Agent。"
return base_prompt + tools_desc
return base_prompt
if __name__ == "__main__":
# 运行示例
print("\n🚀 动态 Handoff 工具创建示例\n")
example_usage()
print("\n\n" + "=" * 60)
print("完成!查看上面的输出了解工具创建流程。")
print("=" * 60)

View File

@@ -0,0 +1,314 @@
"""Multi-Agent Service 的 Handoffs 集成
将 Agent Handoffs 功能集成到现有的 Multi-Agent 系统中
"""
import uuid
import time
from typing import Dict, Any, Optional, AsyncGenerator
from sqlalchemy.orm import Session
from app.services.agent_handoff import get_handoff_manager
from app.services.collaborative_orchestrator import CollaborativeOrchestrator
from app.schemas.multi_agent_schema import MultiAgentRunRequest
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
logger = get_business_logger()
class MultiAgentHandoffsService:
"""Multi-Agent Handoffs 服务 - 扩展现有的 Multi-Agent Service"""
def __init__(self, db: Session, multi_agent_service):
"""初始化服务
Args:
db: 数据库会话
multi_agent_service: 现有的 MultiAgentService 实例
"""
self.db = db
self.multi_agent_service = multi_agent_service
self.handoff_manager = get_handoff_manager()
logger.info("Multi-Agent Handoffs 服务初始化完成")
async def run_with_handoffs(
self,
app_id: uuid.UUID,
request: MultiAgentRunRequest
) -> Dict[str, Any]:
"""运行支持 handoffs 的多 Agent 任务
Args:
app_id: 应用 ID
request: 运行请求
Returns:
执行结果
"""
start_time = time.time()
try:
# 1. 获取配置
config = self.multi_agent_service.get_config(app_id)
if not config:
raise BusinessException(
"多 Agent 配置不存在",
BizCode.RESOURCE_NOT_FOUND
)
# 2. 检查是否启用 handoffs
execution_config = config.execution_config or {}
enable_handoffs = execution_config.get("enable_handoffs", False)
if not enable_handoffs:
# 降级到普通模式
logger.info("Handoffs 未启用,使用普通模式")
return await self.multi_agent_service.run(app_id, request)
# 3. 创建协作编排器
orchestrator = CollaborativeOrchestrator(
db=self.db,
config=config,
handoff_manager=self.handoff_manager
)
# 4. 执行协作
result = await orchestrator.execute_with_handoffs(
message=request.message,
conversation_id=str(request.conversation_id) if request.conversation_id else None,
user_id=request.user_id,
variables=request.variables
)
# 5. 增强结果
result["mode"] = "handoffs"
result["elapsed_time"] = time.time() - start_time
logger.info(
"Handoffs 执行完成",
extra={
"app_id": str(app_id),
"handoff_count": result.get("handoff_count", 0),
"final_agent": result.get("final_agent_id"),
"elapsed_time": result["elapsed_time"]
}
)
return result
except Exception as e:
logger.error(f"Handoffs 执行失败: {str(e)}")
# 降级到普通模式
logger.info("降级到普通模式")
return await self.multi_agent_service.run(app_id, request)
async def run_stream_with_handoffs(
self,
app_id: uuid.UUID,
request: MultiAgentRunRequest
) -> AsyncGenerator[str, None]:
"""流式运行支持 handoffs 的多 Agent 任务
Args:
app_id: 应用 ID
request: 运行请求
Yields:
SSE 格式的事件流
"""
try:
# 1. 获取配置
config = self.multi_agent_service.get_config(app_id)
if not config:
yield f"data: {{\"event\": \"error\", \"error\": \"配置不存在\"}}\n\n"
return
# 2. 检查是否启用 handoffs
execution_config = config.execution_config or {}
enable_handoffs = execution_config.get("enable_handoffs", False)
if not enable_handoffs:
# 降级到普通流式模式
async for event in self.multi_agent_service.run_stream(app_id, request):
yield event
return
# 3. 创建协作编排器
orchestrator = CollaborativeOrchestrator(
db=self.db,
config=config,
handoff_manager=self.handoff_manager
)
# 4. 流式执行
async for event in orchestrator.execute_stream_with_handoffs(
message=request.message,
conversation_id=str(request.conversation_id) if request.conversation_id else None,
user_id=request.user_id,
variables=request.variables
):
yield event
except Exception as e:
logger.error(f"流式 Handoffs 执行失败: {str(e)}")
yield f"data: {{\"event\": \"error\", \"error\": \"{str(e)}\"}}\n\n"
def get_handoff_history(
self,
conversation_id: str
) -> Optional[Dict[str, Any]]:
"""获取会话的 handoff 历史
Args:
conversation_id: 会话 ID
Returns:
Handoff 历史信息
"""
state = self.handoff_manager.get_state(conversation_id)
if not state:
return None
return {
"conversation_id": state.conversation_id,
"current_agent_id": state.current_agent_id,
"handoff_count": state.get_handoff_count(),
"handoff_history": [
{
"from_agent": h.from_agent_id,
"to_agent": h.to_agent_id,
"reason": h.reason,
"timestamp": h.timestamp.isoformat(),
"user_message": h.user_message,
"context_summary": h.context_summary
}
for h in state.handoff_history
],
"created_at": state.created_at.isoformat(),
"updated_at": state.updated_at.isoformat()
}
def clear_handoff_state(self, conversation_id: str):
"""清除会话的 handoff 状态
Args:
conversation_id: 会话 ID
"""
self.handoff_manager.clear_state(conversation_id)
logger.info(f"清除 handoff 状态: {conversation_id}")
async def test_handoff_routing(
self,
app_id: uuid.UUID,
message: str
) -> Dict[str, Any]:
"""测试 handoff 路由决策(不实际执行)
Args:
app_id: 应用 ID
message: 测试消息
Returns:
路由决策结果
"""
# 1. 获取配置
config = self.multi_agent_service.get_config(app_id)
if not config:
raise BusinessException(
"多 Agent 配置不存在",
BizCode.RESOURCE_NOT_FOUND
)
# 2. 解析 sub agents
sub_agents = {}
for agent_data in config.sub_agents:
agent_id = agent_data.get("agent_id")
if agent_id:
sub_agents[str(agent_id)] = {
"info": agent_data
}
# 3. 测试路由
test_conversation_id = f"test-{uuid.uuid4()}"
# 选择初始 Agent
initial_agent_id = None
message_lower = message.lower()
for agent_id, agent_data in sub_agents.items():
agent_info = agent_data.get("info", {})
capabilities = agent_info.get("capabilities", [])
role = agent_info.get("role", "")
keywords = capabilities + ([role] if role else [])
for keyword in keywords:
if keyword.lower() in message_lower:
initial_agent_id = agent_id
break
if initial_agent_id:
break
if not initial_agent_id:
initial_agent_id = next(iter(sub_agents.keys()))
# 4. 生成 handoff 工具
handoff_tools = self.handoff_manager.generate_handoff_tools(
initial_agent_id,
sub_agents
)
# 5. 检查是否需要 handoff
handoff_suggestion = self.handoff_manager.should_handoff(
conversation_id=test_conversation_id,
current_agent_id=initial_agent_id,
message=message,
available_agents=sub_agents
)
return {
"message": message,
"initial_agent_id": initial_agent_id,
"initial_agent_name": sub_agents[initial_agent_id]["info"].get("name", ""),
"available_handoff_tools": [
{
"name": tool.name,
"target_agent_id": tool.target_agent_id,
"target_agent_name": tool.target_agent_name,
"description": tool.description
}
for tool in handoff_tools
],
"handoff_suggestion": handoff_suggestion,
"total_agents": len(sub_agents)
}
# 使用示例
"""
from app.services.multi_agent_service import MultiAgentService
from app.services.multi_agent_handoffs_integration import MultiAgentHandoffsService
# 创建服务
multi_agent_service = MultiAgentService(db)
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 运行 handoffs
result = await handoffs_service.run_with_handoffs(
app_id=app_id,
request=MultiAgentRunRequest(
message="帮我解方程然后写诗",
conversation_id=uuid.uuid4(),
user_id="user-123"
)
)
# 查看 handoff 历史
history = handoffs_service.get_handoff_history(str(result["conversation_id"]))
print(f"Handoff 次数: {history['handoff_count']}")
for h in history['handoff_history']:
print(f"{h['from_agent']}{h['to_agent']}: {h['reason']}")
"""

View File

@@ -82,7 +82,6 @@ dependencies = [
"pytest>=9.0.1",
"matplotlib>=3.10.7",
"langfuse>=3.10.0",
"beartype==0.22.5",
"pdfplumber==0.11.7",
"olefile==0.47",
"cachetools==6.2.1",

2852
api/uv.lock generated

File diff suppressed because it is too large Load Diff