- 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
93 lines
4.6 KiB
Python
93 lines
4.6 KiB
Python
"""Report builder: assemble a human-readable analysis report from sales kit + personas."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
TIER_NAMES = {
|
|
"A": ("Ready to buy", "ตั้งใจซื้อ"),
|
|
"B": ("Unsure / educating", "ไม่แน่ใจ"),
|
|
"C": ("Not interested but has pain", "ไม่สนใจแต่มี pain"),
|
|
}
|
|
|
|
|
|
def build_report(*, sales_kit: dict[str, Any], personas: list[dict[str, Any]], language: str = "th") -> dict[str, Any]:
|
|
thai = language == "th"
|
|
tiers: dict[str, list[dict[str, Any]]] = {"A": [], "B": [], "C": []}
|
|
for p in personas:
|
|
tiers.get(p.get("tier", "B"), []).append(p)
|
|
|
|
sections = []
|
|
sections.append({
|
|
"title": "Sales Kit / ข้อมูลสินค้า" if thai else "Sales Kit",
|
|
"content": _render_sales_kit(sales_kit, thai),
|
|
})
|
|
for tier, personas_list in tiers.items():
|
|
label = TIER_NAMES[tier][1 if thai else 0]
|
|
sections.append({
|
|
"title": f"Tier {tier} — {label}",
|
|
"content": _render_tier(personas_list, thai),
|
|
})
|
|
|
|
return {
|
|
"title": f"{sales_kit.get('productName', 'Product')} — Sales Training Analysis",
|
|
"summary": "Customer personas + pain analysis for sales training.",
|
|
"language": language,
|
|
"sections": sections,
|
|
"raw_personas": personas,
|
|
}
|
|
|
|
|
|
def _render_sales_kit(kit: dict[str, Any], thai: bool) -> str:
|
|
lines = []
|
|
lines.append(f"**{'สินค้า' if thai else 'Product'}:** {kit.get('productName', '-')}")
|
|
if kit.get("category"):
|
|
lines.append(f"**{'หมวดหมู่' if thai else 'Category'}:** {kit['category']}")
|
|
if kit.get("valueProps"):
|
|
lines.append(f"**{'คุณค่า' if thai else 'Value props'}:** " + "; ".join(kit["valueProps"]))
|
|
if kit.get("features"):
|
|
lines.append(f"**{'ฟีเจอร์' if thai else 'Features'}:** " + "; ".join(kit["features"]))
|
|
if kit.get("pricingAnchors"):
|
|
lines.append(f"**{'ราคา' if thai else 'Pricing'}:** " + "; ".join(kit["pricingAnchors"]))
|
|
ta = kit.get("targetAudience") or {}
|
|
if ta.get("segment"):
|
|
lines.append(f"**{'กลุ่มเป้าหมาย' if thai else 'Target segment'}:** {ta['segment']}")
|
|
if kit.get("initialPainFit"):
|
|
lines.append(f"**{'Pain ที่สินค้าแก้ได้เบื้องต้น' if thai else 'Initial pain-fit'}:**")
|
|
for p in kit["initialPainFit"]:
|
|
lines.append(f"- ({p.get('fit', '?')}) {p.get('pain', '')}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _render_tier(personas: list[dict[str, Any]], thai: bool) -> str:
|
|
if not personas:
|
|
return "_" + ("ไม่มี" if thai else "none") + "_"
|
|
blocks = []
|
|
for p in personas:
|
|
blocks.append(_render_persona(p, thai))
|
|
return "\n\n---\n\n".join(blocks)
|
|
|
|
|
|
def _render_persona(p: dict[str, Any], thai: bool) -> str:
|
|
lines = [f"### {p.get('name', '-')} (difficulty {p.get('difficulty', 1)})"]
|
|
lines.append(f"- {'อาชีพ' if thai else 'Profession'}: {p.get('profession', '-')} | "
|
|
f"{'อายุ' if thai else 'Age'}: {p.get('age_group', '-')} | "
|
|
f"{'ช่องทาง' if thai else 'Channel'}: {p.get('channel', 'facebook')} | "
|
|
f"{'เปิดบท' if thai else 'Initiation'}: {p.get('initiation_mode', 'customer')}")
|
|
if p.get("special"):
|
|
lines.append(f"- SPECIAL: {p['special']}")
|
|
lines.append(f"- {'พื้นหลัง' if thai else 'Background'}: {p.get('background', '-')}")
|
|
lines.append(f"- {'รายได้' if thai else 'Income'}: {p.get('income', '-')} | "
|
|
f"{'ไลฟ์สไตล์' if thai else 'Lifestyle'}: {p.get('lifestyle', '-')}")
|
|
lines.append(f"- {'นิสัย' if thai else 'Personality'}: {p.get('personality', '-')}")
|
|
if p.get("pains"):
|
|
lines.append(f"- {'Pain points (latent)' if thai else 'Pains (latent)'}:")
|
|
for pain in p.get("pains", []):
|
|
conds = "; ".join(pain.get("resolutionConditions", [])) if isinstance(pain, dict) else ""
|
|
lines.append(f" - [{pain.get('fit', '?') if isinstance(pain, dict) else '?'}] "
|
|
f"{pain.get('name', pain) if isinstance(pain, dict) else pain}"
|
|
f"{' — resolve: ' + conds if conds else ''}")
|
|
if p.get("negotiation_levers"):
|
|
levers = p.get("negotiation_levers") or []
|
|
lines.append(f"- {'ต่อรอง' if thai else 'Negotiation levers'}: " + ", ".join(str(x) for x in levers))
|
|
return "\n".join(lines)
|