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.
This commit is contained in:
@@ -104,6 +104,10 @@ class MockLLM:
|
||||
"coaching": [],
|
||||
"painProgress": {"slow checkout": 100},
|
||||
}
|
||||
if "neutral sales-coaching judge" in sp:
|
||||
# Per-turn state evaluation: mock decides to buy on the first seller message
|
||||
# (keeps E2E deterministic: first send auto-finishes as won), else pending.
|
||||
return {"mood": 1, "decision": "buy", "score_delta": 5, "reason": "mock buy"}
|
||||
return {}
|
||||
|
||||
def complete_conversation(self, messages, **kw) -> str:
|
||||
|
||||
@@ -40,11 +40,22 @@ 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 detection: unit-test the text detector
|
||||
from app.api.chat_routes import _detect_customer_decision
|
||||
assert _detect_customer_decision("ผมซื้อไม่ไหวแล้วครับ ขอตัวก่อน") == "walk"
|
||||
assert _detect_customer_decision("สวัสดีครับ ผมสนใจสินค้าครับ") is None
|
||||
assert _detect_customer_decision("ok ผมเอาครับ รับเลย") == "buy"
|
||||
print("[ok] _detect_customer_decision: walk/buy/None cases correct")
|
||||
# 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")
|
||||
|
||||
Reference in New Issue
Block a user