ForcePilot/src/agents/common/tools.py

247 lines
8.6 KiB
Python
Raw Normal View History

import os
import traceback
import uuid
from typing import Annotated, Any
import requests
from langchain.tools import tool
from langgraph.types import interrupt
from src import config, graph_base
from src.agents.common.toolkits.kbs import get_kb_based_tools
from src.services.mcp_service import get_enabled_mcp_tools
from src.storage.minio import aupload_file_to_minio
from src.utils import logger
2025-03-24 23:00:14 +08:00
# Lazy initialization for TavilySearch (only when TAVILY_API_KEY is available)
_tavily_search_instance = None
def get_tavily_search():
"""Get TavilySearch instance lazily, only when API key is available."""
global _tavily_search_instance
if _tavily_search_instance is None and config.enable_web_search:
from langchain_tavily import TavilySearch
_tavily_search_instance = TavilySearch()
_tavily_search_instance.metadata = {"name": "Tavily 网页搜索"}
return _tavily_search_instance
@tool(name_or_callable="calculator", description="可以对给定的2个数字选择进行 add, subtract, multiply, divide 运算")
def calculator(a: float, b: float, operation: str) -> float:
try:
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
else:
raise ValueError(f"不支持的运算类型: {operation},仅支持 add, subtract, multiply, divide")
except Exception as e:
logger.error(f"Calculator error: {e}")
raise
@tool
async def text_to_img_demo(text: str) -> str:
"""【测试用】使用模型生成图片, 会返回图片的URL"""
url = "https://api.siliconflow.cn/v1/images/generations"
payload = {
"model": "Qwen/Qwen-Image",
"prompt": text,
}
headers = {"Authorization": f"Bearer {os.getenv('SILICONFLOW_API_KEY')}", "Content-Type": "application/json"}
try:
response = requests.post(url, json=payload, headers=headers)
response_json = response.json()
except Exception as e:
logger.error(f"Failed to generate image with: {e}")
raise ValueError(f"Image generation failed: {e}")
try:
image_url = response_json["images"][0]["url"]
except (KeyError, IndexError, TypeError) as e:
logger.error(f"Failed to parse image URL from response: {e}, {response_json=}")
raise ValueError(f"Image URL extraction failed: {e}")
# 2. Upload to MinIO (Simplified)
response = requests.get(image_url)
file_data = response.content
file_name = f"{uuid.uuid4()}.jpg"
image_url = await aupload_file_to_minio(
bucket_name="generated-images", file_name=file_name, data=file_data, file_extension="jpg"
)
logger.info(f"Image uploaded. URL: {image_url}")
return image_url
@tool(name_or_callable="human_in_the_loop_debug", description="请求人工审批工具,用于在执行重要操作前获得人类确认。")
def get_approved_user_goal(
operation_description: str,
) -> dict:
"""
请求人工审批在执行重要操作前获得人类确认
Args:
operation_description: 需要审批的操作描述例如 "调用知识库工具"
Returns:
dict: 包含审批结果的字典格式为 {"approved": bool, "message": str}
"""
# 构建详细的中断信息
interrupt_info = {
"question": "是否批准以下操作?",
"operation": operation_description,
}
# 触发人工审批
is_approved = interrupt(interrupt_info)
# 返回审批结果
if is_approved:
result = {
"approved": True,
"message": f"✅ 操作已批准:{operation_description}",
}
print(f"✅ 人工审批通过: {operation_description}")
else:
result = {
"approved": False,
"message": f"❌ 操作被拒绝:{operation_description}",
}
print(f"❌ 人工审批被拒绝: {operation_description}")
return result
2025-03-24 19:07:51 +08:00
KG_QUERY_DESCRIPTION = """
使用这个工具可以查询知识图谱中包含的三元组信息
关键词query使用可能帮助回答这个问题的关键词进行查询不要直接使用用户的原始输入去查询
"""
@tool(name_or_callable="查询知识图谱", description=KG_QUERY_DESCRIPTION)
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
"""使用这个工具可以查询知识图谱中包含的三元组信息。关键词query使用可能帮助回答这个问题的关键词进行查询不要直接使用用户的原始输入去查询。"""
try:
logger.debug(f"Querying knowledge graph with: {query}")
result = graph_base.query_node(query, hops=2, return_format="triples")
logger.debug(
f"Knowledge graph query returned "
f"{len(result.get('triples', [])) if isinstance(result, dict) else 'N/A'} triples"
)
return result
except Exception as e:
logger.error(f"Knowledge graph query error: {e}, {traceback.format_exc()}")
return f"知识图谱查询失败: {str(e)}"
def gen_tool_info(tools) -> list[dict[str, Any]]:
"""获取所有工具的信息(用于前端展示)"""
tools_info = []
try:
# 获取注册的工具信息
for tool_obj in tools:
try:
metadata = getattr(tool_obj, "metadata", {}) or {}
info = {
"id": tool_obj.name,
"name": metadata.get("name", tool_obj.name),
"description": tool_obj.description,
"metadata": metadata,
"args": [],
# "is_async": is_async # Include async information
}
if hasattr(tool_obj, "args_schema") and tool_obj.args_schema:
if isinstance(tool_obj.args_schema, dict):
schema = tool_obj.args_schema
else:
schema = tool_obj.args_schema.schema()
for arg_name, arg_info in schema.get("properties", {}).items():
info["args"].append(
{
"name": arg_name,
"type": arg_info.get("type", ""),
"description": arg_info.get("description", ""),
}
)
tools_info.append(info)
# logger.debug(f"Successfully processed tool info for {tool_obj.name}")
except Exception as e:
logger.error(
f"Failed to process tool {getattr(tool_obj, 'name', 'unknown')}: {e}\n{traceback.format_exc()}. "
f"Details: {dict(tool_obj.__dict__)}"
)
continue
except Exception as e:
logger.error(f"Failed to get tools info: {e}\n{traceback.format_exc()}")
return []
logger.info(f"Successfully extracted info for {len(tools_info)} tools")
return tools_info
def get_buildin_tools() -> list:
"""注册静态工具"""
static_tools = [
query_knowledge_graph,
get_approved_user_goal,
calculator,
text_to_img_demo,
]
# subagents 工具
from .subagents import calc_agent_tool
static_tools.append(calc_agent_tool)
# 检查是否启用网页搜索(即是否配置了 API_KEY
if config.enable_web_search:
tavily_search = get_tavily_search()
if tavily_search:
static_tools.append(tavily_search)
return static_tools
async def get_tools_from_context(context, extra_tools=None) -> list:
"""从上下文配置中获取工具列表"""
# 1. 基础工具 (从 context.tools 中筛选)
all_basic_tools = get_buildin_tools() + (extra_tools or [])
selected_tools = []
if context.tools:
# 创建工具映射表
tools_map = {t.name: t for t in all_basic_tools}
for tool_name in context.tools:
if tool_name in tools_map:
selected_tools.append(tools_map[tool_name])
# 2. 知识库工具
if context.knowledges:
kb_tools = get_kb_based_tools(db_names=context.knowledges)
selected_tools.extend(kb_tools)
# 3. MCP 工具(使用统一入口,自动过滤 disabled_tools
if context.mcps:
for server_name in context.mcps:
mcp_tools = await get_enabled_mcp_tools(server_name)
selected_tools.extend(mcp_tools)
return selected_tools