"""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 /app ; this script lives in /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()