import json from typing import List, Union from fastapi import APIRouter, Path, Query, Body from fastapi.responses import StreamingResponse from sqlmodel import func, select, update, delete from apps.ai_model.model_factory import LLMConfig, LLMFactory from apps.swagger.i18n import PLACEHOLDER_PREFIX from apps.system.crud.aimodel_manage import get_ai_model_list_by_workspace from apps.system.models.system_model import AiModelDetail, AiModelWorkspaceMapping, AiModelBrief from apps.system.schemas.ai_model_schema import AiModelConfigItem, AiModelCreator, AiModelEditor, AiModelGridItem from apps.system.schemas.permission import SqlbotPermission, require_permissions from common.core.deps import SessionDep, Trans, CurrentUser from common.utils.crypto import sqlbot_decrypt from common.utils.time import get_timestamp from common.utils.utils import SQLBotLogUtil, prepare_model_arg router = APIRouter(tags=["system_model"], prefix="/system/aimodel") from common.audit.models.log_model import OperationType, OperationModules from common.audit.schemas.logger_decorator import LogConfig, system_log @router.post("/status", include_in_schema=False) @require_permissions(permission=SqlbotPermission(role=['admin'])) async def check_llm(info: AiModelCreator, trans: Trans): async def generate(): try: additional_params = {item.key: prepare_model_arg(item.val) for item in info.config_list if item.key and item.val} config = LLMConfig( model_type="openai" if info.protocol == 1 else "vllm", model_name=info.base_model, api_key=info.api_key, api_base_url=info.api_domain, additional_params=additional_params, ) llm_instance = LLMFactory.create_llm(config) async for chunk in llm_instance.llm.astream("1+1=?"): SQLBotLogUtil.info(chunk) if chunk and isinstance(chunk, str): yield json.dumps({"content": chunk}) + "\n" if chunk and isinstance(chunk, dict) and chunk.content: yield json.dumps({"content": chunk.content}) + "\n" except Exception as e: SQLBotLogUtil.error(f"Error checking LLM: {e}") error_msg = trans('i18n_llm.validate_error', msg=str(e)) yield json.dumps({"error": error_msg}) + "\n" return StreamingResponse(generate(), media_type="application/x-ndjson") @router.get("/default", include_in_schema=False) async def check_default(session: SessionDep, trans: Trans): db_model = session.exec( select(AiModelDetail).where(AiModelDetail.default_model == True) ).first() if not db_model: raise Exception(trans('i18n_llm.miss_default')) @router.put("/default/{id}", summary=f"{PLACEHOLDER_PREFIX}system_model_default", description=f"{PLACEHOLDER_PREFIX}system_model_default") @require_permissions(permission=SqlbotPermission(role=['admin'])) @system_log(LogConfig(operation_type=OperationType.UPDATE, module=OperationModules.AI_MODEL, resource_id_expr="id")) async def set_default(session: SessionDep, id: int = Path(description="ID")): db_model = session.get(AiModelDetail, id) if not db_model: raise ValueError(f"AiModelDetail with id {id} not found") if db_model.default_model: return try: session.exec( update(AiModelDetail).values(default_model=False) ) db_model.default_model = True session.add(db_model) session.commit() except Exception as e: session.rollback() raise e @router.get("", response_model=list[AiModelGridItem], summary=f"{PLACEHOLDER_PREFIX}system_model_grid", description=f"{PLACEHOLDER_PREFIX}system_model_grid") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def query( session: SessionDep, keyword: Union[str, None] = Query(default=None, max_length=255, description=f"{PLACEHOLDER_PREFIX}keyword") ): # 子查询:统计每个 model 绑定的 workspace 数量 count_sub = ( select( AiModelWorkspaceMapping.ai_model_id, func.count().label("ws_mapping_count") ) .group_by(AiModelWorkspaceMapping.ai_model_id) .subquery() ) statement = ( select( AiModelDetail.id, AiModelDetail.name, AiModelDetail.model_type, AiModelDetail.base_model, AiModelDetail.supplier, AiModelDetail.protocol, AiModelDetail.default_model, func.coalesce(count_sub.c.ws_mapping_count, 0).label("ws_mapping_count"), ) .outerjoin(count_sub, AiModelDetail.id == count_sub.c.ai_model_id) ) if keyword is not None: statement = statement.where(AiModelDetail.name.like(f"%{keyword}%")) statement = statement.order_by(AiModelDetail.default_model.desc(), AiModelDetail.name, AiModelDetail.create_time) items = session.exec(statement).all() return items @router.get("/{id}", response_model=AiModelEditor, summary=f"{PLACEHOLDER_PREFIX}system_model_query", description=f"{PLACEHOLDER_PREFIX}system_model_query") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def get_model_by_id( session: SessionDep, id: int = Path(description="ID") ): db_model = session.get(AiModelDetail, id) if not db_model: raise ValueError(f"AiModelDetail with id {id} not found") config_list: List[AiModelConfigItem] = [] if db_model.config: try: raw = json.loads(db_model.config) config_list = [AiModelConfigItem(**item) for item in raw] except Exception: pass try: if db_model.api_key: db_model.api_key = await sqlbot_decrypt(db_model.api_key) if db_model.api_domain: db_model.api_domain = await sqlbot_decrypt(db_model.api_domain) except Exception: pass data = AiModelDetail.model_validate(db_model).model_dump(exclude_unset=True) data.pop("config", None) data["config_list"] = config_list return AiModelEditor(**data) @router.post("", summary=f"{PLACEHOLDER_PREFIX}system_model_create", description=f"{PLACEHOLDER_PREFIX}system_model_create") @require_permissions(permission=SqlbotPermission(role=['admin'])) @system_log(LogConfig(operation_type=OperationType.CREATE, module=OperationModules.AI_MODEL, result_id_expr="id")) async def add_model( session: SessionDep, creator: AiModelCreator ): data = creator.model_dump(exclude_unset=True) data["config"] = json.dumps([item.model_dump(exclude_unset=True) for item in creator.config_list]) data.pop("config_list", None) detail = AiModelDetail.model_validate(data) detail.create_time = get_timestamp() count = session.exec(select(func.count(AiModelDetail.id))).one() if count == 0: detail.default_model = True session.add(detail) session.commit() return detail @router.put("", summary=f"{PLACEHOLDER_PREFIX}system_model_update", description=f"{PLACEHOLDER_PREFIX}system_model_update") @require_permissions(permission=SqlbotPermission(role=['admin'])) @system_log( LogConfig(operation_type=OperationType.UPDATE, module=OperationModules.AI_MODEL, resource_id_expr="editor.id")) async def update_model( session: SessionDep, editor: AiModelEditor ): id = int(editor.id) data = editor.model_dump(exclude_unset=True) data["config"] = json.dumps([item.model_dump(exclude_unset=True) for item in editor.config_list]) data.pop("config_list", None) db_model = session.get(AiModelDetail, id) # update_data = AiModelDetail.model_validate(data) db_model.sqlmodel_update(data) session.add(db_model) session.commit() @router.delete("/{id}", summary=f"{PLACEHOLDER_PREFIX}system_model_del", description=f"{PLACEHOLDER_PREFIX}system_model_del") @require_permissions(permission=SqlbotPermission(role=['admin'])) @system_log(LogConfig(operation_type=OperationType.DELETE, module=OperationModules.AI_MODEL, resource_id_expr="id")) async def delete_model( session: SessionDep, trans: Trans, id: int = Path(description="ID") ): item = session.get(AiModelDetail, id) if item.default_model: raise Exception(trans('i18n_llm.delete_default_error', key=item.name)) session.delete(item) session.commit() @router.get("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping", description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def get_model_ws_mapping_by_id( session: SessionDep, id: int = Path(description="ID") ): db_model = session.get(AiModelDetail, id) if not db_model: raise ValueError(f"AiModelDetail with id {id} not found") # 根据 ai_model_id 查询关联的 workspace_id 列表 stmt = ( select(AiModelWorkspaceMapping.workspace_id) .where(AiModelWorkspaceMapping.ai_model_id == id) .distinct() ) ws_ids: List[int] = session.exec(stmt).all() return [str(ws_id) for ws_id in ws_ids] @router.put("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_update", description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_update") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def update_model_ws_mapping_by_id( session: SessionDep, id: int = Path(description="ID"), ws_ids: List[str] = Body(description="workspace id list"), ): if ws_ids is None: ws_ids = [] # 提前去重 ws_ids = list({int(ws_id) for ws_id in ws_ids}) db_model = session.get(AiModelDetail, id) if not db_model: raise ValueError(f"AiModelDetail with id {id} not found") # 根据 ai_model_id 更新关联的 workspace_id 列表 # 1. 批量删除旧映射 session.execute( delete(AiModelWorkspaceMapping) .where(AiModelWorkspaceMapping.ai_model_id == id) ) # 2. 插入去重后的映射关系 for ws_id in ws_ids: session.add( AiModelWorkspaceMapping(ai_model_id=id, workspace_id=ws_id) ) session.commit() return [str(ws_id) for ws_id in ws_ids] # 新增映射(在已有基础上追加) @router.post("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_add", description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_add") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def add_model_ws_mapping_by_id( session: SessionDep, id: int = Path(description="ID"), ws_ids: List[str] = Body(description="workspace id list"), ): if ws_ids is None: ws_ids = [] ws_ids = list({int(ws_id) for ws_id in ws_ids}) db_model = session.get(AiModelDetail, id) if not db_model: raise ValueError(f"AiModelDetail with id {id} not found") # 查询已存在的映射,过滤掉重复的 existing_stmt = ( select(AiModelWorkspaceMapping.workspace_id) .where( AiModelWorkspaceMapping.ai_model_id == id, AiModelWorkspaceMapping.workspace_id.in_(ws_ids), ) ) existing_ws_ids = set(session.exec(existing_stmt).all()) # 只插入不存在的映射 new_ws_ids = [ws_id for ws_id in ws_ids if ws_id not in existing_ws_ids] for ws_id in new_ws_ids: session.add( AiModelWorkspaceMapping(ai_model_id=id, workspace_id=ws_id) ) session.commit() # 返回完整的映射列表 all_stmt = ( select(AiModelWorkspaceMapping.workspace_id) .where(AiModelWorkspaceMapping.ai_model_id == id) .distinct() ) all_ws_ids: List[int] = session.exec(all_stmt).all() return [str(ws_id) for ws_id in all_ws_ids] # 删除指定映射 @router.delete("/{id}/ws_mapping", response_model=List[str], summary=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_delete", description=f"{PLACEHOLDER_PREFIX}system_model_ws_mapping_delete") @require_permissions(permission=SqlbotPermission(role=['admin'])) async def delete_model_ws_mapping_by_id( session: SessionDep, id: int = Path(description="ID"), ws_ids: List[str] = Body(description="workspace id list"), ): if ws_ids is None: ws_ids = [] ws_ids = list({int(ws_id) for ws_id in ws_ids}) db_model = session.get(AiModelDetail, id) if not db_model: raise ValueError(f"AiModelDetail with id {id} not found") # 只删除指定的映射 if ws_ids: session.execute( delete(AiModelWorkspaceMapping) .where( AiModelWorkspaceMapping.ai_model_id == id, AiModelWorkspaceMapping.workspace_id.in_(ws_ids), ) ) session.commit() # 返回剩余的映射列表 stmt = ( select(AiModelWorkspaceMapping.workspace_id) .where(AiModelWorkspaceMapping.ai_model_id == id) .distinct() ) remaining_ws_ids: List[int] = session.exec(stmt).all() return [str(ws_id) for ws_id in remaining_ws_ids] @router.get("/list/by_ws", response_model=List[AiModelBrief], summary=f"{PLACEHOLDER_PREFIX}system_model_list_by_ws", description=f"{PLACEHOLDER_PREFIX}system_model_list_by_ws") @require_permissions(permission=SqlbotPermission(role=['ws_admin'])) async def get_model_by_ws( session: SessionDep, current_user: CurrentUser ): return get_ai_model_list_by_workspace(session, current_user.oid, False)