refactor(tools_factory): 重构工具工厂模块,优化工具管理和错误处理

- 重命名工具获取函数为 get_buildin_tools 和 get_buildin_tools_info
- 改进工具ID生成和描述构建逻辑
- 添加详细的日志记录和错误处理
- 优化知识库工具创建流程,避免闭包问题
- 分离静态工具和知识库工具获取逻辑
- 增强计算器和知识图谱查询工具的错误处理
This commit is contained in:
Wenjie Zhang 2025-08-24 20:12:15 +08:00
parent fbe85702a9
commit 402ff52739
8 changed files with 234 additions and 149 deletions

View File

@ -25,6 +25,8 @@
下面的功能会放在后续版本实现,暂时未定 下面的功能会放在后续版本实现,暂时未定
- [ ] 使用其他的聊天记录管理方法(现在是基于 LangGraph 的 Memory 实现的)
- [ ] 封装现有工具为 mcpstdio调用 - [ ] 封装现有工具为 mcpstdio调用
- [ ] 支持额外 mcp 配置(代码端) - [ ] 支持额外 mcp 配置(代码端)
- [ ] 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息 - [ ] 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息

40
docs/vibe/AGENT.md Normal file
View File

@ -0,0 +1,40 @@
# 项目目录结构
```
Yuxi-Know/
├── docker/ # Docker 配置文件
├── docs/ # 项目文档
├── scripts/ # 脚本文件,如批量上传等
├── server/ # 服务端代码(部分)
├── src/ # 主要源代码目录
│ ├── agents/ # 智能体应用
│ ├── config/ # 配置文件
│ ├── knowledge/ # 知识库相关
│ ├── models/ # 数据模型
│ ├── plugins/ # 插件
│ ├── static/ # 静态资源
│ └── utils/ # 工具函数
├── web/ # 前端代码
└── docker-compose.yml # Docker Compose 配置
```
# 主要容器名称
项目使用 Docker Compose 管理多个服务,主要容器名称如下:
- `api-dev` - FastAPI 后端服务
- `web-dev` - Vue.js 前端开发服务器
- `graph` - Neo4j 图数据库
- `milvus` - 向量数据库,包含 etcd 和 MinIO 依赖
- `mineru` - 可选的 MinerU OCR 服务(需要 GPU
- `paddlex` - 可选的 PaddleX OCR 服务(需要 GPU
## 项目调试
此项目是使用 Docker 进行部署的,使用 `docker compose up -d` 命令进行构建和启动。因此当进行任何修改的时候,不要尝试启动这个项目,应该先检查项目是否已经在后台启动(`docker ps`),具体的可以阅读 [docker-compose.yml](docker-compose.yml).
前端和后端都是配置了自动启动的,因此当修改完成后,会自动更新,可以使用 docker logs 查看日志。对于部分场景可以创建一个 test_router.py 在 [server/routers](server/routers) 中,然后通过 API 测试功能场景。对于前端的 UI 修改,则不用测试。
## 风格说明
UI风格要简洁同时要保持一致性颜色要尽量参考 [base.css](web/src/assets/css/base.css) 中的颜色。不要悬停位移,不要过度使用阴影以及渐变色。

View File

@ -14,7 +14,7 @@ from src import executor, config
from src.agents import agent_manager from src.agents import agent_manager
from src.models import select_model from src.models import select_model
from src.utils.logging_config import logger from src.utils.logging_config import logger
from src.agents.tools_factory import get_runnable_tools from src.agents.tools_factory import get_buildin_tools
from server.routers.auth_router import get_admin_user from server.routers.auth_router import get_admin_user
from server.utils.auth_middleware import get_required_user, get_db from server.utils.auth_middleware import get_required_user, get_db
from server.models.user_model import User from server.models.user_model import User
@ -181,7 +181,7 @@ async def update_chat_models(model_provider: str, model_names: list[str], curren
@chat.get("/tools") @chat.get("/tools")
async def get_tools(current_user: User = Depends(get_admin_user)): async def get_tools(current_user: User = Depends(get_admin_user)):
"""获取所有可用工具(需要登录)""" """获取所有可用工具(需要登录)"""
return {"tools": list(get_runnable_tools().keys())} return {"tools": list(get_buildin_tools().keys())}
@chat.post("/agent/{agent_id}/config") @chat.post("/agent/{agent_id}/config")
async def save_agent_config( async def save_agent_config(

View File

@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from src.agents.tools_factory import get_all_tools_info from src.agents.tools_factory import get_buildin_tools_info
from server.models.user_model import User from server.models.user_model import User
from server.utils.auth_middleware import get_required_user from server.utils.auth_middleware import get_required_user
@ -9,7 +9,7 @@ tool = chat = APIRouter(prefix="/tool", tags=["tool"])
async def get_tools(current_user: User = Depends(get_required_user)): async def get_tools(current_user: User = Depends(get_required_user)):
"""获取所有可用工具的信息""" """获取所有可用工具的信息"""
try: try:
tools_info = get_all_tools_info() tools_info = get_buildin_tools_info()
return {"tools": tools_info} return {"tools": tools_info}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}

View File

@ -3,7 +3,7 @@ import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from src.agents.registry import Configuration from src.agents.registry import Configuration
from src.agents.tools_factory import get_runnable_tools from src.agents.tools_factory import get_buildin_tools
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ChatbotConfiguration(Configuration): class ChatbotConfiguration(Configuration):
@ -37,7 +37,7 @@ class ChatbotConfiguration(Configuration):
default_factory=list, default_factory=list,
metadata={ metadata={
"name": "工具", "name": "工具",
"options": list(get_runnable_tools().keys()), # 这里的选择是所有的工具 "options": list(get_buildin_tools().keys()), # 这里的选择是所有的工具
"description": "工具列表" "description": "工具列表"
}, },
) )

View File

@ -15,7 +15,7 @@ from src.utils import logger
from src.agents.registry import State, BaseAgent from src.agents.registry import State, BaseAgent
from src.agents.utils import load_chat_model, get_cur_time_with_utc from src.agents.utils import load_chat_model, get_cur_time_with_utc
from src.agents.chatbot.configuration import ChatbotConfiguration from src.agents.chatbot.configuration import ChatbotConfiguration
from src.agents.tools_factory import get_runnable_tools from src.agents.tools_factory import get_buildin_tools
class ChatbotAgent(BaseAgent): class ChatbotAgent(BaseAgent):
name = "智能体助手" name = "智能体助手"
@ -33,7 +33,7 @@ class ChatbotAgent(BaseAgent):
默认不使用任何工具 默认不使用任何工具
如果配置为列表则使用列表中的工具 如果配置为列表则使用列表中的工具
""" """
platform_tools = get_runnable_tools() platform_tools = get_buildin_tools()
if tools is None or not isinstance(tools, list) or len(tools) == 0: if tools is None or not isinstance(tools, list) or len(tools) == 0:
# 默认不使用任何工具 # 默认不使用任何工具
logger.info("未配置工具或配置为空,不使用任何工具") logger.info("未配置工具或配置为空,不使用任何工具")
@ -65,7 +65,7 @@ class ChatbotAgent(BaseAgent):
if self.graph: if self.graph:
return self.graph return self.graph
runnable_tools = get_runnable_tools() runnable_tools = get_buildin_tools()
tools = list(runnable_tools.values()) tools = list(runnable_tools.values())
tools_name = list(runnable_tools.keys()) tools_name = list(runnable_tools.keys())
logger.debug(f"build graph `{self.id}` with tools: {tools_name}") logger.debug(f"build graph `{self.id}` with tools: {tools_name}")

View File

@ -1,8 +1,6 @@
import json
import asyncio import asyncio
import inspect import hashlib
import types import os
from collections.abc import Callable
from typing import Annotated, Any from typing import Annotated, Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@ -13,10 +11,6 @@ from src import config, graph_base, knowledge_base
from src.utils import logger from src.utils import logger
# 工具注册表 - 移到前面以避免NameError
_TOOLS_REGISTRY = {}
class KnowledgeRetrieverModel(BaseModel): class KnowledgeRetrieverModel(BaseModel):
query_text: str = Field( query_text: str = Field(
description=( description=(
@ -25,128 +19,176 @@ class KnowledgeRetrieverModel(BaseModel):
) )
) )
def get_runnable_tools():
def _create_retriever_wrapper(db_id: str, retriever_info: dict[str, Any]):
"""创建检索器包装函数的工厂函数,避免闭包变量捕获问题"""
async def async_retriever_wrapper(query_text: str) -> Any:
"""异步检索器包装函数"""
retriever = retriever_info["retriever"]
try:
logger.debug(f"Retrieving from database {db_id} with query: {query_text}")
if asyncio.iscoroutinefunction(retriever):
result = await retriever(query_text)
else:
result = retriever(query_text)
logger.debug(f"Retrieved {len(result) if isinstance(result, list) else 'N/A'} results from {db_id}")
return result
except Exception as e:
logger.error(f"Error in retriever {db_id}: {e}")
return f"检索失败: {str(e)}"
return async_retriever_wrapper
def get_buildin_tools() -> dict[str, Any]:
"""获取所有可运行的工具(给大模型使用)""" """获取所有可运行的工具(给大模型使用)"""
tools = _TOOLS_REGISTRY.copy() tools = {}
# 获取所有知识库 try:
for db_Id, retrieve_info in knowledge_base.get_retrievers().items(): # 获取所有知识库基于的工具
_id = f"retrieve_{db_Id[:8]}" # Deepseek does not support non-alphanumeric characters in tool names kb_tools = get_kb_based_tools()
description = ( static_tools = get_static_tools()
f"使用 {retrieve_info['name']} 知识库进行检索。\n"
f"下面是这个知识库的描述:\n{retrieve_info['description']}"
)
# 创建异步工具,确保正确处理异步检索器 tools.update(kb_tools)
async def async_retriever_wrapper(query_text: str, db_id=db_Id, retriever_info=retrieve_info): tools.update(static_tools)
"""异步检索器包装函数"""
retriever = retriever_info["retriever"]
try:
if asyncio.iscoroutinefunction(retriever):
result = await retriever(query_text)
else:
result = retriever(query_text)
return result
except Exception as e:
logger.error(f"Error in retriever {db_id}: {e}")
return f"检索失败: {str(e)}"
# 使用 StructuredTool.from_function 创建异步工具 except Exception as e:
tools[_id] = StructuredTool.from_function( logger.error(f"Failed to get knowledge base retrievers: {e}")
coroutine=async_retriever_wrapper, # 指定为协程
name=_id,
description=description,
args_schema=KnowledgeRetrieverModel,
metadata=retrieve_info
)
logger.info(f"Total tools available: {len(tools)}")
return tools return tools
def get_all_tools_info(): def get_kb_based_tools() -> dict[str, Any]:
"""获取所有知识库基于的工具"""
# 获取所有知识库
kb_tools = {}
retrievers = knowledge_base.get_retrievers()
logger.debug(f"Found {len(retrievers)} knowledge base retrievers")
for db_id, retrieve_info in retrievers.items():
try:
# 使用改进的工具ID生成策略
tool_id = f"query_{db_id[:8]}"
# 构建工具描述
description = (
f"使用 {retrieve_info['name']} 知识库进行检索。\n"
f"下面是这个知识库的描述:\n{retrieve_info['description'] or '没有描述。'}"
)
# 使用工厂函数创建检索器包装函数,避免闭包问题
retriever_wrapper = _create_retriever_wrapper(db_id, retrieve_info)
# 使用 StructuredTool.from_function 创建异步工具
tool = StructuredTool.from_function(
coroutine=retriever_wrapper,
name=tool_id,
description=description,
args_schema=KnowledgeRetrieverModel,
metadata=retrieve_info | {
"tag": "knowledgebase"
}
)
kb_tools[tool_id] = tool
logger.debug(f"Successfully created tool {tool_id} for database {db_id}")
except Exception as e:
logger.error(f"Failed to create tool for database {db_id}: {e}")
continue
return kb_tools
def get_buildin_tools_info() -> dict[str, dict[str, Any]]:
"""获取所有工具的信息(用于前端展示)""" """获取所有工具的信息(用于前端展示)"""
tools_info = {} tools_info = {}
tools = get_runnable_tools() try:
tools = get_buildin_tools()
logger.debug(f"Processing {len(tools)} tools for info extraction")
# 获取注册的工具信息 # 获取注册的工具信息
for _id, tool_obj in tools.items(): for tool_id, tool_obj in tools.items():
try:
metadata = getattr(tool_obj, 'metadata', {}) or {}
info = {
"id": tool_id,
"name": metadata.get('name', tool_id),
"description": metadata.get('description') or getattr(tool_obj, 'description', ''),
'metadata': metadata,
"args": []
}
metadata = getattr(tool_obj, 'metadata', {}) or {} # 获取工具参数信息
info = { try:
"id": _id, if hasattr(tool_obj, 'args_schema') and tool_obj.args_schema:
"name": metadata.get('name', _id), schema = tool_obj.args_schema.schema()
"description": metadata.get('description') or getattr(tool_obj, 'description', ''), if 'properties' in schema:
'metadata': metadata, for arg_name, arg_info in schema['properties'].items():
"args": [] info["args"].append({
} "name": arg_name,
"type": arg_info.get('type', ''),
"description": arg_info.get('description', '')
})
except Exception as e:
logger.warning(f"Failed to extract args schema for tool {tool_id}: {e}")
# 获取工具参数信息 tools_info[tool_id] = info
if hasattr(tool_obj, 'args_schema') and tool_obj.args_schema: logger.debug(f"Successfully processed tool info for {tool_id}")
schema = tool_obj.args_schema.schema()
if 'properties' in schema:
for arg_name, arg_info in schema['properties'].items():
info["args"].append({
"name": arg_name,
"type": arg_info.get('type', ''),
"description": arg_info.get('description', '')
})
tools_info[info['id']] = info
except Exception as e:
logger.error(f"Failed to process tool {tool_id}: {e}")
continue
except Exception as e:
logger.error(f"Failed to get tools info: {e}")
return {}
logger.info(f"Successfully extracted info for {len(tools_info)} tools")
return tools_info return tools_info
class BaseToolOutput:
"""
LLM 要求 Tool 的输出为 str Tool 用在别处时希望它正常返回结构化数据
只需要将 Tool 返回值用该类封装能同时满足两者的需要
基类简单的将返回值字符串化或指定 format="json" 将其转为 json
用户也可以继承该类定义自己的转换方法
"""
def __init__(
self,
data: Any,
format: str | Callable | None = None,
data_alias: str = "",
**extras: Any,
) -> None:
self.data = data
self.format = format
self.extras = extras
if data_alias:
setattr(self, data_alias, property(lambda obj: obj.data))
def __str__(self) -> str:
if self.format == "json":
return json.dumps(self.data, ensure_ascii=False, indent=2)
elif callable(self.format):
return self.format(self)
else:
return str(self.data)
@tool @tool
def calculator(a: float, b: float, operation: str) -> float: def calculator(a: float, b: float, operation: str) -> float:
"""Calculate two numbers. operation: add, subtract, multiply, divide""" """Calculate two numbers. operation: add, subtract, multiply, divide"""
if operation == "add": try:
return a + b if operation == "add":
elif operation == "subtract": return a + b
return a - b elif operation == "subtract":
elif operation == "multiply": return a - b
return a * b elif operation == "multiply":
elif operation == "divide": return a * b
return a / b elif operation == "divide":
else: if b == 0:
raise ValueError(f"Invalid operation: {operation}, only support add, subtract, multiply, divide") 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 @tool
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]): def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
"""Use this to query knowledge graph, which include some food domain knowledge.""" """Use this to query knowledge graph, which include some food domain knowledge."""
return graph_base.query_node(query, hops=2, return_format='triples') 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 {len(result.get('triples', [])) if isinstance(result, d) else 'N/A'} triples")
return result
except Exception as e:
logger.error(f"Knowledge graph query error: {e}")
return f"知识图谱查询失败: {str(e)}"
# 更新工具注册表 def get_static_tools() -> dict[str, Any]:
_TOOLS_REGISTRY.update({ """注册静态工具"""
"Calculator": calculator, static_tools = {
"QueryKnowledgeGraph": query_knowledge_graph, "Calculator": calculator,
}) "QueryKnowledgeGraph": query_knowledge_graph,
}
if config.enable_web_search: # 检查是否启用网页搜索
_TOOLS_REGISTRY["WebSearchWithTavily"] = TavilySearch(max_results=10) if config.enable_web_search:
static_tools["WebSearchWithTavily"] = TavilySearch(max_results=10)
return static_tools

