2025-09-01 22:37:03 +08:00
|
|
|
|
import os
|
2025-03-17 19:58:00 +08:00
|
|
|
|
import traceback
|
2025-09-21 23:48:56 +08:00
|
|
|
|
from urllib.parse import quote, unquote
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, Request, UploadFile
|
2025-08-10 21:58:41 +08:00
|
|
|
|
from fastapi.responses import FileResponse
|
2025-09-21 23:48:56 +08:00
|
|
|
|
from starlette.responses import FileResponse as StarletteFileResponse
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
|
from server.models.user_model import User
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from server.utils.auth_middleware import get_admin_user
|
2025-09-02 01:08:42 +08:00
|
|
|
|
from src import config, knowledge_base
|
2025-09-01 22:37:03 +08:00
|
|
|
|
from src.knowledge.indexing import process_file_to_markdown
|
|
|
|
|
|
from src.utils import hashstr, logger
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
knowledge = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 数据库管理分组 ===
|
|
|
|
|
|
# =============================================================================
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.get("/databases")
|
2025-05-02 23:56:59 +08:00
|
|
|
|
async def get_databases(current_user: User = Depends(get_admin_user)):
|
2025-07-22 17:29:38 +08:00
|
|
|
|
"""获取所有知识库"""
|
2024-10-02 20:11:28 +08:00
|
|
|
|
try:
|
2025-03-20 19:51:46 +08:00
|
|
|
|
database = knowledge_base.get_databases()
|
2025-07-22 17:29:38 +08:00
|
|
|
|
return database
|
2024-10-02 20:11:28 +08:00
|
|
|
|
except Exception as e:
|
2025-03-20 19:51:46 +08:00
|
|
|
|
logger.error(f"获取数据库列表失败 {e}, {traceback.format_exc()}")
|
2024-10-02 20:11:28 +08:00
|
|
|
|
return {"message": f"获取数据库列表失败 {e}", "databases": []}
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.post("/databases")
|
|
|
|
|
|
async def create_database(
|
|
|
|
|
|
database_name: str = Body(...),
|
|
|
|
|
|
description: str = Body(...),
|
|
|
|
|
|
embed_model_name: str = Body(...),
|
|
|
|
|
|
kb_type: str = Body("lightrag"),
|
2025-07-26 03:36:54 +08:00
|
|
|
|
additional_params: dict = Body({}),
|
2025-08-16 20:07:12 +08:00
|
|
|
|
llm_info: dict = Body(None),
|
2025-09-01 22:37:03 +08:00
|
|
|
|
current_user: User = Depends(get_admin_user),
|
2025-07-22 17:29:38 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""创建知识库"""
|
2025-09-01 22:37:03 +08:00
|
|
|
|
logger.debug(
|
2025-09-02 01:08:42 +08:00
|
|
|
|
f"Create database {database_name} with kb_type {kb_type}, "
|
|
|
|
|
|
f"additional_params {additional_params}, llm_info {llm_info}"
|
2025-09-01 22:37:03 +08:00
|
|
|
|
)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
try:
|
|
|
|
|
|
embed_info = config.embed_model_names[embed_model_name]
|
2025-07-28 18:53:06 +08:00
|
|
|
|
database_info = await knowledge_base.create_database(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
database_name, description, kb_type=kb_type, embed_info=embed_info, llm_info=llm_info, **additional_params
|
2025-07-22 17:29:38 +08:00
|
|
|
|
)
|
2025-07-26 03:36:54 +08:00
|
|
|
|
|
|
|
|
|
|
# 需要重新加载所有智能体,因为工具刷新了
|
|
|
|
|
|
from src.agents import agent_manager
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
await agent_manager.reload_all()
|
|
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
return database_info
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"创建数据库失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"创建数据库失败 {e}", "status": "failed"}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.get("/databases/{db_id}")
|
|
|
|
|
|
async def get_database_info(db_id: str, current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""获取知识库详细信息"""
|
|
|
|
|
|
database = knowledge_base.get_database_info(db_id)
|
|
|
|
|
|
if database is None:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Database not found")
|
2024-10-02 20:11:28 +08:00
|
|
|
|
return database
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.put("/databases/{db_id}")
|
|
|
|
|
|
async def update_database_info(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
db_id: str, name: str = Body(...), description: str = Body(...), current_user: User = Depends(get_admin_user)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""更新知识库信息"""
|
|
|
|
|
|
logger.debug(f"Update database {db_id} info: {name}, {description}")
|
2025-07-21 18:18:47 +08:00
|
|
|
|
try:
|
2025-07-27 01:02:14 +08:00
|
|
|
|
database = await knowledge_base.update_database(db_id, name, description)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
return {"message": "更新成功", "database": database}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
except Exception as e:
|
2025-07-22 17:29:38 +08:00
|
|
|
|
logger.error(f"更新数据库失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"更新数据库失败: {e}")
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.delete("/databases/{db_id}")
|
|
|
|
|
|
async def delete_database(db_id: str, current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""删除知识库"""
|
|
|
|
|
|
logger.debug(f"Delete database {db_id}")
|
2025-07-21 18:18:47 +08:00
|
|
|
|
try:
|
2025-07-27 01:02:14 +08:00
|
|
|
|
await knowledge_base.delete_database(db_id)
|
2025-07-26 03:36:54 +08:00
|
|
|
|
|
|
|
|
|
|
# 需要重新加载所有智能体,因为工具刷新了
|
|
|
|
|
|
from src.agents import agent_manager
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-26 03:36:54 +08:00
|
|
|
|
await agent_manager.reload_all()
|
|
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
return {"message": "删除成功"}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
except Exception as e:
|
2025-07-22 17:29:38 +08:00
|
|
|
|
logger.error(f"删除数据库失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"删除数据库失败: {e}")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-08-10 21:58:41 +08:00
|
|
|
|
@knowledge.get("/databases/{db_id}/export")
|
|
|
|
|
|
async def export_database(
|
|
|
|
|
|
db_id: str,
|
|
|
|
|
|
format: str = Query("csv", enum=["csv", "xlsx", "md", "txt"]),
|
|
|
|
|
|
include_vectors: bool = Query(False, description="是否在导出中包含向量数据"),
|
2025-09-01 22:37:03 +08:00
|
|
|
|
current_user: User = Depends(get_admin_user),
|
2025-08-10 21:58:41 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""导出知识库数据"""
|
|
|
|
|
|
logger.debug(f"Exporting database {db_id} with format {format}")
|
|
|
|
|
|
try:
|
2025-09-01 22:37:03 +08:00
|
|
|
|
file_path = await knowledge_base.export_data(db_id, format=format, include_vectors=include_vectors)
|
2025-08-10 21:58:41 +08:00
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(file_path):
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Exported file not found.")
|
|
|
|
|
|
|
|
|
|
|
|
media_types = {
|
|
|
|
|
|
"csv": "text/csv",
|
|
|
|
|
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
|
|
|
"md": "text/markdown",
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"txt": "text/plain",
|
2025-08-10 21:58:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
media_type = media_types.get(format, "application/octet-stream")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
return FileResponse(path=file_path, filename=os.path.basename(file_path), media_type=media_type)
|
2025-08-10 21:58:41 +08:00
|
|
|
|
except NotImplementedError as e:
|
|
|
|
|
|
logger.warning(f"A disabled feature was accessed: {e}")
|
|
|
|
|
|
raise HTTPException(status_code=501, detail=str(e))
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"导出数据库失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"导出数据库失败: {e}")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 文档管理分组 ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.post("/databases/{db_id}/documents")
|
|
|
|
|
|
async def add_documents(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
db_id: str, items: list[str] = Body(...), params: dict = Body(...), current_user: User = Depends(get_admin_user)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""添加文档到知识库"""
|
|
|
|
|
|
logger.debug(f"Add documents for db_id {db_id}: {items} {params=}")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
content_type = params.get("content_type", "file")
|
2025-07-22 17:29:38 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
processed_items = await knowledge_base.add_content(db_id, items, params=params)
|
2025-09-01 22:37:03 +08:00
|
|
|
|
item_type = "URLs" if content_type == "url" else "files"
|
|
|
|
|
|
processed_failed_count = len([_p for _p in processed_items if _p["status"] == "failed"])
|
2025-07-22 17:29:38 +08:00
|
|
|
|
processed_info = f"Processed {len(processed_items)} {item_type}, {processed_failed_count} {item_type} failed"
|
|
|
|
|
|
return {"message": processed_info, "items": processed_items, "status": "success"}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Failed to process {content_type}s: {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"Failed to process {content_type}s: {e}", "status": "failed"}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.get("/databases/{db_id}/documents/{doc_id}")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def get_document_info(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
2025-09-21 23:48:56 +08:00
|
|
|
|
"""获取文档详细信息(包含基本信息和内容信息)"""
|
2025-07-22 17:29:38 +08:00
|
|
|
|
logger.debug(f"GET document {doc_id} info in {db_id}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
info = await knowledge_base.get_file_info(db_id, doc_id)
|
|
|
|
|
|
return info
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Failed to get file info, {e}, {db_id=}, {doc_id=}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": "Failed to get file info", "status": "failed"}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
@knowledge.get("/databases/{db_id}/documents/{doc_id}/basic")
|
|
|
|
|
|
async def get_document_basic_info(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""获取文档基本信息(仅元数据)"""
|
|
|
|
|
|
logger.debug(f"GET document {doc_id} basic info in {db_id}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
info = await knowledge_base.get_file_basic_info(db_id, doc_id)
|
|
|
|
|
|
return info
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Failed to get file basic info, {e}, {db_id=}, {doc_id=}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": "Failed to get file basic info", "status": "failed"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@knowledge.get("/databases/{db_id}/documents/{doc_id}/content")
|
|
|
|
|
|
async def get_document_content(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""获取文档内容信息(chunks和lines)"""
|
|
|
|
|
|
logger.debug(f"GET document {doc_id} content in {db_id}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
info = await knowledge_base.get_file_content(db_id, doc_id)
|
|
|
|
|
|
return info
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Failed to get file content, {e}, {db_id=}, {doc_id=}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": "Failed to get file content", "status": "failed"}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.delete("/databases/{db_id}/documents/{doc_id}")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def delete_document(db_id: str, doc_id: str, current_user: User = Depends(get_admin_user)):
|
2025-07-22 17:29:38 +08:00
|
|
|
|
"""删除文档"""
|
|
|
|
|
|
logger.debug(f"DELETE document {doc_id} info in {db_id}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
await knowledge_base.delete_file(db_id, doc_id)
|
|
|
|
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"删除文档失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"删除文档失败: {e}")
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
@knowledge.get("/databases/{db_id}/documents/{doc_id}/download")
|
|
|
|
|
|
async def download_document(db_id: str, doc_id: str, request: Request, current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""下载原始文件"""
|
|
|
|
|
|
logger.debug(f"Download document {doc_id} from {db_id}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
file_info = await knowledge_base.get_file_basic_info(db_id, doc_id)
|
|
|
|
|
|
if not file_info:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="File not found")
|
|
|
|
|
|
|
|
|
|
|
|
file_path = file_info.get("meta", {}).get("path")
|
|
|
|
|
|
if not file_path or not os.path.exists(file_path):
|
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"File not found on disk: {file_info=}")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取文件扩展名和MIME类型,解码URL编码的文件名
|
|
|
|
|
|
filename = file_info.get("meta", {}).get("filename", "file")
|
|
|
|
|
|
logger.debug(f"Original filename from database: {filename}")
|
|
|
|
|
|
|
|
|
|
|
|
# 解码URL编码的文件名(如果有的话)
|
|
|
|
|
|
try:
|
2025-09-22 17:18:07 +08:00
|
|
|
|
decoded_filename = unquote(filename, encoding="utf-8")
|
2025-09-21 23:48:56 +08:00
|
|
|
|
logger.debug(f"Decoded filename: {decoded_filename}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.debug(f"Failed to decode filename {filename}: {e}")
|
|
|
|
|
|
decoded_filename = filename # 如果解码失败,使用原文件名
|
|
|
|
|
|
|
|
|
|
|
|
_, ext = os.path.splitext(decoded_filename)
|
|
|
|
|
|
|
|
|
|
|
|
media_types = {
|
|
|
|
|
|
".pdf": "application/pdf",
|
|
|
|
|
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
|
|
|
|
".doc": "application/msword",
|
|
|
|
|
|
".txt": "text/plain",
|
|
|
|
|
|
".md": "text/markdown",
|
|
|
|
|
|
".json": "application/json",
|
|
|
|
|
|
".csv": "text/csv",
|
|
|
|
|
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
|
|
|
".xls": "application/vnd.ms-excel",
|
|
|
|
|
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
|
|
|
|
".ppt": "application/vnd.ms-powerpoint",
|
|
|
|
|
|
".jpg": "image/jpeg",
|
|
|
|
|
|
".jpeg": "image/jpeg",
|
|
|
|
|
|
".png": "image/png",
|
|
|
|
|
|
".gif": "image/gif",
|
|
|
|
|
|
".bmp": "image/bmp",
|
|
|
|
|
|
".svg": "image/svg+xml",
|
|
|
|
|
|
".zip": "application/zip",
|
|
|
|
|
|
".rar": "application/x-rar-compressed",
|
|
|
|
|
|
".7z": "application/x-7z-compressed",
|
|
|
|
|
|
".tar": "application/x-tar",
|
|
|
|
|
|
".gz": "application/gzip",
|
|
|
|
|
|
".html": "text/html",
|
|
|
|
|
|
".htm": "text/html",
|
|
|
|
|
|
".xml": "text/xml",
|
|
|
|
|
|
".css": "text/css",
|
|
|
|
|
|
".js": "application/javascript",
|
|
|
|
|
|
".py": "text/x-python",
|
|
|
|
|
|
".java": "text/x-java-source",
|
|
|
|
|
|
".cpp": "text/x-c++src",
|
|
|
|
|
|
".c": "text/x-csrc",
|
|
|
|
|
|
".h": "text/x-chdr",
|
|
|
|
|
|
".hpp": "text/x-c++hdr",
|
|
|
|
|
|
}
|
|
|
|
|
|
media_type = media_types.get(ext.lower(), "application/octet-stream")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建自定义FileResponse,避免文件名编码问题
|
2025-09-22 17:18:07 +08:00
|
|
|
|
response = StarletteFileResponse(path=file_path, media_type=media_type)
|
2025-09-21 23:48:56 +08:00
|
|
|
|
|
|
|
|
|
|
# 正确处理中文文件名的HTTP头部设置
|
|
|
|
|
|
# HTTP头部只能包含ASCII字符,所以需要对中文文件名进行编码
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 尝试使用ASCII编码(适用于英文文件名)
|
2025-09-22 17:18:07 +08:00
|
|
|
|
decoded_filename.encode("ascii")
|
2025-09-21 23:48:56 +08:00
|
|
|
|
# 如果成功,直接使用简单格式
|
|
|
|
|
|
response.headers["Content-Disposition"] = f'attachment; filename="{decoded_filename}"'
|
|
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
|
|
# 如果包含非ASCII字符(如中文),使用RFC 2231格式
|
2025-09-22 17:18:07 +08:00
|
|
|
|
encoded_filename = quote(decoded_filename.encode("utf-8"))
|
2025-09-21 23:48:56 +08:00
|
|
|
|
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
|
|
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"下载文件失败: {e}, {traceback.format_exc()}")
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"下载失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 查询分组 ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.post("/databases/{db_id}/query")
|
|
|
|
|
|
async def query_knowledge_base(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
db_id: str, query: str = Body(...), meta: dict = Body(...), current_user: User = Depends(get_admin_user)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""查询知识库"""
|
|
|
|
|
|
logger.debug(f"Query knowledge base {db_id}: {query}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await knowledge_base.aquery(query, db_id=db_id, **meta)
|
|
|
|
|
|
return {"result": result, "status": "success"}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"知识库查询失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"知识库查询失败: {e}", "status": "failed"}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.post("/databases/{db_id}/query-test")
|
|
|
|
|
|
async def query_test(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
db_id: str, query: str = Body(...), meta: dict = Body(...), current_user: User = Depends(get_admin_user)
|
2025-07-22 17:29:38 +08:00
|
|
|
|
):
|
|
|
|
|
|
"""测试查询知识库"""
|
|
|
|
|
|
logger.debug(f"Query test in {db_id}: {query}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await knowledge_base.aquery(query, db_id=db_id, **meta)
|
|
|
|
|
|
return result
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"测试查询失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"测试查询失败: {e}", "status": "failed"}
|
2025-07-21 18:18:47 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.get("/databases/{db_id}/query-params")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def get_knowledge_base_query_params(db_id: str, current_user: User = Depends(get_admin_user)):
|
2025-07-21 18:18:47 +08:00
|
|
|
|
"""获取知识库类型特定的查询参数"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 获取数据库信息
|
|
|
|
|
|
db_info = knowledge_base.get_database_info(db_id)
|
|
|
|
|
|
if not db_info:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Database not found")
|
|
|
|
|
|
|
|
|
|
|
|
kb_type = db_info.get("kb_type", "lightrag")
|
|
|
|
|
|
|
|
|
|
|
|
# 根据知识库类型返回不同的查询参数
|
|
|
|
|
|
if kb_type == "lightrag":
|
|
|
|
|
|
params = {
|
|
|
|
|
|
"type": "lightrag",
|
|
|
|
|
|
"options": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "mode",
|
|
|
|
|
|
"label": "检索模式",
|
|
|
|
|
|
"type": "select",
|
|
|
|
|
|
"default": "mix",
|
|
|
|
|
|
"options": [
|
|
|
|
|
|
{"value": "local", "label": "Local", "description": "上下文相关信息"},
|
|
|
|
|
|
{"value": "global", "label": "Global", "description": "全局知识"},
|
|
|
|
|
|
{"value": "hybrid", "label": "Hybrid", "description": "本地和全局混合"},
|
|
|
|
|
|
{"value": "naive", "label": "Naive", "description": "基本搜索"},
|
|
|
|
|
|
{"value": "mix", "label": "Mix", "description": "知识图谱和向量检索混合"},
|
2025-09-01 22:37:03 +08:00
|
|
|
|
],
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "only_need_context",
|
|
|
|
|
|
"label": "只使用上下文",
|
|
|
|
|
|
"type": "boolean",
|
|
|
|
|
|
"default": True,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "只返回上下文,不生成回答",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "only_need_prompt",
|
|
|
|
|
|
"label": "只使用提示",
|
|
|
|
|
|
"type": "boolean",
|
|
|
|
|
|
"default": False,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "只返回提示,不进行检索",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "top_k",
|
|
|
|
|
|
"label": "TopK",
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
"default": 10,
|
|
|
|
|
|
"min": 1,
|
|
|
|
|
|
"max": 100,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "返回的最大结果数量",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
elif kb_type == "chroma":
|
|
|
|
|
|
params = {
|
|
|
|
|
|
"type": "chroma",
|
|
|
|
|
|
"options": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "top_k",
|
|
|
|
|
|
"label": "TopK",
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
"default": 10,
|
|
|
|
|
|
"min": 1,
|
|
|
|
|
|
"max": 100,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "返回的最大结果数量",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "similarity_threshold",
|
|
|
|
|
|
"label": "相似度阈值",
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
"default": 0.0,
|
|
|
|
|
|
"min": 0.0,
|
|
|
|
|
|
"max": 1.0,
|
|
|
|
|
|
"step": 0.1,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "过滤相似度低于此值的结果",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "include_distances",
|
|
|
|
|
|
"label": "显示相似度",
|
|
|
|
|
|
"type": "boolean",
|
|
|
|
|
|
"default": True,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "在结果中显示相似度分数",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
elif kb_type == "milvus":
|
|
|
|
|
|
params = {
|
|
|
|
|
|
"type": "milvus",
|
|
|
|
|
|
"options": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "top_k",
|
|
|
|
|
|
"label": "TopK",
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
"default": 10,
|
|
|
|
|
|
"min": 1,
|
|
|
|
|
|
"max": 100,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "返回的最大结果数量",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "similarity_threshold",
|
|
|
|
|
|
"label": "相似度阈值",
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
"default": 0.0,
|
|
|
|
|
|
"min": 0.0,
|
|
|
|
|
|
"max": 1.0,
|
|
|
|
|
|
"step": 0.1,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "过滤相似度低于此值的结果",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "include_distances",
|
|
|
|
|
|
"label": "显示相似度",
|
|
|
|
|
|
"type": "boolean",
|
|
|
|
|
|
"default": True,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "在结果中显示相似度分数",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "metric_type",
|
|
|
|
|
|
"label": "距离度量类型",
|
|
|
|
|
|
"type": "select",
|
|
|
|
|
|
"default": "COSINE",
|
|
|
|
|
|
"options": [
|
|
|
|
|
|
{"value": "COSINE", "label": "余弦相似度", "description": "适合文本语义相似度"},
|
|
|
|
|
|
{"value": "L2", "label": "欧几里得距离", "description": "适合数值型数据"},
|
2025-09-01 22:37:03 +08:00
|
|
|
|
{"value": "IP", "label": "内积", "description": "适合标准化向量"},
|
2025-07-21 18:18:47 +08:00
|
|
|
|
],
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "向量相似度计算方法",
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 未知类型,返回基本参数
|
|
|
|
|
|
params = {
|
|
|
|
|
|
"type": "unknown",
|
|
|
|
|
|
"options": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"key": "top_k",
|
|
|
|
|
|
"label": "TopK",
|
|
|
|
|
|
"type": "number",
|
|
|
|
|
|
"default": 10,
|
|
|
|
|
|
"min": 1,
|
|
|
|
|
|
"max": 100,
|
2025-09-01 22:37:03 +08:00
|
|
|
|
"description": "返回的最大结果数量",
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
2025-09-01 22:37:03 +08:00
|
|
|
|
],
|
2025-07-21 18:18:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {"params": params, "message": "success"}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取知识库查询参数失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"获取知识库查询参数失败 {e}", "params": {}}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 文件管理分组 ===
|
|
|
|
|
|
# =============================================================================
|
2025-06-27 01:55:04 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.post("/files/upload")
|
2025-03-20 20:31:14 +08:00
|
|
|
|
async def upload_file(
|
2025-09-01 22:37:03 +08:00
|
|
|
|
file: UploadFile = File(...), db_id: str | None = Query(None), current_user: User = Depends(get_admin_user)
|
2025-03-20 20:31:14 +08:00
|
|
|
|
):
|
2025-07-22 17:29:38 +08:00
|
|
|
|
"""上传文件"""
|
2024-10-02 20:11:28 +08:00
|
|
|
|
if not file.filename:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="No selected file")
|
|
|
|
|
|
|
2025-09-21 23:48:56 +08:00
|
|
|
|
logger.debug(f"Received upload file with filename: {file.filename}")
|
|
|
|
|
|
|
2025-03-20 20:31:14 +08:00
|
|
|
|
# 根据db_id获取上传路径,如果db_id为None则使用默认路径
|
|
|
|
|
|
if db_id:
|
|
|
|
|
|
upload_dir = knowledge_base.get_db_upload_path(db_id)
|
|
|
|
|
|
else:
|
2025-06-28 20:19:26 +08:00
|
|
|
|
upload_dir = os.path.join(config.save_dir, "database", "uploads")
|
2025-03-20 20:31:14 +08:00
|
|
|
|
|
2024-10-23 16:24:29 +08:00
|
|
|
|
basename, ext = os.path.splitext(file.filename)
|
2025-03-06 23:55:20 +08:00
|
|
|
|
filename = f"{basename}_{hashstr(basename, 4, with_salt=True)}{ext}".lower()
|
2024-10-02 20:11:28 +08:00
|
|
|
|
file_path = os.path.join(upload_dir, filename)
|
2025-04-01 18:37:30 +08:00
|
|
|
|
os.makedirs(upload_dir, exist_ok=True)
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
|
|
|
|
|
with open(file_path, "wb") as buffer:
|
|
|
|
|
|
buffer.write(await file.read())
|
|
|
|
|
|
|
2025-03-20 20:31:14 +08:00
|
|
|
|
return {"message": "File successfully uploaded", "file_path": file_path, "db_id": db_id}
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-29 12:58:13 +08:00
|
|
|
|
@knowledge.post("/files/markdown")
|
2025-09-01 22:37:03 +08:00
|
|
|
|
async def mark_it_down(file: UploadFile = File(...), current_user: User = Depends(get_admin_user)):
|
2025-07-29 12:58:13 +08:00
|
|
|
|
"""调用 src.knowledge.indexing 下面的 process_file_to_markdown 解析为 markdown,参数是文件,需要管理员权限"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
content = await file.read()
|
|
|
|
|
|
markdown_content = await process_file_to_markdown(content)
|
|
|
|
|
|
return {"markdown_content": markdown_content, "message": "success"}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"文件解析失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"文件解析失败 {e}", "markdown_content": ""}
|
|
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === 知识库类型分组 ===
|
|
|
|
|
|
# =============================================================================
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.get("/types")
|
|
|
|
|
|
async def get_knowledge_base_types(current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""获取支持的知识库类型"""
|
2025-04-24 22:55:10 +08:00
|
|
|
|
try:
|
2025-07-22 17:29:38 +08:00
|
|
|
|
kb_types = knowledge_base.get_supported_kb_types()
|
|
|
|
|
|
return {"kb_types": kb_types, "message": "success"}
|
2025-04-24 22:55:10 +08:00
|
|
|
|
except Exception as e:
|
2025-07-22 17:29:38 +08:00
|
|
|
|
logger.error(f"获取知识库类型失败 {e}, {traceback.format_exc()}")
|
|
|
|
|
|
return {"message": f"获取知识库类型失败 {e}", "kb_types": {}}
|
2024-10-02 20:11:28 +08:00
|
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
|
2025-07-22 17:29:38 +08:00
|
|
|
|
@knowledge.get("/stats")
|
|
|
|
|
|
async def get_knowledge_base_statistics(current_user: User = Depends(get_admin_user)):
|
|
|
|
|
|
"""获取知识库统计信息"""
|
2025-05-10 10:24:18 +08:00
|
|
|
|
try:
|
2025-07-22 17:29:38 +08:00
|
|
|
|
stats = knowledge_base.get_statistics()
|
|
|
|
|
|
return {"stats": stats, "message": "success"}
|
2025-05-10 10:24:18 +08:00
|
|
|
|
except Exception as e:
|
2025-07-22 17:29:38 +08:00
|
|
|
|
logger.error(f"获取知识库统计失败 {e}, {traceback.format_exc()}")
|
2025-08-23 17:39:47 +08:00
|
|
|
|
return {"message": f"获取知识库统计失败 {e}", "stats": {}}
|