InsuranceBot / tools /audit /personas.py
rohitsar567's picture
feat(audit): 100-persona multi-scenario simulation framework
3dab53c
Raw
History Blame Contribute Delete
10.4 kB
"""Generate 100 diverse personas for end-to-end bot audit.
Each persona is the product of:
- 10 archetypes (intent shape: first-buyer, upgrader, senior-care, …)
- 10 demographic profiles (age Γ— dependents Γ— income Γ— city tier)
- 1 conversational style (deterministic-pick from a curated list)
The cross-product is engineered so that no two personas have the same
(archetype, demo, style) triple. Each persona is stable across runs (the
order is index-based), so report diffs reveal regressions rather than
shuffle noise.
Run as a script:
python tools/audit/personas.py
β†’ writes tools/audit/personas.json (100 entries)
Read from another script:
from tools.audit.personas import generate
personas = generate() # β†’ list[dict]
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
# ----------------------------------------------------------------------------
# Archetypes β€” the user's INTENT shape. Drives flow design downstream.
# ----------------------------------------------------------------------------
ARCHETYPES = [
{
"id": "first_buyer",
"label": "First-time health insurance buyer",
"primary_goal": "first_buy",
"anchor_concerns": ["coverage_breadth", "premium_value", "claim_settlement"],
},
{
"id": "upgrader",
"label": "Existing-cover upgrader",
"primary_goal": "upgrade",
"anchor_concerns": ["sum_insured_size", "ped_waiting", "restoration_benefit"],
},
{
"id": "senior_care",
"label": "Buying for senior parents",
"primary_goal": "first_buy",
"anchor_concerns": ["parents_age_max", "ped_waiting", "specific_disease_waiting"],
},
{
"id": "comparer",
"label": "Comparing 2-3 specific policies",
"primary_goal": "compare_specific",
"anchor_concerns": ["sub_limits", "network_hospitals", "no_claim_bonus"],
},
{
"id": "anxious",
"label": "Anxious skeptic β€” claim-denial fears",
"primary_goal": "first_buy",
"anchor_concerns": ["claim_settlement", "exclusions", "free_look"],
},
{
"id": "savvy",
"label": "Financially literate, asks pointed questions",
"primary_goal": "compare_specific",
"anchor_concerns": ["irdai_mandate", "tax_treatment", "ombudsman"],
},
{
"id": "tax_planner",
"label": "Tax-planning oriented",
"primary_goal": "tax_planning",
"anchor_concerns": ["section_80d", "premium_band", "sum_insured_size"],
},
{
"id": "low_trust",
"label": "Low trust in insurers; needs proof",
"primary_goal": "first_buy",
"anchor_concerns": ["claim_settlement", "reviews", "regulatory_overlay"],
},
{
"id": "code_switcher",
"label": "Switches English ↔ Hinglish mid-flow",
"primary_goal": "first_buy",
"anchor_concerns": ["language_switch", "premium_band", "coverage_breadth"],
},
{
"id": "specific_condition",
"label": "Has a specific condition driving the buy",
"primary_goal": "upgrade",
"anchor_concerns": ["ped_waiting", "specific_disease_waiting", "sub_limits"],
},
]
# ----------------------------------------------------------------------------
# Demographic variations β€” 10 stable cells across age Γ— family Γ— income Γ— city.
# Each cell is a different life situation; together they span ~70% of Indian
# middle-class health-insurance buyers.
# ----------------------------------------------------------------------------
DEMOGRAPHICS = [
{"age": 24, "dependents": "self", "income_band": "under_5L", "location_tier": "metro", "marital": "single"},
{"age": 28, "dependents": "self", "income_band": "5L-10L", "location_tier": "metro", "marital": "single"},
{"age": 31, "dependents": "self+spouse", "income_band": "10L-25L", "location_tier": "metro", "marital": "married"},
{"age": 34, "dependents": "self+spouse+kids", "income_band": "10L-25L", "location_tier": "tier1", "marital": "married_kids"},
{"age": 38, "dependents": "self+spouse+kids", "income_band": "25L+", "location_tier": "metro", "marital": "married_kids"},
{"age": 42, "dependents": "self+spouse+kids+parents", "income_band": "25L+", "location_tier": "metro", "marital": "sandwich_gen"},
{"age": 47, "dependents": "self+spouse+parents", "income_band": "10L-25L", "location_tier": "tier1", "marital": "married_no_kids"},
{"age": 52, "dependents": "self+spouse", "income_band": "10L-25L", "location_tier": "tier2", "marital": "empty_nester"},
{"age": 58, "dependents": "self+spouse", "income_band": "5L-10L", "location_tier": "tier2", "marital": "near_retire"},
{"age": 63, "dependents": "self+spouse", "income_band": "under_5L", "location_tier": "tier3", "marital": "retired"},
]
# ----------------------------------------------------------------------------
# Conversational styles β€” how the persona TALKS, independent of what they
# want. The 10 styles cycle so each (archetype Γ— demo) cell gets a different
# voice across the 100 personas.
# ----------------------------------------------------------------------------
STYLES = [
{"id": "terse", "label": "Terse β€” 3-7 word answers", "lang": "en", "hedges": []},
{"id": "verbose", "label": "Verbose β€” paragraphs of context", "lang": "en", "hedges": ["um, ", "well, ", "you know, "]},
{"id": "hinglish", "label": "English with Hindi words sprinkled", "lang": "hinglish", "hedges": []},
{"id": "formal_en", "label": "Formal English, full sentences", "lang": "en", "hedges": []},
{"id": "casual_en", "label": "Casual English with typos and lowercase", "lang": "en", "hedges": ["btw ", "lol ", "tbh "]},
{"id": "hindi_primary","label": "Mostly Hindi (Devanagari letters)", "lang": "hi", "hedges": []},
{"id": "anxious_q", "label": "Asks lots of follow-up questions", "lang": "en", "hedges": ["but ", "wait, ", "what about "]},
{"id": "numbers_heavy","label": "Quotes specific numbers and policies", "lang": "en", "hedges": []},
{"id": "stream", "label": "Stream-of-consciousness, rambles", "lang": "en", "hedges": ["uh ", "hmm ", "and ", "also "]},
{"id": "tester", "label": "Tries to trip up the bot", "lang": "en", "hedges": []},
]
# ----------------------------------------------------------------------------
# Synthetic name pool β€” 10 first names Γ— 10 surnames covers our 100 personas
# without recycling the same identity. Names are common Indian names with a
# light regional spread (north + south + west).
# ----------------------------------------------------------------------------
FIRST_NAMES = ["Aarav", "Diya", "Rohan", "Ananya", "Vikram", "Priya", "Karthik", "Meera", "Saif", "Ishita"]
SURNAMES = ["Sharma", "Iyer", "Mehta", "Reddy", "Banerjee", "Kapoor", "Nair", "Joshi", "Khan", "Pillai"]
# Health condition presets keyed by archetype + age bucket.
def _condition_for(archetype_id: str, age: int) -> list[str]:
if archetype_id == "specific_condition":
if age < 35:
return ["asthma"]
if age < 50:
return ["hypertension"]
return ["diabetes", "hypertension"]
if archetype_id == "senior_care":
return [] # parents' conditions handled separately in flow
if age >= 50:
return ["hypertension"] # age-bucket baseline
return []
def generate() -> list[dict[str, Any]]:
personas: list[dict[str, Any]] = []
pid = 1
for ai, arch in enumerate(ARCHETYPES):
for di, demo in enumerate(DEMOGRAPHICS):
style = STYLES[(ai + di) % len(STYLES)] # rotates so styles spread evenly across archetypes
name_first = FIRST_NAMES[(ai + di) % len(FIRST_NAMES)]
name_last = SURNAMES[(ai * 3 + di * 7) % len(SURNAMES)]
persona = {
"persona_id": f"P{pid:03d}",
"name": f"{name_first} {name_last}",
"archetype": arch["id"],
"archetype_label": arch["label"],
"primary_goal": arch["primary_goal"],
"anchor_concerns": arch["anchor_concerns"],
"age": demo["age"],
"dependents": demo["dependents"],
"income_band": demo["income_band"],
"location_tier": demo["location_tier"],
"marital_stage": demo["marital"],
"existing_cover_inr": 0 if arch["id"] in ("first_buyer", "senior_care", "anxious", "tax_planner", "low_trust") else 500_000,
"health_conditions": _condition_for(arch["id"], demo["age"]),
"parents_to_insure": arch["id"] == "senior_care" or "parents" in demo["dependents"],
"parents_age_max": 75 if arch["id"] == "senior_care" or "parents" in demo["dependents"] else None,
"parents_has_ped": arch["id"] == "senior_care",
"budget_band": {
"under_5L": "under_15k",
"5L-10L": "15k_30k",
"10L-25L": "30k_60k",
"25L+": "60k+",
}[demo["income_band"]],
"style": style["id"],
"style_label": style["label"],
"lang": style["lang"],
"style_hedges": style["hedges"],
}
personas.append(persona)
pid += 1
return personas
def main() -> None:
personas = generate()
out = Path(__file__).resolve().parent / "personas.json"
out.write_text(json.dumps(personas, indent=2, ensure_ascii=False))
print(f"wrote {out} ({len(personas)} personas)")
# Quick distribution sanity check
by_arch: dict[str, int] = {}
by_style: dict[str, int] = {}
for p in personas:
by_arch[p["archetype"]] = by_arch.get(p["archetype"], 0) + 1
by_style[p["style"]] = by_style.get(p["style"], 0) + 1
print(f" archetypes (each should be 10): {by_arch}")
print(f" styles (each should be 10): {by_style}")
if __name__ == "__main__":
main()