diff --git a/main_v2.py b/main_v2.py
index 0658763..24cabb5 100644
--- a/main_v2.py
+++ b/main_v2.py
@@ -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:
diff --git a/models_v2.py b/models_v2.py
index 1809653..833cfba 100644
--- a/models_v2.py
+++ b/models_v2.py
@@ -229,31 +229,58 @@ class MatrixRoomMapping(Base):
# ==================== 搜索工具配置 ====================
-class SearchToolConfig(Base):
- """搜索工具配置(Tavily等)"""
- __tablename__ = 'search_tool_config'
+class ToolConfig(Base):
+ """工具配置(通用,支持搜索、计算器、代码执行等)"""
+ __tablename__ = 'tool_configs'
id = Column(Integer, primary_key=True, index=True)
- name = Column(String(100)) # 工具名称,如 "Tavily Search"
- provider = Column(String(50)) # 提供商:tavily, google, bing
+ name = Column(String(100)) # 工具名称,如 "Tavily Search"、"Calculator"
+ tool_type = Column(String(50), index=True) # 工具类型:search, calculator, code_runner, image_gen, etc.
+ provider = Column(String(50), nullable=True) # 提供商(可选):tavily, google, wolfram, etc.
- # API配置
- api_key = Column(String(200)) # API密钥
- api_base = Column(String(200), nullable=True) # API地址(可选)
-
- # 搜索参数
- max_results = Column(Integer, default=5) # 最大返回结果数
- include_raw_content = Column(Boolean, default=False) # 是否包含原始内容
- search_depth = Column(String(20), default='basic') # basic 或 advanced
+ # API配置(JSON,不同工具可能有不同配置)
+ config = Column(JSON, default=dict)
+ # search示例: {"api_key": "xxx", "max_results": 5, "search_depth": "basic"}
+ # calculator示例: {"api_base": "xxx"}
# 状态
is_active = Column(Boolean, default=True)
- is_default = Column(Boolean, default=False) # 是否为默认搜索工具
+ is_default = Column(Boolean, default=False) # 是否为该类型的默认工具
+
+ # 统计
+ total_calls = Column(Integer, default=0) # 总调用次数
+ success_calls = Column(Integer, default=0) # 成功次数
+ failed_calls = Column(Integer, default=0) # 失败次数
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+class ToolUsageLog(Base):
+ """工具使用日志"""
+ __tablename__ = 'tool_usage_logs'
+
+ id = Column(Integer, primary_key=True, index=True)
+ tool_id = Column(Integer, ForeignKey('tool_configs.id'))
+ tool_type = Column(String(50), index=True)
+
+ # 调用信息
+ query = Column(Text) # 调用参数/查询内容
+ success = Column(Boolean, default=True)
+ error_message = Column(Text, nullable=True)
+ result_summary = Column(Text, nullable=True) # 结果摘要
+
+ # 关联信息
+ conversation_id = Column(String(100), nullable=True)
+ agent_id = Column(Integer, nullable=True)
+ user_id = Column(String(100), nullable=True)
+
+ # 性能
+ duration_ms = Column(Integer, nullable=True) # 调用耗时(毫秒)
+
+ called_at = Column(DateTime, default=datetime.utcnow)
+
+
# ==================== 系统配置(保留) ====================
class SystemConfig(Base):
diff --git a/services/agent_service.py b/services/agent_service.py
index e07b346..d5612d5 100644
--- a/services/agent_service.py
+++ b/services/agent_service.py
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from typing import List, Optional, Dict
import logging
-from models_v2 import Agent, LLMProvider, ChannelAgentMapping, Channel, SearchToolConfig, init_default_data
+from models_v2 import Agent, LLMProvider, ChannelAgentMapping, Channel, ToolConfig, ToolUsageLog, init_default_data
logger = logging.getLogger(__name__)
@@ -409,84 +409,169 @@ class ChannelService:
}
-class SearchToolService:
- """搜索工具管理服务"""
+class ToolService:
+ """工具管理服务"""
def __init__(self, db: Session):
self.db = db
- def get_all_configs(self) -> List[SearchToolConfig]:
- """获取所有搜索工具配置"""
- return self.db.query(SearchToolConfig).order_by(SearchToolConfig.is_default.desc(), SearchToolConfig.name).all()
+ def get_all_tools(self) -> List[ToolConfig]:
+ """获取所有工具配置"""
+ return self.db.query(ToolConfig).order_by(ToolConfig.tool_type, ToolConfig.is_default.desc()).all()
- def get_active_configs(self) -> List[SearchToolConfig]:
- """获取活跃的搜索工具配置"""
- return self.db.query(SearchToolConfig).filter(SearchToolConfig.is_active == True).all()
+ def get_tools_by_type(self, tool_type: str) -> List[ToolConfig]:
+ """获取指定类型的工具"""
+ return self.db.query(ToolConfig).filter(ToolConfig.tool_type == tool_type).all()
- def get_config(self, config_id: int) -> Optional[SearchToolConfig]:
- """获取单个配置"""
- return self.db.query(SearchToolConfig).filter(SearchToolConfig.id == config_id).first()
+ def get_active_tools(self) -> List[ToolConfig]:
+ """获取活跃的工具"""
+ return self.db.query(ToolConfig).filter(ToolConfig.is_active == True).all()
- def get_default_config(self) -> Optional[SearchToolConfig]:
- """获取默认配置"""
- config = self.db.query(SearchToolConfig).filter(
- SearchToolConfig.is_default == True,
- SearchToolConfig.is_active == True
+ def get_tool(self, tool_id: int) -> Optional[ToolConfig]:
+ """获取单个工具"""
+ return self.db.query(ToolConfig).filter(ToolConfig.id == tool_id).first()
+
+ def get_default_tool(self, tool_type: str) -> Optional[ToolConfig]:
+ """获取指定类型的默认工具"""
+ tool = self.db.query(ToolConfig).filter(
+ ToolConfig.tool_type == tool_type,
+ ToolConfig.is_default == True,
+ ToolConfig.is_active == True
).first()
- if not config:
- config = self.db.query(SearchToolConfig).filter(SearchToolConfig.is_active == True).first()
- return config
+ if not tool:
+ tool = self.db.query(ToolConfig).filter(
+ ToolConfig.tool_type == tool_type,
+ ToolConfig.is_active == True
+ ).first()
+ return tool
- def create_config(self, data: Dict) -> SearchToolConfig:
- """创建搜索工具配置"""
- config = SearchToolConfig(
+ def create_tool(self, data: Dict) -> ToolConfig:
+ """创建工具配置"""
+ tool = ToolConfig(
name=data.get('name'),
- provider=data.get('provider', 'tavily'),
- api_key=data.get('api_key'),
- api_base=data.get('api_base'),
- max_results=data.get('max_results', 5),
- include_raw_content=data.get('include_raw_content', False),
- search_depth=data.get('search_depth', 'basic'),
+ tool_type=data.get('tool_type', 'search'),
+ provider=data.get('provider'),
+ config=data.get('config', {}),
is_active=data.get('is_active', True),
is_default=data.get('is_default', False)
)
- self.db.add(config)
+ self.db.add(tool)
self.db.commit()
- self.db.refresh(config)
- return config
+ self.db.refresh(tool)
+ return tool
- def update_config(self, config_id: int, data: Dict) -> Optional[SearchToolConfig]:
- """更新配置"""
- config = self.get_config(config_id)
- if not config:
+ def update_tool(self, tool_id: int, data: Dict) -> Optional[ToolConfig]:
+ """更新工具配置"""
+ tool = self.get_tool(tool_id)
+ if not tool:
return None
for key, value in data.items():
- if hasattr(config, key) and value is not None:
- setattr(config, key, value)
+ if hasattr(tool, key) and value is not None:
+ setattr(tool, key, value)
self.db.commit()
- self.db.refresh(config)
- return config
+ self.db.refresh(tool)
+ return tool
- def delete_config(self, config_id: int) -> bool:
- """删除配置"""
- config = self.get_config(config_id)
- if not config:
+ def delete_tool(self, tool_id: int) -> bool:
+ """删除工具配置"""
+ tool = self.get_tool(tool_id)
+ if not tool:
return False
- self.db.delete(config)
+ self.db.delete(tool)
self.db.commit()
return True
- def set_default_config(self, config_id: int) -> bool:
- """设置默认配置"""
- self.db.query(SearchToolConfig).update({SearchToolConfig.is_default: False})
-
- config = self.get_config(config_id)
- if not config:
+ def set_default_tool(self, tool_id: int) -> bool:
+ """设置默认工具"""
+ tool = self.get_tool(tool_id)
+ if not tool:
return False
- config.is_default = True
+ # 清除同类型的其他默认
+ self.db.query(ToolConfig).filter(
+ ToolConfig.tool_type == tool.tool_type
+ ).update({ToolConfig.is_default: False})
+
+ tool.is_default = True
self.db.commit()
- return True
\ No newline at end of file
+ return True
+
+ def increment_stats(self, tool_id: int, success: bool):
+ """更新工具调用统计"""
+ tool = self.get_tool(tool_id)
+ if tool:
+ tool.total_calls += 1
+ if success:
+ tool.success_calls += 1
+ else:
+ tool.failed_calls += 1
+ self.db.commit()
+
+ def log_usage(self, data: Dict) -> ToolUsageLog:
+ """记录工具使用日志"""
+ log = ToolUsageLog(
+ tool_id=data.get('tool_id'),
+ tool_type=data.get('tool_type'),
+ query=data.get('query'),
+ success=data.get('success', True),
+ error_message=data.get('error_message'),
+ result_summary=data.get('result_summary'),
+ conversation_id=data.get('conversation_id'),
+ agent_id=data.get('agent_id'),
+ user_id=data.get('user_id'),
+ duration_ms=data.get('duration_ms')
+ )
+ self.db.add(log)
+ self.db.commit()
+ self.db.refresh(log)
+ return log
+
+ def get_usage_stats(self, days: int = 7) -> Dict:
+ """获取工具使用统计"""
+ from datetime import timedelta
+ start_date = datetime.utcnow() - timedelta(days=days)
+
+ # 按工具类型统计
+ logs = self.db.query(ToolUsageLog).filter(
+ ToolUsageLog.called_at >= start_date
+ ).all()
+
+ stats = {
+ 'total_calls': len(logs),
+ 'success_rate': sum(1 for l in logs if l.success) / len(logs) * 100 if logs else 0,
+ 'by_type': {},
+ 'by_tool': {},
+ 'recent_errors': []
+ }
+
+ for log in logs:
+ # 按类型
+ if log.tool_type not in stats['by_type']:
+ stats['by_type'][log.tool_type] = {'total': 0, 'success': 0, 'failed': 0}
+ stats['by_type'][log.tool_type]['total'] += 1
+ if log.success:
+ stats['by_type'][log.tool_type]['success'] += 1
+ else:
+ stats['by_type'][log.tool_type]['failed'] += 1
+
+ # 按工具
+ tool = self.get_tool(log.tool_id) if log.tool_id else None
+ tool_name = tool.name if tool else f'Tool#{log.tool_id}'
+ if tool_name not in stats['by_tool']:
+ stats['by_tool'][tool_name] = {'total': 0, 'success': 0}
+ stats['by_tool'][tool_name]['total'] += 1
+ if log.success:
+ stats['by_tool'][tool_name]['success'] += 1
+
+ # 最近错误
+ if not log.success and log.error_message:
+ stats['recent_errors'].append({
+ 'tool': tool_name,
+ 'error': log.error_message[:100],
+ 'time': log.called_at.isoformat()
+ })
+
+ return stats
\ No newline at end of file
diff --git a/templates/admin_v2/index.html b/templates/admin_v2/index.html
index e92b362..724603f 100644
--- a/templates/admin_v2/index.html
+++ b/templates/admin_v2/index.html
@@ -42,7 +42,7 @@
大模型池
Agent管理
渠道管理
- 搜索工具
+ 工具管理
统计数据
@@ -110,19 +110,29 @@
-
-