""" 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. [AC-AISVC-99] Prompt template preview endpoint. """ import logging import uuid from typing import Any, Optional from fastapi import APIRouter, Depends, Header, HTTPException from pydantic import BaseModel 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 from app.services.monitoring.prompt_monitor import PromptMonitor 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") class PromptPreviewRequest(BaseModel): """Request body for previewing a prompt template.""" variables: dict[str, str] | None = None sample_history: list[dict[str, str]] | None = None sample_message: str | None = None @router.post("/{tpl_id}/preview") async def preview_template( tpl_id: uuid.UUID, body: PromptPreviewRequest, tenant_id: str = Depends(get_tenant_id), session: AsyncSession = Depends(get_session), ) -> dict[str, Any]: """ [AC-AISVC-99] Preview a prompt template with variable substitution. Returns rendered content and token count estimation. """ logger.info( f"[AC-AISVC-99] Previewing template for tenant={tenant_id}, id={tpl_id}" ) monitor = PromptMonitor(session) result = await monitor.preview_template( tenant_id=tenant_id, template_id=tpl_id, variables=body.variables, sample_history=body.sample_history, sample_message=body.sample_message, ) if not result: raise HTTPException(status_code=404, detail="Template not found") return result.to_dict()