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

209 lines
6.5 KiB
Python

"""
Prompt Template Management API.
[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58, AC-AISVC-54, AC-AISVC-55] Prompt template CRUD and publish/rollback endpoints.
"""
import logging
import uuid
from typing import Any
from fastapi import APIRouter, Depends, Header, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_session
from app.models.entities import PromptTemplateCreate, PromptTemplateUpdate
from app.services.prompt.template_service import PromptTemplateService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/prompt-templates", tags=["Prompt Management"])
def get_tenant_id(x_tenant_id: str = Header(..., alias="X-Tenant-Id")) -> str:
"""Extract tenant ID from header."""
if not x_tenant_id:
raise HTTPException(status_code=400, detail="X-Tenant-Id header is required")
return x_tenant_id
@router.get("")
async def list_templates(
tenant_id: str = Depends(get_tenant_id),
scene: str | None = None,
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-57] List all prompt templates for a tenant.
"""
logger.info(f"[AC-AISVC-57] Listing prompt templates for tenant={tenant_id}, scene={scene}")
service = PromptTemplateService(session)
templates = await service.list_templates(tenant_id, scene)
data = []
for t in templates:
published_version = await service.get_published_version_info(tenant_id, t.id)
data.append({
"id": str(t.id),
"name": t.name,
"scene": t.scene,
"description": t.description,
"is_default": t.is_default,
"published_version": published_version,
"created_at": t.created_at.isoformat(),
"updated_at": t.updated_at.isoformat(),
})
return {"data": data}
@router.post("", status_code=201)
async def create_template(
body: PromptTemplateCreate,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-52] Create a new prompt template.
"""
logger.info(f"[AC-AISVC-52] Creating prompt template for tenant={tenant_id}, name={body.name}")
service = PromptTemplateService(session)
template = await service.create_template(tenant_id, body)
return {
"id": str(template.id),
"name": template.name,
"scene": template.scene,
"description": template.description,
"is_default": template.is_default,
"created_at": template.created_at.isoformat(),
"updated_at": template.updated_at.isoformat(),
}
@router.get("/{tpl_id}")
async def get_template_detail(
tpl_id: uuid.UUID,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-58] Get prompt template detail with version history.
"""
logger.info(f"[AC-AISVC-58] Getting template detail for tenant={tenant_id}, id={tpl_id}")
service = PromptTemplateService(session)
detail = await service.get_template_detail(tenant_id, tpl_id)
if not detail:
raise HTTPException(status_code=404, detail="Template not found")
return detail
@router.put("/{tpl_id}")
async def update_template(
tpl_id: uuid.UUID,
body: PromptTemplateUpdate,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-53] Update prompt template (creates a new version).
"""
logger.info(f"[AC-AISVC-53] Updating template for tenant={tenant_id}, id={tpl_id}")
service = PromptTemplateService(session)
template = await service.update_template(tenant_id, tpl_id, body)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
published_version = await service.get_published_version_info(tenant_id, template.id)
return {
"id": str(template.id),
"name": template.name,
"scene": template.scene,
"description": template.description,
"is_default": template.is_default,
"published_version": published_version,
"created_at": template.created_at.isoformat(),
"updated_at": template.updated_at.isoformat(),
}
@router.post("/{tpl_id}/publish")
async def publish_template(
tpl_id: uuid.UUID,
body: dict[str, int],
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-54] Publish a specific version of the template.
"""
version = body.get("version")
if version is None:
raise HTTPException(status_code=400, detail="version is required")
logger.info(
f"[AC-AISVC-54] Publishing template version for tenant={tenant_id}, "
f"id={tpl_id}, version={version}"
)
service = PromptTemplateService(session)
success = await service.publish_version(tenant_id, tpl_id, version)
if not success:
raise HTTPException(status_code=404, detail="Template or version not found")
return {"success": True, "message": f"Version {version} published successfully"}
@router.post("/{tpl_id}/rollback")
async def rollback_template(
tpl_id: uuid.UUID,
body: dict[str, int],
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-55] Rollback to a specific historical version.
"""
version = body.get("version")
if version is None:
raise HTTPException(status_code=400, detail="version is required")
logger.info(
f"[AC-AISVC-55] Rolling back template for tenant={tenant_id}, "
f"id={tpl_id}, version={version}"
)
service = PromptTemplateService(session)
success = await service.rollback_version(tenant_id, tpl_id, version)
if not success:
raise HTTPException(status_code=404, detail="Template or version not found")
return {"success": True, "message": f"Rolled back to version {version} successfully"}
@router.delete("/{tpl_id}", status_code=204)
async def delete_template(
tpl_id: uuid.UUID,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> None:
"""
Delete a prompt template and all its versions.
"""
logger.info(f"Deleting template for tenant={tenant_id}, id={tpl_id}")
service = PromptTemplateService(session)
success = await service.delete_template(tenant_id, tpl_id)
if not success:
raise HTTPException(status_code=404, detail="Template not found")