File size: 6,713 Bytes
160245d 7e54ab9 160245d f7d6a8a 160245d 80cecae 160245d 7e54ab9 160245d f7d6a8a 7e54ab9 160245d 9cd90d7 7e54ab9 9cd90d7 7e54ab9 9cd90d7 f7d6a8a 7e54ab9 f7d6a8a 7e54ab9 f7d6a8a 160245d f7d6a8a 9cd90d7 f62bf0e 160245d f7d6a8a 80cecae f62bf0e 9cd90d7 f62bf0e 9cd90d7 f62bf0e 160245d 7e54ab9 f62bf0e 160245d 7e54ab9 160245d 7e54ab9 f7d6a8a 160245d 7e54ab9 f7d6a8a 7e54ab9 160245d 7e54ab9 160245d f7d6a8a 7e54ab9 f7d6a8a 7e54ab9 160245d f7d6a8a 7e54ab9 160245d 7e54ab9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | # caller_info_extractor.py
# Extracts caller fields from conversation based on optional per-session field spec.
# Uses regex for immediate hot-path extraction when fields are configured.
import re
import logging
from typing import List, Dict, Any, Optional
from language_config import normalize_language
logger = logging.getLogger("caller_info_extractor")
PHONE_PATTERNS = [
r'\b(?:\+92|0092|92)[\s\-]?3\d{2}[\s\-]?\d{7}\b(?!\d)',
r'\b(?:\+966|00966|966)[\s\-]?5\d{8}\b(?!\d)',
r'\b(?:\+971|00971|971)[\s\-]?5[024568]\d{7}\b(?!\d)',
r'\b(?:\+91|0091|91)[\s\-]?[6789]\d{9}\b(?!\d)',
r'\b(?:\+1|1)[\s\-]?\(?[2-9]\d{2}\)?[\s\-]?[2-9]\d{2}[\s\-]?\d{4}\b(?!\d)',
r'\b0\d{2,3}[\s\-]?\d{7,8}\b(?!\d)',
r'\b\d{10,13}\b(?!\d)',
]
NAME_PATTERNS_EN = [
r"(?:my name is|i am|i'm|this is|call me)\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)?)",
r"([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)?)\s+(?:here|speaking|calling)",
]
NAME_PATTERNS_ROMAN_UR = [
r"(?:mera\s+na(?:am|me?)|apna\s+naam?)\s+([A-Za-z][a-z]{2,}(?:\s+[A-Za-z][a-z]+)?)\s+(?:hai|he|hun|hoon|ho)\b",
r"(?:mera\s+na(?:am|me?)|apna\s+naam?)\s+([A-Za-z][a-z]{2,}(?:\s+[A-Za-z][a-z]+)?)",
r"([A-Za-z][a-z]+)\s+(?:ye|yeh|ye\s+mera|mera)\s+na(?:am|me?)",
r"\bna(?:am|me?)\s+(?:and\s+)?([A-Za-z][a-z]{2,})",
r"(?:main|mein)\s+([A-Za-z][a-z]{2,})\s+(?:hoon|hun|ho)",
r"(?:mujhe|humein)\s+([A-Za-z][a-z]{2,})\s+(?:kehte|bolte|bulao|kahte)",
]
NAME_PATTERNS_UR = [
r"(?:میرا نام|میں ہوں)\s+([\u0600-\u06FF\s]+)",
r"([\u0600-\u06FF\s]+)\s+(?:بات کر رہا ہوں|بات کر رہی ہوں|بول رہا ہوں)",
]
ID_PATTERNS = [
r'\b\d{5}[\s\-]?\d{7}[\s\-]?\d\b', # CNIC Pakistan
r'\b[A-Z]{0,3}\d{4,12}\b', # Generic student/employee ID
]
NAME_STOP_WORDS = {
"he", "hai", "hun", "hoon", "ho", "and", "ye", "yeh", "mera", "name",
"naam", "number", "num", "the", "my", "is", "am", "me", "id",
}
def extract_phone_from_text(text: str) -> Optional[str]:
for pattern in PHONE_PATTERNS:
match = re.search(pattern, text, re.IGNORECASE)
if match:
phone = re.sub(r'[\s\-\(\)\+]', '', match.group())
digit_count = sum(c.isdigit() for c in phone)
if digit_count < 7 or digit_count > 15:
logger.warning(f"Rejected implausible phone number ({digit_count} digits): {phone}")
continue
logger.info(f"Regex pipeline intercepted telephone credentials: {phone}")
return phone
return None
def extract_name_from_text(text: str, language: str = "en") -> Optional[str]:
lang = normalize_language(language)
latin_chars = sum(1 for c in text if c.isascii() and c.isalpha())
total_chars = sum(1 for c in text if c.isalpha())
is_roman_script = total_chars > 0 and (latin_chars / total_chars) > 0.5
if lang == "ur" and is_roman_script:
target_patterns = NAME_PATTERNS_ROMAN_UR + NAME_PATTERNS_EN
elif lang == "ur":
target_patterns = NAME_PATTERNS_UR
else:
target_patterns = NAME_PATTERNS_EN + NAME_PATTERNS_ROMAN_UR
for pattern in target_patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
name = match.group(1).strip()
if 2 <= len(name) <= 40 and name.lower() not in NAME_STOP_WORDS:
logger.info(f"Regex pipeline intercepted identity credentials: {name} [Lang: {lang}]")
return name
return None
def extract_id_from_text(text: str) -> Optional[str]:
for pattern in ID_PATTERNS:
match = re.search(pattern, text, re.IGNORECASE)
if match:
value = match.group().strip()
if len(value) >= 4:
logger.info(f"Regex pipeline intercepted ID credentials: {value}")
return value
return None
def extract_generic_text_field(text: str) -> Optional[str]:
match = re.search(r'\b([A-Za-z0-9][A-Za-z0-9\-]{2,20})\b', text)
if match:
return match.group(1)
return None
class CallerInfoCollector:
"""Tracks caller profile fields defined by per-session collection spec."""
def __init__(self, fields_spec: Optional[List[Dict[str, Any]]] = None):
self.fields_spec: List[Dict[str, Any]] = fields_spec or []
self.collected: Dict[str, Optional[str]] = {
f["key"]: None for f in self.fields_spec if f.get("key")
}
self.language: Optional[str] = "en"
def is_enabled(self) -> bool:
return bool(self.fields_spec)
def process_message(self, text: str, language: str = "en") -> None:
if not self.is_enabled():
return
self.language = language
for field in self.fields_spec:
key = field.get("key")
if not key or self.collected.get(key):
continue
field_type = (field.get("type") or "text").lower()
value = None
if field_type == "name":
value = extract_name_from_text(text, language=language)
elif field_type == "phone":
value = extract_phone_from_text(text)
elif field_type == "id":
value = extract_id_from_text(text)
else:
value = extract_generic_text_field(text)
if value:
self.collected[key] = value
def update_from_llm_extraction(self, extracted: Dict[str, Any]) -> None:
if not extracted or not self.is_enabled():
return
for field in self.fields_spec:
key = field.get("key")
if not key or self.collected.get(key):
continue
if extracted.get(key):
clean = str(extracted[key]).strip()
if clean and clean.lower() != "unknown":
self.collected[key] = clean
def to_dict(self) -> Dict[str, Any]:
if not self.is_enabled():
return {}
return {k: v for k, v in self.collected.items()}
def missing_fields(self) -> List[str]:
if not self.is_enabled():
return []
return [f["key"] for f in self.fields_spec if f.get("key") and not self.collected.get(f["key"])]
def is_complete(self) -> bool:
return self.is_enabled() and len(self.missing_fields()) == 0
def get_field_labels(self) -> List[Dict[str, str]]:
return [
{
"key": f["key"],
"label": f.get("label", f["key"]),
"label_ur": f.get("label_ur", f.get("label", f["key"])),
"type": f.get("type", "text"),
}
for f in self.fields_spec
if f.get("key")
]
|