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

251 lines
7.7 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.
[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()