대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·misconduct 반응 + 게이트웨이 격리·RAG 비차단 수정

SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리

페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침

버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)

검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
This commit is contained in:
Yun Chan 2026-06-27 02:30:46 +09:00
parent cb2aebd76c
commit 085460b5e0
327 changed files with 31226 additions and 1829 deletions

View file

@ -2,15 +2,23 @@
from __future__ import annotations
from typing import Any
from typing import Annotated, Any, Literal
from fastapi import APIRouter, HTTPException, Response, status
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from ..deps import CurrentPrincipal
from ..persona_repository import CatalogPersona, list_catalog_personas
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):
@ -25,6 +33,24 @@ class PersonaSummary(BaseModel):
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():
@ -46,6 +72,27 @@ def _summary(entry: CatalogPersona) -> PersonaSummary:
)
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."""
@ -64,3 +111,47 @@ async def list_personas(response: Response, _principal: CurrentPrincipal) -> lis
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)