feat: 重构工具配置为通用模型,增加使用统计

- ToolConfig 模型:支持多种工具类型(搜索、计算器、代码执行等)
- ToolUsageLog 模型:记录工具调用日志
- 工具使用统计:调用次数、成功率、错误记录
- 后台管理界面:工具列表+统计展示
- API 重构:/api/v2/tools(替代 search-tools)
This commit is contained in:
2026-04-13 16:15:26 +08:00
parent b9e99da01b
commit 142904dff4
4 changed files with 513 additions and 231 deletions

View File

@@ -18,11 +18,11 @@ import os
from models_v2 import (
init_db, get_db, SessionLocal,
User, Conversation, Message, SystemConfig,
LLMProvider, Agent, Channel, ChannelAgentMapping, MatrixRoomMapping, SearchToolConfig,
LLMProvider, Agent, Channel, ChannelAgentMapping, MatrixRoomMapping, ToolConfig, ToolUsageLog,
init_default_data
)
from services.llm_service import llm_service
from services.agent_service import AgentService, LLMProviderService, ChannelService, SearchToolService
from services.agent_service import AgentService, LLMProviderService, ChannelService, ToolService
from services.conversation_service import ConversationService
# 配置日志
@@ -419,123 +419,157 @@ async def unbind_agent(mapping_id: int, db: Session = Depends(get_db)):
return {"success": success}
# ==================== 搜索工具 API ====================
# ==================== 工具配置 API ====================
@app.get("/api/v2/search-tools")
async def get_search_tools(db: Session = Depends(get_db)):
"""获取所有搜索工具配置"""
service = SearchToolService(db)
configs = service.get_all_configs()
@app.get("/api/v2/tools")
async def get_tools(tool_type: str = None, db: Session = Depends(get_db)):
"""获取所有工具配置"""
service = ToolService(db)
if tool_type:
tools = service.get_tools_by_type(tool_type)
else:
tools = service.get_all_tools()
return {
"configs": [
"tools": [
{
"id": c.id,
"name": c.name,
"provider": c.provider,
"api_key": c.api_key,
"api_base": c.api_base,
"max_results": c.max_results,
"include_raw_content": c.include_raw_content,
"search_depth": c.search_depth,
"is_active": c.is_active,
"is_default": c.is_default
"id": t.id,
"name": t.name,
"tool_type": t.tool_type,
"provider": t.provider,
"config": t.config,
"is_active": t.is_active,
"is_default": t.is_default,
"total_calls": t.total_calls,
"success_calls": t.success_calls,
"failed_calls": t.failed_calls
}
for c in configs
for t in tools
]
}
@app.post("/api/v2/search-tools")
async def create_search_tool(data: dict, db: Session = Depends(get_db)):
"""创建搜索工具配置"""
service = SearchToolService(db)
# 如果设为默认,更新其他
if data.get('is_default'):
service.set_default_config(0) # 先清除所有默认
config = service.create_config(data)
return {"success": True, "config": {"id": config.id, "name": config.name}}
@app.post("/api/v2/tools")
async def create_tool(data: dict, db: Session = Depends(get_db)):
"""创建工具配置"""
service = ToolService(db)
tool = service.create_tool(data)
return {"success": True, "tool": {"id": tool.id, "name": tool.name, "tool_type": tool.tool_type}}
@app.put("/api/v2/search-tools/{config_id}")
async def update_search_tool(config_id: int, data: dict, db: Session = Depends(get_db)):
"""更新搜索工具配置"""
service = SearchToolService(db)
@app.put("/api/v2/tools/{tool_id}")
async def update_tool(tool_id: int, data: dict, db: Session = Depends(get_db)):
"""更新工具配置"""
service = ToolService(db)
if data.get('is_default'):
service.set_default_config(config_id)
service.set_default_tool(tool_id)
config = service.update_config(config_id, data)
tool = service.update_tool(tool_id, data)
if not config:
return {"success": False, "message": "配置不存在"}
if not tool:
return {"success": False, "message": "工具不存在"}
return {"success": True, "config": {"id": config.id, "name": config.name}}
return {"success": True, "tool": {"id": tool.id, "name": tool.name}}
@app.delete("/api/v2/search-tools/{config_id}")
async def delete_search_tool(config_id: int, db: Session = Depends(get_db)):
"""删除搜索工具配置"""
service = SearchToolService(db)
success = service.delete_config(config_id)
@app.delete("/api/v2/tools/{tool_id}")
async def delete_tool(tool_id: int, db: Session = Depends(get_db)):
"""删除工具配置"""
service = ToolService(db)
success = service.delete_tool(tool_id)
return {"success": success}
@app.post("/api/v2/search-tools/{config_id}/default")
async def set_search_tool_default(config_id: int, db: Session = Depends(get_db)):
"""设置默认搜索工具"""
service = SearchToolService(db)
success = service.set_default_config(config_id)
@app.post("/api/v2/tools/{tool_id}/default")
async def set_tool_default(tool_id: int, db: Session = Depends(get_db)):
"""设置默认工具"""
service = ToolService(db)
success = service.set_default_tool(tool_id)
return {"success": success}
@app.post("/api/v2/search")
@app.get("/api/v2/tools/stats")
async def get_tool_stats(days: int = 7, db: Session = Depends(get_db)):
"""获取工具使用统计"""
service = ToolService(db)
stats = service.get_usage_stats(days=days)
return stats
@app.post("/api/v2/tools/search")
async def perform_search(data: dict, db: Session = Depends(get_db)):
"""执行搜索供前端或Agent调用"""
import httpx
import time
query = data.get('query')
if not query:
return {"success": False, "message": "缺少搜索关键词"}
# 获取搜索工具配置
service = SearchToolService(db)
config_id = data.get('config_id')
service = ToolService(db)
tool_id = data.get('tool_id')
if config_id:
config = service.get_config(config_id)
if tool_id:
tool = service.get_tool(tool_id)
else:
config = service.get_default_config()
tool = service.get_default_tool('search')
if not config or not config.api_key:
if not tool or not tool.config.get('api_key'):
return {"success": False, "message": "未配置搜索工具"}
# Tavily Search API
if config.provider == 'tavily':
if tool.provider == 'tavily' or tool.tool_type == 'search':
start_time = time.time()
try:
tavily_url = "https://api.tavily.com/search"
config = tool.config
payload = {
"api_key": config.api_key,
"api_key": config.get('api_key'),
"query": query,
"max_results": config.max_results,
"include_raw_content": config.include_raw_content,
"search_depth": config.search_depth
"max_results": config.get('max_results', 5),
"include_raw_content": config.get('include_raw_content', False),
"search_depth": config.get('search_depth', 'basic')
}
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(tavily_url, json=payload)
result = response.json()
duration_ms = int((time.time() - start_time) * 1000)
# 更新统计和日志
service.increment_stats(tool.id, True)
service.log_usage({
'tool_id': tool.id,
'tool_type': 'search',
'query': query,
'success': True,
'result_summary': f'{len(result.get("results", []))} results',
'conversation_id': data.get('conversation_id'),
'agent_id': data.get('agent_id'),
'duration_ms': duration_ms
})
return {
"success": True,
"results": result.get("results", []),
"query": query
}
except Exception as e:
duration_ms = int((time.time() - start_time) * 1000)
service.increment_stats(tool.id, False)
service.log_usage({
'tool_id': tool.id,
'tool_type': 'search',
'query': query,
'success': False,
'error_message': str(e),
'conversation_id': data.get('conversation_id'),
'duration_ms': duration_ms
})
return {"success": False, "message": str(e)}
return {"success": False, "message": "不支持的搜索提供商"}
@@ -755,34 +789,63 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str):
if should_search:
# 执行搜索
search_service = SearchToolService(db)
search_config = search_service.get_default_config()
tool_service = ToolService(db)
search_tool = tool_service.get_default_tool('search')
if search_config and search_config.api_key:
if search_tool and search_tool.config.get('api_key'):
import httpx
import time
start_time = time.time()
try:
logger.info(f"执行搜索: query={message}")
tavily_url = "https://api.tavily.com/search"
config = search_tool.config
payload = {
"api_key": search_config.api_key,
"api_key": config.get('api_key'),
"query": message,
"max_results": search_config.max_results,
"search_depth": search_config.search_depth
"max_results": config.get('max_results', 5),
"search_depth": config.get('search_depth', 'basic')
}
# 同步调用(简化处理)
# 同步调用
with httpx.Client(timeout=30) as client:
resp = client.post(tavily_url, json=payload)
search_result = resp.json()
duration_ms = int((time.time() - start_time) * 1000)
if search_result.get("results"):
# 构建搜索上下文
search_context = "\n\n【搜索结果】\n"
for i, r in enumerate(search_result["results"][:5], 1):
search_context += f"{i}. {r.get('title', 'N/A')}\n {r.get('content', r.get('snippet', 'N/A'))[:200]}\n 来源: {r.get('url', 'N/A')}\n"
logger.info(f"搜索完成: {len(search_result['results'])} 条结果")
# 更新统计和日志
tool_service.increment_stats(search_tool.id, True)
tool_service.log_usage({
'tool_id': search_tool.id,
'tool_type': 'search',
'query': message,
'success': True,
'result_summary': f'{len(search_result["results"])} results',
'conversation_id': conversation_id,
'agent_id': current_agent_id,
'duration_ms': duration_ms
})
except Exception as e:
duration_ms = int((time.time() - start_time) * 1000)
logger.error(f"搜索失败: {e}")
tool_service.increment_stats(search_tool.id, False)
tool_service.log_usage({
'tool_id': search_tool.id,
'tool_type': 'search',
'query': message,
'success': False,
'error_message': str(e),
'conversation_id': conversation_id,
'duration_ms': duration_ms
})
# 获取或创建会话
if conversation_id: