Files
sales-trainer/backend/app/api/chat_routes.py
Macky c3d31c06e2 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
2026-08-07 15:31:06 +07:00

172 lines
5.7 KiB
Python

"""Chat/session API: start a one-shot session, send messages, finish + debrief."""
from __future__ import annotations
from flask import Blueprint, jsonify, request
from ..llm import LLMError
from ..services.simulator import Simulator
from .helpers import ApiError, current_user, require_auth, require_roles
chat_bp = Blueprint("chat", __name__)
def _stores():
from flask import current_app
return {
"groups": current_app.extensions["group_store"],
"sessions": current_app.extensions["session_store"],
"llm": current_app.extensions["llm"],
}
def _sim(group, persona):
llm = _stores()["llm"]
if not llm:
raise ApiError("LLM not configured", 500)
return Simulator(llm)
@chat_bp.post("/<gid>/personas/<pid>/chat/start")
@require_auth
@require_roles("user")
def start_session(gid: str, pid: str):
s = _stores()
group = s["groups"].get_or_none(gid)
if not group or group.get("status") != "ready":
raise ApiError("group not ready", 404)
persona = s["groups"].get_persona(gid, pid)
if not persona:
raise ApiError("persona not found", 404)
actor = current_user()
# One-shot: reject if already finished this persona
try:
session = s["sessions"].create(
user_id=actor["id"], group_id=gid, persona_id=pid,
persona_name=persona.get("name", "?"),
persona_meta={
"tier": persona.get("tier"),
"initiation_mode": persona.get("initiation_mode"),
"channel": persona.get("channel"),
},
)
except ValueError as exc:
raise ApiError(str(exc), 400)
sim = _sim(group, persona)
# Seller-initiated: give the trainee an opening task (no persona message yet).
init_mode = persona.get("initiation_mode", "customer")
if init_mode == "customer":
# Customer opens: inject the persona's opener as the first message.
opener = persona.get("opener") or "Hi, I saw your product and had a question."
s["sessions"].update(session["id"], messages=[{"role": "customer", "text": opener}])
else:
s["sessions"].update(
session["id"],
task="The customer did NOT message first. You must open the sale — start the "
"conversation with this lead (e.g. introduce yourself and engage with interest).",
)
return jsonify({"session": s["sessions"].get(session["id"]), "initiation_mode": init_mode})
@chat_bp.post("/<gid>/personas/<pid>/chat/send")
@require_auth
@require_roles("user")
def send_message(gid: str, pid: str):
s = _stores()
actor = current_user()
session = s["sessions"].active_for_persona(actor["id"], pid)
if not session or session.get("group_id") != gid:
raise ApiError("no active session for this persona", 404)
data = request.get_json(silent=True) or {}
text = (data.get("text") or "").strip()
if not text:
raise ApiError("message is empty")
if len(text) > 2000:
raise ApiError("message too long")
group = s["groups"].get_or_none(gid)
persona = s["groups"].get_persona(gid, pid)
messages = list(session.get("messages", []))
messages.append({"role": "seller", "text": text})
sim = _sim(group, persona)
try:
reply = sim.persona_reply(
persona=persona,
sales_kit=group.get("sales_kit") or {},
messages=messages,
internal=session.get("internal", {}),
)
except LLMError as exc:
raise ApiError(f"LLM error: {exc}", 500)
messages.append({"role": "customer", "text": reply})
s["sessions"].update(session["id"], messages=messages)
return jsonify({"reply": reply, "messages": messages})
@chat_bp.post("/<gid>/personas/<pid>/chat/finish")
@require_auth
@require_roles("user")
def finish_session(gid: str, pid: str):
"""End the chat and produce the debrief via the judge-LLM (reveals latent fields)."""
s = _stores()
actor = current_user()
session = s["sessions"].active_for_persona(actor["id"], pid)
if not session or session.get("group_id") != gid:
raise ApiError("no active session for this persona", 404)
group = s["groups"].get_or_none(gid)
persona = s["groups"].get_persona(gid, pid)
sim = _sim(group, persona)
messages = session.get("messages", [])
try:
verdict = sim.judge(persona=persona, messages=messages)
except LLMError as exc:
raise ApiError(f"LLM error: {exc}", 500)
outcome = "won" if verdict.get("outcome") == "won" else "lost"
debrief = {
**verdict,
"revealed_persona": {
"pains": persona.get("pains", []),
"income": persona.get("income", ""),
"personality": persona.get("personality", ""),
"budget": persona.get("budget", ""),
"negotiation_levers": persona.get("negotiation_levers", []),
"opener": persona.get("opener", ""),
"background": persona.get("background", ""),
},
}
s["sessions"].update(
session["id"],
status="finished",
outcome=outcome,
debrief=debrief,
internal=session.get("internal", {}),
)
return jsonify({"session": s["sessions"].get(session["id"]), "debrief": debrief})
@chat_bp.get("/sessions")
@require_auth
@require_roles("user")
def my_sessions():
s = _stores()
uid = current_user()["id"]
sessions = s["sessions"].list_for_user(uid)
return jsonify({"sessions": sessions})
@chat_bp.get("/sessions/<sid>")
@require_auth
@require_roles("user")
def get_session(sid: str):
s = _stores()
session = s["sessions"].get_or_none(sid)
if not session or session.get("user_id") != current_user()["id"]:
raise ApiError("session not found", 404)
return jsonify({"session": session})