Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
知识库服务
|
||||
|
||||
知识库 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
|
||||
]
|
||||
Reference in New Issue
Block a user