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

297 lines
9.5 KiB
Python
Raw Normal View History

"""
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,
}
)