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

297 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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