"""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("//personas//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("//personas//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("//personas//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/") @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})