482 lines
16 KiB
Python
482 lines
16 KiB
Python
"""动态 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)
|