From 479d77757f06b2b781aea796f6721f8100fcbf6f Mon Sep 17 00:00:00 2001 From: Macky Date: Sun, 9 Aug 2026 11:25:50 +0700 Subject: [PATCH] fix(chat): map customer/seller roles for OpenAI-compat LLM; re-contact = mid-chat time-lapse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - llm.complete_conversation now maps internal roles (customer->assistant, seller->user, system->system) before the API call — fixes 'Unknown role: customer' (501). - Re-contact personality: the customer now chats normally, at turn 2 goes quiet and a system time-lapse note is shown ('⏳ ผ่านไป 2-3 สัปดาห์...'), then re-engages warmer — instead of 'pretending you asked before' at start. Driven by persona.recontact trait. All 9 backend suites pass. Rebuilt dist. --- backend/app/api/chat_routes.py | 14 ++++++++++++++ backend/app/llm.py | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/app/api/chat_routes.py b/backend/app/api/chat_routes.py index b00d2ab..7b4b3bc 100644 --- a/backend/app/api/chat_routes.py +++ b/backend/app/api/chat_routes.py @@ -257,6 +257,20 @@ def send_message(gid: str, pid: str): elif mood >= 1: internal["signals"].append({"turn": internal["turns"], "mood": mood, "type": "warm"}) + # Re-contact persona behavior: after enough info is exchanged (turn 2), the customer + # goes quiet, a time-lapse system note is shown, and the customer re-engages warmer. + if persona.get("recontact") and not internal.get("recontact_done") and internal["turns"] >= 2: + unit = "สัปดาห์" if slocale != "en" else "weeks" + sys_txt = ( + f"⏳ ผ่านไป 2-3 {unit} ... ลูกค้าที่เคยสอบถามไปเงียบไประยะหนึ่ง ตอนนี้กลับมาติดต่ออีกครั้ง (พร้อมตัดสินใจมากขึ้น)" + if slocale != "en" + else "⏳ 2-3 weeks later ... the customer who asked earlier went quiet; now they re-contact, more ready to decide." + ) + messages.append({"role": "system", "text": sys_txt}) + internal["recontact_done"] = True + # Save the time-lapse note immediately so the UI shows it even if send ends here. + s["sessions"].update(session["id"], messages=messages, internal=internal) + # Decision by the persona ends the session (one-shot lock). decision = meta.get("decision") if decision in ("buy", "walk"): diff --git a/backend/app/llm.py b/backend/app/llm.py index 2c009ef..4e77c07 100644 --- a/backend/app/llm.py +++ b/backend/app/llm.py @@ -116,12 +116,27 @@ class LLMClient: temperature: float = 0.6, max_tokens: int = 1200, ) -> str: + # Normalize internal role labels (customer/seller) to the roles an OpenAI-compatible + # chat endpoint accepts: system/user/assistant. customer=assistant (the persona/LLM), + # seller=user (the trainee). Anything else maps to a safe default. + api_messages = [] + for m in messages: + role = (m.get("role") or "").lower() + if role == "system": + mapped = "system" + elif role in ("customer", "assistant"): + mapped = "assistant" + elif role in ("seller", "user"): + mapped = "user" + else: + mapped = "user" + api_messages.append({"role": mapped, "content": m.get("text") or m.get("content") or ""}) try: resp = self.client.chat.completions.create( model=self.model, temperature=temperature, max_tokens=max_tokens, - messages=messages, + messages=api_messages, ) except Exception as exc: raise LLMError(f"LLM call failed: {exc}") from exc