[add] multi agent handoff
This commit is contained in:
@@ -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="路由测试完成"
|
||||
)
|
||||
|
||||
@@ -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 数量")
|
||||
|
||||
375
api/app/services/agent_handoff.py
Normal file
375
api/app/services/agent_handoff.py
Normal 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
|
||||
686
api/app/services/collaborative_orchestrator.py
Normal file
686
api/app/services/collaborative_orchestrator.py
Normal 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"
|
||||
481
api/app/services/dynamic_handoff_tools.py
Normal file
481
api/app/services/dynamic_handoff_tools.py
Normal 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)
|
||||
314
api/app/services/multi_agent_handoffs_integration.py
Normal file
314
api/app/services/multi_agent_handoffs_integration.py
Normal 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']}")
|
||||
"""
|
||||
@@ -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
2852
api/uv.lock
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user