fix(chat): resume unfinished session without re-picking scenario; detect win/loss from real-LLM text
- start_session now RESUMES an existing ACTIVE session for the persona instead of creating a new one / forcing re-pick of the scenario (keep original scenario+messages). - send_message falls back to a text-based decision detector (_detect_customer_decision) because real LLMs rarely emit structured meta.decision — so a customer who says 'ซื้อไม่ไหว'/'no thanks' now actually ENDS the chat as lost (was stuck active forever). Fragments handled in TH + EN; buy + walk. All 11 backend suites pass. Rebuilt dist.
This commit is contained in:
@@ -27,6 +27,39 @@ def _sim(group, persona):
|
||||
return Simulator(llm)
|
||||
|
||||
|
||||
WALK_FRAGMENTS = [
|
||||
"no thanks", "not interested", "forget it", "never mind", "no longer", "out of budget",
|
||||
"too expensive", "can't afford", "cannot afford", "ซื้อไม่ไหว", "ไม่ซื้อ", "ไม่เอาแล้ว",
|
||||
"ไม่สนใจ", "พอแค่นี้", "ไม่ไหว", "แพงไป", "ลืมไปเถอะ", "ตัดสินใจไม่ซื้อ", "ขอไม่ซื้อ",
|
||||
"ยังไม่ซื้อ", "ไว้ก่อน", "ไปก่อนนะ", "ขอบคุณแต่ไม่เอา", "ไม่เอาครับ", "ไม่เอาค่ะ", "ไม่เอา",
|
||||
]
|
||||
BUY_FRAGMENTS = [
|
||||
"i'll take it", "i'll go with it", "i'll buy", "deal,", "let's do it", "count me in",
|
||||
"how do i pay", "where do i sign", "ok i'll buy", "ซื้อเลย", "ตกลงซื้อ", "เอาครับ", "เอาค่ะ",
|
||||
"ซื้อครับ", "ซื้อค่ะ", "ตัดสินใจซื้อ", "เอาเลย", "ตกลง", "ซื้อ", "รับไว้", "โอเคค่ะ รับ", "โอเคครับ รับ",
|
||||
]
|
||||
|
||||
|
||||
def _detect_customer_decision(text: str, locale: str = "th") -> str | None:
|
||||
"""Detect whether the customer clearly walked away or decided to buy, from their message.
|
||||
|
||||
Returns 'walk' | 'buy' | None. Used as a fallback because real LLMs rarely emit a
|
||||
structured meta.decision — they just state it in the conversation.
|
||||
"""
|
||||
if not text:
|
||||
return None
|
||||
low = text.lower()
|
||||
# Walk-away signals can appear anywhere; be conservative to avoid false ends on
|
||||
# phrases like "I can't afford that, what's the discount?" (question) vs "too expensive, no."
|
||||
for frag in WALK_FRAGMENTS:
|
||||
if frag in low:
|
||||
return "walk"
|
||||
for frag in BUY_FRAGMENTS:
|
||||
if frag in low:
|
||||
return "buy"
|
||||
return None
|
||||
|
||||
|
||||
def _scenarios(locale: str = "th"):
|
||||
"""Scenario presets, localized. Returns {id: {label, init, adapt}}."""
|
||||
t = locale != "en"
|
||||
@@ -153,6 +186,18 @@ def start_session(gid: str, pid: str):
|
||||
if locale not in ("en", "th"):
|
||||
locale = "th"
|
||||
scenario_meta, init_mode = _scenario_config(scenario, persona, locale)
|
||||
|
||||
# RESUME: if this user already has an ACTIVE (unfinished) session for this persona, keep
|
||||
# chatting it — do NOT create a new one and do NOT force re-picking the scenario.
|
||||
existing = s["sessions"].active_for_persona(actor["id"], pid)
|
||||
if existing and existing.get("group_id") == gid:
|
||||
return jsonify({
|
||||
"session": existing,
|
||||
"initiation_mode": existing.get("persona_meta", {}).get("initiation_mode") or "customer",
|
||||
"scenario": existing.get("scenario", scenario),
|
||||
"scenario_meta": _scenario_config(existing.get("scenario", scenario), persona, locale),
|
||||
})
|
||||
|
||||
# One-shot: reject if already finished this persona
|
||||
try:
|
||||
session = s["sessions"].create(
|
||||
@@ -273,6 +318,11 @@ def send_message(gid: str, pid: str):
|
||||
|
||||
# Decision by the persona ends the session (one-shot lock).
|
||||
decision = meta.get("decision")
|
||||
# Real LLMs usually DON'T emit a structured meta.decision — they just say it in the
|
||||
# customer's reply. Fall back to a lightweight text detector so "ซื้อไม่ไหว"/"no thanks"
|
||||
# actually ENDS the chat (otherwise it never reports win/loss).
|
||||
if decision not in ("buy", "walk"):
|
||||
decision = _detect_customer_decision(reply, locale=slocale)
|
||||
if decision in ("buy", "walk"):
|
||||
outcome = "won" if decision == "buy" else "lost"
|
||||
debrief = _build_abbrev_debrief(outcome, persona, internal, slocale)
|
||||
|
||||
Reference in New Issue
Block a user