File size: 8,510 Bytes
68923a4 813b923 68923a4 a2c51f3 68923a4 813b923 80cecae 9cd90d7 68923a4 a986742 68923a4 a2c51f3 7247174 a2c51f3 7e54ab9 68923a4 a2c51f3 68923a4 7e54ab9 68923a4 813b923 68923a4 7e54ab9 68923a4 7e54ab9 68923a4 e3d8805 59431d4 e3d8805 2b0a596 e3d8805 59431d4 e3d8805 813b923 68923a4 a2c51f3 813b923 68923a4 813b923 68923a4 80cecae 68923a4 9cd90d7 813b923 80cecae 9cd90d7 813b923 a986742 813b923 68923a4 80cecae a2c51f3 68923a4 813b923 68923a4 813b923 68923a4 e3d8805 68923a4 a2c51f3 68923a4 a2c51f3 68923a4 813b923 68923a4 a2c51f3 813b923 68923a4 813b923 7e54ab9 68923a4 a2c51f3 68923a4 7e54ab9 68923a4 813b923 68923a4 813b923 68923a4 813b923 68923a4 813b923 68923a4 813b923 68923a4 813b923 68923a4 7e54ab9 68923a4 813b923 68923a4 | 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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | # conversation_manager.py
# Manages conversation sessions — memory within an active voice call.
#
# v2 — arabic_clarify removed
# get_arabic_clarification_flag() removed from ConversationSession.
# All callers (voice_pipeline._build_prompt, llm_server._build_prompt)
# updated to not pass arabic_clarification to build_system_prompt.
import time
import uuid
import logging
from typing import List, Dict, Any, Optional
from language_config import normalize_language
from utils import needs70B
from language_detector import SmartLanguageDetector
from caller_info_extractor import CallerInfoCollector
logger = logging.getLogger("conversation_manager")
SESSION_TIMEOUT_SECONDS = 30 * 60
MAX_HISTORY_MESSAGES = 20
class ConversationSession:
"""Single conversation session tracking an active phone call instance."""
def __init__(
self,
session_id: str,
company_name: str,
agent_name: str,
agent_gender: str = "male",
custom_rules: Optional[List[str]] = None,
document_context: str = "",
caller_collection_fields: Optional[List[Dict[str, Any]]] = None,
):
self.session_id = session_id
self.company_name = company_name
self.agent_name = agent_name
self.agent_gender = agent_gender
self.custom_rules = custom_rules or []
self.document_context = document_context
self.caller_collection_fields: List[Dict[str, Any]] = caller_collection_fields or []
self.messages: List[Dict[str, str]] = []
self.language_detector = SmartLanguageDetector()
self.caller_info = CallerInfoCollector(fields_spec=self.caller_collection_fields)
self.created_at = time.time()
self.last_active = time.time()
self.turn_count = 0
self.greeting_streak = 0
# Database mapping properties
self.company_id = None
self.company_name_ur = None
self.overview = None
self.overview_ur = None
self.agent_name_ur = None
self.agent_role = None
self.agent_role_ur = None
self.phone_line_id = None
self.line_token = None
self.agent_config_id = None
self.rag_enabled = False
@classmethod
def from_tenant_config(cls, config: Dict[str, Any], session_id: str) -> "ConversationSession":
"""Creates a ConversationSession from a database configuration dictionary."""
rules_raw = config.get("system_instruction_rules") or ""
custom_rules = []
if isinstance(rules_raw, list):
custom_rules = rules_raw
elif isinstance(rules_raw, str):
custom_rules = [r.strip() for r in rules_raw.split(".") if r.strip()]
session = cls(
session_id=session_id,
company_name=config.get("company_name") or "Our Company",
agent_name=config.get("agent_name") or "Sara",
agent_gender=config.get("agent_gender") or "male",
custom_rules=custom_rules,
document_context="",
caller_collection_fields=config.get("caller_details_to_collect") or []
)
session.company_id = config.get("company_id")
session.company_name_ur = config.get("company_name_ur")
session.overview = config.get("overview")
session.overview_ur = config.get("overview_ur")
session.agent_name_ur = config.get("agent_name_ur")
session.agent_role = config.get("agent_role")
session.agent_role_ur = config.get("agent_role_ur")
session.phone_line_id = config.get("phone_line_id")
session.line_token = config.get("line_token")
session.agent_config_id = config.get("agent_config_id")
session.rag_enabled = config.get("rag_enabled", False)
return session
def add_message(self, role: str, content: str) -> None:
self.messages.append({"role": role, "content": content})
self.last_active = time.time()
if len(self.messages) > MAX_HISTORY_MESSAGES:
self.messages = self.messages[-MAX_HISTORY_MESSAGES:]
def process_user_message(self, text: str) -> str:
"""
Process user message — update language detector and caller info.
Returns the resolved language code string.
"""
self.turn_count += 1
self.last_active = time.time()
language = normalize_language(self.language_detector.process_message(text))
self.caller_info.process_message(text, language)
return language
def classify_intent(self, text: str, lang_code: str) -> str:
quick_exact = [
"yes", "no", "hello", "hi", "hey", "hold on", "wait", "stop", "ok",
"okay", "thanks", "thank you", "جی", "ہاں", "نا", "شکریہ", "نہیں"
]
normalized = (text or "").strip().lower()
effective_lang = normalize_language(lang_code)
if normalized in quick_exact or len(normalized) <= 6:
return "fast"
return "heavy" if needs70B(text, effective_lang) else "fast"
def get_current_language(self) -> str:
return normalize_language(self.language_detector.get_language())
# REMOVED: get_arabic_clarification_flag()
# arabic_clarify tracking has been removed entirely (Urdu-only mode).
# prompt_builder.py no longer accepts arabic_clarification parameter.
# Re-enable alongside multi-language mode if needed.
def is_expired(self) -> bool:
return (time.time() - self.last_active) > SESSION_TIMEOUT_SECONDS
def get_history_for_api(self) -> List[Dict[str, str]]:
return self.messages.copy()
def to_summary(self) -> Dict[str, Any]:
return {
"session_id": self.session_id,
"company_id": self.company_id,
"company": self.company_name,
"agent": self.agent_name,
"agent_gender": self.agent_gender,
"language": self.get_current_language(),
"turns": self.turn_count,
"caller_info": self.caller_info.to_dict(),
"duration_seconds": int(time.time() - self.created_at),
}
class ConversationManager:
"""Manages active voice-channel conversation tracks."""
def __init__(self):
self.sessions: Dict[str, ConversationSession] = {}
logger.info("ConversationManager tracking pool initialized successfully.")
def create_session(
self,
company_name: str,
agent_name: str,
agent_gender: str = "male",
custom_rules: List[str] = None,
document_context: str = "",
session_id: Optional[str] = None,
caller_collection_fields: Optional[List[Dict[str, Any]]] = None,
) -> ConversationSession:
sid = session_id or str(uuid.uuid4())
session = ConversationSession(
session_id=sid,
company_name=company_name,
agent_name=agent_name,
agent_gender=agent_gender,
custom_rules=custom_rules,
document_context=document_context,
caller_collection_fields=caller_collection_fields,
)
self.sessions[sid] = session
logger.info(f"Session established: {sid} [{company_name} / Agent: {agent_name}]")
return session
def get_session(self, session_id: str) -> Optional[ConversationSession]:
session = self.sessions.get(session_id)
if session:
if session.is_expired():
logger.info(f"Evicting expired communication stack: {session_id}")
del self.sessions[session_id]
return None
return session
return None
def end_session(self, session_id: str) -> Optional[Dict[str, Any]]:
session = self.sessions.pop(session_id, None)
if session:
summary = session.to_summary()
logger.info(f"Session terminated via clean closure: {session_id} — Summary metrics: {summary}")
return summary
return None
def cleanup_expired(self) -> None:
expired = [
sid for sid, session in self.sessions.items()
if session.is_expired()
]
for sid in expired:
logger.info(f"Garbage collection cycle purged expired session: {sid}")
del self.sessions[sid]
if expired:
logger.info(f"Garbage collection cycle purged {len(expired)} expired session allocations.")
def active_count(self) -> int:
return len(self.sessions) |