297 lines
9.5 KiB
Python
297 lines
9.5 KiB
Python
"""
|
||
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,
|
||
}
|
||
)
|