mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 21:06:32 +00:00
feat(backend): 添加ChromaDB向量数据库支持 feat(backend): 实现智能体配置、知识库和文档管理 feat(backend): 重构WebSocket聊天服务为AgentService feat(frontend): 实现完整的聊天界面组件 feat(frontend): 添加智能体配置、知识库和文档管理页面 fix(user): 修复用户导入时性别转换问题 style(import): 优化导入组件加载状态处理
730 lines
26 KiB
Python
730 lines
26 KiB
Python
from collections.abc import AsyncGenerator
|
|
from typing import Any
|
|
|
|
from langchain_core.documents import Document
|
|
from langchain_core.messages import HumanMessage, SystemMessage
|
|
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
|
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
|
from app.config.setting import settings
|
|
from app.core.exceptions import CustomException
|
|
from app.core.logger import log
|
|
|
|
from .chroma import chroma_manager
|
|
from .crud import AgentConfigCRUD, KnowledgeCRUD, KnowledgeDocumentCRUD
|
|
from .schema import (
|
|
AgentConfigCreateSchema,
|
|
AgentConfigOutSchema,
|
|
AgentConfigSchema,
|
|
AgentConfigUpdateSchema,
|
|
ChatQuerySchema,
|
|
KnowledgeCreateSchema,
|
|
KnowledgeDocumentCreateSchema,
|
|
KnowledgeDocumentOutSchema,
|
|
KnowledgeDocumentUpdateSchema,
|
|
KnowledgeOutSchema,
|
|
KnowledgeQueryParam,
|
|
KnowledgeUpdateSchema,
|
|
)
|
|
|
|
|
|
class AgentConfigService:
|
|
"""智能体配置服务层"""
|
|
|
|
@classmethod
|
|
async def get_by_id_service(
|
|
cls, auth: AuthSchema, id: int
|
|
) -> dict[str, Any]:
|
|
"""
|
|
获取智能体配置详情
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 智能体配置ID
|
|
|
|
返回:
|
|
- dict[str, Any]: 智能体配置详情字典
|
|
"""
|
|
obj = await AgentConfigCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="智能体配置不存在")
|
|
return AgentConfigOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def get_default_service(
|
|
cls, auth: AuthSchema
|
|
) -> dict[str, Any]:
|
|
"""
|
|
获取默认智能体配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 智能体配置详情字典
|
|
"""
|
|
obj = await AgentConfigCRUD(auth).get_default_crud()
|
|
if not obj:
|
|
raise CustomException(msg="默认智能体配置不存在")
|
|
return AgentConfigOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def get_list_service(
|
|
cls, auth: AuthSchema, query_params: Any
|
|
) -> dict[str, Any]:
|
|
"""
|
|
列表查询智能体配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- query_params (Any): 查询参数
|
|
|
|
返回:
|
|
- dict[str, Any]: 智能体配置列表字典
|
|
"""
|
|
search = {}
|
|
if query_params.name:
|
|
search["name"] = query_params.name
|
|
if query_params.provider:
|
|
search["provider"] = query_params.provider
|
|
if query_params.is_default is not None:
|
|
search["is_default"] = query_params.is_default
|
|
if query_params.is_active is not None:
|
|
search["is_active"] = query_params.is_active
|
|
if query_params.created_id:
|
|
search["created_id"] = query_params.created_id
|
|
if query_params.updated_id:
|
|
search["updated_id"] = query_params.updated_id
|
|
if hasattr(query_params, "created_time") and query_params.created_time:
|
|
search["created_time"] = query_params.created_time
|
|
if hasattr(query_params, "updated_time") and query_params.updated_time:
|
|
search["updated_time"] = query_params.updated_time
|
|
|
|
objs = await AgentConfigCRUD(auth).get_list_crud(
|
|
search=search, order_by=[{"id": "desc"}]
|
|
)
|
|
data = [AgentConfigOutSchema.model_validate(obj).model_dump() for obj in objs]
|
|
return {"total": len(data), "data": data}
|
|
|
|
@classmethod
|
|
async def create_service(
|
|
cls, auth: AuthSchema, data: AgentConfigCreateSchema
|
|
) -> dict[str, Any]:
|
|
"""
|
|
创建智能体配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (AgentConfigCreateSchema): 创建智能体配置模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 创建的智能体配置字典
|
|
"""
|
|
if data.is_default:
|
|
existing_default = await AgentConfigCRUD(auth).get_default_crud()
|
|
if existing_default:
|
|
update_data = AgentConfigUpdateSchema.model_construct(is_default=False)
|
|
await AgentConfigCRUD(auth).update_crud(id=existing_default.id, data=update_data)
|
|
|
|
obj = await AgentConfigCRUD(auth).create_crud(data=data)
|
|
if not obj:
|
|
raise CustomException(msg="创建智能体配置失败")
|
|
return AgentConfigOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def update_service(
|
|
cls, auth: AuthSchema, id: int, data: AgentConfigUpdateSchema
|
|
) -> dict[str, Any]:
|
|
"""
|
|
更新智能体配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 智能体配置ID
|
|
- data (AgentConfigUpdateSchema): 更新智能体配置模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 更新的智能体配置字典
|
|
"""
|
|
obj = await AgentConfigCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="智能体配置不存在")
|
|
|
|
if data.is_default:
|
|
existing_default = await AgentConfigCRUD(auth).get_default_crud()
|
|
if existing_default and existing_default.id != id:
|
|
update_data = AgentConfigUpdateSchema.model_construct(is_default=False)
|
|
await AgentConfigCRUD(auth).update_crud(id=existing_default.id, data=update_data)
|
|
|
|
obj = await AgentConfigCRUD(auth).update_crud(id=id, data=data)
|
|
if not obj:
|
|
raise CustomException(msg="更新智能体配置失败")
|
|
return AgentConfigOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""
|
|
批量删除智能体配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- ids (list[int]): 智能体配置ID列表
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
await AgentConfigCRUD(auth).delete_crud(ids=ids)
|
|
|
|
|
|
class RAGService:
|
|
"""RAG 检索增强生成服务层"""
|
|
|
|
@classmethod
|
|
async def retrieve_documents(
|
|
cls,
|
|
query: str,
|
|
knowledge_ids: list[int],
|
|
top_k: int = 3,
|
|
auth: AuthSchema | None = None,
|
|
) -> list[Document]:
|
|
"""
|
|
从知识库中检索相关文档
|
|
|
|
参数:
|
|
- query (str): 查询文本
|
|
- knowledge_ids (list[int]): 知识库ID列表
|
|
- top_k (int): 返回最相关的文档数量
|
|
- auth (AuthSchema | None): 认证信息模型
|
|
|
|
返回:
|
|
- list[Document]: 相关文档列表
|
|
"""
|
|
embeddings = OpenAIEmbeddings(
|
|
api_key=lambda: settings.OPENAI_API_KEY,
|
|
base_url=settings.OPENAI_BASE_URL,
|
|
)
|
|
|
|
query_embedding = await embeddings.aembed_query(query)
|
|
|
|
results = chroma_manager.query_documents(
|
|
query_embeddings=[query_embedding],
|
|
n_results=top_k,
|
|
where={"knowledge_id": {"$in": knowledge_ids}},
|
|
)
|
|
|
|
documents = []
|
|
if results and "documents" in results and len(results["documents"]) > 0:
|
|
for i, doc_content in enumerate(results["documents"][0]):
|
|
metadata = results["metadatas"][0][i] if "metadatas" in results and len(results["metadatas"]) > 0 else {}
|
|
documents.append(
|
|
Document(
|
|
page_content=doc_content,
|
|
metadata=metadata,
|
|
)
|
|
)
|
|
|
|
return documents
|
|
|
|
@classmethod
|
|
def format_context(cls, documents: list[Document]) -> str:
|
|
"""
|
|
格式化检索到的文档为上下文
|
|
|
|
参数:
|
|
- documents (list[Document]): 文档列表
|
|
|
|
返回:
|
|
- str: 格式化的上下文文本
|
|
"""
|
|
if not documents:
|
|
return "没有找到相关的知识库内容。"
|
|
|
|
context_parts = []
|
|
for i, doc in enumerate(documents, 1):
|
|
title = doc.metadata.get("title", "未知文档")
|
|
content = doc.page_content
|
|
context_parts.append(f"[文档 {i}] {title}\n{content}")
|
|
|
|
return "\n\n".join(context_parts)
|
|
|
|
|
|
class AgentService:
|
|
"""智能体服务层"""
|
|
|
|
@classmethod
|
|
async def chat_query(
|
|
cls, query: ChatQuerySchema, config: AgentConfigSchema | None = None
|
|
) -> AsyncGenerator[str, Any]:
|
|
"""
|
|
处理聊天查询
|
|
|
|
参数:
|
|
- query (ChatQuerySchema): 聊天查询模型
|
|
- config (AgentConfigSchema | None): 智能体配置模型
|
|
|
|
返回:
|
|
- AsyncGenerator[str, None]: 异步生成器,每次返回一个聊天响应
|
|
"""
|
|
if config is None:
|
|
config = AgentConfigSchema(
|
|
provider="openai",
|
|
model=settings.OPENAI_MODEL,
|
|
api_key=settings.OPENAI_API_KEY,
|
|
base_url=settings.OPENAI_BASE_URL,
|
|
temperature=0.7,
|
|
system_prompt="你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。",
|
|
)
|
|
|
|
system_prompt = config.system_prompt
|
|
|
|
if query.knowledge_ids:
|
|
retrieved_docs = await RAGService.retrieve_documents(
|
|
query=query.message,
|
|
knowledge_ids=query.knowledge_ids,
|
|
top_k=3,
|
|
)
|
|
context = RAGService.format_context(retrieved_docs)
|
|
system_prompt = f"""{config.system_prompt}
|
|
|
|
以下是从知识库中检索到的相关内容,请参考这些内容回答用户的问题:
|
|
|
|
{context}
|
|
|
|
如果检索到的内容与问题无关,请忽略这些内容,直接回答用户的问题。"""
|
|
|
|
llm = ChatOpenAI(
|
|
api_key=lambda: config.api_key,
|
|
model=config.model,
|
|
base_url=config.base_url,
|
|
temperature=config.temperature,
|
|
streaming=True,
|
|
)
|
|
|
|
messages = [
|
|
SystemMessage(content=system_prompt),
|
|
HumanMessage(content=query.message),
|
|
]
|
|
|
|
try:
|
|
async for chunk in llm.astream(messages):
|
|
yield chunk.text
|
|
|
|
except Exception as e:
|
|
log.debug(f"关闭 LLM 客户端时发生异常(预期行为,服务可能正在关闭): {e}")
|
|
|
|
status_code = getattr(e, "status_code", None)
|
|
body = getattr(e, "body", None)
|
|
message = None
|
|
error_type = None
|
|
error_code = None
|
|
try:
|
|
if isinstance(body, dict) and "error" in body:
|
|
err = body.get("error") or {}
|
|
error_type = err.get("type")
|
|
error_code = err.get("code")
|
|
message = err.get("message")
|
|
except Exception:
|
|
raise CustomException(f"解析 OpenAI 错误失败: {e!s}")
|
|
|
|
text = str(e)
|
|
msg = message or text
|
|
|
|
if (
|
|
(error_code == "Arrearage")
|
|
or (error_type == "Arrearage")
|
|
or ("in good standing" in (msg or ""))
|
|
):
|
|
raise ValueError(
|
|
"账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。"
|
|
)
|
|
if status_code == 401 or "invalid api key" in msg.lower():
|
|
raise ValueError("鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。")
|
|
if status_code == 403 or error_type in {
|
|
"PermissionDenied",
|
|
"permission_denied",
|
|
}:
|
|
raise ValueError("访问被拒绝,权限不足或账号受限。请检查账户权限设置。")
|
|
if status_code == 429 or error_type in {
|
|
"insufficient_quota",
|
|
"rate_limit_exceeded",
|
|
}:
|
|
raise ValueError("请求过于频繁或配额已用尽。请稍后重试或提升账户配额。")
|
|
if status_code == 400:
|
|
raise ValueError(f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}")
|
|
if status_code in {500, 502, 503, 504}:
|
|
raise ValueError("服务暂时不可用,请稍后重试。")
|
|
|
|
raise CustomException(f"处理您的请求时出现错误:{msg}")
|
|
|
|
|
|
class KnowledgeService:
|
|
"""知识库服务层"""
|
|
|
|
@classmethod
|
|
async def detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
|
|
"""
|
|
获取知识库详情
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 知识库ID
|
|
|
|
返回:
|
|
- dict[str, Any]: 知识库详情字典
|
|
"""
|
|
obj = await KnowledgeCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="知识库不存在")
|
|
return KnowledgeOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def list_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
search: KnowledgeQueryParam | None = None,
|
|
order_by: list[dict[str, str]] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
列表查询知识库
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- search (KnowledgeQueryParam | None): 查询参数模型
|
|
- order_by (list[dict[str, str]] | None): 排序参数列表
|
|
|
|
返回:
|
|
- list[dict[str, Any]]: 知识库详情字典列表
|
|
"""
|
|
search_dict = search.__dict__ if search else None
|
|
obj_list = await KnowledgeCRUD(auth).get_list_crud(search=search_dict, order_by=order_by)
|
|
return [KnowledgeOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
|
|
@classmethod
|
|
async def create_service(cls, auth: AuthSchema, data: KnowledgeCreateSchema) -> dict[str, Any]:
|
|
"""
|
|
创建知识库
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (KnowledgeCreateSchema): 创建知识库模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 创建的知识库详情字典
|
|
"""
|
|
obj = await KnowledgeCRUD(auth).get_by_name_crud(name=data.name)
|
|
if obj:
|
|
raise CustomException(msg="创建失败,知识库已存在")
|
|
obj = await KnowledgeCRUD(auth).create_crud(data=data)
|
|
return KnowledgeOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def update_service(
|
|
cls, auth: AuthSchema, id: int, data: KnowledgeUpdateSchema
|
|
) -> dict[str, Any]:
|
|
"""
|
|
更新知识库
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 知识库ID
|
|
- data (KnowledgeUpdateSchema): 更新知识库模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 更新的知识库详情字典
|
|
"""
|
|
obj = await KnowledgeCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="更新失败,该数据不存在")
|
|
exist_obj = await KnowledgeCRUD(auth).get_by_name_crud(name=data.name)
|
|
if exist_obj and exist_obj.id != id:
|
|
raise CustomException(msg="更新失败,知识库名称重复")
|
|
obj = await KnowledgeCRUD(auth).update_crud(id=id, data=data)
|
|
return KnowledgeOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""
|
|
批量删除知识库
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- ids (list[int]): 知识库ID列表
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
if len(ids) < 1:
|
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
|
for id in ids:
|
|
obj = await KnowledgeCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="删除失败,该数据不存在")
|
|
await KnowledgeCRUD(auth).delete_crud(ids=ids)
|
|
|
|
@classmethod
|
|
async def document_detail_service(cls, auth: AuthSchema, id: int) -> dict[str, Any]:
|
|
"""
|
|
获取知识库文档详情
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 文档ID
|
|
|
|
返回:
|
|
- dict[str, Any]: 文档详情字典
|
|
"""
|
|
obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="文档不存在")
|
|
return KnowledgeDocumentOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def document_list_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
search: Any | None = None,
|
|
order_by: list[dict[str, str]] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
列表查询知识库文档
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- search (Any | None): 查询参数模型
|
|
- order_by (list[dict[str, str]] | None): 排序参数列表
|
|
|
|
返回:
|
|
- list[dict[str, Any]]: 文档详情字典列表
|
|
"""
|
|
search_dict = search.__dict__ if search else None
|
|
obj_list = await KnowledgeDocumentCRUD(auth).get_list_crud(
|
|
search=search_dict, order_by=order_by
|
|
)
|
|
return [KnowledgeDocumentOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
|
|
|
@classmethod
|
|
async def document_create_service(
|
|
cls, auth: AuthSchema, data: KnowledgeDocumentCreateSchema
|
|
) -> dict[str, Any]:
|
|
"""
|
|
创建知识库文档
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (KnowledgeDocumentCreateSchema): 创建文档模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 创建的文档详情字典
|
|
"""
|
|
knowledge = await KnowledgeCRUD(auth).get_by_id_crud(id=data.knowledge_id)
|
|
if not knowledge:
|
|
raise CustomException(msg="创建失败,知识库不存在")
|
|
|
|
obj = await KnowledgeDocumentCRUD(auth).create_crud(data=data)
|
|
if not obj:
|
|
raise CustomException(msg="创建文档失败")
|
|
|
|
try:
|
|
text_splitter = RecursiveCharacterTextSplitter(
|
|
chunk_size=knowledge.chunk_size,
|
|
chunk_overlap=knowledge.chunk_overlap,
|
|
)
|
|
chunks = text_splitter.split_text(data.content)
|
|
|
|
if settings.OPENAI_API_KEY and settings.OPENAI_BASE_URL:
|
|
try:
|
|
embeddings = OpenAIEmbeddings(
|
|
model=settings.OPENAI_MODEL,
|
|
)
|
|
|
|
chunk_embeddings = await embeddings.aembed_documents(chunks)
|
|
|
|
ids = []
|
|
documents = []
|
|
metadatas = []
|
|
for i, chunk in enumerate(chunks):
|
|
chunk_id = f"{obj.id}_chunk_{i}"
|
|
ids.append(chunk_id)
|
|
documents.append(chunk)
|
|
metadatas.append(
|
|
{
|
|
"document_id": obj.id,
|
|
"knowledge_id": obj.knowledge_id,
|
|
"title": obj.title,
|
|
"chunk_index": i,
|
|
}
|
|
)
|
|
|
|
chroma_manager.add_documents(
|
|
ids=ids,
|
|
embeddings=chunk_embeddings,
|
|
documents=documents,
|
|
metadatas=metadatas,
|
|
)
|
|
|
|
update_data = KnowledgeDocumentUpdateSchema.model_construct(
|
|
chunk_count=len(chunks), is_indexed=True
|
|
)
|
|
except Exception as e:
|
|
log.warning(f"嵌入生成失败,使用虚拟嵌入: {e!s}")
|
|
chunk_embeddings = [[0.0] * 1536 for _ in chunks]
|
|
|
|
ids = []
|
|
documents = []
|
|
metadatas = []
|
|
for i, chunk in enumerate(chunks):
|
|
chunk_id = f"{obj.id}_chunk_{i}"
|
|
ids.append(chunk_id)
|
|
documents.append(chunk)
|
|
metadatas.append(
|
|
{
|
|
"document_id": obj.id,
|
|
"knowledge_id": obj.knowledge_id,
|
|
"title": obj.title,
|
|
"chunk_index": i,
|
|
}
|
|
)
|
|
|
|
chroma_manager.add_documents(
|
|
ids=ids,
|
|
embeddings=chunk_embeddings,
|
|
documents=documents,
|
|
metadatas=metadatas,
|
|
)
|
|
|
|
update_data = KnowledgeDocumentUpdateSchema.model_construct(
|
|
chunk_count=len(chunks), is_indexed=True
|
|
)
|
|
else:
|
|
log.info("未配置嵌入模型,跳过向量索引")
|
|
update_data = KnowledgeDocumentUpdateSchema.model_construct(
|
|
chunk_count=len(chunks), is_indexed=False
|
|
)
|
|
|
|
await KnowledgeDocumentCRUD(auth).update_crud(id=obj.id, data=update_data)
|
|
|
|
updated_obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=obj.id)
|
|
return KnowledgeDocumentOutSchema.model_validate(updated_obj).model_dump()
|
|
except CustomException:
|
|
raise
|
|
except Exception as e:
|
|
log.error(f"创建知识库文档时发生错误: {e!s}")
|
|
raise CustomException(msg=f"创建知识库文档失败: {e!s}")
|
|
|
|
@classmethod
|
|
async def document_update_service(
|
|
cls, auth: AuthSchema, id: int, data: KnowledgeDocumentUpdateSchema
|
|
) -> dict[str, Any]:
|
|
"""
|
|
更新知识库文档
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 文档ID
|
|
- data (KnowledgeDocumentUpdateSchema): 更新文档模型
|
|
|
|
返回:
|
|
- dict[str, Any]: 更新的文档详情字典
|
|
"""
|
|
obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="更新失败,该数据不存在")
|
|
|
|
content_changed = data.content is not None and data.content != obj.content
|
|
|
|
if content_changed:
|
|
chunk_ids = [f"{obj.id}_chunk_{i}" for i in range(obj.chunk_count or 0)]
|
|
chroma_manager.delete_documents(ids=chunk_ids)
|
|
|
|
obj = await KnowledgeDocumentCRUD(auth).update_crud(id=id, data=data)
|
|
if not obj:
|
|
raise CustomException(msg="更新文档失败")
|
|
|
|
if content_changed and obj.content:
|
|
knowledge = await KnowledgeCRUD(auth).get_by_id_crud(id=obj.knowledge_id)
|
|
if knowledge:
|
|
text_splitter = RecursiveCharacterTextSplitter(
|
|
chunk_size=knowledge.chunk_size,
|
|
chunk_overlap=knowledge.chunk_overlap,
|
|
)
|
|
chunks = text_splitter.split_text(obj.content)
|
|
|
|
if settings.OPENAI_API_KEY and settings.OPENAI_BASE_URL:
|
|
try:
|
|
embeddings = OpenAIEmbeddings(
|
|
api_key=lambda: settings.OPENAI_API_KEY,
|
|
base_url=settings.OPENAI_BASE_URL,
|
|
)
|
|
|
|
chunk_embeddings = await embeddings.aembed_documents(chunks)
|
|
|
|
ids = []
|
|
documents = []
|
|
metadatas = []
|
|
for i, chunk in enumerate(chunks):
|
|
chunk_id = f"{obj.id}_chunk_{i}"
|
|
ids.append(chunk_id)
|
|
documents.append(chunk)
|
|
metadatas.append(
|
|
{
|
|
"document_id": obj.id,
|
|
"knowledge_id": obj.knowledge_id,
|
|
"title": obj.title,
|
|
"chunk_index": i,
|
|
}
|
|
)
|
|
|
|
chroma_manager.add_documents(
|
|
ids=ids,
|
|
embeddings=chunk_embeddings,
|
|
documents=documents,
|
|
metadatas=metadatas,
|
|
)
|
|
|
|
update_data = KnowledgeDocumentUpdateSchema.model_construct(
|
|
chunk_count=len(chunks), is_indexed=True
|
|
)
|
|
except Exception as e:
|
|
log.warning(f"嵌入生成失败,跳过向量索引: {e!s}")
|
|
update_data = KnowledgeDocumentUpdateSchema.model_construct(
|
|
chunk_count=len(chunks), is_indexed=False
|
|
)
|
|
else:
|
|
log.info("未配置嵌入模型,跳过向量索引")
|
|
update_data = KnowledgeDocumentUpdateSchema.model_construct(
|
|
chunk_count=len(chunks), is_indexed=False
|
|
)
|
|
|
|
obj = await KnowledgeDocumentCRUD(auth).update_crud(id=obj.id, data=update_data)
|
|
|
|
return KnowledgeDocumentOutSchema.model_validate(obj).model_dump()
|
|
|
|
@classmethod
|
|
async def document_delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""
|
|
批量删除知识库文档
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- ids (list[int]): 文档ID列表
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
if len(ids) < 1:
|
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
|
|
|
chunk_ids = []
|
|
for id in ids:
|
|
obj = await KnowledgeDocumentCRUD(auth).get_by_id_crud(id=id)
|
|
if not obj:
|
|
raise CustomException(msg="删除失败,该数据不存在")
|
|
if obj.chunk_count:
|
|
chunk_ids.extend([f"{obj.id}_chunk_{i}" for i in range(obj.chunk_count)])
|
|
|
|
if chunk_ids:
|
|
chroma_manager.delete_documents(ids=chunk_ids)
|
|
|
|
await KnowledgeDocumentCRUD(auth).delete_crud(ids=ids)
|