""" Decomposition Template API. [AC-IDSMETA-21, AC-IDSMETA-22] 拆解模板管理接口,支持文本拆解为结构化数据。 """ 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 ( DecompositionRequest, DecompositionTemplateCreate, DecompositionTemplateStatus, DecompositionTemplateUpdate, ) from app.services.decomposition_template_service import DecompositionTemplateService logger = logging.getLogger(__name__) router = APIRouter(prefix="/admin/decomposition-templates", tags=["DecompositionTemplates"]) 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 @router.get( "", operation_id="listDecompositionTemplates", summary="List decomposition templates", description="[AC-IDSMETA-22] 获取拆解模板列表,支持按状态过滤", ) async def list_templates( tenant_id: Annotated[str, Depends(get_current_tenant_id)], session: Annotated[AsyncSession, Depends(get_session)], status: Annotated[str | None, Query( description="按状态过滤: draft/published/archived" )] = None, ) -> JSONResponse: """ [AC-IDSMETA-22] 列出拆解模板 """ logger.info( f"[AC-IDSMETA-22] Listing decomposition templates: " f"tenant={tenant_id}, status={status}" ) if status and status not in [s.value for s in DecompositionTemplateStatus]: return JSONResponse( status_code=400, content={ "code": "INVALID_STATUS", "message": f"Invalid status: {status}", "details": { "valid_values": [s.value for s in DecompositionTemplateStatus] } } ) service = DecompositionTemplateService(session) templates = await service.list_templates(tenant_id, status) return JSONResponse( content={ "items": [ { "id": str(t.id), "name": t.name, "description": t.description, "version": t.version, "status": t.status, "template_schema": t.template_schema, "extraction_hints": t.extraction_hints, "example_input": t.example_input, "example_output": t.example_output, "created_at": t.created_at.isoformat(), "updated_at": t.updated_at.isoformat(), } for t in templates ] } ) @router.post( "", operation_id="createDecompositionTemplate", summary="Create decomposition template", description="[AC-IDSMETA-22] 创建新的拆解模板", status_code=201, ) async def create_template( tenant_id: Annotated[str, Depends(get_current_tenant_id)], session: Annotated[AsyncSession, Depends(get_session)], template_create: DecompositionTemplateCreate, ) -> JSONResponse: """ [AC-IDSMETA-22] 创建拆解模板 """ logger.info( f"[AC-IDSMETA-22] Creating decomposition template: " f"tenant={tenant_id}, name={template_create.name}" ) service = DecompositionTemplateService(session) template = await service.create_template(tenant_id, template_create) await session.commit() return JSONResponse( status_code=201, content={ "id": str(template.id), "name": template.name, "description": template.description, "version": template.version, "status": template.status, "template_schema": template.template_schema, "extraction_hints": template.extraction_hints, "example_input": template.example_input, "example_output": template.example_output, "created_at": template.created_at.isoformat(), "updated_at": template.updated_at.isoformat(), } ) @router.get( "/latest", operation_id="getLatestPublishedTemplate", summary="Get latest published template", description="[AC-IDSMETA-22] 获取最近生效的发布版本模板", ) async def get_latest_template( tenant_id: Annotated[str, Depends(get_current_tenant_id)], session: Annotated[AsyncSession, Depends(get_session)], ) -> JSONResponse: """ [AC-IDSMETA-22] 获取最近生效的发布版本模板 """ logger.info( f"[AC-IDSMETA-22] Getting latest published template: tenant={tenant_id}" ) service = DecompositionTemplateService(session) template = await service.get_latest_published_template(tenant_id) if not template: return JSONResponse( status_code=404, content={ "code": "NOT_FOUND", "message": "No published template found", } ) return JSONResponse( content={ "id": str(template.id), "name": template.name, "description": template.description, "version": template.version, "status": template.status, "template_schema": template.template_schema, "extraction_hints": template.extraction_hints, "example_input": template.example_input, "example_output": template.example_output, "created_at": template.created_at.isoformat(), "updated_at": template.updated_at.isoformat(), } ) @router.put( "/{id}", operation_id="updateDecompositionTemplate", summary="Update decomposition template", description="[AC-IDSMETA-22] 更新拆解模板,支持状态切换", ) async def update_template( tenant_id: Annotated[str, Depends(get_current_tenant_id)], session: Annotated[AsyncSession, Depends(get_session)], id: str, template_update: DecompositionTemplateUpdate, ) -> JSONResponse: """ [AC-IDSMETA-22] 更新拆解模板 """ logger.info( f"[AC-IDSMETA-22] Updating decomposition template: " f"tenant={tenant_id}, id={id}" ) if template_update.status and template_update.status not in [s.value for s in DecompositionTemplateStatus]: return JSONResponse( status_code=400, content={ "code": "INVALID_STATUS", "message": f"Invalid status: {template_update.status}", "details": { "valid_values": [s.value for s in DecompositionTemplateStatus] } } ) service = DecompositionTemplateService(session) template = await service.update_template(tenant_id, id, template_update) if not template: return JSONResponse( status_code=404, content={ "code": "NOT_FOUND", "message": f"Template {id} not found", } ) await session.commit() return JSONResponse( content={ "id": str(template.id), "name": template.name, "description": template.description, "version": template.version, "status": template.status, "template_schema": template.template_schema, "extraction_hints": template.extraction_hints, "example_input": template.example_input, "example_output": template.example_output, "created_at": template.created_at.isoformat(), "updated_at": template.updated_at.isoformat(), } ) @router.post( "/decompose", operation_id="decomposeText", summary="Decompose text to structured data", description="[AC-IDSMETA-21] 将待录入文本拆解为固定模板输出", ) async def decompose_text( tenant_id: Annotated[str, Depends(get_current_tenant_id)], session: Annotated[AsyncSession, Depends(get_session)], request: DecompositionRequest, ) -> JSONResponse: """ [AC-IDSMETA-21] 将待录入文本拆解为固定模板输出 如果不指定 template_id,则使用最近生效的发布版本模板 """ logger.info( f"[AC-IDSMETA-21] Decomposing text: tenant={tenant_id}, " f"template_id={request.template_id}, text_length={len(request.text)}" ) from app.services.llm import get_llm_client llm_client = get_llm_client() service = DecompositionTemplateService(session, llm_client) result = await service.decompose_text(tenant_id, request) if not result.success: return JSONResponse( status_code=400, content={ "code": "DECOMPOSITION_FAILED", "message": result.error, "details": { "template_id": result.template_id, "template_version": result.template_version, "latency_ms": result.latency_ms, } } ) return JSONResponse( content={ "success": result.success, "data": result.data, "template_id": result.template_id, "template_version": result.template_version, "confidence": result.confidence, "latency_ms": result.latency_ms, } )