Restored Urdu grammar configuration from 90d92cd
Browse files
python-services/caller_info_extractor.py
CHANGED
|
@@ -43,11 +43,8 @@ def extract_phone_from_text(text: str) -> Optional[str]:
|
|
| 43 |
if match:
|
| 44 |
# Strip structural characters to normalize string digits
|
| 45 |
phone = re.sub(r'[\s\-\(\)\+]', '', match.group())
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
return phone
|
| 49 |
-
else:
|
| 50 |
-
logger.warning(f"Extracted phone number '{phone}' has invalid length {len(phone)}")
|
| 51 |
return None
|
| 52 |
|
| 53 |
|
|
@@ -57,42 +54,20 @@ def extract_name_from_text(text: str, language: str = "en") -> Optional[str]:
|
|
| 57 |
Branches into script-specific regex blocks depending on runtime language tracking.
|
| 58 |
"""
|
| 59 |
lang = (language or "en").lower().strip()
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
NAME_PATTERNS_ROMAN_UR = [
|
| 69 |
-
r"(?:mera\s+)?(?:naam|name)\s+(?:hai\s+|he\s+|is\s+)?([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)?)",
|
| 70 |
-
r"([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)?)\s+(?:ye\s+)?(?:mera\s+)?(?:naam|name|number)\b",
|
| 71 |
-
r"(?:i am|i'm|this is|call me)\s+([A-Za-z][a-z]+(?:\s+[A-Za-z][a-z]+)?)"
|
| 72 |
-
]
|
| 73 |
-
target_patterns.extend(NAME_PATTERNS_ROMAN_UR)
|
| 74 |
-
|
| 75 |
-
# Support native script patterns
|
| 76 |
-
if lang == "ur" or not has_latin:
|
| 77 |
-
target_patterns.extend(NAME_PATTERNS_UR)
|
| 78 |
-
if lang == "ar":
|
| 79 |
-
target_patterns.extend(NAME_PATTERNS_AR)
|
| 80 |
-
|
| 81 |
-
# Use a list to check unique patterns in order
|
| 82 |
-
seen = set()
|
| 83 |
-
unique_patterns = []
|
| 84 |
-
for p in target_patterns:
|
| 85 |
-
if p not in seen:
|
| 86 |
-
seen.add(p)
|
| 87 |
-
unique_patterns.append(p)
|
| 88 |
-
|
| 89 |
-
for pattern in unique_patterns:
|
| 90 |
match = re.search(pattern, text, re.IGNORECASE)
|
| 91 |
if match:
|
| 92 |
name = match.group(1).strip()
|
| 93 |
# Basic character validation step — filtering out short artifacts or junk blocks
|
| 94 |
if 2 <= len(name) <= 40:
|
| 95 |
-
logger.info(f"Regex pipeline intercepted identity credentials: {name}")
|
| 96 |
return name
|
| 97 |
return None
|
| 98 |
|
|
@@ -140,11 +115,9 @@ class CallerInfoCollector:
|
|
| 140 |
|
| 141 |
if not self.phone and extracted.get("phone"):
|
| 142 |
clean_phone = re.sub(r'[\s\-\(\)\+]', '', str(extracted["phone"]))
|
| 143 |
-
if clean_phone
|
| 144 |
self.phone = clean_phone
|
| 145 |
logger.info(f"LLM data backfill applied telephone configuration: {self.phone}")
|
| 146 |
-
else:
|
| 147 |
-
logger.warning(f"LLM extracted phone number '{clean_phone}' has invalid length")
|
| 148 |
|
| 149 |
if extracted.get("issue"):
|
| 150 |
self.issue = str(extracted["issue"]).strip()
|
|
|
|
| 43 |
if match:
|
| 44 |
# Strip structural characters to normalize string digits
|
| 45 |
phone = re.sub(r'[\s\-\(\)\+]', '', match.group())
|
| 46 |
+
logger.info(f"Regex pipeline intercepted telephone credentials: {phone}")
|
| 47 |
+
return phone
|
|
|
|
|
|
|
|
|
|
| 48 |
return None
|
| 49 |
|
| 50 |
|
|
|
|
| 54 |
Branches into script-specific regex blocks depending on runtime language tracking.
|
| 55 |
"""
|
| 56 |
lang = (language or "en").lower().strip()
|
| 57 |
+
target_patterns = NAME_PATTERNS_EN
|
| 58 |
+
|
| 59 |
+
if lang == "ur":
|
| 60 |
+
target_patterns = NAME_PATTERNS_UR
|
| 61 |
+
elif lang == "ar":
|
| 62 |
+
target_patterns = NAME_PATTERNS_AR
|
| 63 |
+
|
| 64 |
+
for pattern in target_patterns:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
match = re.search(pattern, text, re.IGNORECASE)
|
| 66 |
if match:
|
| 67 |
name = match.group(1).strip()
|
| 68 |
# Basic character validation step — filtering out short artifacts or junk blocks
|
| 69 |
if 2 <= len(name) <= 40:
|
| 70 |
+
logger.info(f"Regex pipeline intercepted identity credentials: {name} [Lang: {lang}]")
|
| 71 |
return name
|
| 72 |
return None
|
| 73 |
|
|
|
|
| 115 |
|
| 116 |
if not self.phone and extracted.get("phone"):
|
| 117 |
clean_phone = re.sub(r'[\s\-\(\)\+]', '', str(extracted["phone"]))
|
| 118 |
+
if clean_phone:
|
| 119 |
self.phone = clean_phone
|
| 120 |
logger.info(f"LLM data backfill applied telephone configuration: {self.phone}")
|
|
|
|
|
|
|
| 121 |
|
| 122 |
if extracted.get("issue"):
|
| 123 |
self.issue = str(extracted["issue"]).strip()
|
python-services/client.py
CHANGED
|
@@ -3,14 +3,10 @@
|
|
| 3 |
|
| 4 |
import os
|
| 5 |
import logging
|
| 6 |
-
from dotenv import load_dotenv
|
| 7 |
from openai import OpenAI
|
| 8 |
|
| 9 |
logger = logging.getLogger("client")
|
| 10 |
|
| 11 |
-
# Load environment configuration
|
| 12 |
-
load_dotenv()
|
| 13 |
-
|
| 14 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
| 15 |
FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
|
| 16 |
|
|
|
|
| 3 |
|
| 4 |
import os
|
| 5 |
import logging
|
|
|
|
| 6 |
from openai import OpenAI
|
| 7 |
|
| 8 |
logger = logging.getLogger("client")
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
| 11 |
FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
|
| 12 |
|
python-services/conversation_manager.py
CHANGED
|
@@ -122,7 +122,7 @@ class ConversationSession:
|
|
| 122 |
)
|
| 123 |
|
| 124 |
# Bound the network execution cycle to match tight audio latency constraints
|
| 125 |
-
res = await asyncio.wait_for(classify_task, timeout=0.
|
| 126 |
out = (res.choices[0].message.content or "").strip().lower()
|
| 127 |
|
| 128 |
if "quick" in out:
|
|
|
|
| 122 |
)
|
| 123 |
|
| 124 |
# Bound the network execution cycle to match tight audio latency constraints
|
| 125 |
+
res = await asyncio.wait_for(classify_task, timeout=0.25)
|
| 126 |
out = (res.choices[0].message.content or "").strip().lower()
|
| 127 |
|
| 128 |
if "quick" in out:
|
python-services/language_detector.py
CHANGED
|
@@ -77,7 +77,7 @@ def detect_language_from_text(text: str) -> tuple[str, float]:
|
|
| 77 |
return "ur", 0.95
|
| 78 |
|
| 79 |
# If a distinct non-English unicode layout matches, commit with high confidence
|
| 80 |
-
if script_guess in ('zh', 'bn'):
|
| 81 |
mapped_lang = LANG_CODE_MAP.get(script_guess, script_guess)
|
| 82 |
return mapped_lang, 0.95
|
| 83 |
|
|
@@ -112,8 +112,6 @@ def detect_language_from_text(text: str) -> tuple[str, float]:
|
|
| 112 |
if top.lang == "hi":
|
| 113 |
return "ur", top.prob
|
| 114 |
lang = LANG_CODE_MAP.get(top.lang, top.lang)
|
| 115 |
-
if lang not in ("ur", "ar", "en", "zh", "fa", "tr", "bn"):
|
| 116 |
-
lang = "en"
|
| 117 |
return lang, top.prob
|
| 118 |
except Exception:
|
| 119 |
return DEFAULT_LANGUAGE, 0.50
|
|
@@ -194,6 +192,4 @@ def detect_language(text: str) -> str:
|
|
| 194 |
raw_detected = detect_language_from_content(text)
|
| 195 |
if raw_detected in ('hi', 'pa'):
|
| 196 |
return "ur"
|
| 197 |
-
if raw_detected not in ["ur", "ar", "en", "zh", "fa", "tr", "bn"]:
|
| 198 |
-
return "en"
|
| 199 |
return raw_detected
|
|
|
|
| 77 |
return "ur", 0.95
|
| 78 |
|
| 79 |
# If a distinct non-English unicode layout matches, commit with high confidence
|
| 80 |
+
if script_guess in ('zh', 'bn', 'gu', 'ta', 'te', 'kn', 'ml', 'he', 'ko', 'ja'):
|
| 81 |
mapped_lang = LANG_CODE_MAP.get(script_guess, script_guess)
|
| 82 |
return mapped_lang, 0.95
|
| 83 |
|
|
|
|
| 112 |
if top.lang == "hi":
|
| 113 |
return "ur", top.prob
|
| 114 |
lang = LANG_CODE_MAP.get(top.lang, top.lang)
|
|
|
|
|
|
|
| 115 |
return lang, top.prob
|
| 116 |
except Exception:
|
| 117 |
return DEFAULT_LANGUAGE, 0.50
|
|
|
|
| 192 |
raw_detected = detect_language_from_content(text)
|
| 193 |
if raw_detected in ('hi', 'pa'):
|
| 194 |
return "ur"
|
|
|
|
|
|
|
| 195 |
return raw_detected
|
python-services/llm_server.py
CHANGED
|
@@ -7,7 +7,7 @@ import time
|
|
| 7 |
import logging
|
| 8 |
import asyncio
|
| 9 |
from typing import List, Optional, Dict, Any
|
| 10 |
-
from fastapi import FastAPI, HTTPException
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
from fastapi.responses import JSONResponse
|
| 13 |
from pydantic import BaseModel
|
|
@@ -83,50 +83,6 @@ class EndSessionRequest(BaseModel):
|
|
| 83 |
session_id: str
|
| 84 |
|
| 85 |
|
| 86 |
-
async def run_background_extraction(session_id: str, messages: List[Dict[str, str]]):
|
| 87 |
-
"""
|
| 88 |
-
Asynchronously calls Groq 8B model to extract name, phone, and issue
|
| 89 |
-
from the session history, then updates the session's caller_info.
|
| 90 |
-
"""
|
| 91 |
-
session = manager.get_session(session_id)
|
| 92 |
-
if not session:
|
| 93 |
-
return
|
| 94 |
-
|
| 95 |
-
history_text = ""
|
| 96 |
-
for msg in messages:
|
| 97 |
-
history_text += f"{msg['role']}: {msg['content']}\n"
|
| 98 |
-
|
| 99 |
-
prompt = f"""You are a data extraction assistant. Analyze the conversation history below and extract the caller's name, phone number, and the core issue they are calling about.
|
| 100 |
-
|
| 101 |
-
Conversation history:
|
| 102 |
-
{history_text}
|
| 103 |
-
|
| 104 |
-
Provide your response strictly in the following JSON format:
|
| 105 |
-
{{
|
| 106 |
-
"name": "extracted name or null",
|
| 107 |
-
"phone": "extracted phone number or null",
|
| 108 |
-
"issue": "concise description of the user's issue, query, or reason for calling or null"
|
| 109 |
-
}}
|
| 110 |
-
Reply ONLY with the raw JSON object. Do not include markdown formatting, code block backticks (like ```json), or any other conversational text."""
|
| 111 |
-
|
| 112 |
-
try:
|
| 113 |
-
loop = asyncio.get_running_loop()
|
| 114 |
-
response = await loop.run_in_executor(
|
| 115 |
-
None,
|
| 116 |
-
lambda: groq.chat.completions.create(
|
| 117 |
-
model=MODEL_FAST,
|
| 118 |
-
messages=[{"role": "user", "content": prompt}],
|
| 119 |
-
temperature=0.0,
|
| 120 |
-
response_format={"type": "json_object"}
|
| 121 |
-
)
|
| 122 |
-
)
|
| 123 |
-
import json
|
| 124 |
-
extracted_data = json.loads(response.choices[0].message.content.strip())
|
| 125 |
-
session.caller_info.update_from_llm_extraction(extracted_data)
|
| 126 |
-
except Exception as e:
|
| 127 |
-
logger.error(f"Error during background LLM info extraction: {e}")
|
| 128 |
-
|
| 129 |
-
|
| 130 |
# ── REST API Router Endpoints ────────────────────────────────────────────────
|
| 131 |
|
| 132 |
@app.post("/session/start")
|
|
@@ -155,7 +111,7 @@ async def start_session(req: StartSessionRequest):
|
|
| 155 |
|
| 156 |
|
| 157 |
@app.post("/chat")
|
| 158 |
-
async def chat(req: ChatRequest
|
| 159 |
"""
|
| 160 |
Processes voice transcript inputs, executes dynamic RAG context assembly,
|
| 161 |
evaluates historical payload token depth, and executes low-latency model inference.
|
|
@@ -187,15 +143,13 @@ async def chat(req: ChatRequest, background_tasks: BackgroundTasks):
|
|
| 187 |
|
| 188 |
# 3. Rolling window processing and continuous background text summary compilation
|
| 189 |
new_summary = req.currentSummary or ""
|
| 190 |
-
if len(session.messages) >
|
| 191 |
-
messages_to_keep = session.messages[-
|
| 192 |
-
messages_to_summarize = session.messages[:-
|
| 193 |
-
|
| 194 |
-
# Slice messages BEFORE the await to prevent race condition deletes
|
| 195 |
-
session.messages = messages_to_keep
|
| 196 |
|
| 197 |
# Non-blocking context thread synthesis execution
|
| 198 |
new_summary = await generate_summary(new_summary, messages_to_summarize)
|
|
|
|
| 199 |
|
| 200 |
# 4. Synthesize structural text templates with active script sniffing indicators
|
| 201 |
system_prompt = build_system_prompt(
|
|
@@ -257,7 +211,7 @@ async def chat(req: ChatRequest, background_tasks: BackgroundTasks):
|
|
| 257 |
|
| 258 |
# 7. Model Router Intent Classification Integration
|
| 259 |
intent = await session.classify_intent(req.message, detected_language)
|
| 260 |
-
primary_model = MODEL_HEAVY if intent == "heavy" else MODEL_FAST
|
| 261 |
|
| 262 |
inference_parameters: Dict[str, Any] = {
|
| 263 |
"model": primary_model,
|
|
@@ -313,9 +267,6 @@ async def chat(req: ChatRequest, background_tasks: BackgroundTasks):
|
|
| 313 |
if extracted_phone:
|
| 314 |
session.caller_info.phone = extracted_phone
|
| 315 |
|
| 316 |
-
# Trigger background LLM caller info extraction (backfill issue, phone, and name)
|
| 317 |
-
background_tasks.add_task(run_background_extraction, session.session_id, session.get_history_for_api())
|
| 318 |
-
|
| 319 |
elapsed_ms = int((time.time() - start_timestamp) * 1000)
|
| 320 |
logger.info(f"Execution wrapped. Metrics: ID={session.session_id} | Provider={provider_tag} | Latency={elapsed_ms}ms | Language={detected_language}")
|
| 321 |
|
|
|
|
| 7 |
import logging
|
| 8 |
import asyncio
|
| 9 |
from typing import List, Optional, Dict, Any
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
from fastapi.responses import JSONResponse
|
| 13 |
from pydantic import BaseModel
|
|
|
|
| 83 |
session_id: str
|
| 84 |
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
# ── REST API Router Endpoints ────────────────────────────────────────────────
|
| 87 |
|
| 88 |
@app.post("/session/start")
|
|
|
|
| 111 |
|
| 112 |
|
| 113 |
@app.post("/chat")
|
| 114 |
+
async def chat(req: ChatRequest):
|
| 115 |
"""
|
| 116 |
Processes voice transcript inputs, executes dynamic RAG context assembly,
|
| 117 |
evaluates historical payload token depth, and executes low-latency model inference.
|
|
|
|
| 143 |
|
| 144 |
# 3. Rolling window processing and continuous background text summary compilation
|
| 145 |
new_summary = req.currentSummary or ""
|
| 146 |
+
if len(session.messages) > 6:
|
| 147 |
+
messages_to_keep = session.messages[-6:]
|
| 148 |
+
messages_to_summarize = session.messages[:-6]
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
# Non-blocking context thread synthesis execution
|
| 151 |
new_summary = await generate_summary(new_summary, messages_to_summarize)
|
| 152 |
+
session.messages = messages_to_keep
|
| 153 |
|
| 154 |
# 4. Synthesize structural text templates with active script sniffing indicators
|
| 155 |
system_prompt = build_system_prompt(
|
|
|
|
| 211 |
|
| 212 |
# 7. Model Router Intent Classification Integration
|
| 213 |
intent = await session.classify_intent(req.message, detected_language)
|
| 214 |
+
primary_model = MODEL_HEAVY if (intent == "heavy" or needs70B(req.message, detected_language)) else MODEL_FAST
|
| 215 |
|
| 216 |
inference_parameters: Dict[str, Any] = {
|
| 217 |
"model": primary_model,
|
|
|
|
| 267 |
if extracted_phone:
|
| 268 |
session.caller_info.phone = extracted_phone
|
| 269 |
|
|
|
|
|
|
|
|
|
|
| 270 |
elapsed_ms = int((time.time() - start_timestamp) * 1000)
|
| 271 |
logger.info(f"Execution wrapped. Metrics: ID={session.session_id} | Provider={provider_tag} | Latency={elapsed_ms}ms | Language={detected_language}")
|
| 272 |
|
python-services/prompt_builder.py
CHANGED
|
@@ -91,12 +91,7 @@ def build_system_prompt(
|
|
| 91 |
Collect the caller's name and contact number naturally throughout the dialogue.
|
| 92 |
- Do NOT ask for name or phone on the first greeting turn. Wait until the conversation has an established direction.
|
| 93 |
- Weave information collection into the flow as a personal courtesy, not as a system requirement (e.g., "May I know who I am speaking with?").
|
| 94 |
-
- CRITICAL: NEVER justify collecting name or phone by referencing administrative, tracking, record, or database reasons. This makes callers feel surveilled and tense.
|
| 95 |
-
* English forbidden phrases include: "so I can track your record", "for our database", "for verification purposes", "to complete your file", "to check your details".
|
| 96 |
-
* Urdu (اردو) forbidden phrases include: "تاکہ میں آپ کی معلومات کو چیک کر سکوں", "تاکہ میں آپ کی معلومات ریکارڈ کر سکوں", "تاکہ میں آپ کی فائل کو مکمل کر سکوں", "تاکہ میں ریکارڈ دیکھ سکوں", "تصدیق کے لیے", "تاکہ میں سسٹم میں دیکھ سکوں".
|
| 97 |
-
* Arabic (العربية) forbidden phrases include: "لأتحقق من ملفك", "لتسجيل بياناتك", "لأغراض التحقق", "لقاعدة البيانات".
|
| 98 |
-
NEVER use these or any similar expressions in any language.
|
| 99 |
-
- CRITICAL: If the caller provides a phone number, check if it is valid (typically 7 to 15 digits depending on the region). If the number is clearly invalid (e.g. contains too many digits like "033525252525252525" or is too short), do NOT accept it. Instead, politely ask the caller to repeat or clarify their number.
|
| 100 |
- If a caller declines to share their name or phone, accept it immediately and gracefully without apologizing excessively or re-asking. Transition directly to answering their question.
|
| 101 |
- Currently tracked caller status profile: {caller_context}
|
| 102 |
|
|
|
|
| 91 |
Collect the caller's name and contact number naturally throughout the dialogue.
|
| 92 |
- Do NOT ask for name or phone on the first greeting turn. Wait until the conversation has an established direction.
|
| 93 |
- Weave information collection into the flow as a personal courtesy, not as a system requirement (e.g., "May I know who I am speaking with?").
|
| 94 |
+
- CRITICAL: NEVER justify collecting name or phone by referencing administrative, tracking, record, or database reasons. This makes callers feel surveilled and tense. Phrases like "so I can track your record", "for our database", "for verification purposes" are FORBIDDEN.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
- If a caller declines to share their name or phone, accept it immediately and gracefully without apologizing excessively or re-asking. Transition directly to answering their question.
|
| 96 |
- Currently tracked caller status profile: {caller_context}
|
| 97 |
|