feat(chat): scenario-based training — choice of channel/Situation + realistic per-turn evaluation
Backend:
- Channel/initiation now driven by a SCENARIO chosen at chat start, not baked into the
persona: social (customer opens), f2f_call (seller must open, proactive), recontact
(customer re-contacts after 1-3 months).
- /chat/start accepts {scenario}; session stores scenario + internal{turns,score}.
- persona_reply takes scenario + adapts tone; system-role transcript entries are fed to
the persona as hidden scene notes.
- JUDGE updated for realism: good response can WIN even in hard/tough-text scenarios;
long/no-close chats (turns >~12) lose; pushy/ignoring-need loses. Efficiency rewarded.
Frontend:
- Scenario picker before chat (choose Social / Face-to-face-call / Re-contact).
- Chat thread renders role=system as a centered time-lapse/scene note.
- Choose-scenario i18n (EN+TH).
Rebuilt dist.
This commit is contained in:
@@ -79,7 +79,8 @@ def main():
|
||||
assert r.status_code == 200, r.get_json()
|
||||
session = r.get_json()["session"]
|
||||
assert session["status"] == "active"
|
||||
assert len(session["messages"]) >= 1 and session["messages"][0]["role"] == "customer", "customer should open"
|
||||
assert len(session["messages"]) >= 1
|
||||
assert any(m["role"] == "customer" for m in session["messages"]), "customer should open"
|
||||
print("[ok] customer-initiated session starts with customer opener")
|
||||
|
||||
# send messages
|
||||
@@ -102,14 +103,15 @@ def main():
|
||||
assert r.status_code == 400, r.get_json()
|
||||
print("[ok] one-shot enforced (cannot re-chat same persona)")
|
||||
|
||||
# seller-initiated persona -> session starts WITHOUT opener (task for seller)
|
||||
# seller-facing persona + f2f_call scenario -> session starts WITHOUT opener (seller must open)
|
||||
sel = next(p for p in personas if p["initiation_mode"] == "seller")
|
||||
r = client.post(f"/api/chat/{gid}/personas/{sel['id']}/chat/start", headers=UH)
|
||||
r = client.post(f"/api/chat/{gid}/personas/{sel['id']}/chat/start",
|
||||
json={"scenario": "f2f_call"}, headers=UH)
|
||||
assert r.status_code == 200, r.get_json()
|
||||
s2 = r.get_json()["session"]
|
||||
assert "task" in r.get_json() or "initiation_mode" in r.get_json()
|
||||
assert s2["messages"] == [] , "seller-initiated should not have a customer opener"
|
||||
print("[ok] seller-initiated session (no customer opener, seller must open)")
|
||||
assert "initiation_mode" in r.get_json()
|
||||
assert not any(m["role"] == "customer" for m in s2["messages"]), "f2f_call: no customer opener (seller must open)"
|
||||
print("[ok] seller-first (f2f/call) session — no customer opener, seller must open")
|
||||
|
||||
# board
|
||||
r = client.get("/api/me/board", headers=UH)
|
||||
|
||||
79
backend/scripts/test_scenario.py
Normal file
79
backend/scripts/test_scenario.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Test: scenario-based chat start (scenario determines who opens)."""
|
||||
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)
|
||||
|
||||
from app import factory
|
||||
|
||||
def make_app():
|
||||
app = create_app()
|
||||
# mock LLM so analyze works deterministically
|
||||
sys.path.insert(0, BACKEND + "/scripts")
|
||||
from mock_llm import MockLLM
|
||||
app.extensions["llm"] = MockLLM()
|
||||
return app
|
||||
|
||||
app = make_app()
|
||||
C = app.test_client()
|
||||
|
||||
def login(u, p):
|
||||
return C.post("/api/auth/login", json={"username": u, "password": p}).get_json()["token"]
|
||||
|
||||
AT = login("admin", "1234")
|
||||
AH = {"Authorization": f"Bearer {AT}"}
|
||||
C.post("/api/auth/setup", headers=AH, json={"username": "admin", "email": "a@b.co", "password": "newpass"}).get_json()
|
||||
# re-login with new password
|
||||
AT = login("admin", "newpass")
|
||||
AH = {"Authorization": f"Bearer {AT}"}
|
||||
|
||||
# create group + analyze
|
||||
r = C.post("/api/groups", headers=AH, json={"product": "Inbound CRM", "segment": "SME", "channel": "line", "language": "th"})
|
||||
gid = r.get_json()["group"]["id"]
|
||||
C.post(f"/api/groups/{gid}/analyze", headers=AH)
|
||||
personas = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"]
|
||||
# build a ready group accepted by trainees
|
||||
C.post(f"/api/groups/{gid}/reanalyze", headers=AH)
|
||||
|
||||
# create a trainee user in same org
|
||||
C.post("/api/admin/users", headers=AH, json={"username": "t1", "name": "T", "password": "pppp", "role": "user"})
|
||||
T = login("t1", "pppp")
|
||||
TH = {"Authorization": f"Bearer {T}"}
|
||||
|
||||
pid = personas[0]["id"]
|
||||
|
||||
# social -> customer opens
|
||||
r = C.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=TH, json={"scenario": "social"})
|
||||
assert r.status_code == 200, r.get_json()
|
||||
s = r.get_json()["session"]
|
||||
assert any(m["role"] == "customer" for m in s["messages"]), "social should have customer opener"
|
||||
print("[ok] social -> customer opens")
|
||||
|
||||
# f2f_call -> seller must open (no customer opener)
|
||||
# use a different persona (one-shot per persona)
|
||||
pid2 = personas[1]["id"]
|
||||
r = C.post(f"/api/chat/{gid}/personas/{pid2}/chat/start", headers=TH, json={"scenario": "f2f_call"})
|
||||
assert r.status_code == 200, r.get_json()
|
||||
s2 = r.get_json()["session"]
|
||||
assert not any(m["role"] == "customer" for m in s2["messages"]), "f2f should NOT have customer opener"
|
||||
assert any(m["role"] == "system" for m in s2["messages"]), "f2f should have a scenario system note"
|
||||
print("[ok] f2f_call -> seller must open (system note present)")
|
||||
|
||||
# recontact -> system preamble about time-lapse + customer opens
|
||||
pid3 = personas[2]["id"]
|
||||
r = C.post(f"/api/chat/{gid}/personas/{pid3}/chat/start", headers=TH, json={"scenario": "recontact"})
|
||||
assert r.status_code == 200, r.get_json()
|
||||
s3 = r.get_json()["session"]
|
||||
assert any(m["role"] == "customer" for m in s3["messages"]), "recontact should open w/ customer"
|
||||
assert any(m["role"] == "system" and "เดือน" in m["text"] for m in s3["messages"]), "recontact system note (months)"
|
||||
print("[ok] recontact -> time-lapse system note + customer opens")
|
||||
|
||||
print("ALL SCENARIO TESTS PASSED")
|
||||
Reference in New Issue
Block a user