Files
sales-trainer/backend/scripts/fix_openers.py
Macky 4bff7db098 fix(persona): openers must ASK or say สนใจ — never 'ลองดู/ลอง/แค่มาดู'
- persona_prompts OPENER RULE: openers must be a direct question OR declare
  interest ('สนใจ/สอบถาม'); explicitly forbid 'ลองดู / ลอง / มาลอง / แค่มาดู /
  กดดู / แวะดู' (trying/browsing) phrasing which Thai customers don't use
- fix_openers.py: rewrite browse/try openers into a polite natural interest
  opener ('สวัสดีครับ/ค่ะ สนใจสอบถามสินค้าครับ ขอรายละเอียดได้ไหม'), leaving
  openers that already ask a question or say สนใจ untouched
2026-08-19 10:19:29 +07:00

124 lines
5.7 KiB
Python

"""One-time data fix (run in the EasyPanel container console):
Rewrite existing personas' openers (opener) to natural Thai.
Only rewords openers that:
- open with a casual interjection/filler (เฮ้ / เอ้ / เอ่อ / hey / hi / yo ...),
- open with ellipsis/dots ("..", "..."), or
- end with a dismissive clause Thais would not say first
("แต่ไม่คิดจะซื้อหรอกนะ", "ไม่ได้จะซื้อ", "แค่มาดูเล่น", "ก็แค่มาเมิน"...).
Everyone else is left untouched (safe & reversible). Prints before -> after.
Usage:
cd /app/backend
python3 scripts/fix_openers.py
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
# Make `app` importable no matter what directory the script is invoked from.
# The repo layout is <backend>/app ; this script lives in <backend>/scripts/.
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.config import Config
from app.services.groups import GroupStore
# Leading casual interjections/filler that make a Thai opener sound unnatural.
FILLER = re.compile(
r"^(?:เฮ้|เอ้|เอ่อ|เอ๊ะ|อืม|เออ|นี่ๆ|นี่|ว่าไง|เฮย์|hey|hiya|hi|hello|yo)"
r"[\s:,\-–—]*",
re.IGNORECASE,
)
BARE_POLITE = re.compile(r"^(ครับ|ค่ะ)(\s|$)")
THAI_CHAR = re.compile(r"[\u0E00-\u0E7F]")
# Leading punctuation/dots ("..", "...") that look unfinished/un-natural.
LEAD_DOTS = re.compile(r"^[\.…\s]*")
# Trailing dismissive clauses Thais would not say in an opener
# ("แต่ไม่คิดจะซื้อหรอกนะ", "ไม่ได้จะซื้อ", "แค่มาดูเล่น", "ก็แค่มาเมิน"...).
DISMISS_TAIL = re.compile(
r"\s*(?:แต่(?:ก็)?(?:ไม่(?:ได้)?คิดจะซื้อ|ไม่รู้จะซื้อ|ไม่ได้จะซื้อ|ไม่ตั้งใจซื้อ)|"
r"แค่(?:มา)?ดู(?:เล่น)?|ก็แค่(?:มา)?ดู(?:เล่น)?|ก็แค่มาเมิน|"
r"แต่กะแค่ลองดู|ก็แค่ลองดู)(?:หรอกนะ|หรอก|นะ|ล่ะ)?$"
)
# "just trying/looking" phrasing Thai customers do NOT use as an opener.
BROWSE_TRY = re.compile(
r"ลอง(?:เข้าไปดู|กดดู|ปล่อย|ใช้|ดู)?|แค่(?:มา)?ดู|มาลอง|กดดู|แวะดู|ดูเล่น|แค่มาอ่าน"
)
QUESTION = re.compile(r"ไหม|หรือ|ยังไง|เท่าไหร่|ไหน|กี่|ตัวไหน|มีมั้ย|\?$")
INTEREST = re.compile(r"สนใจ|สอบถาม|อยาก|ขอถาม|ต้องการ|สนใจอยาก")
def fix_opener(existing: str) -> str | None:
"""Return the rewritten opener, or None when nothing needs changing."""
if not isinstance(existing, str) or not existing.strip():
return None
s = existing.strip()
# Only target Thai openers; leave English ones untouched.
if not THAI_CHAR.search(s):
return None
# 0) Strip leading dots/ellipsis and any casual filler.
new = LEAD_DOTS.sub("", s)
m = FILLER.match(new)
if m:
new = new[m.end():]
new = new.strip(" .…,:-–—;")
# 1) Remove a trailing dismissive clause ("แต่ไม่คิดจะซื้อหรอกนะ" etc.),
# so the opener stays politely interested instead of pre-dismissing.
new = DISMISS_TAIL.sub("", new).strip(" .…,:-–—;")
# 2) If it now opens with a bare politeness word, prepend the greeting:
# "ครับ สนใจ..." -> "สวัสดีครับ สนใจ..."
bp = BARE_POLITE.match(new)
if bp:
polite = new[: len(bp.group(1))]
new = new[len(polite):].strip(" .…,:-–—;") or ""
new = "สวัสดี" + polite + ((" " + new) if new else "")
# 3) A "ลองดู / ลอง / แค่มาดู / กดดู" opener (just trying/browsing, no real
# question, not declaring interest) is unnatural in Thai. Rewrite it into
# a polite interest opener. (Any opener that already asks a question or
# says "สนใจ/สอบถาม" is left alone.)
if BROWSE_TRY.search(new) and not QUESTION.search(new) and not INTEREST.search(new):
polite = "ค่ะ" if "ค่ะ" in new else "ครับ"
new = f"สวัสดี{polite} สนใจสอบถามสินค้าครับ ขอรายละเอียดได้ไหม"
cleaned = " ".join(new.split())
if not cleaned or cleaned == s:
return None # nothing meaningful changed (or was empty)
return cleaned[:400]
def main() -> None:
store = GroupStore(Config.DATA_DIR)
groups = store.groups.all()
fixed = skipped = 0
seen: set[str] = set()
for group in groups:
gid = group.get("id") or group.get("_id")
if not gid or gid in seen:
continue
seen.add(gid)
raw = group.get("personas") or []
personas = [p for p in raw if isinstance(p, dict) and p.get("id")]
for p in personas:
oid = p.get("id")
old = p.get("opener") or ""
new = fix_opener(old)
if new is None:
skipped += 1
continue
print(f"[{gid}] persona={oid!r}")
print(f" before: {old!r}")
print(f" after : {new!r}")
store.update_persona(gid, oid, {"opener": new})
fixed += 1
print(f"\nDONE — fixed: {fixed}, left-as-is: {skipped}")
if __name__ == "__main__":
main()