- list_groups returns lightweight summaries (id/title/status/persona_count + product) —
never the full personas array, so Training no longer shows a long JSON blob.
- PersonaForm toLines extracts .description/.text from pain/objection objects (fixes
'[object Object]'); save re-wraps pains as {description}.
- GroupEdit persona cards get a 'แชท' (MessageSquare) button so you can start chatting
right where you land after creating (chat was only on Personas page before).
- channel: default neutral 'social', removed facebook/line validation + removed from
revealable_view (scenario now sets the tone, not fb/line).
All 9 backend suites pass. Rebuilt dist.
74 lines
2.9 KiB
Python
74 lines
2.9 KiB
Python
"""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)
|