"""Persona catalog routes.""" from __future__ import annotations from typing import Annotated, Any, Literal from fastapi import APIRouter, Depends, HTTPException, Response, status from pydantic import BaseModel from ..deps import CurrentPrincipal, Principal, Role, require_role from ..persona_repository import ( CatalogPersona, PersonaReviewAction, PersonaReviewItem, list_catalog_personas, list_persona_review_queue, update_persona_review_status, ) router = APIRouter(prefix="/personas", tags=["personas"]) TeacherOrAdmin = Annotated[Principal, Depends(require_role(Role.TEACHER, Role.ADMIN))] class PersonaSummary(BaseModel): code: str display_name: str difficulty: str theory_target: list[str] demographics: dict[str, Any] presenting_summary: str voice_preset: str | None = None source: str = "database" degraded: bool = False class PersonaReviewSummary(BaseModel): persona_id: str code: str version: int status: Literal["draft", "review", "approved", "archived"] display_name: str difficulty: str theory_target: list[str] source_provenance: str is_synthetic: bool created_at: str | None = None approved_at: str | None = None class PersonaReviewDecisionRequest(BaseModel): action: PersonaReviewAction def _first_text_value(data: dict[str, Any]) -> str: for value in data.values(): if isinstance(value, str) and value.strip(): return value.strip() return "" def _summary(entry: CatalogPersona) -> PersonaSummary: card = entry.card return PersonaSummary( code=card.code, display_name=card.display_name, difficulty=card.difficulty, theory_target=card.theory_target, demographics=card.demographics, presenting_summary=_first_text_value(card.presenting), source=entry.source, degraded=entry.degraded, ) def _review_summary(entry: PersonaReviewItem) -> PersonaReviewSummary: return PersonaReviewSummary( persona_id=entry.persona_id, code=entry.code, version=entry.version, status=entry.status, display_name=entry.display_name, difficulty=entry.difficulty, theory_target=entry.theory_target, source_provenance=entry.source_provenance, is_synthetic=entry.is_synthetic, created_at=entry.created_at, approved_at=entry.approved_at, ) def _ensure_teacher_or_admin(principal: Principal) -> None: if principal.role not in {Role.TEACHER, Role.ADMIN}: raise HTTPException(status.HTTP_403_FORBIDDEN, detail="only teachers and admins can review personas") @router.get("", response_model=list[PersonaSummary]) async def list_personas(response: Response, _principal: CurrentPrincipal) -> list[PersonaSummary]: """Return latest approved personas from app.persona_card.""" try: personas = await list_catalog_personas() except Exception as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, detail="persona catalog database unavailable", ) from exc if any(entry.degraded for entry in personas): response.headers["X-Vignette-Degraded"] = "true" response.headers["X-Vignette-Catalog-Source"] = "seed_fallback" else: response.headers["X-Vignette-Catalog-Source"] = "database" return [_summary(entry) for entry in personas] @router.get("/review", response_model=list[PersonaReviewSummary]) async def list_persona_reviews(principal: TeacherOrAdmin) -> list[PersonaReviewSummary]: """Return draft/review personas awaiting faculty approval.""" _ensure_teacher_or_admin(principal) try: queue = await list_persona_review_queue(role=principal.role.value) except Exception as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, detail="persona review queue database unavailable", ) from exc return [_review_summary(entry) for entry in queue] @router.post("/review/{persona_id}", response_model=PersonaReviewSummary) async def decide_persona_review( persona_id: str, request: PersonaReviewDecisionRequest, principal: TeacherOrAdmin, ) -> PersonaReviewSummary: """Approve a persona for learners or return it to draft for changes.""" _ensure_teacher_or_admin(principal) try: updated = await update_persona_review_status( persona_id=persona_id, action=request.action, reviewer_id=principal.user_id, role=principal.role.value, ) except ValueError as exc: raise HTTPException(status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc except Exception as exc: raise HTTPException( status.HTTP_503_SERVICE_UNAVAILABLE, detail="persona review update database unavailable", ) from exc if updated is None: raise HTTPException( status.HTTP_404_NOT_FOUND, detail="persona review item not found or not pending review", ) return _review_summary(updated)