268 lines
9.5 KiB
Python
268 lines
9.5 KiB
Python
"""
|
||
知识库服务
|
||
|
||
知识库 CRUD 操作
|
||
|
||
数据权限:
|
||
- 使用 get_list_with_data_scope() 自动应用数据权限
|
||
- 支持本人、本部门、本部门及下级、全部等数据范围
|
||
"""
|
||
import logging
|
||
from typing import Optional, List, Tuple
|
||
|
||
from sqlalchemy import select, func, or_, and_
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from ai_platform.knowledge.models import KnowledgeBase, KnowledgeDocument, KnowledgeSegment
|
||
from ai_platform.knowledge.schemas.knowledge_base_schema import KnowledgeBaseCreate, KnowledgeBaseUpdate
|
||
from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 资源类型(用于数据权限配置)
|
||
RESOURCE_TYPE = "knowledge_base"
|
||
RESOURCE_DISPLAY_NAME = "知识库管理"
|
||
|
||
|
||
class KnowledgeService:
|
||
"""知识库服务"""
|
||
|
||
def __init__(self, db: AsyncSession):
|
||
self._db = db
|
||
|
||
async def get_list(
|
||
self,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
name: Optional[str] = None,
|
||
status: Optional[str] = None,
|
||
application_id: Optional[str] = None,
|
||
) -> Tuple[List[KnowledgeBase], int]:
|
||
"""获取知识库列表"""
|
||
query = select(KnowledgeBase).where(KnowledgeBase.is_deleted == False)
|
||
|
||
if application_id:
|
||
query = query.where(or_(
|
||
KnowledgeBase.application_id == application_id,
|
||
and_(KnowledgeBase.application_id.is_(None), KnowledgeBase.is_global == True)
|
||
))
|
||
if name:
|
||
query = query.where(KnowledgeBase.name.ilike(f"%{name}%"))
|
||
if status:
|
||
query = query.where(KnowledgeBase.status == status)
|
||
|
||
# 总数
|
||
count_result = await self._db.execute(select(func.count()).select_from(query.subquery()))
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
query = query.order_by(KnowledgeBase.sort.desc(), KnowledgeBase.sys_create_datetime.desc())
|
||
query = query.offset(offset).limit(page_size)
|
||
|
||
result = await self._db.execute(query)
|
||
items = result.scalars().all()
|
||
|
||
return items, total
|
||
|
||
async def get_list_with_data_scope(
|
||
self,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
name: Optional[str] = None,
|
||
status: Optional[str] = None,
|
||
application_id: Optional[str] = None,
|
||
) -> Tuple[List[KnowledgeBase], int]:
|
||
"""
|
||
获取知识库列表(带数据权限过滤)
|
||
|
||
自动从上下文获取当前用户信息,应用数据权限过滤
|
||
"""
|
||
conditions = [KnowledgeBase.is_deleted == False]
|
||
|
||
if application_id:
|
||
conditions.append(or_(
|
||
KnowledgeBase.application_id == application_id,
|
||
and_(KnowledgeBase.application_id.is_(None), KnowledgeBase.is_global == True)
|
||
))
|
||
if name:
|
||
conditions.append(KnowledgeBase.name.ilike(f"%{name}%"))
|
||
if status:
|
||
conditions.append(KnowledgeBase.status == status)
|
||
|
||
# 获取数据权限过滤条件并应用
|
||
data_scope_filter = await get_data_scope_filter(self._db, RESOURCE_TYPE)
|
||
scope_conditions = apply_data_scope_to_conditions(KnowledgeBase, data_scope_filter)
|
||
conditions.extend(scope_conditions)
|
||
|
||
# 总数
|
||
query = select(KnowledgeBase).where(and_(*conditions))
|
||
count_result = await self._db.execute(select(func.count()).select_from(query.subquery()))
|
||
total = count_result.scalar() or 0
|
||
|
||
# 分页
|
||
offset = (page - 1) * page_size
|
||
query = query.order_by(KnowledgeBase.sort.desc(), KnowledgeBase.sys_create_datetime.desc())
|
||
query = query.offset(offset).limit(page_size)
|
||
|
||
result = await self._db.execute(query)
|
||
items = result.scalars().all()
|
||
|
||
return items, total
|
||
|
||
async def get_by_id(self, kb_id: str) -> Optional[KnowledgeBase]:
|
||
"""获取知识库详情"""
|
||
result = await self._db.execute(
|
||
select(KnowledgeBase).where(
|
||
KnowledgeBase.id == kb_id,
|
||
KnowledgeBase.is_deleted == False
|
||
)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def get_by_code(self, code: str) -> Optional[KnowledgeBase]:
|
||
"""根据编码获取知识库"""
|
||
result = await self._db.execute(
|
||
select(KnowledgeBase).where(
|
||
KnowledgeBase.code == code,
|
||
KnowledgeBase.is_deleted == False
|
||
)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def create(self, data: KnowledgeBaseCreate) -> KnowledgeBase:
|
||
"""创建知识库"""
|
||
# 检查编码唯一性
|
||
existing = await self.get_by_code(data.code)
|
||
if existing:
|
||
raise ValueError(f'知识库编码 {data.code} 已存在')
|
||
|
||
kb_data = data.model_dump()
|
||
|
||
# 自动检测 embedding 模型的真实维度
|
||
if data.embedding_model_id:
|
||
try:
|
||
from ai_platform.knowledge.services.embedding_service import EmbeddingService
|
||
embedding_service = EmbeddingService(self._db)
|
||
real_dim = await embedding_service.get_embedding_dimensions(data.embedding_model_id)
|
||
kb_data['embedding_dimensions'] = real_dim
|
||
logger.info(f'自动检测 embedding 维度: {real_dim}')
|
||
except Exception as e:
|
||
logger.warning(f'自动检测 embedding 维度失败,使用默认值: {e}')
|
||
|
||
kb = KnowledgeBase(**kb_data)
|
||
|
||
# 自动填充创建人和部门
|
||
from utils.context import get_current_user_info_from_context
|
||
user_info = get_current_user_info_from_context()
|
||
if user_info:
|
||
if not kb.sys_creator_id:
|
||
kb.sys_creator_id = user_info.get('user_id')
|
||
if not kb.sys_dept_id and user_info.get('dept_id'):
|
||
kb.sys_dept_id = user_info.get('dept_id')
|
||
|
||
self._db.add(kb)
|
||
await self._db.commit()
|
||
await self._db.refresh(kb)
|
||
return kb
|
||
|
||
async def update(self, kb_id: str, data: KnowledgeBaseUpdate) -> Optional[KnowledgeBase]:
|
||
"""更新知识库"""
|
||
kb = await self.get_by_id(kb_id)
|
||
if not kb:
|
||
return None
|
||
|
||
update_data = data.model_dump(exclude_unset=True)
|
||
|
||
# 如果更换了 embedding 模型,自动重新检测维度
|
||
new_model_id = update_data.get('embedding_model_id')
|
||
if new_model_id and new_model_id != kb.embedding_model_id:
|
||
try:
|
||
from ai_platform.knowledge.services.embedding_service import EmbeddingService
|
||
embedding_service = EmbeddingService(self._db)
|
||
real_dim = await embedding_service.get_embedding_dimensions(new_model_id)
|
||
update_data['embedding_dimensions'] = real_dim
|
||
logger.info(f'更换模型后自动检测 embedding 维度: {real_dim}')
|
||
except Exception as e:
|
||
logger.warning(f'自动检测 embedding 维度失败: {e}')
|
||
|
||
for key, value in update_data.items():
|
||
setattr(kb, key, value)
|
||
|
||
await self._db.commit()
|
||
await self._db.refresh(kb)
|
||
return kb
|
||
|
||
async def delete(self, kb_id: str) -> bool:
|
||
"""删除知识库(软删除 + 清理 Qdrant collection)"""
|
||
kb = await self.get_by_id(kb_id)
|
||
if not kb:
|
||
return False
|
||
|
||
kb.is_deleted = True
|
||
|
||
# 同时软删除所有文档和分段
|
||
doc_result = await self._db.execute(
|
||
select(KnowledgeDocument).where(
|
||
KnowledgeDocument.knowledge_base_id == kb_id,
|
||
KnowledgeDocument.is_deleted == False
|
||
)
|
||
)
|
||
docs = doc_result.scalars().all()
|
||
for doc in docs:
|
||
doc.is_deleted = True
|
||
|
||
# 软删除分段
|
||
from sqlalchemy import update
|
||
await self._db.execute(
|
||
update(KnowledgeSegment).where(
|
||
KnowledgeSegment.knowledge_base_id == kb_id
|
||
).values(is_deleted=True)
|
||
)
|
||
|
||
await self._db.commit()
|
||
|
||
# 删除 Qdrant 中对应的 collection
|
||
try:
|
||
from ai_platform.knowledge.vector_store import get_vector_store
|
||
vector_store = get_vector_store()
|
||
await vector_store.delete_collection(kb_id)
|
||
except Exception as e:
|
||
logger.warning(f'删除 Qdrant collection 失败: {e}')
|
||
|
||
return True
|
||
|
||
async def get_simple_list(self, application_id: Optional[str] = None) -> List[dict]:
|
||
"""获取知识库简单列表(用于下拉选择)"""
|
||
query = select(
|
||
KnowledgeBase.id,
|
||
KnowledgeBase.name,
|
||
KnowledgeBase.code,
|
||
KnowledgeBase.document_count,
|
||
KnowledgeBase.segment_count,
|
||
).where(
|
||
KnowledgeBase.is_deleted == False,
|
||
KnowledgeBase.status == 'active',
|
||
)
|
||
|
||
if application_id:
|
||
query = query.where(or_(
|
||
KnowledgeBase.application_id == application_id,
|
||
and_(KnowledgeBase.application_id.is_(None), KnowledgeBase.is_global == True)
|
||
))
|
||
|
||
query = query.order_by(KnowledgeBase.sort.desc(), KnowledgeBase.sys_create_datetime.desc())
|
||
result = await self._db.execute(query)
|
||
rows = result.all()
|
||
|
||
return [
|
||
{
|
||
'id': row.id,
|
||
'name': row.name,
|
||
'code': row.code,
|
||
'document_count': row.document_count or 0,
|
||
'segment_count': row.segment_count or 0,
|
||
}
|
||
for row in rows
|
||
]
|