Sales Trainer v0.1: corporate sales-training simulator (Flask+Vue, 15 personas, chat simulator, judge, analytics)
- Auth/roles (no self-reg), admin user provision, JWT - Analyze: sales kit + initial pain-fit from form/upload - Persona generator: 15 personas (5/tier) w/ pain variety, negotiation, init mode, channel, latent/revealable, wrong_text special - Chat simulator: per-mode initiation, one-shot, hidden signals, judge-LLM debrief+coaching - Trainee loop: win/lose board, weak-areas, user-generated personas - Admin analytics; EN+TH Vue SPA served by Flask - Deploy: Dockerfile, docker-compose, README, eng-log + HANDOFF - Tests (mock LLM): m0/m1/routes/e2e all pass
This commit is contained in:
75
backend/app/services/store.py
Normal file
75
backend/app/services/store.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""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", "facebook"), # facebook | line
|
||||
# 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
|
||||
"notes": p.get("notes", ""),
|
||||
}
|
||||
# validate
|
||||
if base["tier"] not in DEFAULT_TIERS:
|
||||
base["tier"] = "B"
|
||||
if base["initiation_mode"] not in ("customer", "seller"):
|
||||
base["initiation_mode"] = "customer"
|
||||
if base["channel"] not in ("facebook", "line"):
|
||||
base["channel"] = "facebook"
|
||||
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"),
|
||||
"channel": p.get("channel"),
|
||||
"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)
|
||||
Reference in New Issue
Block a user