refactor: 重构知识库工具并集成新工具

- 新增 KnowledgeBaseMiddleware,提供通用知识库工具(list_kbs、get_mindmap、query_kb)
- 新增 debug/tools.py 调试工具集
- 重构 kbs/tools.py,分离通用工具与 MySQL 工具
- 移除旧的 toolkits registry 简化架构
- API 变更:get_databases_by_user → get_databases_by_user_id
- 前端拆分 KnowledgeBaseTool.vue 为多个专用工具组件
This commit is contained in:
Wenjie Zhang 2026-03-04 04:06:48 +08:00
parent b4ea2ab948
commit 76656ab95e
28 changed files with 677 additions and 588 deletions

View File

@ -77,7 +77,8 @@ class MyAgentContext(BaseContext):
```python
from src.agents.common import BaseContext, gen_tool_info
from src.agents.common.tools import get_buildin_tools
from src.agents.common.toolkits.buildin import calculator, query_knowledge_graph
from src.agents.common.toolkits.buildin.tools import _create_tavily_search
from src.agents.common.toolkits.mysql import get_mysql_tools
@dataclass(kw_only=True)
@ -86,7 +87,9 @@ class ReporterContext(BaseContext):
default_factory=lambda: [t.name for t in get_mysql_tools()],
metadata={
"name": "工具",
"options": lambda: gen_tool_info(get_buildin_tools() + get_mysql_tools()),
"options": lambda: gen_tool_info(
[calculator, query_knowledge_graph, _create_tavily_search()] + get_mysql_tools()
),
"description": "包含内置工具和 MySQL 工具包。",
},
)

View File

@ -551,8 +551,7 @@ async def save_agent_config(
if "knowledges" in config and config["knowledges"]:
# 获取用户有权访问的知识库名称
try:
user_info = {"role": current_user.role, "department_id": current_user.department_id}
accessible_databases = await knowledge_base.get_databases_by_user(user_info)
accessible_databases = await knowledge_base.get_databases_by_user_id(current_user.user_id)
accessible_kb_names = {
db.get("name") for db in accessible_databases.get("databases", []) if db.get("name")
}

View File

@ -89,8 +89,7 @@ async def _ensure_database_not_dify(db_id: str, operation: str) -> None:
async def get_databases(current_user: User = Depends(get_admin_user)):
"""获取所有知识库(根据用户权限过滤)"""
try:
user_info = {"role": current_user.role, "department_id": current_user.department_id}
return await knowledge_base.get_databases_by_user(user_info)
return await knowledge_base.get_databases_by_user_id(current_user.user_id)
except Exception as e:
logger.error(f"获取数据库列表失败 {e}, {traceback.format_exc()}")
return {"message": f"获取数据库列表失败 {e}", "databases": []}
@ -180,8 +179,7 @@ async def create_database(
async def get_accessible_databases(current_user: User = Depends(get_required_user)):
"""获取当前用户有权访问的知识库列表(用于智能体配置)"""
try:
user_info = {"role": current_user.role, "department_id": current_user.department_id}
databases = await knowledge_base.get_databases_by_user(user_info)
databases = await knowledge_base.get_databases_by_user_id(current_user.user_id)
accessible = [
{
@ -664,10 +662,10 @@ async def batch_delete_documents(db_id: str, file_ids: list[str] = Body(...), cu
"""批量删除文档或文件夹"""
logger.debug(f"BATCH DELETE documents {file_ids} in {db_id}")
await _ensure_database_not_dify(db_id, "批量文档删除")
deleted_count = 0
failed_items = []
for doc_id in file_ids:
try:
file_meta_info = await knowledge_base.get_file_basic_info(db_id, doc_id)
@ -700,7 +698,7 @@ async def batch_delete_documents(db_id: str, file_ids: list[str] = Body(...), cu
if deleted_count == 0:
raise HTTPException(status_code=400, detail=f"批量删除失败: 所有 {len(failed_items)} 个文件均未删除。")
return {
"message": f"部分删除成功: 已删除 {deleted_count} 个文件,失败 {len(failed_items)}",
"message": f"部分删除成功: 已删除 {deleted_count} 个文件,失败 {len(failed_items)}",
"deleted_count": deleted_count,
"failed_items": failed_items
}

View File

@ -286,8 +286,7 @@ async def get_databases_overview(current_user: User = Depends(get_admin_user)):
知识库列表
"""
try:
user_info = {"role": current_user.role, "department_id": current_user.department_id}
databases = await knowledge_base.get_databases_by_user(user_info)
databases = await knowledge_base.get_databases_by_user_id(current_user.user_id)
# databases["databases"] 是一个列表,每个元素已经包含了基本信息
db_list_raw = databases.get("databases", [])

View File

@ -12,6 +12,7 @@ from src.agents.common.middlewares import (
RuntimeConfigMiddleware,
save_attachments_to_fs,
)
from src.agents.common.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
from src.services.mcp_service import get_tools_from_all_servers
@ -43,7 +44,8 @@ class ChatbotAgent(BaseAgent):
middleware=[
save_attachments_to_fs, # 附件注入提示词
FilesystemMiddleware(backend=_create_fs_backend), # 文件系统后端
RuntimeConfigMiddleware(extra_tools=all_mcp_tools), # 运行时配置应用(模型/工具/知识库/MCP/提示词)
KnowledgeBaseMiddleware(), # 知识库工具
RuntimeConfigMiddleware(extra_tools=all_mcp_tools), # 运行时配置应用(模型/工具/MCP/提示词)
ModelRetryMiddleware(), # 模型重试中间件
TodoListMiddleware(),
PatchToolCallsMiddleware(),

View File

@ -7,7 +7,6 @@ from src.agents.common.models import load_chat_model
from src.agents.common.state import BaseState
# Tools - 核心工具函数
from src.agents.common.toolkits.buildin import get_buildin_tools
from src.agents.common.toolkits.utils import gen_tool_info
# MCP - Agent 层统一入口(自动过滤 disabled_tools
@ -21,7 +20,6 @@ __all__ = [
# Model utilities
"load_chat_model",
# Core tools
"get_buildin_tools",
"gen_tool_info",
# Core MCP
"get_enabled_mcp_tools",

View File

@ -12,9 +12,6 @@ from src import config as sys_config
from src.services.mcp_service import get_mcp_server_names
from src.utils import logger
from .toolkits.buildin import get_buildin_tools
from .toolkits.utils import gen_tool_info
@dataclass(kw_only=True)
class BaseContext:
@ -66,7 +63,6 @@ class BaseContext:
default_factory=list,
metadata={
"name": "工具",
"options": lambda: gen_tool_info(get_buildin_tools()),
"description": "内置的工具。",
},
)

View File

@ -0,0 +1,26 @@
"""知识库中间件 - 提供通用知识库工具"""
from collections.abc import Callable
from typing import Any
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain.tools.tool_node import ToolCallRequest
from src.agents.common.toolkits.kbs import get_common_kb_tools
from src.utils.logging_config import logger
class KnowledgeBaseMiddleware(AgentMiddleware):
"""知识库中间件 - 提供通用知识库工具
提供 3 个通用工具
- list_kbs: 列出用户可访问的知识库
- get_mindmap: 获取指定知识库的思维导图
- query_kb: 在指定知识库中检索
"""
def __init__(self):
super().__init__()
# 预加载通用知识库工具
self.kb_tools = get_common_kb_tools()
self.tools = self.kb_tools
logger.debug(f"Initialized KnowledgeBaseMiddleware with {len(self.kb_tools)} tools")

View File

@ -12,8 +12,7 @@ from langchain_core.messages import SystemMessage, ToolMessage
from langgraph.types import Command
from src.agents.common import load_chat_model
from src.agents.common.toolkits.buildin import get_buildin_tools
from src.agents.common.toolkits.kbs import get_kb_based_tools
from src.agents.common.toolkits import get_all_tool_instances
from src.services.mcp_service import get_enabled_mcp_tools
from src.services.skill_resolver import (
SkillSessionSnapshot,
@ -48,10 +47,9 @@ class RuntimeConfigState(AgentState):
class RuntimeConfigMiddleware(AgentMiddleware):
"""运行时配置中间件 - 应用模型/工具/知识库/MCP/提示词配置
"""运行时配置中间件 - 应用模型/工具/MCP/提示词配置
注意所有可能用到的知识库工具必须在初始化时预加载并注册到 self.tools
运行时根据配置从 self.tools 中筛选工具不能动态添加新工具
知识库工具已移至独立的 KnowledgeBaseMiddleware
支持自定义上下文字段名称以便在不同场景如主智能体/子智能体使用不同的配置字段
"""
@ -106,10 +104,10 @@ class RuntimeConfigMiddleware(AgentMiddleware):
self.tools: list[Any] = []
# 预加载工具列表(仅当启用工具覆盖时)
# 注意:知识库工具已移至独立的 KnowledgeBaseMiddleware
if self.enable_tools_override:
self.kb_tools = get_kb_based_tools()
self.buildin_tools = get_buildin_tools()
self.tools = self.kb_tools + self.buildin_tools + (extra_tools or [])
self.base_tools = get_all_tool_instances()
self.tools = self.base_tools + (extra_tools or [])
elif extra_tools:
logger.warning(
"RuntimeConfigMiddleware: extra_tools 参数已提供,但 enable_tools_override=False"
@ -239,13 +237,7 @@ class RuntimeConfigMiddleware(AgentMiddleware):
continue
logger.warning(f"RuntimeConfigMiddleware: tool dependency not found, skip: {tool_name}")
# 2. 知识库工具
knowledges = getattr(context, self.knowledges_context_name, None)
if knowledges:
kb_tools = get_kb_based_tools(db_names=knowledges)
selected_tools.extend(kb_tools)
# 3. MCP 工具(使用统一入口,自动过滤 disabled_tools
# 2. MCP 工具(使用统一入口,自动过滤 disabled_tools
mcps = getattr(context, self.mcps_context_name, None) or []
all_mcp_names: list[str] = []
for server_name in mcps:

View File

@ -4,51 +4,23 @@ from .registry import (
get_all_extra_metadata,
get_all_tool_instances,
get_extra_metadata,
register_tool,
tool,
)
# 工具获取函数
from .buildin import get_buildin_tools
from .kbs import get_kb_based_tools
from src.services.mcp_service import get_enabled_mcp_tools
from .kbs import get_common_kb_tools
async def get_tools_from_context(context, extra_tools=None):
"""从上下文配置中获取工具列表"""
# 1. 基础工具 (从 context.tools 中筛选)
all_basic_tools = get_buildin_tools() + (extra_tools or [])
selected_tools = []
if context.tools:
# 创建工具映射表
tools_map = {t.name: t for t in all_basic_tools}
for tool_name in context.tools:
if tool_name in tools_map:
selected_tools.append(tools_map[tool_name])
# 2. 知识库工具
if context.knowledges:
kb_tools = get_kb_based_tools(db_names=context.knowledges)
selected_tools.extend(kb_tools)
# 3. MCP 工具(使用统一入口,自动过滤 disabled_tools
if context.mcps:
for server_name in context.mcps:
mcp_tools = await get_enabled_mcp_tools(server_name)
selected_tools.extend(mcp_tools)
return selected_tools
# 触发各模块的 @tool 装饰器执行,自动注册工具
from . import buildin
from . import debug
from . import mysql
__all__ = [
"register_tool",
"get_extra_metadata",
"get_all_extra_metadata",
"get_all_tool_instances",
"ToolExtraMetadata",
"tool",
"get_buildin_tools",
"get_kb_based_tools",
"get_tools_from_context",
"get_common_kb_tools",
]

View File

@ -1,7 +1,7 @@
# buildin 工具包
from .tools import get_buildin_tools, query_knowledge_graph
from .tools import calculator, query_knowledge_graph
__all__ = [
"get_buildin_tools",
"calculator",
"query_knowledge_graph",
]

View File

@ -1,33 +1,48 @@
import os
import traceback
import uuid
from typing import Annotated, Any
import requests
from langgraph.types import interrupt
from src import config, graph_base
from src.agents.common.toolkits import tool
from src.storage.minio import aupload_file_to_minio
from src.agents.common.toolkits.registry import _all_tool_instances, _extra_registry, ToolExtraMetadata
from src.utils import logger
# Lazy initialization for TavilySearch (only when TAVILY_API_KEY is available)
# Lazy initialization for TavilySearch (only when API key is available)
_tavily_search_instance = None
def get_tavily_search():
"""Get TavilySearch instance lazily, only when API key is available."""
def _create_tavily_search():
"""Create and register TavilySearch tool with metadata."""
global _tavily_search_instance
if _tavily_search_instance is None and config.enable_web_search:
if _tavily_search_instance is None:
from langchain_tavily import TavilySearch
_tavily_search_instance = TavilySearch()
_tavily_search_instance.metadata = {"name": "Tavily 网页搜索", "category": "buildin", "tags": ["搜索"]}
# 即使没有配置 API_KEY 也返回实例,调用时会出错
return _tavily_search_instance
# 注册 TavilySearch 工具(延迟初始化)
def _register_tavily_tool():
"""Register TavilySearch tool with extra metadata."""
tavily_instance = _create_tavily_search()
# 手动注册到全局注册表
_extra_registry["tavily_search"] = ToolExtraMetadata(
category="buildin",
tags=["搜索"],
display_name="Tavily 网页搜索",
)
# 添加到工具实例列表
_all_tool_instances.append(tavily_instance)
# 模块加载时注册
if config.enable_web_search:
try:
_register_tavily_tool()
except Exception as e:
logger.warning(f"Failed to register TavilySearch tool: {e}")
@tool(category="buildin", tags=["计算"], display_name="计算器")
def calculator(a: float, b: float, operation: str) -> float:
"""计算器对给定的2个数字进行基本数学运算"""
@ -49,81 +64,6 @@ def calculator(a: float, b: float, operation: str) -> float:
raise
@tool(category="buildin", tags=["图片", "测试"], display_name="文生图测试")
async def text_to_img_demo(text: str) -> str:
"""【测试用】使用模型生成图片, 会返回图片的URL"""
url = "https://api.siliconflow.cn/v1/images/generations"
payload = {
"model": "Qwen/Qwen-Image",
"prompt": text,
}
headers = {"Authorization": f"Bearer {os.getenv('SILICONFLOW_API_KEY')}", "Content-Type": "application/json"}
try:
response = requests.post(url, json=payload, headers=headers)
response_json = response.json()
except Exception as e:
logger.error(f"Failed to generate image with: {e}")
raise ValueError(f"Image generation failed: {e}")
try:
image_url = response_json["images"][0]["url"]
except (KeyError, IndexError, TypeError) as e:
logger.error(f"Failed to parse image URL from response: {e}, {response_json=}")
raise ValueError(f"Image URL extraction failed: {e}")
# 2. Upload to MinIO (Simplified)
response = requests.get(image_url)
file_data = response.content
file_name = f"{uuid.uuid4()}.jpg"
image_url = await aupload_file_to_minio(
bucket_name="generated-images", file_name=file_name, data=file_data, file_extension="jpg"
)
logger.info(f"Image uploaded. URL: {image_url}")
return image_url
@tool(category="debug", tags=["内置", "审批"], display_name="人工审批")
def get_approved_user_goal(
operation_description: str,
) -> dict:
"""
请求人工审批在执行重要操作前获得人类确认
Args:
operation_description: 需要审批的操作描述例如 "调用知识库工具"
Returns:
dict: 包含审批结果的字典格式为 {"approved": bool, "message": str}
"""
# 构建详细的中断信息
interrupt_info = {
"question": "是否批准以下操作?",
"operation": operation_description,
}
# 触发人工审批
is_approved = interrupt(interrupt_info)
# 返回审批结果
if is_approved:
result = {
"approved": True,
"message": f"✅ 操作已批准:{operation_description}",
}
print(f"✅ 人工审批通过: {operation_description}")
else:
result = {
"approved": False,
"message": f"❌ 操作被拒绝:{operation_description}",
}
print(f"❌ 人工审批被拒绝: {operation_description}")
return result
KG_QUERY_DESCRIPTION = """
使用这个工具可以查询知识图谱中包含的三元组信息
关键词query使用可能帮助回答这个问题的关键词进行查询不要直接使用用户的原始输入去查询
@ -144,18 +84,3 @@ def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge
except Exception as e:
logger.error(f"Knowledge graph query error: {e}, {traceback.format_exc()}")
return f"知识图谱查询失败: {str(e)}"
def get_buildin_tools() -> list:
"""获取内置工具列表"""
static_tools = [
query_knowledge_graph,
get_approved_user_goal,
calculator,
text_to_img_demo,
]
# 始终添加 tavily_search无论是否配置 API_KEY调用时会出错
static_tools.append(get_tavily_search())
return static_tools

View File

@ -0,0 +1,7 @@
# debug 工具包
from .tools import get_approved_user_goal, text_to_img_demo
__all__ = [
"get_approved_user_goal",
"text_to_img_demo",
]

View File

@ -0,0 +1,86 @@
import os
import uuid
from typing import Any
import requests
from langgraph.types import interrupt
from src import config
from src.agents.common.toolkits import tool
from src.storage.minio import aupload_file_to_minio
from src.utils import logger
@tool(category="debug", tags=["图片", "测试"], display_name="文生图测试")
async def text_to_img_demo(text: str) -> str:
"""【测试用】使用模型生成图片, 会返回图片的URL"""
url = "https://api.siliconflow.cn/v1/images/generations"
payload = {
"model": "Qwen/Qwen-Image",
"prompt": text,
}
headers = {"Authorization": f"Bearer {os.getenv('SILICONFLOW_API_KEY')}", "Content-Type": "application/json"}
try:
response = requests.post(url, json=payload, headers=headers)
response_json = response.json()
except Exception as e:
logger.error(f"Failed to generate image with: {e}")
raise ValueError(f"Image generation failed: {e}")
try:
image_url = response_json["images"][0]["url"]
except (KeyError, IndexError, TypeError) as e:
logger.error(f"Failed to parse image URL from response: {e}, {response_json=}")
raise ValueError(f"Image URL extraction failed: {e}")
# 2. Upload to MinIO (Simplified)
response = requests.get(image_url)
file_data = response.content
file_name = f"{uuid.uuid4()}.jpg"
image_url = await aupload_file_to_minio(
bucket_name="generated-images", file_name=file_name, data=file_data, file_extension="jpg"
)
logger.info(f"Image uploaded. URL: {image_url}")
return image_url
@tool(category="debug", tags=["内置", "审批"], display_name="人工审批")
def get_approved_user_goal(
operation_description: str,
) -> dict:
"""
请求人工审批在执行重要操作前获得人类确认
Args:
operation_description: 需要审批的操作描述例如 "调用知识库工具"
Returns:
dict: 包含审批结果的字典格式为 {"approved": bool, "message": str}
"""
# 构建详细的中断信息
interrupt_info = {
"question": "是否批准以下操作?",
"operation": operation_description,
}
# 触发人工审批
is_approved = interrupt(interrupt_info)
# 返回审批结果
if is_approved:
result = {
"approved": True,
"message": f"✅ 操作已批准:{operation_description}",
}
print(f"✅ 人工审批通过: {operation_description}")
else:
result = {
"approved": False,
"message": f"❌ 操作被拒绝:{operation_description}",
}
print(f"❌ 人工审批被拒绝: {operation_description}")
return result

View File

@ -1,3 +1,5 @@
from .tools import get_kb_based_tools
from .tools import (
get_common_kb_tools,
)
__all__ = ["get_kb_based_tools"]
__all__ = ["get_common_kb_tools"]

View File

@ -1,159 +1,224 @@
"""知识库工具模块"""
import inspect
import traceback
from typing import Any
from langchain_core.tools import StructuredTool
from langchain_core.tools import tool
from langgraph.prebuilt.tool_node import ToolRuntime
from pydantic import BaseModel, Field
from src import knowledge_base
from src.utils import logger
class KnowledgeRetrieverModel(BaseModel):
query_text: str | None = Field(
default=None,
# ========== 通用知识库工具函数 ==========
class ListKBsInput(BaseModel):
"""列出用户可访问的知识库输入模型"""
# Langchain 的 runtime 注入机制要求必须有参数
dummy: str = Field(default="", description="Dummy parameter - ignore") # Add this
@tool(args_schema=ListKBsInput)
async def list_kbs(dummy: str, runtime: ToolRuntime) -> str: # Now has 2 params
"""列出当前用户可访问的知识库列表
返回用户基于权限可访问的知识库名称列表这个列表是根据用户的角色和部门信息过滤后的结果
但不包括用户在当前对话中未启用的知识库
Returns:
用户可访问的知识库名称列表字符串格式
"""
# 从 runtime.context 获取用户信息
runtime_context = runtime.context
user_id = getattr(runtime_context, "user_id", None)
if not user_id:
return "无法获取用户信息"
# 打印 runtime—context 中的所有信息以进行调试
logger.debug(f"Runtime context: {runtime_context.__dict__}")
# 获取用户在当前对话中启用的知识库列表
enabled_kb_names = getattr(runtime_context, "knowledges", []) or []
# 获取用户可访问的知识库列表(包含名称和描述)
try:
result = await knowledge_base.get_databases_by_raw_id(user_id)
all_kbs = result.get("databases", [])
except Exception as e:
logger.error(f"获取用户知识库列表失败: {e}")
return f"获取知识库列表失败: {str(e)}"
all_kb_names = [kb["name"] for kb in all_kbs]
logger.debug(f"用户 {user_id} 可访问的知识库列表: {all_kb_names}")
logger.debug(f"用户 {user_id} 当前对话启用的知识库列表: {enabled_kb_names}")
# 与启用的知识库取交集
available_kbs = [kb for kb in all_kbs if kb["name"] in enabled_kb_names]
if not available_kbs:
return "当前没有可访问的知识库"
# 格式化输出(包含名称和描述)
kb_list = []
for kb in available_kbs:
name = kb.get("name", "")
desc = kb.get("description") or "无描述"
kb_list.append({"name": name, "description": desc})
return kb_list
class GetMindmapInput(BaseModel):
"""获取思维导图输入模型"""
kb_name: str = Field(
description="知识库名称,用于指定要获取思维导图的知识库"
)
@tool(args_schema=GetMindmapInput)
async def get_mindmap(kb_name: str, runtime: ToolRuntime) -> str:
"""获取指定知识库的思维导图结构
当用户想要了解知识库的整体结构文件分类知识架构时使用此工具
返回知识库的思维导图层级结构
Args:
kb_name: 知识库名称
Returns:
知识库的思维导图结构文本格式
"""
if not kb_name:
return "请提供知识库名称"
# 获取所有检索器
retrievers = knowledge_base.get_retrievers()
# 查找对应的知识库
target_db_id = None
target_info = None
for db_id, info in retrievers.items():
if info["name"] == kb_name:
target_db_id = db_id
target_info = info
break
if not target_db_id:
return f"知识库 '{kb_name}' 不存在"
try:
from src.repositories.knowledge_base_repository import KnowledgeBaseRepository
kb_repo = KnowledgeBaseRepository()
kb = await kb_repo.get_by_id(target_db_id)
if kb is None:
return f"知识库 {target_info['name']} 不存在"
mindmap_data = kb.mindmap
if not mindmap_data:
return f"知识库 {target_info['name']} 还没有生成思维导图。"
# 将思维导图数据转换为文本格式
def mindmap_to_text(node, level=0):
"""递归将思维导图JSON转换为层级文本"""
indent = " " * level
text = f"{indent}- {node.get('content', '')}\n"
for child in node.get("children", []):
text += mindmap_to_text(child, level + 1)
return text
mindmap_text = f"知识库 {target_info['name']} 的思维导图结构:\n\n"
mindmap_text += mindmap_to_text(mindmap_data)
return mindmap_text
except Exception as e:
logger.error(f"获取思维导图失败: {e}")
return f"获取思维导图失败: {str(e)}"
class QueryKBInput(BaseModel):
"""知识库检索输入模型"""
kb_name: str = Field(
description="知识库名称,用于指定要在哪个知识库中检索"
)
query_text: str = Field(
description=(
"查询的关键词,查询的时候,应该尽量以可能帮助回答这个问题的关键词进行查询,不要直接使用用户的原始输入去查询。"
"查询的关键词,查询的时候,应该尽量以可能帮助回答这个问题的关键词进行查询,"
"不要直接使用用户的原始输入去查询。"
)
)
operation: str = Field(
default="search",
description=(
"操作类型:'search' 表示检索知识库内容,'get_mindmap' 表示获取知识库的思维导图结构。"
"当用户询问知识库的整体结构、文件分类、知识架构时,使用 'get_mindmap'"
"当用户需要查询具体内容时,使用 'search'"
),
)
class CommonKnowledgeRetriever(KnowledgeRetrieverModel):
"""Common knowledge retriever model."""
file_name: str | None = Field(
default=None,
description=(
"(非必要不启用此参数,留空即可)当操作类型为 'search' 且已经读取思维导图之后,可以指定文件关键词,支持模糊匹配。\n"
"(非必要不启用此参数,留空即可)当已经读取思维导图之后,可以指定文件关键词,支持模糊匹配。\n"
"仅当检索结果过多且不相关,需要进一步缩小范围时使用。"
)
)
def get_kb_based_tools(db_names: list[str] | None = None) -> list:
"""获取所有知识库基于的工具"""
# 获取所有知识库
kb_tools = []
@tool(args_schema=QueryKBInput)
async def query_kb(kb_name: str, query_text: str, file_name: str | None = None, runtime: ToolRuntime = None) -> Any:
"""在指定知识库中检索内容
当用户需要查询具体内容时使用此工具根据关键词在知识库中检索相关文档片段
Args:
kb_name: 知识库名称
query_text: 查询的关键词
file_name: 可选文件名称过滤
Returns:
检索结果
"""
if not kb_name:
return "请提供知识库名称"
if not query_text:
return "请提供查询内容"
# 获取所有检索器
retrievers = knowledge_base.get_retrievers()
if db_names is None:
db_ids = None
else:
db_ids = [kb_id for kb_id, kb in retrievers.items() if kb["name"] in db_names]
def _create_retriever_wrapper(db_id: str, retriever_info: dict[str, Any]):
"""创建检索器包装函数的工厂函数,避免闭包变量捕获问题"""
# 查找对应的知识库
target_info = None
for db_id, info in retrievers.items():
if info["name"] == kb_name:
target_info = info
break
async def async_retriever_wrapper(
query_text: str, operation: str = "search", file_name: str | None = None
) -> Any:
"""异步检索器包装函数,支持检索和获取思维导图"""
if not target_info:
return f"知识库 '{kb_name}' 不存在"
# 获取思维导图
if operation == "get_mindmap":
try:
logger.debug(f"Getting mindmap for database {db_id}")
try:
retriever = target_info["retriever"]
kwargs = {}
if file_name:
kwargs["file_name"] = file_name
from src.repositories.knowledge_base_repository import KnowledgeBaseRepository
if inspect.iscoroutinefunction(retriever):
result = await retriever(query_text, **kwargs)
else:
result = retriever(query_text, **kwargs)
kb_repo = KnowledgeBaseRepository()
kb = await kb_repo.get_by_id(db_id)
return result
if kb is None:
return f"知识库 {retriever_info['name']} 不存在"
except Exception as e:
logger.error(f"检索失败: {e}")
return f"检索失败: {str(e)}"
mindmap_data = kb.mindmap
if not mindmap_data:
return f"知识库 {retriever_info['name']} 还没有生成思维导图。"
def get_common_kb_tools() -> list:
"""获取通用知识库工具列表
# 将思维导图数据转换为文本格式便于AI理解
def mindmap_to_text(node, level=0):
"""递归将思维导图JSON转换为层级文本"""
indent = " " * level
text = f"{indent}- {node.get('content', '')}\n"
for child in node.get("children", []):
text += mindmap_to_text(child, level + 1)
return text
mindmap_text = f"知识库 {retriever_info['name']} 的思维导图结构:\n\n"
mindmap_text += mindmap_to_text(mindmap_data)
logger.debug(f"Successfully retrieved mindmap for {db_id}")
return mindmap_text
except Exception as e:
logger.error(f"Error getting mindmap for {db_id}: {e}")
return f"获取思维导图失败: {str(e)}"
# 默认:检索知识库
retriever = retriever_info["retriever"]
try:
logger.debug(f"Retrieving from database {db_id} with query: {query_text}")
kwargs = {}
if file_name:
kwargs["file_name"] = file_name
if inspect.iscoroutinefunction(retriever):
result = await retriever(query_text, **kwargs)
else:
result = retriever(query_text, **kwargs)
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
for db_id, retrieve_info in retrievers.items():
if db_ids is not None and db_id not in db_ids:
continue
try:
# 构建工具描述
description = (
f"使用 {retrieve_info['name']} 知识库的多功能工具。\n"
f"知识库描述:{retrieve_info['description'] or '没有描述。'}\n\n"
f"支持的操作:\n"
f"1. 'search' - 检索知识库内容:根据关键词查询相关文档片段\n"
f"2. 'get_mindmap' - 获取思维导图:查看知识库的整体结构和文件分类\n\n"
f"使用建议:\n"
f"- 需要查询具体内容时,使用 operation='search'\n"
f"- 想了解知识库结构、文件分类时,使用 operation='get_mindmap'"
)
# 使用工厂函数创建检索器包装函数,避免闭包问题
retriever_wrapper = _create_retriever_wrapper(db_id, retrieve_info)
safename = retrieve_info["name"].replace(" ", "_")[:20]
args_schema = KnowledgeRetrieverModel
if retrieve_info["metadata"]["kb_type"] in ["milvus"]:
args_schema = CommonKnowledgeRetriever
# 使用 StructuredTool.from_function 创建异步工具
tool = StructuredTool.from_function(
coroutine=retriever_wrapper,
name=safename,
description=description,
args_schema=args_schema,
metadata=retrieve_info["metadata"] | {"tag": ["knowledgebase"]},
)
kb_tools.append(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}, \n{traceback.format_exc()}")
continue
return kb_tools
返回 3 个通用工具
- list_kbs: 列出用户可访问的知识库
- get_mindmap: 获取指定知识库的思维导图
- query_kb: 在指定知识库中检索
"""
return [list_kbs, get_mindmap, query_kb]

View File

@ -19,36 +19,6 @@ _extra_registry: dict[str, ToolExtraMetadata] = {}
_all_tool_instances: list = []
def register_tool(
category: str,
tags: list[str] = None,
display_name: str = "",
icon: str = "",
):
"""装饰器:注册工具附加元数据
使用方式:
@register_tool(category="buildin", tags=["内置"], display_name="计算器")
def calculator(...):
...
"""
def decorator(func_or_tool: Callable) -> Callable:
# 获取工具名称(优先用 metadata.name
tool_name = getattr(func_or_tool, "name", None) or getattr(
func_or_tool, "__name__", ""
)
_extra_registry[tool_name] = ToolExtraMetadata(
category=category,
tags=tags or [],
display_name=display_name,
icon=icon,
)
return func_or_tool
return decorator
def get_extra_metadata(tool_name: str) -> ToolExtraMetadata | None:
"""获取工具附加元数据"""
return _extra_registry.get(tool_name)

View File

@ -11,8 +11,9 @@ from langchain.agents.middleware import (
from src.agents.common import BaseAgent, load_chat_model
from src.agents.common.backends import create_agent_composite_backend
from src.agents.common.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
from src.agents.common.middlewares import RuntimeConfigMiddleware, SummaryOffloadMiddleware, save_attachments_to_fs
from src.agents.common.toolkits.buildin.tools import get_tavily_search
from src.agents.common.toolkits.buildin.tools import _create_tavily_search
from src.services.mcp_service import get_tools_from_all_servers
from src.utils import logger
@ -79,19 +80,16 @@ class DeepAgent(BaseAgent):
async def get_tools(self):
"""返回 Deep Agent 的专用工具"""
tools = []
tavily_search = get_tavily_search()
if tavily_search:
tools.append(tavily_search)
from src import config
tools = []
if config.enable_web_search:
tavily = _create_tavily_search()
if tavily:
tools.append(tavily)
# # Assert that search tool is available for DeepAgent
# assert tools, (
# "DeepAgent requires at least one search tool. "
# "Please configure TAVILY_API_KEY environment variable to enable web search."
# )
if not tools:
logger.warning("No search tools configured, DeepAgent will work without web search")
tools = []
return tools
async def get_graph(self, **kwargs):
@ -159,6 +157,7 @@ class DeepAgent(BaseAgent):
save_attachments_to_fs, # 附件注入提示词
TodoListMiddleware(),
PatchToolCallsMiddleware(),
KnowledgeBaseMiddleware(), # 知识库工具
subagents_middleware,
summary_middleware,
# 工具调用限制tavily_search 总调用最多 20 次

View File

@ -7,6 +7,7 @@ from src.knowledge.chunking.ragflow_like.presets import (
ensure_chunk_defaults_in_additional_params,
)
from src.knowledge.factory import KnowledgeBaseFactory
from src.storage.postgres.models_business import User
from src.utils import logger
from src.utils.datetime_utils import utc_isoformat
@ -243,19 +244,46 @@ class KnowledgeBaseManager:
return user_department_id in accessible_departments
async def get_databases_by_user(self, user: dict) -> dict:
"""根据用户权限获取知识库列表
async def get_databases_by_raw_id(self, user_id: int) -> dict:
"""根据用户ID获取知识库列表原始ID版本兼容旧接口"""
from src.repositories.user_repository import UserRepository
Args:
user: 用户信息字典包含 role department_id
# 通过数据库获取用户信息
user_repo = UserRepository()
user: User | None = await user_repo.get_by_id(id=int(user_id))
if not user:
logger.warning(f"User not found: {user_id}")
return {"databases": []}
return await self.get_databases_by_user(user)
async def get_databases_by_user_id(self, user_id: str) -> dict:
"""根据用户ID获取知识库列表字符串ID版本"""
from src.repositories.user_repository import UserRepository
# 通过数据库获取用户信息
user_repo = UserRepository()
user: User | None = await user_repo.get_by_user_id(user_id)
if not user:
logger.warning(f"User not found: {user_id}")
return {"databases": []}
return await self.get_databases_by_user(user)
async def get_databases_by_user(self, user: User) -> dict:
"""根据用户权限获取知识库列表"""
# 构建用户信息字典
user_info = {
"role": user.role,
"department_id": user.department_id,
}
logger.info(f"Getting databases for user {user.id} with role {user.role} and department {user.department_id}")
Returns:
过滤后的知识库列表
"""
all_databases = (await self.get_databases()).get("databases", [])
# 超级管理员可以看到所有知识库
if user.get("role") == "superadmin":
if user_info.get("role") == "superadmin":
return {"databases": all_databases}
filtered_databases = []
@ -265,7 +293,7 @@ class KnowledgeBaseManager:
if not db_id:
continue
if await self.check_accessible(user, db_id):
if await self.check_accessible(user_info, db_id):
filtered_databases.append(db)
return {"databases": filtered_databases}

View File

@ -379,28 +379,6 @@ async def stream_agent_chat(
# 先构建 langgraph_config
langgraph_config = {"configurable": {"thread_id": thread_id, "user_id": user_id}}
# 注意LangGraph 会自动从 checkpointer 恢复 state包括 attachments 和 files
# 无需手动加载或传递
# 根据用户权限过滤知识库
requested_knowledge_names = input_context["agent_config"].get("knowledges")
logger.info(f"Requesting knowledges: {requested_knowledge_names}")
if requested_knowledge_names and isinstance(requested_knowledge_names, list) and requested_knowledge_names:
user_info = {"role": "user", "department_id": department_id}
accessible_databases = await knowledge_base.get_databases_by_user(user_info)
accessible_kb_names = {
db.get("name")
for db in accessible_databases.get("databases", [])
if isinstance(db, dict) and db.get("name")
}
logger.info(f"Accessible knowledges: {accessible_kb_names}")
filtered_knowledge_names = [kb for kb in requested_knowledge_names if kb in accessible_kb_names]
blocked_knowledge_names = [kb for kb in requested_knowledge_names if kb not in accessible_kb_names]
if blocked_knowledge_names:
logger.warning(f"用户 {user_id} 无权访问知识库: {blocked_knowledge_names}, 已自动过滤")
input_context["agent_config"]["knowledges"] = filtered_knowledge_names
full_msg = None
accumulated_content = []
async for msg, metadata in agent.stream_messages(messages, input_context=input_context):

View File

@ -78,13 +78,6 @@ def validate_skill_slug(slug: str) -> str:
raise ValueError("无效 skill slug")
return normalized
def _get_buildin_tool_names() -> list[str]:
from src.agents.common.toolkits.buildin import get_buildin_tools
return [tool.name for tool in get_buildin_tools()]
def get_skills_root_dir() -> Path:
root = Path(sys_config.save_dir) / "skills"
root.mkdir(parents=True, exist_ok=True)

View File

@ -242,11 +242,6 @@ async def test_awrap_tool_call_denies_invisible_skill():
@pytest.mark.asyncio
async def test_model_call_injects_dependency_tools_and_mcps_after_activation(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
runtime_middleware,
"get_buildin_tools",
lambda: [_FakeTool(name="calculator"), _FakeTool(name="dep-tool")],
)
monkeypatch.setattr(runtime_middleware, "get_kb_based_tools", lambda db_names=None: [])
snapshot = _build_snapshot(

View File

@ -5,11 +5,17 @@
<!-- 网页搜索 -->
<WebSearchTool v-else-if="isWebSearchResult" :tool-call="toolCall" />
<!-- 知识库 -->
<KnowledgeBaseTool v-else-if="isKnowledgeBaseResult" :tool-call="toolCall" />
<!-- 知识库列表 -->
<ListKbsTool v-else-if="toolName === 'list_kbs'" :tool-call="toolCall" />
<!-- 思维导图 -->
<GetMindmapTool v-else-if="toolName === 'get_mindmap'" :tool-call="toolCall" />
<!-- 知识库检索 -->
<QueryKbTool v-else-if="toolName === 'query_kb'" :tool-call="toolCall" />
<!-- 待办事项 -->
<TodoListTool v-else-if="isTodoListResult" :tool-call="toolCall" />
<TodoListTool v-else-if="toolName === 'write_todos'" :tool-call="toolCall" />
<!-- 计算器 -->
<CalculatorTool v-else-if="isCalculatorResult" :tool-call="toolCall" />
@ -21,31 +27,31 @@
<TaskTool v-else-if="isTaskResult" :tool-call="toolCall" />
<!-- 写文件 -->
<WriteFileTool v-else-if="isWriteFileResult" :tool-call="toolCall" />
<WriteFileTool v-else-if="toolName === 'write_file'" :tool-call="toolCall" />
<!-- 读文件 -->
<ReadFileTool v-else-if="isReadFileResult" :tool-call="toolCall" />
<ReadFileTool v-else-if="toolName === 'read_file'" :tool-call="toolCall" />
<!-- 列目录 -->
<ListDirectoryTool v-else-if="isListDirectoryResult" :tool-call="toolCall" />
<ListDirectoryTool v-else-if="toolName === 'list_directory' || toolName === 'ls'" :tool-call="toolCall" />
<!-- 搜索文件内容 -->
<SearchFileContentTool v-else-if="isSearchFileContentResult" :tool-call="toolCall" />
<SearchFileContentTool v-else-if="toolName === 'search_file_content'" :tool-call="toolCall" />
<!-- Glob 搜索 -->
<GlobTool v-else-if="isGlobResult" :tool-call="toolCall" />
<GlobTool v-else-if="toolName === 'glob'" :tool-call="toolCall" />
<!-- 编辑文件 -->
<EditFileTool v-else-if="isEditFileResult" :tool-call="toolCall" />
<EditFileTool v-else-if="toolName === 'edit_file' || toolName === 'replace'" :tool-call="toolCall" />
<!-- MySQL 查询 -->
<MysqlQueryTool v-else-if="isMysqlQueryResult" :tool-call="toolCall" />
<MysqlQueryTool v-else-if="toolName === 'mysql_query'" :tool-call="toolCall" />
<!-- MySQL 描述表 -->
<MysqlDescribeTableTool v-else-if="isMysqlDescribeTableResult" :tool-call="toolCall" />
<MysqlDescribeTableTool v-else-if="toolName === 'mysql_describe_table'" :tool-call="toolCall" />
<!-- MySQL 列出表 -->
<MysqlListTablesTool v-else-if="isMysqlListTablesResult" :tool-call="toolCall" />
<MysqlListTablesTool v-else-if="toolName === 'mysql_list_tables'" :tool-call="toolCall" />
<!-- 默认展示 -->
<BaseToolCall v-else :tool-call="toolCall" />
@ -54,11 +60,11 @@
<script setup>
import { computed, ref } from 'vue'
import BaseToolCall from './BaseToolCall.vue'
import { useAgentStore } from '@/stores/agent'
import { useDatabaseStore } from '@/stores/database'
import WebSearchTool from './tools/WebSearchTool.vue'
import KnowledgeBaseTool from './tools/KnowledgeBaseTool.vue'
import ListKbsTool from './tools/ListKbsTool.vue'
import GetMindmapTool from './tools/GetMindmapTool.vue'
import QueryKbTool from './tools/QueryKbTool.vue'
import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue'
import CalculatorTool from './tools/CalculatorTool.vue'
import TodoListTool from './tools/TodoListTool.vue'
@ -81,17 +87,7 @@ const props = defineProps({
}
})
const agentStore = useAgentStore()
const databaseStore = useDatabaseStore()
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '')
const tool = computed(() => {
const toolsList = agentStore?.availableTools ? Object.values(agentStore.availableTools) : []
const tool = toolsList.find((t) => t.name === toolName.value)
return tool || null
})
const databases = computed(() => databaseStore.databases || [])
const parseData = (content) => {
if (typeof content === 'string') {
@ -122,14 +118,6 @@ const isTaskResult = computed(() => {
return args && typeof args === 'object' && 'subagent_type' in args
})
const isKnowledgeBaseResult = computed(() => {
const databaseInfo = databases.value.find((db) => db.name === toolName.value)
if (databaseInfo && databaseInfo.kb_type !== 'lightrag') {
return true
}
return false
})
const isKnowledgeGraphResult = computed(() => {
const name = toolName.value.toLowerCase()
const hasGraphKeyword = name.includes('graph') || name.includes('图谱') || name.includes('kg')
@ -142,10 +130,6 @@ const isKnowledgeGraphResult = computed(() => {
)
})
const isTodoListResult = computed(() => {
return toolName.value === 'write_todos'
})
const isCalculatorResult = computed(() => {
const name = toolName.value.toLowerCase()
return name.includes('calculator') || name.includes('calc') || name.includes('math')
@ -158,42 +142,6 @@ const isImageResult = computed(() => {
return data && typeof data === 'string' && data.startsWith('http')
})
const isWriteFileResult = computed(() => {
return toolName.value === 'write_file'
})
const isReadFileResult = computed(() => {
return toolName.value === 'read_file'
})
const isListDirectoryResult = computed(() => {
return toolName.value === 'list_directory' || toolName.value === 'ls'
})
const isSearchFileContentResult = computed(() => {
return toolName.value === 'search_file_content'
})
const isGlobResult = computed(() => {
return toolName.value === 'glob'
})
const isEditFileResult = computed(() => {
return toolName.value === 'edit_file' || toolName.value === 'replace'
})
const isMysqlQueryResult = computed(() => {
return toolName.value === 'mysql_query'
})
const isMysqlDescribeTableResult = computed(() => {
return toolName.value === 'mysql_describe_table'
})
const isMysqlListTablesResult = computed(() => {
return toolName.value === 'mysql_list_tables'
})
//
const graphToolCallRef = ref(null)
const refreshGraph = () => {

View File

@ -4,7 +4,9 @@ export { default as ToolCallRenderer } from './ToolCallRenderer.vue'
// 专用工具组件导出
export { default as WebSearchTool } from './tools/WebSearchTool.vue'
export { default as KnowledgeBaseTool } from './tools/KnowledgeBaseTool.vue'
export { default as ListKbsTool } from './tools/ListKbsTool.vue'
export { default as GetMindmapTool } from './tools/GetMindmapTool.vue'
export { default as QueryKbTool } from './tools/QueryKbTool.vue'
export { default as KnowledgeGraphTool } from './tools/KnowledgeGraphTool.vue'
export { default as CalculatorTool } from './tools/CalculatorTool.vue'
export { default as TodoListTool } from './tools/TodoListTool.vue'

View File

@ -0,0 +1,71 @@
<template>
<BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header>
<div class="sep-header">
<span class="note">{{ operationLabel }}</span>
<span class="separator" v-if="kbName">|</span>
<span class="description" v-if="kbName">知识库: {{ kbName }}</span>
</div>
</template>
<template #result="{ resultContent }">
<div class="get-mindmap-result">
<pre class="mindmap-content">{{ formatMindmapResult(resultContent) }}</pre>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const args = computed(() => {
const value = props.toolCall.args || props.toolCall.function?.arguments
if (!value) return {}
if (typeof value === 'object') return value
try {
return JSON.parse(value)
} catch {
return {}
}
})
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '思维导图')
const operationLabel = computed(() => toolName.value)
const kbName = computed(() => args.value.kb_name || '')
const formatMindmapResult = (content) => {
if (typeof content === 'string') return content
if (typeof content === 'object') return JSON.stringify(content, null, 2)
return String(content)
}
</script>
<style scoped lang="less">
.get-mindmap-result {
background: var(--gray-0);
border-radius: 8px;
padding: 12px 16px;
max-height: 300px;
overflow-y: auto;
.mindmap-content {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
}
</style>

View File

@ -1,127 +0,0 @@
<template>
<BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header>
<div class="sep-header">
<span class="note">{{ operationLabel }}</span>
<span class="separator" v-if="queryText">|</span>
<span class="description">{{ queryText }}</span>
<span class="separator" v-if="fileName">|</span>
<span class="description" v-if="fileName">文件: {{ fileName }}</span>
</div>
</template>
<template #result="{ resultContent }">
<div v-if="operation === 'get_mindmap'" class="knowledge-base-result">
<div class="mindmap-result">
<pre class="mindmap-content">{{ formatMindmapResult(resultContent) }}</pre>
</div>
</div>
<div v-else-if="isArrayResult(resultContent)" class="knowledge-base-result">
<KbResultGroupedList :chunks="parsedData(resultContent)" />
</div>
<div v-else class="knowledge-base-result">
<div class="plain-text-result">
<pre class="plain-text-content">{{ resultContent }}</pre>
</div>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
import KbResultGroupedList from '@/components/sources/KbResultGroupedList.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const args = computed(() => {
const value = props.toolCall.args || props.toolCall.function?.arguments
if (!value) return {}
if (typeof value === 'object') return value
try {
return JSON.parse(value)
} catch {
return {}
}
})
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '知识库')
const operation = computed(() => args.value.operation || 'search')
const operationLabel = computed(() => {
const labels = {
search: `${toolName.value} 搜索`,
get_mindmap: toolName.value
}
return labels[operation.value] || operation.value
})
const queryText = computed(() => args.value.query_text || '')
const fileName = computed(() => args.value.file_name || '')
const parseData = (content) => {
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return []
}
}
return content || []
}
const parsedData = (content) => parseData(content)
const isArrayResult = (content) => {
if (Array.isArray(content)) return true
if (typeof content === 'string') {
try {
return Array.isArray(JSON.parse(content))
} catch {
return false
}
}
return false
}
const formatMindmapResult = (content) => {
if (typeof content === 'string') return content
if (typeof content === 'object') return JSON.stringify(content, null, 2)
return String(content)
}
</script>
<style scoped lang="less">
.knowledge-base-result {
background: var(--gray-0);
border-radius: 8px;
.mindmap-result,
.plain-text-result {
padding: 12px 16px;
max-height: 300px;
overflow-y: auto;
}
.mindmap-content,
.plain-text-content {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
}
.mindmap-content {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header>
<div class="sep-header">
<span class="note">{{ operationLabel }}</span>
</div>
</template>
<template #result="{ resultContent }">
<div class="list-kbs-result">
<div class="kb-count"> {{ kbList.length }} 个知识库</div>
<div class="kb-list">
<div v-for="kb in kbList" :key="kb.name" class="kb-item">
<div class="kb-name">{{ kb.name }}</div>
<div class="kb-description">{{ kb.description || '无描述' }}</div>
</div>
</div>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '知识库')
const operationLabel = computed(() => `${toolName.value} 列表`)
const parseData = (content) => {
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return []
}
}
return content || []
}
const kbList = computed(() => {
const resultContent = props.toolCall.tool_call_result?.content
const data = parseData(resultContent)
return Array.isArray(data) ? data : []
})
</script>
<style scoped lang="less">
.list-kbs-result {
background: var(--gray-0);
border-radius: 8px;
padding: 12px 16px;
.kb-count {
font-size: 12px;
color: var(--gray-700);
margin-bottom: 12px;
}
.kb-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.kb-item {
padding: 10px 12px;
background: var(--gray-10);
border-radius: 6px;
border: 1px solid var(--gray-100);
.kb-name {
font-size: 13px;
font-weight: 500;
color: var(--gray-700);
margin-bottom: 4px;
}
.kb-description {
font-size: 12px;
color: var(--gray-600);
}
}
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<BaseToolCall :tool-call="toolCall" :hide-params="true">
<template #header>
<div class="sep-header">
<span class="note">{{ operationLabel }}</span>
<span class="separator" v-if="kbName">|</span>
<span class="description" v-if="kbName">知识库: {{ kbName }}</span>
<span class="separator" v-if="queryText">|</span>
<span class="description">{{ queryText }}</span>
</div>
</template>
<template #result="{ resultContent }">
<div class="query-kb-result">
<KbResultGroupedList :chunks="parsedData(resultContent)" />
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
import KbResultGroupedList from '@/components/sources/KbResultGroupedList.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const args = computed(() => {
const value = props.toolCall.args || props.toolCall.function?.arguments
if (!value) return {}
if (typeof value === 'object') return value
try {
return JSON.parse(value)
} catch {
return {}
}
})
const toolName = computed(() => props.toolCall.name || props.toolCall.function?.name || '知识库')
const operationLabel = computed(() => `${toolName.value} 搜索`)
const kbName = computed(() => args.value.kb_name || '')
const queryText = computed(() => args.value.query_text || '')
const parseData = (content) => {
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch {
return []
}
}
return content || []
}
const parsedData = (content) => parseData(content)
</script>
<style scoped lang="less">
.query-kb-result {
background: var(--gray-0);
border-radius: 8px;
padding: 4px;
}
</style>