대시보드 폴드아웃/드릴다운 정리 + 페르소나 역린·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

@ -10,7 +10,7 @@ from __future__ import annotations
import json
import uuid
from dataclasses import dataclass
from typing import Any, Iterable
from typing import Any, Iterable, Literal, cast
from .config import settings
from .db import acquire, get_pool
@ -18,6 +18,7 @@ from .services.persona import PersonaCard, SEED_PERSONAS, get_seed_persona
SEED_VERSION = 1
PersonaStatus = Literal["draft", "review", "approved", "archived"]
_CARD_COLUMNS = """
persona_id, code, version, status, display_name, difficulty, theory_target,
@ -25,6 +26,15 @@ _CARD_COLUMNS = """
affect_baseline, ccd, dsm5_dimensional, source_provenance, is_synthetic
"""
_REVIEW_COLUMNS = """
persona_id, code, version, status, display_name, difficulty, theory_target,
source_provenance, is_synthetic, created_at, approved_at
"""
_PERSONA_STATUSES = {"draft", "review", "approved", "archived"}
_REVIEW_QUEUE_STATUSES = ("draft", "review")
PersonaReviewAction = Literal["approve", "reject"]
@dataclass(frozen=True, slots=True)
class CatalogPersona:
@ -35,6 +45,21 @@ class CatalogPersona:
degraded: bool = False
@dataclass(frozen=True, slots=True)
class PersonaReviewItem:
persona_id: str
code: str
version: int
status: PersonaStatus
display_name: str
difficulty: str
theory_target: list[str]
source_provenance: str
is_synthetic: bool
created_at: str | None
approved_at: str | None
def seed_persona_id(code: str) -> str:
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"vignette:persona:{code.upper()}"))
@ -54,6 +79,24 @@ def _string_list(value: Iterable[Any] | None) -> list[str]:
return [str(item) for item in value]
def _optional_text(value: Any) -> str | None:
if value is None:
return None
isoformat = getattr(value, "isoformat", None)
if callable(isoformat):
return str(isoformat())
return str(value)
def _normalize_statuses(statuses: Iterable[str]) -> list[str]:
normalized: list[str] = []
for status in statuses:
value = str(status).strip().lower()
if value in _PERSONA_STATUSES and value not in normalized:
normalized.append(value)
return normalized
def card_from_row(row: Any) -> PersonaCard:
return PersonaCard(
code=str(row["code"]).upper(),
@ -84,6 +127,22 @@ def catalog_persona_from_row(row: Any) -> CatalogPersona:
)
def persona_review_item_from_row(row: Any) -> PersonaReviewItem:
return PersonaReviewItem(
persona_id=str(row["persona_id"]),
code=str(row["code"]).upper(),
version=int(row["version"]),
status=cast(PersonaStatus, str(row["status"]).lower()),
display_name=str(row["display_name"]),
difficulty=str(row["difficulty"]),
theory_target=_string_list(row["theory_target"]),
source_provenance=str(row["source_provenance"] or ""),
is_synthetic=bool(row["is_synthetic"]),
created_at=_optional_text(row["created_at"]),
approved_at=_optional_text(row["approved_at"]),
)
def seed_fallback_persona(code: str) -> CatalogPersona | None:
card = get_seed_persona(code)
if card is None:
@ -209,6 +268,93 @@ async def get_approved_persona(code: str) -> CatalogPersona | None:
return catalog_persona_from_row(row) if row is not None else None
async def list_persona_review_queue(
*,
role: str,
statuses: Iterable[str] = _REVIEW_QUEUE_STATUSES,
) -> list[PersonaReviewItem]:
if role not in {"teacher", "admin"}:
raise ValueError("persona review queue requires teacher or admin role")
status_values = _normalize_statuses(statuses)
if not status_values:
return []
get_pool()
async with acquire(role=role) as conn:
rows = await conn.fetch(
f"""
SELECT {_REVIEW_COLUMNS}
FROM app.persona_card
WHERE status = ANY($1::text[])
ORDER BY
CASE status
WHEN 'review' THEN 0
WHEN 'draft' THEN 1
ELSE 2
END,
code,
version DESC
""",
status_values,
)
return [persona_review_item_from_row(row) for row in rows]
async def update_persona_review_status(
*,
persona_id: str,
action: PersonaReviewAction,
reviewer_id: str,
role: str,
) -> PersonaReviewItem | None:
if role not in {"teacher", "admin"}:
raise ValueError("persona review update requires teacher or admin role")
if action not in {"approve", "reject"}:
raise ValueError("unsupported persona review action")
next_status = "approved" if action == "approve" else "draft"
approved_by = reviewer_id if action == "approve" else None
approved_at_expr = "now()" if action == "approve" else "NULL"
get_pool()
async with acquire(role=role, user_id=reviewer_id) as conn:
row = await conn.fetchrow(
f"""
UPDATE app.persona_card
SET
status = $2,
approved_by = $3::uuid,
approved_at = {approved_at_expr}
WHERE persona_id = $1::uuid
AND status IN ('draft', 'review')
RETURNING {_REVIEW_COLUMNS}
""",
persona_id,
next_status,
approved_by,
)
if row is None:
return None
await conn.execute(
"""
INSERT INTO audit.audit_log (
actor_uid, action, target_kind, target_id, detail
)
VALUES ($1::uuid, $2, $3, $4, $5::jsonb)
""",
reviewer_id,
f"persona_{action}",
"persona_card",
persona_id,
{
"next_status": next_status,
"code": str(row["code"]).upper(),
"version": int(row["version"]),
},
)
return persona_review_item_from_row(row)
async def list_catalog_personas() -> list[CatalogPersona]:
try:
return await list_approved_personas()
@ -229,6 +375,9 @@ async def get_catalog_persona(code: str) -> CatalogPersona | None:
__all__ = [
"CatalogPersona",
"PersonaReviewItem",
"PersonaReviewAction",
"PersonaStatus",
"SEED_VERSION",
"card_from_row",
"catalog_persona_from_row",
@ -236,8 +385,11 @@ __all__ = [
"get_catalog_persona",
"list_approved_personas",
"list_catalog_personas",
"list_persona_review_queue",
"materialize_seed_personas",
"persona_review_item_from_row",
"seed_fallback_persona",
"seed_fallback_personas",
"seed_persona_id",
"update_persona_review_status",
]