P4-P7 페르소나와 Tailnet 허용 보강

This commit is contained in:
Yun Chan 2026-06-27 19:04:46 +09:00
parent 17b114f647
commit 7f4a2d90c7
12 changed files with 121 additions and 30 deletions

View file

@ -10,6 +10,7 @@ from __future__ import annotations
import json
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable, Literal, cast
from .config import settings
@ -18,6 +19,7 @@ from .services.persona import PersonaCard, SEED_PERSONAS, get_seed_persona
SEED_VERSION = 1
REPO_PERSONA_DIR = Path(__file__).resolve().parents[3] / "data" / "personas"
PersonaStatus = Literal["draft", "review", "approved", "archived"]
_CARD_COLUMNS = """
@ -70,6 +72,46 @@ def seed_persona_id(code: str) -> str:
return str(uuid.uuid5(uuid.NAMESPACE_URL, f"vignette:persona:{code.upper()}"))
def _card_from_payload(payload: dict[str, Any]) -> PersonaCard:
return PersonaCard(
code=str(payload["code"]).strip().upper(),
display_name=str(payload["display_name"]),
difficulty=str(payload["difficulty"]),
theory_target=_string_list(payload.get("theory_target")),
demographics=_json_dict(payload.get("demographics")),
presenting=_json_dict(payload.get("presenting")),
history=_json_dict(payload.get("history")),
big5=_json_dict(payload.get("big5")),
resistance=_json_dict(payload.get("resistance")),
speech_style=_json_dict(payload.get("speech_style")),
affect_baseline=_json_dict(payload.get("affect_baseline")),
ccd=_json_dict(payload.get("ccd")),
dsm5_dimensional=_json_dict(payload.get("dsm5_dimensional")),
source_provenance=str(payload.get("source_provenance") or ""),
is_synthetic=bool(payload.get("is_synthetic", True)),
)
def load_file_personas(persona_dir: Path = REPO_PERSONA_DIR) -> list[PersonaCard]:
"""Load repository-managed persona cards that are not hard-coded in persona.py."""
if not persona_dir.exists():
return []
cards: list[PersonaCard] = []
for path in sorted(persona_dir.glob("P*.json")):
payload = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
cards.append(_card_from_payload(payload))
return cards
def built_in_personas() -> list[PersonaCard]:
"""Return deterministic built-in catalog cards from code seeds plus repo JSON cards."""
cards: dict[str, PersonaCard] = {card.code.upper(): card for card in SEED_PERSONAS.values()}
for card in load_file_personas():
cards.setdefault(card.code.upper(), card)
return [cards[code] for code in sorted(cards)]
def _json_dict(value: Any) -> dict[str, Any]:
if value is None:
return {}
@ -157,7 +199,10 @@ def persona_draft_record_from_row(row: Any) -> PersonaDraftRecord:
def seed_fallback_persona(code: str) -> CatalogPersona | None:
card = get_seed_persona(code)
normalized = code.strip().upper()
card = get_seed_persona(normalized)
if card is None:
card = next((entry for entry in load_file_personas() if entry.code.upper() == normalized), None)
if card is None:
return None
return CatalogPersona(
@ -178,7 +223,7 @@ def seed_fallback_personas() -> list[CatalogPersona]:
source="seed_fallback",
degraded=True,
)
for card in SEED_PERSONAS.values()
for card in built_in_personas()
]
@ -187,7 +232,7 @@ async def materialize_seed_personas() -> int:
get_pool()
count = 0
async with acquire(role="admin") as conn:
for card in SEED_PERSONAS.values():
for card in built_in_personas():
await conn.execute(
"""
INSERT INTO app.persona_card (
@ -574,6 +619,7 @@ __all__ = [
"PersonaReviewAction",
"PersonaStatus",
"SEED_VERSION",
"built_in_personas",
"card_from_row",
"catalog_persona_from_row",
"create_persona_draft",
@ -583,6 +629,7 @@ __all__ = [
"list_approved_personas",
"list_catalog_personas",
"list_persona_review_queue",
"load_file_personas",
"materialize_seed_personas",
"persona_draft_record_from_row",
"persona_review_item_from_row",

View file

@ -208,6 +208,36 @@ def _draft_payload(
class PersonaApprovalBoundaryTest(unittest.IsolatedAsyncioTestCase):
async def test_repository_loads_repo_persona_files_for_p4_to_p7(self) -> None:
cards = persona_repository.load_file_personas()
self.assertEqual([card.code for card in cards], ["P4", "P5", "P6", "P7"])
self.assertEqual(cards[0].display_name, "하늘(가명) · 고2 · 학업/시험 불안")
self.assertEqual(cards[0].difficulty, "easy")
self.assertIn("cbt", cards[0].theory_target)
async def test_seed_fallback_includes_repo_persona_files(self) -> None:
entry = persona_repository.seed_fallback_persona("p4")
self.assertIsNotNone(entry)
assert entry is not None
self.assertEqual(entry.card.code, "P4")
self.assertEqual(entry.source, "seed_fallback")
self.assertTrue(entry.degraded)
async def test_materialize_seed_personas_upserts_code_and_repo_file_cards(self) -> None:
conn = _PersonaCardConn([])
with (
patch.object(persona_repository, "get_pool", return_value=object()),
patch.object(persona_repository, "acquire", lambda **_: _Acquire(conn)),
):
count = await persona_repository.materialize_seed_personas()
self.assertEqual(count, 7)
inserted_codes = [args[1] for _, args in conn.execute_calls]
self.assertEqual(inserted_codes, ["P1", "P2", "P3", "P4", "P5", "P6", "P7"])
async def test_catalog_repository_lists_only_approved_personas(self) -> None:
conn = _PersonaCardConn(
[

View file

@ -9,10 +9,16 @@ const nodeEnv = (
process?: { env?: { VITE_ALLOWED_HOSTS?: string; VITE_API_PROXY_TARGET?: string } };
}
).process?.env;
const allowedHosts = (nodeEnv?.VITE_ALLOWED_HOSTS ?? "")
.split(",")
.map((host: string) => host.trim())
.filter(Boolean);
const defaultAllowedHosts = ["alpaca-home.taile93291.ts.net"];
const allowedHosts = Array.from(
new Set([
...defaultAllowedHosts,
...(nodeEnv?.VITE_ALLOWED_HOSTS ?? "")
.split(",")
.map((host: string) => host.trim())
.filter(Boolean),
]),
);
const apiProxyTarget = nodeEnv?.VITE_API_PROXY_TARGET?.trim() || "http://127.0.0.1:8000";
export default defineConfig({