"""Persona data model + shape normalization. A persona has a canonical schema. Fields are split into: - revealable: shown to trainees up front (what a real seller could plausibly know) - latent: hidden until the conversation ends (pain, income, personality, budget, negotiation levers, hidden opener, etc.) Every persona also carries an `intent_tier` (A/B/C), an `initiation_mode` (customer/seller), a `channel` (facebook/line), a set of `pains` with resolution conditions, `negotiation_levers`, and optional `special` flags (e.g. wrong_text). """ from __future__ import annotations from typing import Any DEFAULT_TIERS = ["A", "B", "C"] def ensure_persona_shape(p: dict[str, Any]) -> dict[str, Any]: """Fill defaults so a persona dict is always structurally complete.""" pid = p.get("id") or p.get("name", "persona") base = { "id": pid, "name": p.get("name", ""), "tier": p.get("tier", p.get("intent_tier", "B")), "initiation_mode": p.get("initiation_mode", "customer"), # customer | seller "channel": p.get("channel", "social"), # internal tone hint (scenario overrides) # revealable "profession": p.get("profession", ""), "age_group": p.get("age_group", ""), "location": p.get("location", ""), "product_context": p.get("product_context", ""), # latent (hidden until end) "background": p.get("background", ""), "income": p.get("income", ""), "lifestyle": p.get("lifestyle", ""), "personality": p.get("personality", ""), "communication_style": p.get("communication_style", ""), "budget": p.get("budget", ""), "decision_timeline": p.get("decision_timeline", ""), "goal": p.get("goal", ""), "objections": p.get("objections", []), "pains": p.get("pains", []), "negotiation_levers": p.get("negotiation_levers", []), "opener": p.get("opener", ""), "special": p.get("special", ""), # e.g. "wrong_text" | "" "difficulty": p.get("difficulty", 1), # 1..5 "tolerance": p.get("tolerance", 3), # misses before this persona walks away (temper) "notes": p.get("notes", ""), } # validation if base["tier"] not in DEFAULT_TIERS: base["tier"] = "B" if base["initiation_mode"] not in ("customer", "seller"): base["initiation_mode"] = "customer" return base def revealable_view(p: dict[str, Any]) -> dict[str, Any]: """Return ONLY the fields a trainee may see before/while chatting.""" return { "id": p.get("id"), "name": p.get("name"), "tier": p.get("tier"), "initiation_mode": p.get("initiation_mode"), "profession": p.get("profession"), "age_group": p.get("age_group"), "location": p.get("location"), "product_context": p.get("product_context"), } def full_view(p: dict[str, Any]) -> dict[str, Any]: return ensure_persona_shape(p)