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 @@ - + @@ -110,19 +110,29 @@ - -
- -
-
- 搜索工具列表(Tavily、Google等) - + +
+ +
+
+
+
+ 工具列表(搜索、计算器、代码执行等) + +
+
+ + + +
名称类型提供商调用次数成功率默认状态操作
加载中...
+
+
-
- - - -
名称提供商API Key最大结果默认状态操作
加载中...
+
+
+
工具使用统计(近7天)
+
加载中...
+
@@ -200,20 +210,41 @@
- -