280 lines
8.8 KiB
Python
280 lines
8.8 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
|
|
|
|
|
|
class PromptTemplateUpdateAPI(BaseModel):
|
|
"""API model for updating prompt template with metadata field mapping."""
|
|
name: str | None = None
|
|
scene: str | None = None
|
|
description: str | None = None
|
|
system_instruction: str | None = None
|
|
variables: list[dict[str, Any]] | None = None
|
|
is_default: bool | None = None
|
|
metadata: dict[str, Any] | None = None
|
|
|
|
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: PromptTemplateUpdateAPI,
|
|
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).
|
|
[AC-IDSMETA-16] Return current_content and metadata for frontend display.
|
|
"""
|
|
logger.info(f"[AC-AISVC-53] Updating template for tenant={tenant_id}, id={tpl_id}")
|
|
|
|
service = PromptTemplateService(session)
|
|
|
|
# Convert API model to entity model (metadata -> metadata_)
|
|
update_data = PromptTemplateUpdate(
|
|
name=body.name,
|
|
scene=body.scene,
|
|
description=body.description,
|
|
system_instruction=body.system_instruction,
|
|
variables=body.variables,
|
|
is_default=body.is_default,
|
|
metadata_=body.metadata,
|
|
)
|
|
|
|
template = await service.update_template(tenant_id, tpl_id, update_data)
|
|
|
|
if not template:
|
|
raise HTTPException(status_code=404, detail="Template not found")
|
|
|
|
published_version = await service.get_published_version_info(tenant_id, template.id)
|
|
|
|
# Get latest version content for current_content
|
|
latest_version = await service._get_latest_version(template.id)
|
|
|
|
return {
|
|
"id": str(template.id),
|
|
"name": template.name,
|
|
"scene": template.scene,
|
|
"description": template.description,
|
|
"is_default": template.is_default,
|
|
"current_content": latest_version.system_instruction if latest_version else None,
|
|
"metadata": template.metadata_,
|
|
"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()
|