knowledge_base.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. """
  2. 知识库管理 API
  3. 提供文档上传、查询、删除、智能客服配置等接口
  4. """
  5. from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks
  6. from typing import List, Optional
  7. from pydantic import BaseModel
  8. import os
  9. from ..db.knowledge_base import (
  10. get_tenant_config, upsert_tenant_config, get_agent_config, update_agent_config,
  11. create_document, get_document_by_id, list_documents, count_documents,
  12. update_document_status, delete_document, create_chunks, get_chunks_by_doc,
  13. delete_chunks_by_doc, get_tenant_stats
  14. )
  15. from ..core.oss_client import get_oss_client
  16. from ..core.qdrant_client import get_qdrant_manager
  17. from ..utils.document_processor import get_document_processor
  18. from ..utils.embedding_service import get_embedding_service
  19. from ..dependencies.auth import resolve_user_id, UserContext
  20. router = APIRouter()
  21. # 允许的文件类型
  22. ALLOWED_FILE_TYPES = ['.xlsx', '.xls', '.docx', '.doc', '.pdf']
  23. MAX_FILE_SIZE = 20 * 1024 * 1024 # 20MB
  24. # ==================== Pydantic 模型 ====================
  25. class AgentConfigUpdate(BaseModel):
  26. enabled: bool
  27. name: str
  28. greeting: str
  29. class DocumentInfo(BaseModel):
  30. id: str
  31. filename: str
  32. file_type: str
  33. file_size: int
  34. status: str
  35. chunk_count: int
  36. created_at: str
  37. class ChunkInfo(BaseModel):
  38. chunk_index: int
  39. content: str
  40. vector_id: Optional[str]
  41. # ==================== 后台任务:文档处理 ====================
  42. async def process_document_task(doc_id: str, tenant_id: str, file_content: bytes, file_type: str):
  43. """
  44. 后台任务:处理文档(提取文本、切片、向量化、存储)
  45. """
  46. try:
  47. # 更新状态为处理中
  48. update_document_status(doc_id, "processing")
  49. # 1. 提取文本
  50. processor = get_document_processor()
  51. text = processor.extract_text(file_content, file_type)
  52. if not text:
  53. update_document_status(doc_id, "failed", error_msg="文本提取失败")
  54. return
  55. # 清理文本
  56. text = processor.clean_text(text)
  57. # 2. 智能切片
  58. chunks = processor.split_text_into_chunks(text)
  59. if not chunks:
  60. update_document_status(doc_id, "failed", error_msg="文档切片失败")
  61. return
  62. # 3. 向量化(批量)
  63. embedding_service = get_embedding_service()
  64. chunk_contents = [chunk["content"] for chunk in chunks]
  65. vectors = embedding_service.generate_embeddings(chunk_contents, tenant_id)
  66. if len(vectors) != len(chunks):
  67. update_document_status(doc_id, "failed", error_msg="向量化失败")
  68. return
  69. # 4. 存储到 Qdrant
  70. qdrant = get_qdrant_manager()
  71. chunks_with_vectors = [
  72. {"content": chunk["content"], "vector": vector}
  73. for chunk, vector in zip(chunks, vectors)
  74. ]
  75. vector_ids = qdrant.add_vectors(tenant_id, doc_id, chunks_with_vectors)
  76. # 5. 存储切片元数据到 MongoDB
  77. chunks_data = [
  78. {
  79. "content": chunk["content"],
  80. "vector_id": vector_id,
  81. "metadata": chunk.get("metadata", {})
  82. }
  83. for chunk, vector_id in zip(chunks, vector_ids)
  84. ]
  85. create_chunks(doc_id, tenant_id, chunks_data)
  86. # 6. 更新文档状态为完成
  87. update_document_status(doc_id, "completed", chunk_count=len(chunks))
  88. print(f"文档处理完成: {doc_id}, 切片数: {len(chunks)}")
  89. except Exception as e:
  90. print(f"文档处理异常: {str(e)}")
  91. update_document_status(doc_id, "failed", error_msg=str(e))
  92. # ==================== 文档管理接口 ====================
  93. @router.post("/documents/upload")
  94. async def upload_document(
  95. background_tasks: BackgroundTasks,
  96. file: UploadFile = File(...),
  97. user_context: UserContext = Depends(resolve_user_id),
  98. ):
  99. """
  100. 上传文档到知识库
  101. 1. 上传原始文件到阿里云OSS
  102. 2. 后台任务:提取文本、切片、向量化、存储
  103. """
  104. tenant_id = user_context.app_name
  105. # 验证文件类型
  106. file_ext = os.path.splitext(file.filename)[1].lower()
  107. if file_ext not in ALLOWED_FILE_TYPES:
  108. raise HTTPException(status_code=400, detail=f"不支持的文件类型: {file_ext}")
  109. # 读取文件内容
  110. file_content = await file.read()
  111. file_size = len(file_content)
  112. # 验证文件大小
  113. if file_size > MAX_FILE_SIZE:
  114. raise HTTPException(status_code=400, detail=f"文件大小超出限制 ({MAX_FILE_SIZE // 1024 // 1024}MB)")
  115. # 上传到 OSS
  116. oss_client = get_oss_client()
  117. success, oss_path = oss_client.upload_file(tenant_id, file_content, file.filename)
  118. if not success:
  119. raise HTTPException(status_code=500, detail=f"文件上传失败: {oss_path}")
  120. # 创建文档记录
  121. doc_id = create_document(
  122. tenant_id=tenant_id,
  123. filename=file.filename,
  124. file_type=file_ext,
  125. file_size=file_size,
  126. oss_path=oss_path
  127. )
  128. # 后台处理文档
  129. background_tasks.add_task(process_document_task, doc_id, tenant_id, file_content, file_ext)
  130. return {
  131. "success": True,
  132. "doc_id": doc_id,
  133. "message": "文档上传成功,正在后台处理..."
  134. }
  135. @router.get("/documents")
  136. async def get_documents(
  137. skip: int = 0,
  138. limit: int = 100,
  139. user_context: UserContext = Depends(resolve_user_id),
  140. ):
  141. """获取租户的所有文档列表"""
  142. tenant_id = user_context.app_name
  143. docs = list_documents(tenant_id, skip, limit)
  144. total = count_documents(tenant_id)
  145. return {
  146. "success": True,
  147. "total": total,
  148. "documents": docs
  149. }
  150. @router.get("/documents/{doc_id}")
  151. async def get_document_detail(
  152. doc_id: str,
  153. user_context: UserContext = Depends(resolve_user_id),
  154. ):
  155. """获取文档详情(包含切片)"""
  156. tenant_id = user_context.app_name
  157. # 获取文档信息
  158. doc = get_document_by_id(doc_id)
  159. if not doc:
  160. raise HTTPException(status_code=404, detail="文档不存在")
  161. # 验证租户权限
  162. if doc["tenant_id"] != tenant_id:
  163. raise HTTPException(status_code=403, detail="无权访问该文档")
  164. # 获取切片
  165. chunks = get_chunks_by_doc(doc_id)
  166. # 获取原始文件URL(临时访问链接)
  167. oss_client = get_oss_client()
  168. file_url = oss_client.get_file_url(doc["oss_path"])
  169. return {
  170. "success": True,
  171. "document": doc,
  172. "chunks": chunks,
  173. "file_url": file_url
  174. }
  175. @router.delete("/documents/{doc_id}")
  176. async def delete_document_endpoint(
  177. doc_id: str,
  178. user_context: UserContext = Depends(resolve_user_id),
  179. ):
  180. """删除文档(包括OSS文件、向量、切片)"""
  181. tenant_id = user_context.app_name
  182. # 获取文档信息
  183. doc = get_document_by_id(doc_id)
  184. if not doc:
  185. raise HTTPException(status_code=404, detail="文档不存在")
  186. # 验证租户权限
  187. if doc["tenant_id"] != tenant_id:
  188. raise HTTPException(status_code=403, detail="无权删除该文档")
  189. # 1. 删除 OSS 文件
  190. oss_client = get_oss_client()
  191. oss_client.delete_file(doc["oss_path"])
  192. # 2. 删除 Qdrant 向量
  193. qdrant = get_qdrant_manager()
  194. qdrant.delete_vectors_by_doc(doc_id)
  195. # 3. 删除 MongoDB 切片
  196. delete_chunks_by_doc(doc_id)
  197. # 4. 软删除文档记录
  198. delete_document(doc_id)
  199. return {
  200. "success": True,
  201. "message": "文档删除成功"
  202. }
  203. # ==================== 智能客服配置接口 ====================
  204. @router.get("/agent/config")
  205. async def get_agent_config_endpoint(
  206. user_context: UserContext = Depends(resolve_user_id),
  207. ):
  208. """获取智能客服配置"""
  209. tenant_id = user_context.app_name
  210. config = get_agent_config(tenant_id)
  211. if not config:
  212. # 返回默认配置
  213. return {
  214. "success": True,
  215. "config": {
  216. "enabled": False,
  217. "name": "智能小助手",
  218. "greeting": "您好!我是智能客服小助手,有什么可以帮您的吗?",
  219. "h5_url": f"https://aichat.zhongsou.com/?tenant={tenant_id}"
  220. }
  221. }
  222. return {
  223. "success": True,
  224. "config": config
  225. }
  226. @router.post("/agent/config")
  227. async def update_agent_config_endpoint(
  228. config: AgentConfigUpdate,
  229. user_context: UserContext = Depends(resolve_user_id),
  230. ):
  231. """更新智能客服配置"""
  232. tenant_id = user_context.app_name
  233. success = update_agent_config(
  234. tenant_id=tenant_id,
  235. enabled=config.enabled,
  236. name=config.name,
  237. greeting=config.greeting
  238. )
  239. if not success:
  240. raise HTTPException(status_code=500, detail="配置更新失败")
  241. return {
  242. "success": True,
  243. "message": "配置更新成功"
  244. }
  245. # ==================== 统计接口 ====================
  246. @router.get("/stats")
  247. async def get_stats(
  248. user_context: UserContext = Depends(resolve_user_id),
  249. ):
  250. """获取租户的知识库统计数据"""
  251. tenant_id = user_context.app_name
  252. stats = get_tenant_stats(tenant_id)
  253. return {
  254. "success": True,
  255. "stats": stats
  256. }