refactor(tools_factory): 重构工具工厂模块,优化工具管理和错误处理
- 重命名工具获取函数为 get_buildin_tools 和 get_buildin_tools_info - 改进工具ID生成和描述构建逻辑 - 添加详细的日志记录和错误处理 - 优化知识库工具创建流程,避免闭包问题 - 分离静态工具和知识库工具获取逻辑 - 增强计算器和知识图谱查询工具的错误处理
This commit is contained in:
parent
fbe85702a9
commit
402ff52739
@ -25,6 +25,8 @@
|
||||
|
||||
下面的功能会放在后续版本实现,暂时未定
|
||||
|
||||
|
||||
- [ ] 使用其他的聊天记录管理方法(现在是基于 LangGraph 的 Memory 实现的)
|
||||
- [ ] 封装现有工具为 mcp(stdio)调用
|
||||
- [ ] 支持额外 mcp 配置(代码端)
|
||||
- [ ] 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息
|
||||
|
||||
40
docs/vibe/AGENT.md
Normal file
40
docs/vibe/AGENT.md
Normal 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) 中的颜色。不要悬停位移,不要过度使用阴影以及渐变色。
|
||||
@ -14,7 +14,7 @@ from src import executor, config
|
||||
from src.agents import agent_manager
|
||||
from src.models import select_model
|
||||
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.utils.auth_middleware import get_required_user, get_db
|
||||
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")
|
||||
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")
|
||||
async def save_agent_config(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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.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)):
|
||||
"""获取所有可用工具的信息"""
|
||||
try:
|
||||
tools_info = get_all_tools_info()
|
||||
tools_info = get_buildin_tools_info()
|
||||
return {"tools": tools_info}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@ -3,7 +3,7 @@ import uuid
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
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)
|
||||
class ChatbotConfiguration(Configuration):
|
||||
@ -37,7 +37,7 @@ class ChatbotConfiguration(Configuration):
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "工具",
|
||||
"options": list(get_runnable_tools().keys()), # 这里的选择是所有的工具
|
||||
"options": list(get_buildin_tools().keys()), # 这里的选择是所有的工具
|
||||
"description": "工具列表"
|
||||
},
|
||||
)
|
||||
|
||||
@ -15,7 +15,7 @@ from src.utils import logger
|
||||
from src.agents.registry import State, BaseAgent
|
||||
from src.agents.utils import load_chat_model, get_cur_time_with_utc
|
||||
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):
|
||||
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:
|
||||
# 默认不使用任何工具
|
||||
logger.info("未配置工具或配置为空,不使用任何工具")
|
||||
@ -65,7 +65,7 @@ class ChatbotAgent(BaseAgent):
|
||||
if self.graph:
|
||||
return self.graph
|
||||
|
||||
runnable_tools = get_runnable_tools()
|
||||
runnable_tools = get_buildin_tools()
|
||||
tools = list(runnable_tools.values())
|
||||
tools_name = list(runnable_tools.keys())
|
||||
logger.debug(f"build graph `{self.id}` with tools: {tools_name}")
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
import hashlib
|
||||
import os
|
||||
from typing import Annotated, Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@ -13,10 +11,6 @@ from src import config, graph_base, knowledge_base
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
# 工具注册表 - 移到前面以避免NameError
|
||||
_TOOLS_REGISTRY = {}
|
||||
|
||||
|
||||
class KnowledgeRetrieverModel(BaseModel):
|
||||
query_text: str = Field(
|
||||
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 = {}
|
||||
|
||||
# 获取所有知识库
|
||||
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
|
||||
description = (
|
||||
f"使用 {retrieve_info['name']} 知识库进行检索。\n"
|
||||
f"下面是这个知识库的描述:\n{retrieve_info['description']}"
|
||||
)
|
||||
try:
|
||||
# 获取所有知识库基于的工具
|
||||
kb_tools = get_kb_based_tools()
|
||||
static_tools = get_static_tools()
|
||||
|
||||
# 创建异步工具,确保正确处理异步检索器
|
||||
async def async_retriever_wrapper(query_text: str, db_id=db_Id, retriever_info=retrieve_info):
|
||||
"""异步检索器包装函数"""
|
||||
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)}"
|
||||
tools.update(kb_tools)
|
||||
tools.update(static_tools)
|
||||
|
||||
# 使用 StructuredTool.from_function 创建异步工具
|
||||
tools[_id] = StructuredTool.from_function(
|
||||
coroutine=async_retriever_wrapper, # 指定为协程
|
||||
name=_id,
|
||||
description=description,
|
||||
args_schema=KnowledgeRetrieverModel,
|
||||
metadata=retrieve_info
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get knowledge base retrievers: {e}")
|
||||
|
||||
logger.info(f"Total tools available: {len(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 = 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 = {
|
||||
"id": _id,
|
||||
"name": metadata.get('name', _id),
|
||||
"description": metadata.get('description') or getattr(tool_obj, 'description', ''),
|
||||
'metadata': metadata,
|
||||
"args": []
|
||||
}
|
||||
# 获取工具参数信息
|
||||
try:
|
||||
if hasattr(tool_obj, 'args_schema') and tool_obj.args_schema:
|
||||
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', '')
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to extract args schema for tool {tool_id}: {e}")
|
||||
|
||||
# 获取工具参数信息
|
||||
if hasattr(tool_obj, 'args_schema') and tool_obj.args_schema:
|
||||
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
|
||||
tools_info[tool_id] = info
|
||||
logger.debug(f"Successfully processed tool info for {tool_id}")
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
def calculator(a: float, b: float, operation: str) -> float:
|
||||
"""Calculate two numbers. operation: add, subtract, multiply, divide"""
|
||||
if operation == "add":
|
||||
return a + b
|
||||
elif operation == "subtract":
|
||||
return a - b
|
||||
elif operation == "multiply":
|
||||
return a * b
|
||||
elif operation == "divide":
|
||||
return a / b
|
||||
else:
|
||||
raise ValueError(f"Invalid operation: {operation}, only support add, subtract, multiply, divide")
|
||||
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
|
||||
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."""
|
||||
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)}"
|
||||
|
||||
# 更新工具注册表
|
||||
_TOOLS_REGISTRY.update({
|
||||
"Calculator": calculator,
|
||||
"QueryKnowledgeGraph": query_knowledge_graph,
|
||||
})
|
||||
def get_static_tools() -> dict[str, Any]:
|
||||
"""注册静态工具"""
|
||||
static_tools = {
|
||||
"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
|
||||
|
||||
@ -894,7 +894,7 @@ watch(() => props.isOpen, (newVal) => {
|
||||
&:focus-within {
|
||||
border-color: var(--main-color);
|
||||
box-shadow: 0 0 0 2px rgba(var(--main-color-rgb), 0.1);
|
||||
|
||||
|
||||
.search-icon {
|
||||
color: var(--main-color);
|
||||
}
|
||||
@ -907,13 +907,19 @@ watch(() => props.isOpen, (newVal) => {
|
||||
}
|
||||
|
||||
.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;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
background: white;
|
||||
padding: 4px;
|
||||
|
||||
// 在小屏幕下调整为单列布局
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@ -943,38 +949,14 @@ watch(() => props.isOpen, (newVal) => {
|
||||
background: white;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
background: var(--gray-50);
|
||||
border-color: var(--gray-300);
|
||||
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-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
gap: 8px;
|
||||
|
||||
@ -1005,6 +987,25 @@ watch(() => props.isOpen, (newVal) => {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user