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:
Macky
2026-08-09 13:19:15 +07:00
parent 94e75238a3
commit c92400b195
4 changed files with 102 additions and 68 deletions

View File

@@ -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")