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)