fix(chat): seed customer-initiated chats with the persona's real opener
Revert the over-redaction from 3c22d88: a customer-initiated
session (social / lead who messages in) now greets with the
persona's generated opener — a realistic 'สวัสดี สนใจ ขอถามราคา'
line — instead of the static 'ลูกค้าเริ่มต้นบทสนทนาแล้ว'
placeholder. The opener is spoken public dialogue by design
(OPENER RULE keeps pains/budget/personality out of it), so
remove the redact_initial_customer strip from serialize_session.
Phone/f2f (seller-initiated) is unchanged: no customer line,
the trainee opens the call. Untrusted system-note filtering and
internal-state/provider-path stripping are untouched.
Tests: 5 redaction regressions flipped to the new contract.
517 passed (3 pre-existing demo-date failures, unrelated).
This commit is contained in:
@@ -251,13 +251,30 @@ def _public_debrief(sim: object, messages: list[dict], outcome: str, score: int)
|
||||
|
||||
|
||||
def _public_customer_opener(locale: object) -> str:
|
||||
# Fallback only when a (legacy) persona has no generated `opener`.
|
||||
# Must read like a real inquiring customer, not a system notice.
|
||||
return (
|
||||
"Hi, a customer has started the conversation."
|
||||
"Hi, I found your page and I'm interested — can I ask a few questions about the product?"
|
||||
if locale == "en"
|
||||
else "ลูกค้าเริ่มต้นบทสนทนาแล้ว ลองทักและค้นหาความต้องการดูครับ"
|
||||
else "สวัสดีครับ เห็นสินค้าของคุณแล้วสนใจเลย ขอสอบถามรายละเอียดหน่อยได้ไหมครับ"
|
||||
)
|
||||
|
||||
|
||||
def _customer_opener_text(persona: dict, locale: object) -> str:
|
||||
"""First customer line = the persona's realistic greeting (public by design).
|
||||
|
||||
The `opener` is what the customer says FIRST for customer-initiated channels
|
||||
(social / a lead who messages in). It is generated to be a clean, polite,
|
||||
in-character greeting (see OPENER RULE) — no latent pains/budget/personality —
|
||||
so it is safe to surface. Fall back to a generic greeting only if the persona
|
||||
has no opener (e.g. legacy records).
|
||||
"""
|
||||
opener = persona.get("opener")
|
||||
if isinstance(opener, str) and opener.strip():
|
||||
return opener.strip()[:2000]
|
||||
return _public_customer_opener(locale)
|
||||
|
||||
|
||||
_SAFE_SYSTEM_MESSAGES = frozenset({
|
||||
"⏳ ผ่านไป 2-3 สัปดาห์ ... ลูกค้าที่เคยสอบถามไปเงียบไประยะหนึ่ง ตอนนี้กลับมาติดต่ออีกครั้ง (พร้อมตัดสินใจมากขึ้น)",
|
||||
"⏳ 2-3 weeks later ... the customer who asked earlier went quiet; now they re-contact, more ready to decide.",
|
||||
@@ -266,16 +283,10 @@ _SAFE_SYSTEM_MESSAGES = frozenset({
|
||||
})
|
||||
|
||||
|
||||
def _safe_session_messages(
|
||||
value: object,
|
||||
*,
|
||||
redact_initial_customer: bool = False,
|
||||
locale: object = "th",
|
||||
) -> list[dict[str, str]]:
|
||||
def _safe_session_messages(value: object) -> list[dict[str, str]]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
messages = []
|
||||
seller_seen = False
|
||||
for item in value[-100:]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
@@ -286,10 +297,6 @@ def _safe_session_messages(
|
||||
if role == "system" and text not in _SAFE_SYSTEM_MESSAGES:
|
||||
continue
|
||||
if role in {"system", "seller", "customer"}:
|
||||
if role == "customer" and redact_initial_customer and not seller_seen:
|
||||
text = _public_customer_opener(locale)
|
||||
if role == "seller":
|
||||
seller_seen = True
|
||||
messages.append({"role": role, "text": text[:4000]})
|
||||
return messages
|
||||
|
||||
@@ -324,11 +331,7 @@ def serialize_session(session: dict) -> dict:
|
||||
"mode": mode,
|
||||
"status": session.get("status") if session.get("status") in ("active", "finished") else "active",
|
||||
"outcome": outcome,
|
||||
"messages": _safe_session_messages(
|
||||
session.get("messages"),
|
||||
redact_initial_customer=True,
|
||||
locale=safe_meta["locale"],
|
||||
),
|
||||
"messages": _safe_session_messages(session.get("messages")),
|
||||
"debrief": debrief,
|
||||
}
|
||||
|
||||
@@ -620,7 +623,7 @@ def start_session(gid: str, pid: str):
|
||||
if scenario_meta.get("preamble"):
|
||||
seeded.append({"role": "system", "text": scenario_meta["preamble"]})
|
||||
if init_mode == "customer":
|
||||
seeded.append({"role": "customer", "text": _public_customer_opener(locale)})
|
||||
seeded.append({"role": "customer", "text": _customer_opener_text(persona, locale)})
|
||||
initial_internal = {"turns": 0, "score": 50, "signals": []}
|
||||
|
||||
# RESUME/create is one atomic operation over the complete tenant scope. A
|
||||
@@ -640,7 +643,7 @@ def start_session(gid: str, pid: str):
|
||||
if scenario_meta.get("preamble"):
|
||||
seeded.append({"role": "system", "text": scenario_meta["preamble"]})
|
||||
if init_mode == "customer":
|
||||
seeded.append({"role": "customer", "text": _public_customer_opener(locale)})
|
||||
seeded.append({"role": "customer", "text": _customer_opener_text(persona, locale)})
|
||||
session, resumed = s["sessions"].start(
|
||||
org_id=org_id,
|
||||
user_id=actor["id"],
|
||||
@@ -825,9 +828,7 @@ def send_message(gid: str, pid: str):
|
||||
outcome = finalized.get("outcome")
|
||||
return jsonify({
|
||||
"reply": reply,
|
||||
"messages": _safe_session_messages(
|
||||
messages, redact_initial_customer=True, locale=slocale
|
||||
),
|
||||
"messages": _safe_session_messages(messages),
|
||||
"finished": True,
|
||||
"outcome": outcome,
|
||||
"debrief": debrief,
|
||||
@@ -837,9 +838,7 @@ def send_message(gid: str, pid: str):
|
||||
s["sessions"].update(session["id"], messages=messages, internal=internal)
|
||||
return jsonify({
|
||||
"reply": reply,
|
||||
"messages": _safe_session_messages(
|
||||
messages, redact_initial_customer=True, locale=slocale
|
||||
),
|
||||
"messages": _safe_session_messages(messages),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1006,19 +1006,19 @@ def test_upload_rejects_control_character_filename(client, user_store, login):
|
||||
assert response.status_code == 400, response.get_json()
|
||||
|
||||
|
||||
def test_customer_opener_is_redacted_from_public_session_transcript(
|
||||
def test_customer_opener_is_seeded_from_persona_and_shown_publicly(
|
||||
client, user_store, login
|
||||
):
|
||||
_setup_default_admin(user_store, login)
|
||||
trainee = user_store.create_user(
|
||||
org_id="org-default",
|
||||
username="opener-redaction-user",
|
||||
password="opener-redaction-password",
|
||||
name="Opener Redaction User",
|
||||
username="opener-seed-user",
|
||||
password="opener-seed-password",
|
||||
name="Opener Seed User",
|
||||
role="user",
|
||||
must_setup=False,
|
||||
)
|
||||
token = login(trainee["username"], "opener-redaction-password")["token"]
|
||||
token = login(trainee["username"], "opener-seed-password")["token"]
|
||||
group_store = client.application.extensions["group_store"]
|
||||
group = group_store.create(org_id="org-default", creator_id="admin", title="Opener")
|
||||
group_store.update(
|
||||
@@ -1030,24 +1030,26 @@ def test_customer_opener_is_redacted_from_public_session_transcript(
|
||||
{
|
||||
"id": "opener-persona",
|
||||
"name": "Opener Persona",
|
||||
"opener": "PRIVATE_PERSONA_OPENER_MUST_NOT_APPEAR",
|
||||
"opener": "สวัสดีค่ะ สนใจสินค้า ขอถามรายละเอียดหน่อยได้ไหมค่ะ",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/chat/{group['id']}/personas/opener-persona/chat/start",
|
||||
json={"scenario": "social", "locale": "en"},
|
||||
json={"scenario": "social", "locale": "th"},
|
||||
headers=_headers(token),
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.get_json()
|
||||
session = response.get_json()["session"]
|
||||
assert "PRIVATE_PERSONA_OPENER_MUST_NOT_APPEAR" not in str(session)
|
||||
assert any(message["role"] == "customer" for message in session["messages"])
|
||||
payload = response.get_json()
|
||||
session = payload["session"]
|
||||
texts = [message["text"] for message in session["messages"] if message["role"] == "customer"]
|
||||
assert "สวัสดีค่ะ สนใจสินค้า ขอถามรายละเอียดหน่อยได้ไหมค่ะ" in texts
|
||||
assert payload["initiation_mode"] == "customer"
|
||||
|
||||
|
||||
def test_session_serializer_replaces_legacy_customer_opener():
|
||||
def test_session_serializer_keeps_the_customer_opener_line():
|
||||
view = serialize_session(
|
||||
{
|
||||
"id": "legacy-opener-session",
|
||||
@@ -1055,16 +1057,18 @@ def test_session_serializer_replaces_legacy_customer_opener():
|
||||
"persona_id": "persona-1",
|
||||
"persona_name": "Customer",
|
||||
"status": "active",
|
||||
"persona_meta": {"initiation_mode": "customer", "locale": "en"},
|
||||
"persona_meta": {"initiation_mode": "customer", "locale": "th"},
|
||||
"messages": [
|
||||
{"role": "customer", "text": "PRIVATE_LEGACY_OPENER"},
|
||||
{"role": "seller", "text": "Hello"},
|
||||
{"role": "customer", "text": "สวัสดีครับ สนใจสินค้า ขอถามราคาหน่อยได้ไหมครับ"},
|
||||
{"role": "seller", "text": "สวัสดีค่ะ ยินดีบริการค่ะ"},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert "PRIVATE_LEGACY_OPENER" not in str(view)
|
||||
assert view["messages"][0]["role"] == "customer"
|
||||
assert view["messages"][0] == {
|
||||
"role": "customer",
|
||||
"text": "สวัสดีครับ สนใจสินค้า ขอถามราคาหน่อยได้ไหมครับ",
|
||||
}
|
||||
|
||||
|
||||
def test_board_ignores_non_ready_groups_and_unauthorized_sessions(
|
||||
|
||||
@@ -879,7 +879,7 @@ def test_foreign_group_readiness_is_indistinguishable_from_missing(client, user_
|
||||
assert response.get_json()["error"] == "group not found"
|
||||
|
||||
|
||||
def test_session_serializer_filters_untrusted_system_notes_and_redacts_legacy_opener():
|
||||
def test_session_serializer_filters_untrusted_system_notes_and_keeps_customer_opener():
|
||||
view = serialize_session(
|
||||
{
|
||||
"id": "session-safe",
|
||||
@@ -895,7 +895,7 @@ def test_session_serializer_filters_untrusted_system_notes_and_redacts_legacy_op
|
||||
"role": "system",
|
||||
"text": "⏳ ผ่านไป 2-3 สัปดาห์ ... ลูกค้าที่เคยสอบถามไปเงียบไประยะหนึ่ง ตอนนี้กลับมาติดต่ออีกครั้ง (พร้อมตัดสินใจมากขึ้น)",
|
||||
},
|
||||
{"role": "customer", "text": "legacy raw opener with hidden details"},
|
||||
{"role": "customer", "text": "สวัสดีครับ สนใจสินค้า ขอถามราคาหน่อยได้ไหมครับ"},
|
||||
{"role": "seller", "text": "สวัสดีครับ ขอทราบความต้องการเพิ่มเติมได้ไหมครับ"},
|
||||
],
|
||||
}
|
||||
@@ -906,7 +906,7 @@ def test_session_serializer_filters_untrusted_system_notes_and_redacts_legacy_op
|
||||
assert any("ผ่านไป 2-3" in text for text in texts)
|
||||
assert view["messages"][1] == {
|
||||
"role": "customer",
|
||||
"text": "ลูกค้าเริ่มต้นบทสนทนาแล้ว ลองทักและค้นหาความต้องการดูครับ",
|
||||
"text": "สวัสดีครับ สนใจสินค้า ขอถามราคาหน่อยได้ไหมครับ",
|
||||
}
|
||||
assert view["scenario"] == "social"
|
||||
assert view["persona_meta"]["scenario"] == "social"
|
||||
|
||||
@@ -209,7 +209,7 @@ def test_session_serializer_omits_hidden_internal_state():
|
||||
assert view["persona_meta"] == {"scenario": "social", "locale": "th"}
|
||||
assert view["messages"] == [{
|
||||
"role": "customer",
|
||||
"text": "ลูกค้าเริ่มต้นบทสนทนาแล้ว ลองทักและค้นหาความต้องการดูครับ",
|
||||
"text": "hello",
|
||||
}]
|
||||
assert "/private" not in str(view)
|
||||
|
||||
|
||||
@@ -28,14 +28,14 @@ def _oauth(client):
|
||||
return client.post("/api/auth/oauth", json={"provider": "google", "token": "verified-token"})
|
||||
|
||||
|
||||
def test_serializer_redacts_every_customer_message_before_first_seller_turn():
|
||||
def test_serializer_keeps_the_customer_openers_line_before_first_seller_turn():
|
||||
"""The first customer line is spoken dialogue (the persona's real greeting) — public."""
|
||||
public = serialize_session(
|
||||
{
|
||||
"id": "session-legacy",
|
||||
"persona_meta": {"locale": "en"},
|
||||
"messages": [
|
||||
{"role": "customer", "text": "SECRET_OPENER_ONE"},
|
||||
{"role": "customer", "text": "SECRET_OPENER_TWO"},
|
||||
{"role": "customer", "text": "สวัสดีครับ สนใจสินค้า ขอถามราคาหน่อยได้ไหมครับ"},
|
||||
{"role": "seller", "text": "Hello"},
|
||||
{"role": "customer", "text": "Public reply"},
|
||||
],
|
||||
@@ -43,13 +43,11 @@ def test_serializer_redacts_every_customer_message_before_first_seller_turn():
|
||||
)
|
||||
|
||||
texts = [row["text"] for row in public["messages"]]
|
||||
assert "SECRET_OPENER_ONE" not in texts
|
||||
assert "SECRET_OPENER_TWO" not in texts
|
||||
assert texts[:2] == [
|
||||
"Hi, a customer has started the conversation.",
|
||||
"Hi, a customer has started the conversation.",
|
||||
assert texts == [
|
||||
"สวัสดีครับ สนใจสินค้า ขอถามราคาหน่อยได้ไหมครับ",
|
||||
"Hello",
|
||||
"Public reply",
|
||||
]
|
||||
assert texts[-1] == "Public reply"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user