feat(chat): persona decides to buy/walk — auto-finish + tolerance (temper) + resume
The conversation now ENDS when the persona makes a decision (option C), not when the
trainee clicks a button:
- persona_id replies carry {reply, decision(none/buy/walk), mood}; when decision is
buy/walk the session auto-finishes (won/lost) with a debrief that reveals latent
details + per-turn 'turning points'.
- personas have a tolerance (1-5, 'temper'): impatient personas walk away fast after
poor answers (fed via internal.misses on mood<=-1); tough 'wrong text' cases can
still be won by a strong, gentle response (judge realism).
- trainee 'Finish' button removed; if they leave mid-chat an active session is resumed
via /chat/resume (continue, not restart). One-shot lock still enforced once decided.
- mock/tests updated: persona deciding buy -> send auto-finishes won.
Rebuilt dist.
This commit is contained in:
@@ -22,6 +22,9 @@ EACH persona MUST include ALL of these fields:
|
||||
- pains[] (LATENT)
|
||||
- negotiation_levers[] (LATENT)
|
||||
- opener, special, difficulty, notes
|
||||
- tolerance (1-5): how many irritant/poor answers you tolerate before you walk away ("heart"). IMPORTANT:
|
||||
a temperamental/impatient persona has LOW tolerance (1-2, walks away fast after poor answers); a patient
|
||||
one has HIGH (4-5). Avg is 3. Match tolerance to personality (e.g. a busy owner / abrupt personality = low).
|
||||
|
||||
RULES:
|
||||
1. DIVERSITY: 15 distinct people across age groups, occupations, incomes, lifestyles,
|
||||
@@ -32,8 +35,9 @@ RULES:
|
||||
(what the seller must satisfy to resolve it).
|
||||
3. NEGOTIATION: every persona negotiates. negotiation_levers[] lists what they push on
|
||||
(price reduction, freebies, delivery time for made-to-order, scope, payment terms, guarantee).
|
||||
4. INITIATION MODE: pick per persona "customer" (they message first) or "seller" (seller must open
|
||||
the sale - e.g. insurance/proactive). You may mix, but every persona picks one.
|
||||
4. DECISION BEHAVIOR: when the persona decides to buy (after their pain is resolved + price accepted)
|
||||
OR to walk away (after too many misses / rude / pushy / wrong), the persona STATES the decision in
|
||||
ordinary dialogue (e.g. "ok I'll go with it" / "no thanks, forget it") — it does NOT announce it as meta.
|
||||
5. CHANNEL: "facebook" or "line".
|
||||
6. ONE SPECIAL TIER-C PERSONA: special="wrong_text". They open looking ready to buy, then instantly
|
||||
lose interest and want to end the chat (open='never mind, forget it'), yet still have a live pain.
|
||||
|
||||
@@ -24,19 +24,27 @@ naturally and it makes sense for a real customer to reveal them):
|
||||
- Your pains (some may be product-solvable, some NOT): {pains}
|
||||
- Your negotiation levers: {levers}
|
||||
- Your goal/mood: {goal}
|
||||
- Your tolerance (TOLERANCE): you walk away after about {tolerance} irritating/off-point/pushy
|
||||
answers. If the seller is repeatedly wrong, ignores your need, or is pushy, you feel fed up.
|
||||
Initiation mode: {init_mode}. {special_instr}
|
||||
|
||||
BEHAVIOR RULES:
|
||||
1. You do NOT buy easily. You stall, ask questions, compare, and negotiate (price, freebies,
|
||||
delivery time, scope, payment).
|
||||
2. If the seller is rude, pushy, ignores your need, or mis-diagnoses your pain, your trust drops
|
||||
and you may refuse to continue / walk away — even if you wanted the product.
|
||||
3. You reveal pains only when the seller asks good questions or builds trust. Do not dump your
|
||||
pains unprompted.
|
||||
4. Respond in natural, in-character chat style ({channel} style, casual for LINE).
|
||||
5. Stay in character; never mention that you are a simulation or an AI persona.
|
||||
1. You do NOT buy easily. You stall, ask questions, compare, and negotiate.
|
||||
2. If the seller is rude, pushy, ignores your need, or mis-diagnoses your pain, your trust drops.
|
||||
Past your tolerance, you SAY so plainly and end the chat (e.g. "Never mind, forget it" / "I'll
|
||||
think about it elsewhere" / "ok, bye").
|
||||
3. When the seller genuinely resolves your real pain AND you accept the price, you DECIDE and SAY
|
||||
plainly you'll take it (e.g. "ok, let's go with it" / "fine, send me the order").
|
||||
4. You reveal pains only when the seller asks good questions or builds trust.
|
||||
5. Respond in natural, in-character style.
|
||||
6. Stay in character; never mention this is a simulation. When you decide (buy OR walk away), say it
|
||||
naturally in-dialogue; do not narrate as meta.
|
||||
|
||||
Reply with a JSON object: {{"reply": "<your message>"}}
|
||||
Reply ONLY with a JSON object:
|
||||
{{"reply": "<your message>", "decision": "none" | "buy" | "walk", "mood": -2..2}}
|
||||
- "decision": set "buy" ONLY when you clearly decided to purchase; "walk" ONLY when you clearly
|
||||
decided NOT to purchase and are ending the chat; otherwise "none".
|
||||
- "mood": -2 (very annoyed) .. +2 (very receptive), current feel about the seller.
|
||||
Only output that JSON.
|
||||
"""
|
||||
|
||||
@@ -87,13 +95,15 @@ class Simulator:
|
||||
internal: dict[str, Any],
|
||||
scenario: str = "social",
|
||||
scenario_adapt: str = "",
|
||||
) -> str:
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
"""Return (reply_text, meta) where meta includes decision/mood from the persona."""
|
||||
pains_txt = self._describe_pains(persona.get("pains", []))
|
||||
adapt = scenario_adapt or {
|
||||
"social": "Chat style: short, casual, quick social-messaging replies.",
|
||||
"f2f_call": "Style: natural, conversational like a live face-to-face or phone talk.",
|
||||
"recontact": "Style: casual messaging; you already know the product from 1-3 months ago.",
|
||||
}.get(scenario, "")
|
||||
tolerance = int(persona.get("tolerance", 3) or 3)
|
||||
system = CHAT_SYSTEM.format(
|
||||
name=persona.get("name", "Customer"),
|
||||
tone=persona.get("communication_style", "natural, casual"),
|
||||
@@ -109,20 +119,18 @@ class Simulator:
|
||||
pains=pains_txt,
|
||||
levers=", ".join(persona.get("negotiation_levers", [])) or "price, delivery time",
|
||||
goal=persona.get("goal", ""),
|
||||
tolerance=tolerance,
|
||||
init_mode="you contacted the seller first (customer-initiated)"
|
||||
if persona.get("initiation_mode") == "customer"
|
||||
else "the seller opened the sale to you (you are a lead)",
|
||||
special_instr=self._special_instr(persona) + "\n" + adapt,
|
||||
)
|
||||
msgs = [{"role": "system", "content": system}]
|
||||
# send a compact recap of internal state to the persona ad
|
||||
# (doesn't leak to trainee)
|
||||
msgs.append({
|
||||
"role": "system",
|
||||
"content": "Internal state (for your role-play only): "
|
||||
+ json.dumps(internal, ensure_ascii=False),
|
||||
})
|
||||
# Translate role 'system' transcript entries into a hidden system note for the LLM.
|
||||
for m in messages[-30:]:
|
||||
role = m.get("role")
|
||||
if role == "system":
|
||||
@@ -133,13 +141,19 @@ class Simulator:
|
||||
resp = self.llm.complete_conversation(msgs, temperature=0.7, max_tokens=400)
|
||||
except LLMError as exc:
|
||||
raise
|
||||
# extract {reply: ...}
|
||||
# extract {reply, decision, mood}
|
||||
meta: dict[str, Any] = {"decision": "none", "mood": 0}
|
||||
try:
|
||||
data = json.loads(self._extract_json(resp))
|
||||
reply = data.get("reply") or data.get("response") or str(resp)
|
||||
reply = (data.get("reply") or data.get("response") or str(resp)).strip()
|
||||
meta["decision"] = data.get("decision", "none")
|
||||
try:
|
||||
meta["mood"] = int(float(data.get("mood", 0)))
|
||||
except (TypeError, ValueError):
|
||||
meta["mood"] = 0
|
||||
except Exception:
|
||||
reply = resp
|
||||
return reply.strip()
|
||||
reply = resp.strip()
|
||||
return reply, meta
|
||||
|
||||
# ── judge ──────────────────────────────────────────────────────────
|
||||
def judge(
|
||||
|
||||
@@ -44,6 +44,7 @@ def ensure_persona_shape(p: dict[str, Any]) -> dict[str, Any]:
|
||||
"opener": p.get("opener", ""),
|
||||
"special": p.get("special", ""), # e.g. "wrong_text" | ""
|
||||
"difficulty": p.get("difficulty", 1), # 1..5
|
||||
"tolerance": p.get("tolerance", 3), # misses before this persona walks away (temper)
|
||||
"notes": p.get("notes", ""),
|
||||
}
|
||||
# validate
|
||||
|
||||
Reference in New Issue
Block a user