# greeting_handler.py # Classifies opening-turn messages and returns template short replies (no LLM). # Supports Latin/Roman and native Urdu (Arabic-script) greetings. # # v2 — Issue 8 fix + arabic_clarify removal # # Issue 8 (English "Yes, I can hear you." reply in Urdu-only session): # The connection_check tier in get_short_reply() had a fallback branch: # if _is_audio_connection_check(text): return "جی ہاں، آواز آ رہی ہے۔" # return "Yes, I can hear you." ← BUG: English fallback # When the connection-check pattern matched BUT _is_audio_connection_check() # returned False (e.g. a bare "ہیلو ہیلو" repetition matched the # connection_check tier via _is_connection_check but didn't contain # آواز/سن/bol keywords), the fallback English string was returned and # synthesised in an English voice — exactly the "Yes, I can hear you." # (chars=20) seen in the TTS log for session1 Turn1. # Fix: both branches of connection_check now return Urdu strings. # The English fallback is removed entirely (Urdu-only mode). # # arabic_clarify removal: # classify_opening_message() previously accepted a `language_locked` # param that was partly driven by arabic_clarify state. That param is # unchanged (it's the session language lock flag, not arabic_clarify). # No further changes needed in this file for arabic_clarify — it was # only consumed by prompt_builder.py and language_detector.py, not here. import re from typing import Literal from language_config import has_arabic_script OpeningTier = Literal["pure_greeting", "connection_check", "greeting_with_substance", "normal"] # Roman / Latin substance markers SUBSTANCE_MARKERS_LATIN = { "aap", "ap", "meri", "mera", "madad", "help", "kesse", "kaise", "kese", "kaisey", "puchna", "poochna", "admission", "course", "problem", "issue", "sawal", "sawaal", "question", "batana", "batao", "bata", "chahiye", "chahte", "chahti", "karna", "how", "what", "when", "where", "why", "can", "could", "would", "need", "want", "tell", "about", "regarding", "fee", "fees", "enroll", "register", "mujhe", "main", "mein", "hum", "humein", "kya", "kab", "kahan", "kyun", } # Urdu-script substance markers (intent beyond a bare greeting) SUBSTANCE_MARKERS_URDU = { "آپ", "اپ", "مدد", "مجھے", "میرا", "میری", "مرا", "کیا", "کہاں", "کیوں", "کیسے", "کسے", "بتائیں", "بتاؤ", "پوچھنا", "چاہیے", "ضرورت", "مسئلہ", "مسئله", "سوال", "داخلہ", "کورس", "فیس", "کرنا", "کریں", "سکتے", "سکتا", "سکتی", "بتانا", "رہنمائی", "معلومات", "جاننا", "جانکارى", } PURE_GREETING_PHRASES_LATIN = { "hi", "hello", "hey", "helo", "hii", "hiii", "salam", "adaab", "assalam u alaikum", "assalamu alaikum", "asalam u alaikum", "assalam o alaikum", "salam u alaikum", "salam alaikum", "assalam alaikum", "alaikum assalam", "walaikum assalam", "walaykum assalam", "walaikum salam", "kher he", "khair he", "khair mubarak", "kher mubarak", "mubarak", "good morning", "good evening", "good afternoon", "good night", "yes", "ok", "okay", "thanks", "thank you", "bye", "goodbye", "shukriya", "ji", "jee", "han", "haan", "nah", "na", } # Native Urdu-script greeting phrases (punctuation stripped before match) PURE_GREETING_PHRASES_URDU = { "السلام علیکم", "السلام عليكم", "سلام علیکم", "سلام عليكم", "وعلیکم السلام", "وعليكم السلام", "و علیکم السلام", "و عليكم السلام", "سلام", "السلام", "آداب", "اداب", "خیر ہے", "خیر مبارک", "خیرمبارک", "مبارک", "جی", "جی ہاں", "جی ہاں", "ہاں", "ہان", "شکریہ", "شکریا", "جزاک اللہ", "ہیلو", "ہائے", "ہیلو ہیلو", "سلام سلام", "الحمد للہ", "الحمد لله", "ماشاء اللہ", "ماشاء الله", } URDU_GREETING_TOKENS = { "السلام", "علیکم", "عليكم", "وعلیکم", "وعليكم", "سلام", "آداب", "اداب", "خیر", "مبارک", "جی", "ہاں", "ہان", "شکریہ", "شکریا", "ہیلو", "ہائے", } CONNECTION_CHECK_PATTERNS = [ # Latin / Roman re.compile(r"\bhello\s+hello\b", re.I), re.compile(r"\bhi\s+hi\b", re.I), re.compile(r"\bhey\s+hey\b", re.I), re.compile(r"awa?z\s+aara?hi", re.I), re.compile(r"awa?z\s+aa\s+rahi", re.I), re.compile(r"sun\s+pa\s+rahe", re.I), re.compile(r"sun\s+rahe", re.I), re.compile(r"koi\s+sun\s+raha", re.I), re.compile(r"sunaai\s+de", re.I), re.compile(r"sunai\s+de", re.I), re.compile(r"bol\s+rahe", re.I), # Urdu script re.compile(r"ہیلو\s+ہیلو"), re.compile(r"سلام\s+سلام"), re.compile(r"آواز\s+آ\s*رہی"), re.compile(r"آواز\s+آرہی"), re.compile(r"آواز\s+آ\s*رہی"), re.compile(r"سن\s+پا\s+رہے"), re.compile(r"سن\s+رہے"), re.compile(r"سن\s+رہی"), re.compile(r"کوئی\s+سن\s+رہا"), re.compile(r"سنائی\s+دے"), re.compile(r"سناائی\s+دے"), re.compile(r"بول\s+رہے"), re.compile(r"بول\s+رہی"), re.compile(r"آواز\s+آ\s*رہی\s+ہے"), ] _GREETING_MIRROR = [ # Latin / Roman (re.compile(r"assalam\s*u?\s*alaikum|assalamu\s*alaikum|salam\s*u?\s*alaikum", re.I), "وعلیکم السلام"), (re.compile(r"walaikum\s*assalam|walaykum\s*assalam|alaikum\s*assalam", re.I), "السلام علیکم"), (re.compile(r"khair\s*mubarak|kher\s*mubarak", re.I), "مبارک"), (re.compile(r"\bkher\s+he\b|\bkhair\s+he\b", re.I), "مبارک"), (re.compile(r"\badaab\b", re.I), "آداب"), (re.compile(r"\bhello\b", re.I), "ہیلو"), (re.compile(r"\bhi\b|\bhey\b|\bhelo\b", re.I), "ہیلو"), (re.compile(r"\bsalam\b", re.I), "وعلیکم السلام"), # Urdu script (re.compile(r"السلام\s*علیکم|السلام\s*عليكم|سلام\s*علیکم|سلام\s*عليكم"), "وعلیکم السلام"), (re.compile(r"وعلیکم\s*السلام|وعليكم\s*السلام"), "السلام علیکم"), (re.compile(r"خیر\s*مبارک|خیرمبارک"), "مبارک"), (re.compile(r"خیر\s*ہے"), "مبارک"), (re.compile(r"آداب|اداب"), "آداب"), (re.compile(r"ہیلو"), "ہیلو"), (re.compile(r"ہائے"), "ہائے"), (re.compile(r"^سلام$|^\s*سلام\s*$"), "وعلیکم السلام"), (re.compile(r"^جی\s*ہاں$|^ہاں$|^ہان$"), "جی"), (re.compile(r"شکریہ|شکریا"), "خوش آمدید"), ] def _strip_punctuation(text: str) -> str: return re.sub(r"[\s\u060C\u061B\u061F\u06D4\.,!?؛:]+", " ", text).strip() def _normalize_latin(text: str) -> str: return re.sub(r"\s+", " ", (text or "").strip().lower()) def _normalize_urdu(text: str) -> str: return re.sub(r"\s+", " ", _strip_punctuation(text)).strip() def _has_substance(text: str) -> bool: if not text: return False latin_words = set(re.findall(r"[a-zA-Z]+", text.lower())) if latin_words & SUBSTANCE_MARKERS_LATIN: return True urdu_words = set(re.findall(r"[\u0600-\u06FF]+", text)) if urdu_words & SUBSTANCE_MARKERS_URDU: return True if "?" in text or "؟" in text: lowered = text.lower() if any(m in lowered for m in SUBSTANCE_MARKERS_LATIN): return True if any(m in text for m in SUBSTANCE_MARKERS_URDU): return True return False def _is_pure_greeting_latin(text: str) -> bool: normalized = _normalize_latin(text) if not normalized: return False if normalized in PURE_GREETING_PHRASES_LATIN: return True words = normalized.split() filler = {"ji", "jee", "han", "haan"} if words and all(w in PURE_GREETING_PHRASES_LATIN or w in filler for w in words): return True return False def _is_pure_greeting_urdu(text: str) -> bool: if not has_arabic_script(text): return False normalized = _normalize_urdu(text) if not normalized: return False if normalized in PURE_GREETING_PHRASES_URDU: return True compact = normalized.replace("و ", "و") if compact in PURE_GREETING_PHRASES_URDU: return True words = re.findall(r"[\u0600-\u06FF]+", normalized) if words and all(w in URDU_GREETING_TOKENS or w == "ہے" for w in words): if set(words) <= (URDU_GREETING_TOKENS | {"ہے"}): return True return False def _is_pure_greeting(text: str) -> bool: return _is_pure_greeting_latin(text) or _is_pure_greeting_urdu(text) def _is_connection_check(text: str) -> bool: for pattern in CONNECTION_CHECK_PATTERNS: if pattern.search(text): return True normalized_latin = _normalize_latin(text) if normalized_latin.count("hello") >= 2 or normalized_latin.count("hi") >= 2: return True normalized_urdu = _normalize_urdu(text) if normalized_urdu.count("ہیلو") >= 2 or normalized_urdu.count("سلام") >= 2: return True return False def _message_length(text: str) -> int: stripped = _strip_punctuation(text) return len(stripped.replace(" ", "")) def classify_opening_message(text: str, turn_count: int, language_locked: bool) -> OpeningTier: """Classify message tier for early-turn routing.""" if turn_count > 3 or language_locked: return "normal" if _has_substance(text): return "greeting_with_substance" if _is_connection_check(text): return "connection_check" if _is_pure_greeting(text): return "pure_greeting" if _message_length(text) < 12 and not _has_substance(text): return "pure_greeting" return "normal" def _is_audio_connection_check(text: str) -> bool: latin = text.lower() if any(c in latin for c in ("awaaz", "awaz", "sun")): return True return any( marker in text for marker in ("آواز", "سن", "سنائی", "سناائی", "بول رہ") ) def get_short_reply(tier: OpeningTier, text: str, greeting_streak: int = 0) -> str: """ Deterministic short reply — no LLM. Urdu-only mode: all replies in Urdu. Issue 8 fix: the previous connection_check fallback branch returned the English string "Yes, I can hear you." when _is_audio_connection_check() was False (e.g. bare "ہیلو ہیلو" repetition). This produced an English TTS response in a Urdu-only session. Both branches now return Urdu. """ if tier == "connection_check": if _is_audio_connection_check(text): return "جی ہاں، آواز آ رہی ہے۔" # Issue 8 fix: was "Yes, I can hear you." — now Urdu return "جی، میں سن رہا ہوں۔" if tier == "pure_greeting": for pattern, reply in _GREETING_MIRROR: if pattern.search(text): return reply if greeting_streak >= 2: return "جی، میں آپ کی کیسے مدد کر سکتا ہوں؟" if has_arabic_script(text): return "وعلیکم السلام" # Issue 8 fix: was "Hello" — now Urdu return "ہیلو" # Issue 8 fix: was "Hello" — now Urdu return "جی" def should_short_circuit(tier: OpeningTier) -> bool: return tier in ("pure_greeting", "connection_check")