ai-robot-core/ai-service/app/api/admin/slot_definition.py

239 lines
6.7 KiB
Python

"""
Slot Definition API.
[AC-MRS-07,08,16] 槽位定义管理接口
"""
import logging
from typing import Annotated, Any
from fastapi import APIRouter, Depends, Query
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_session
from app.core.exceptions import MissingTenantIdException
from app.core.tenant import get_tenant_id
from app.models.entities import SlotDefinitionCreate, SlotDefinitionUpdate
from app.services.slot_definition_service import SlotDefinitionService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/slot-definitions", tags=["SlotDefinition"])
def get_current_tenant_id() -> str:
"""Get current tenant ID from context."""
tenant_id = get_tenant_id()
if not tenant_id:
raise MissingTenantIdException()
return tenant_id
def _slot_to_dict(slot: dict[str, Any] | Any) -> dict[str, Any]:
"""Convert slot definition to dict"""
if isinstance(slot, dict):
return slot
return {
"id": str(slot.id),
"tenant_id": str(slot.tenant_id),
"slot_key": slot.slot_key,
"display_name": slot.display_name,
"description": slot.description,
"type": slot.type,
"required": slot.required,
# [AC-MRS-07-UPGRADE] 返回新旧字段
"extract_strategy": slot.extract_strategy,
"extract_strategies": slot.extract_strategies,
"validation_rule": slot.validation_rule,
"ask_back_prompt": slot.ask_back_prompt,
"default_value": slot.default_value,
"linked_field_id": str(slot.linked_field_id) if slot.linked_field_id else None,
"created_at": slot.created_at.isoformat() if slot.created_at else None,
"updated_at": slot.updated_at.isoformat() if slot.updated_at else None,
}
@router.get(
"",
operation_id="listSlotDefinitions",
summary="List slot definitions",
description="获取槽位定义列表",
)
async def list_slot_definitions(
tenant_id: Annotated[str, Depends(get_current_tenant_id)],
session: Annotated[AsyncSession, Depends(get_session)],
required: Annotated[bool | None, Query(
description="按是否必填过滤"
)] = None,
) -> JSONResponse:
"""
列出槽位定义
"""
logger.info(
f"Listing slot definitions: tenant={tenant_id}, required={required}"
)
service = SlotDefinitionService(session)
slots = await service.list_slot_definitions(tenant_id, required)
return JSONResponse(
content=[_slot_to_dict(s) for s in slots]
)
@router.post(
"",
operation_id="createSlotDefinition",
summary="Create slot definition",
description="[AC-MRS-07,08] 创建新的槽位定义,可关联已有元数据字段",
status_code=201,
)
async def create_slot_definition(
tenant_id: Annotated[str, Depends(get_current_tenant_id)],
session: Annotated[AsyncSession, Depends(get_session)],
slot_create: SlotDefinitionCreate,
) -> JSONResponse:
"""
[AC-MRS-07,08] 创建槽位定义
"""
logger.info(
f"[AC-MRS-07] Creating slot definition: "
f"tenant={tenant_id}, slot_key={slot_create.slot_key}, "
f"linked_field_id={slot_create.linked_field_id}"
)
service = SlotDefinitionService(session)
try:
slot = await service.create_slot_definition(tenant_id, slot_create)
await session.commit()
except ValueError as e:
return JSONResponse(
status_code=400,
content={
"error_code": "VALIDATION_ERROR",
"message": str(e),
}
)
return JSONResponse(
status_code=201,
content=_slot_to_dict(slot)
)
@router.get(
"/{id}",
operation_id="getSlotDefinition",
summary="Get slot definition by ID",
description="获取单个槽位定义",
)
async def get_slot_definition(
tenant_id: Annotated[str, Depends(get_current_tenant_id)],
session: Annotated[AsyncSession, Depends(get_session)],
id: str,
) -> JSONResponse:
"""
获取单个槽位定义
"""
logger.info(
f"Getting slot definition: tenant={tenant_id}, id={id}"
)
service = SlotDefinitionService(session)
slot = await service.get_slot_definition_with_field(tenant_id, id)
if not slot:
return JSONResponse(
status_code=404,
content={
"error_code": "NOT_FOUND",
"message": f"Slot definition {id} not found",
}
)
return JSONResponse(content=slot)
@router.put(
"/{id}",
operation_id="updateSlotDefinition",
summary="Update slot definition",
description="更新槽位定义",
)
async def update_slot_definition(
tenant_id: Annotated[str, Depends(get_current_tenant_id)],
session: Annotated[AsyncSession, Depends(get_session)],
id: str,
slot_update: SlotDefinitionUpdate,
) -> JSONResponse:
"""
更新槽位定义
"""
logger.info(
f"Updating slot definition: tenant={tenant_id}, id={id}"
)
service = SlotDefinitionService(session)
try:
slot = await service.update_slot_definition(tenant_id, id, slot_update)
except ValueError as e:
return JSONResponse(
status_code=400,
content={
"error_code": "VALIDATION_ERROR",
"message": str(e),
}
)
if not slot:
return JSONResponse(
status_code=404,
content={
"error_code": "NOT_FOUND",
"message": f"Slot definition {id} not found",
}
)
await session.commit()
return JSONResponse(content=_slot_to_dict(slot))
@router.delete(
"/{id}",
operation_id="deleteSlotDefinition",
summary="Delete slot definition",
description="[AC-MRS-16] 删除槽位定义,无需考虑历史数据兼容性",
status_code=204,
)
async def delete_slot_definition(
tenant_id: Annotated[str, Depends(get_current_tenant_id)],
session: Annotated[AsyncSession, Depends(get_session)],
id: str,
) -> JSONResponse:
"""
[AC-MRS-16] 删除槽位定义
"""
logger.info(
f"[AC-MRS-16] Deleting slot definition: tenant={tenant_id}, id={id}"
)
service = SlotDefinitionService(session)
success = await service.delete_slot_definition(tenant_id, id)
if not success:
return JSONResponse(
status_code=404,
content={
"error_code": "NOT_FOUND",
"message": f"Slot definition not found: {id}",
}
)
await session.commit()
return JSONResponse(status_code=204, content=None)