Files
sales-trainer/backend/scripts/test_resume_decision.py
Macky c92400b195 refactor(chat): decide buy/walk by per-turn LLM judge (not fixed keywords)
Removed the fixed-value text detector. persona_reply no longer forces JSON meta; instead a
per-turn evaluate_turn() calls the judge LLM after every customer reply to read the persona's
current mood + whether it has decided (buy/walk/pending) + score_delta + reason. send_message
consumes that context-based decision to (a) end the chat as won/lost and (b) move the score.

This is what the user asked: the system evaluates EVERY turn and decides at the moment it's
truly committed — not keyword matching (so 'ซื้อไม่ไหว แต่ว่ามีผ่อนไหม?' stays pending).
Mock updated: judge returns buy on first send (keeps E2E deterministic). 11/11 suites pass.
2026-08-09 13:19:15 +07:00

62 lines
3.3 KiB
Python

"""Test: chat RESUME (no re-pick scenario) + decision detection for real-LLM text."""
import os, sys, tempfile, warnings
from pathlib import Path
warnings.filterwarnings("ignore")
BACKEND = str(Path(__file__).resolve().parents[1])
sys.path.insert(0, BACKEND)
from app.factory import create_app
from app.config import Config
td = tempfile.mkdtemp(); Config.DATA_DIR = Path(td)
sys.path.insert(0, BACKEND + "/scripts")
from mock_llm import MockLLM
app = create_app(); app.extensions["llm"] = MockLLM()
C = app.test_client()
def tok(u,p): return C.post("/api/auth/login", json={"username":u,"password":p}).get_json()["token"]
AT = tok("admin","1234"); AH={"Authorization":f"Bearer {AT}"}
C.post("/api/auth/setup", headers=AH, json={"username":"admin","email":"a@b.co","password":"newpass","accepted_terms":True})
AT = tok("admin","newpass"); AH={"Authorization":f"Bearer {AT}"}
C.post("/api/groups", headers=AH, json={"product":"POS CRM","segment":"SME","language":"th"})
gid = C.get("/api/groups", headers=AH).get_json()["groups"][0]["id"]
C.post(f"/api/groups/{gid}/analyze", headers=AH)
pid = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"][0]["id"]
# 1. start chat
r1 = C.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=AH, json={"scenario":"social","locale":"th"})
assert r1.status_code == 200, r1.get_json()
sid1 = r1.get_json()["session"]["id"]
# 2. call start AGAIN (as if re-entering) -> should RESUME the same session, not create new
r2 = C.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=AH, json={"scenario":"f2f_call","locale":"th"})
assert r2.status_code == 200, r2.get_json()
sid2 = r2.get_json()["session"]["id"]
assert sid1 == sid2, f"resume must return same session (got {sid2}, want {sid1})"
assert r2.get_json()["scenario"] == "social", "resume keeps ORIGINAL scenario (not re-pick)"
print("[ok] resume returns same active session + keeps original scenario")
# 3. resume endpoint also returns the active session
rr = C.get(f"/api/chat/{gid}/personas/{pid}/chat/resume", headers=AH)
assert rr.status_code == 200 and rr.get_json()["session"]["id"] == sid1
print("[ok] /chat/resume returns active session")
# 4. decision comes from the LLM judge (evaluate_turn), NOT a fixed-text list.
# The mock judge returns {mood, decision: buy, ...} for the eval prompt.
from app.services.simulator import Simulator
sim2 = Simulator(MockLLM())
res = sim2.evaluate_turn(
persona={"name": "สมชาย", "pains": [], "tolerance": 2},
messages=[
{"role": "customer", "text": "สวัสดีครับ"},
{"role": "seller", "text": "สวัสดีครับ มีอะไรช่วยได้ไหม"},
{"role": "customer", "text": "แพงเกินไป ผมซื้อไม่ไหวแล้วครับ ขอตัวก่อน"},
],
)
print("[ok] evaluate_turn decision:", res.get("decision"), "| mood:", res.get("mood"))
assert res.get("decision") in ("buy", "walk", "pending"), res
# The decision is produced by the LLM judge object structure (has the fields we consume in send)
assert isinstance(res, dict) and "mood" in res and "score_delta" in res and "reason" in res
print("[ok] evaluate_turn returns mood/decision/score_delta/reason (context-based, not fixed text)")
print("ALL RESUME+DECISION TESTS PASSED")