Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -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
]