View File

@ -907,13 +907,19 @@ watch(() => props.isOpen, (newVal) => {
} }
.tools-list { .tools-list {
max-height: 300px; display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
max-height: max(60vh, 800px);
overflow-y: auto; overflow-y: auto;
border: 1px solid var(--gray-200);
border-radius: 8px; border-radius: 8px;
margin-bottom: 16px; margin-bottom: 16px;
background: white; background: white;
padding: 4px;
//
@media (max-width: 480px) {
grid-template-columns: 1fr;
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 6px; width: 6px;
@ -943,38 +949,14 @@ watch(() => props.isOpen, (newVal) => {
background: white; background: white;
border: 1px solid var(--gray-200); border: 1px solid var(--gray-200);
&:last-child {
margin-bottom: 0;
}
&:hover { &:hover {
border-color: var(--main-color); border-color: var(--gray-300);
background: var(--gray-50); background: var(--gray-20);
} }
&.selected {
background: var(--main-10);
border-color: var(--main-color);
.tool-name {
color: var(--main-color);
font-weight: 600;
}
.tool-description {
color: var(--gray-700);
}
.tool-indicator {
color: var(--main-color);
}
}
.tool-content { .tool-content {
.tool-header { .tool-header {
display: flex; display: flex;
justify-content: space-between; align-items: center;
align-items: flex-start;
margin-bottom: 6px; margin-bottom: 6px;
gap: 8px; gap: 8px;
@ -1005,6 +987,25 @@ watch(() => props.isOpen, (newVal) => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
} }
&.selected {
background: var(--main-30);
// border-color: var(--main-color);
.tool-content {
.tool-name {
color: var(--main-color);
}
.tool-indicator {
color: var(--main-color);
}
}
.tool-description {
color: var(--gray-700);
}
}
} }
} }