Deadmon commited on
Commit
62157ef
·
verified ·
1 Parent(s): 8dac6fb

Update call_connection_manager.py

Browse files
Files changed (1) hide show
  1. call_connection_manager.py +51 -351
call_connection_manager.py CHANGED
@@ -3,269 +3,141 @@
3
  #
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
  #
6
- """call_connection_manager.py.
7
-
8
- Manages customer/operator relationships and call routing for voice bots.
9
- Provides mapping between customers and operators, and functions for retrieving
10
- contact information. Also includes call state management.
11
- """
12
-
13
  import json
14
  import os
 
15
  from typing import Any, Dict, List, Optional
16
-
17
  from loguru import logger
18
 
19
-
20
  class CallFlowState:
21
- """State for tracking call flow operations and state transitions."""
22
-
23
  def __init__(self):
24
- # Operator-related state
25
  self.dialed_operator = False
26
  self.operator_connected = False
27
  self.current_operator_index = 0
28
  self.operator_dialout_settings = []
29
  self.summary_finished = False
30
-
31
- # Voicemail detection state
32
  self.voicemail_detected = False
33
  self.human_detected = False
34
  self.voicemail_message_left = False
35
-
36
- # Call termination state
37
  self.call_terminated = False
38
  self.participant_left_early = False
 
39
 
40
- # Operator-related methods
41
  def set_operator_dialed(self):
42
- """Mark that an operator has been dialed."""
43
  self.dialed_operator = True
44
 
45
  def set_operator_connected(self):
46
- """Mark that an operator has connected to the call."""
47
  self.operator_connected = True
48
- # Summary is not finished when operator first connects
49
  self.summary_finished = False
50
 
51
  def set_operator_disconnected(self):
52
- """Handle operator disconnection."""
53
  self.operator_connected = False
54
  self.summary_finished = False
55
 
56
  def set_summary_finished(self):
57
- """Mark the summary as finished."""
58
  self.summary_finished = True
59
 
60
  def set_operator_dialout_settings(self, settings):
61
- """Set the list of operator dialout settings to try."""
62
  self.operator_dialout_settings = settings
63
  self.current_operator_index = 0
64
 
65
  def get_current_dialout_setting(self):
66
- """Get the current operator dialout setting to try."""
67
- if not self.operator_dialout_settings or self.current_operator_index >= len(
68
- self.operator_dialout_settings
69
- ):
70
  return None
71
  return self.operator_dialout_settings[self.current_operator_index]
72
 
73
  def move_to_next_operator(self):
74
- """Move to the next operator in the list."""
75
  self.current_operator_index += 1
76
  return self.get_current_dialout_setting()
77
 
78
- # Voicemail detection methods
79
  def set_voicemail_detected(self):
80
- """Mark that a voicemail system has been detected."""
81
  self.voicemail_detected = True
82
  self.human_detected = False
83
 
84
  def set_human_detected(self):
85
- """Mark that a human has been detected (not voicemail)."""
86
  self.human_detected = True
87
  self.voicemail_detected = False
88
 
89
  def set_voicemail_message_left(self):
90
- """Mark that a voicemail message has been left."""
91
  self.voicemail_message_left = True
92
 
93
- # Call termination methods
94
  def set_call_terminated(self):
95
- """Mark that the call has been terminated by the bot."""
96
  self.call_terminated = True
97
 
98
  def set_participant_left_early(self):
99
- """Mark that a participant left the call early."""
100
  self.participant_left_early = True
101
 
 
 
102
 
103
- class SessionManager:
104
- """Centralized management of session IDs and state for all call participants."""
105
 
 
106
  def __init__(self):
107
- # Track session IDs of different participant types
108
- self.session_ids = {
109
- "operator": None,
110
- "customer": None,
111
- "bot": None,
112
- # Add other participant types as needed
113
- }
114
-
115
- # References for easy access in processors that need mutable containers
116
- self.session_id_refs = {
117
- "operator": [None],
118
- "customer": [None],
119
- "bot": [None],
120
- # Add other participant types as needed
121
- }
122
-
123
- # State object for call flow
124
  self.call_flow_state = CallFlowState()
125
 
126
  def set_session_id(self, participant_type, session_id):
127
- """Set the session ID for a specific participant type.
128
-
129
- Args:
130
- participant_type: Type of participant (e.g., "operator", "customer", "bot")
131
- session_id: The session ID to set
132
- """
133
  if participant_type in self.session_ids:
134
  self.session_ids[participant_type] = session_id
135
-
136
- # Also update the corresponding reference if it exists
137
  if participant_type in self.session_id_refs:
138
  self.session_id_refs[participant_type][0] = session_id
139
 
140
  def get_session_id(self, participant_type):
141
- """Get the session ID for a specific participant type.
142
-
143
- Args:
144
- participant_type: Type of participant (e.g., "operator", "customer", "bot")
145
-
146
- Returns:
147
- The session ID or None if not set
148
- """
149
  return self.session_ids.get(participant_type)
150
 
151
  def get_session_id_ref(self, participant_type):
152
- """Get the mutable reference for a specific participant type.
153
-
154
- Args:
155
- participant_type: Type of participant (e.g., "operator", "customer", "bot")
156
-
157
- Returns:
158
- A mutable list container holding the session ID or None if not available
159
- """
160
  return self.session_id_refs.get(participant_type)
161
 
162
  def is_participant_type(self, session_id, participant_type):
163
- """Check if a session ID belongs to a specific participant type.
164
-
165
- Args:
166
- session_id: The session ID to check
167
- participant_type: Type of participant (e.g., "operator", "customer", "bot")
168
-
169
- Returns:
170
- True if the session ID matches the participant type, False otherwise
171
- """
172
  return self.session_ids.get(participant_type) == session_id
173
 
174
  def reset_participant(self, participant_type):
175
- """Reset the state for a specific participant type.
176
-
177
- Args:
178
- participant_type: Type of participant (e.g., "operator", "customer", "bot")
179
- """
180
  if participant_type in self.session_ids:
181
  self.session_ids[participant_type] = None
182
-
183
  if participant_type in self.session_id_refs:
184
  self.session_id_refs[participant_type][0] = None
185
-
186
- # Additional reset actions for specific participant types
187
  if participant_type == "operator":
188
  self.call_flow_state.set_operator_disconnected()
189
 
190
-
191
  class CallConfigManager:
192
- """Manages customer/operator relationships and call routing."""
193
-
194
  def __init__(self, body_data: Dict[str, Any] = None):
195
- """Initialize with optional body data.
196
-
197
- Args:
198
- body_data: Optional dictionary containing request body data
199
- """
200
  self.body = body_data or {}
201
-
202
- # Get environment variables with fallbacks
203
- self.dial_in_from_number = os.getenv("DIAL_IN_FROM_NUMBER", "+10000000001")
204
- self.dial_out_to_number = os.getenv("DIAL_OUT_TO_NUMBER", "+10000000002")
205
- self.operator_number = os.getenv("OPERATOR_NUMBER", "+10000000003")
206
-
207
- # Initialize maps with dynamic values
208
  self._initialize_maps()
209
  self._build_reverse_lookup_maps()
210
 
211
  def _initialize_maps(self):
212
- """Initialize the customer and operator maps with environment variables."""
213
- # Maps customer names to their contact information
214
  self.CUSTOMER_MAP = {
215
- "Dominic": {
216
- "phoneNumber": self.dial_in_from_number, # I have two phone numbers, one for dialing in and one for dialing out. I give myself a separate name for each.
217
- },
218
- "Stewart": {
219
- "phoneNumber": self.dial_out_to_number,
220
- },
221
- "James": {
222
- "phoneNumber": "+10000000000",
223
- "callerId": "james-caller-id-uuid",
224
- "sipUri": "sip:[email protected]",
225
- },
226
- "Sarah": {
227
- "sipUri": "sip:[email protected]",
228
- },
229
- "Michael": {
230
- "phoneNumber": "+16505557890",
231
- "callerId": "michael-caller-id-uuid",
232
- },
233
  }
234
-
235
- # Maps customer names to their assigned operator names
236
  self.CUSTOMER_TO_OPERATOR_MAP = {
237
- "Dominic": ["Yunyoung", "Maria"], # Try Yunyoung first, then Maria
238
  "Stewart": "Yunyoung",
239
  "James": "Yunyoung",
240
  "Sarah": "Jennifer",
241
  "Michael": "Paul",
242
- # Default mapping to ensure all customers have an operator
243
  "Default": "Yunyoung",
244
  }
245
-
246
- # Maps operator names to their contact details
247
  self.OPERATOR_CONTACT_MAP = {
248
- "Paul": {
249
- "phoneNumber": "+12345678904",
250
- "callerId": "paul-caller-id-uuid",
251
- },
252
- "Yunyoung": {
253
- "phoneNumber": self.operator_number, # Dials out to my other phone number.
254
- },
255
- "Maria": {
256
- "sipUri": "sip:[email protected]",
257
- },
258
  "Jennifer": {"phoneNumber": "+14155559876", "callerId": "jennifer-caller-id-uuid"},
259
- "Default": {
260
- "phoneNumber": self.operator_number, # Use the operator number as default
261
- },
262
  }
263
 
264
  def _build_reverse_lookup_maps(self):
265
- """Build reverse lookup maps for phone numbers and SIP URIs to customer names."""
266
  self._PHONE_TO_CUSTOMER_MAP = {}
267
  self._SIP_TO_CUSTOMER_MAP = {}
268
-
269
  for customer_name, contact_info in self.CUSTOMER_MAP.items():
270
  if "phoneNumber" in contact_info:
271
  self._PHONE_TO_CUSTOMER_MAP[contact_info["phoneNumber"]] = customer_name
@@ -274,158 +146,59 @@ class CallConfigManager:
274
 
275
  @classmethod
276
  def from_json_string(cls, json_string: str):
277
- """Create a CallRoutingManager from a JSON string.
278
-
279
- Args:
280
- json_string: JSON string containing body data
281
-
282
- Returns:
283
- CallRoutingManager instance with parsed data
284
-
285
- Raises:
286
- json.JSONDecodeError: If JSON string is invalid
287
- """
288
  body_data = json.loads(json_string)
289
  return cls(body_data)
290
 
291
  def find_customer_by_contact(self, contact_info: str) -> Optional[str]:
292
- """Find customer name from a contact identifier (phone number or SIP URI).
293
-
294
- Args:
295
- contact_info: The contact identifier (phone number or SIP URI)
296
-
297
- Returns:
298
- The customer name or None if not found
299
- """
300
- # Check if it's a phone number
301
  if contact_info in self._PHONE_TO_CUSTOMER_MAP:
302
  return self._PHONE_TO_CUSTOMER_MAP[contact_info]
303
-
304
- # Check if it's a SIP URI
305
  if contact_info in self._SIP_TO_CUSTOMER_MAP:
306
  return self._SIP_TO_CUSTOMER_MAP[contact_info]
307
-
308
  return None
309
 
310
  def get_customer_name(self, phone_number: str) -> Optional[str]:
311
- """Get customer name from their phone number.
312
-
313
- Args:
314
- phone_number: The customer's phone number
315
-
316
- Returns:
317
- The customer name or None if not found
318
- """
319
- # Note: In production, this would likely query a database
320
  return self.find_customer_by_contact(phone_number)
321
 
322
  def get_operators_for_customer(self, customer_name: Optional[str]) -> List[str]:
323
- """Get the operator name(s) assigned to a customer.
324
-
325
- Args:
326
- customer_name: The customer's name
327
-
328
- Returns:
329
- List of operator names (single item or multiple)
330
- """
331
- # Note: In production, this would likely query a database
332
  if not customer_name or customer_name not in self.CUSTOMER_TO_OPERATOR_MAP:
333
  return ["Default"]
334
-
335
  operators = self.CUSTOMER_TO_OPERATOR_MAP[customer_name]
336
- # Convert single string to list for consistency
337
  if isinstance(operators, str):
338
  return [operators]
339
  return operators
340
 
341
  def get_operator_dialout_settings(self, operator_name: str) -> Dict[str, str]:
342
- """Get an operator's dialout settings from their name.
343
-
344
- Args:
345
- operator_name: The operator's name
346
-
347
- Returns:
348
- Dictionary with dialout settings for the operator
349
- """
350
- # Note: In production, this would likely query a database
351
  return self.OPERATOR_CONTACT_MAP.get(operator_name, self.OPERATOR_CONTACT_MAP["Default"])
352
 
353
- def get_dialout_settings_for_caller(
354
- self, from_number: Optional[str] = None
355
- ) -> List[Dict[str, str]]:
356
- """Determine the appropriate operator dialout settings based on caller's number.
357
-
358
- This method uses the caller's number to look up the customer name,
359
- then finds the assigned operators for that customer, and returns
360
- an array of operator dialout settings to try in sequence.
361
-
362
- Args:
363
- from_number: The caller's phone number (from dialin_settings)
364
-
365
- Returns:
366
- List of operator dialout settings to try
367
- """
368
  if not from_number:
369
- # If we don't have dialin settings, use the Default operator
370
  return [self.get_operator_dialout_settings("Default")]
371
-
372
- # Get customer name from phone number
373
  customer_name = self.get_customer_name(from_number)
374
-
375
- # Get operator names assigned to this customer
376
  operator_names = self.get_operators_for_customer(customer_name)
377
-
378
- # Get dialout settings for each operator
379
  return [self.get_operator_dialout_settings(name) for name in operator_names]
380
 
381
  def get_caller_info(self) -> Dict[str, Optional[str]]:
382
- """Get caller and dialed numbers from dialin settings in the body.
383
-
384
- Returns:
385
- Dictionary containing caller_number and dialed_number
386
- """
387
  raw_dialin_settings = self.body.get("dialin_settings")
388
  if not raw_dialin_settings:
389
  return {"caller_number": None, "dialed_number": None}
390
-
391
- # Handle different case variations
392
  dialed_number = raw_dialin_settings.get("To") or raw_dialin_settings.get("to")
393
  caller_number = raw_dialin_settings.get("From") or raw_dialin_settings.get("from")
394
-
395
  return {"caller_number": caller_number, "dialed_number": dialed_number}
396
 
397
  def get_caller_number(self) -> Optional[str]:
398
- """Get the caller's phone number from dialin settings in the body.
399
-
400
- Returns:
401
- The caller's phone number or None if not available
402
- """
403
  return self.get_caller_info()["caller_number"]
404
 
405
  async def start_dialout(self, transport, dialout_settings=None):
406
- """Helper function to start dialout using the provided settings or from body.
407
-
408
- Args:
409
- transport: The transport instance to use for dialout
410
- dialout_settings: Optional override for dialout settings
411
-
412
- Returns:
413
- None
414
- """
415
- # Use provided settings or get from body
416
  settings = dialout_settings or self.get_dialout_settings()
417
  if not settings:
418
  logger.warning("No dialout settings available")
419
  return
420
-
421
  for setting in settings:
422
  if "phoneNumber" in setting:
423
  logger.info(f"Dialing number: {setting['phoneNumber']}")
424
  if "callerId" in setting:
425
  logger.info(f"with callerId: {setting['callerId']}")
426
- await transport.start_dialout(
427
- {"phoneNumber": setting["phoneNumber"], "callerId": setting["callerId"]}
428
- )
429
  else:
430
  logger.info("with no callerId")
431
  await transport.start_dialout({"phoneNumber": setting["phoneNumber"]})
@@ -436,59 +209,27 @@ class CallConfigManager:
436
  logger.warning(f"Unknown dialout setting format: {setting}")
437
 
438
  def get_dialout_settings(self) -> Optional[List[Dict[str, Any]]]:
439
- """Extract dialout settings from the body.
440
-
441
- Returns:
442
- List of dialout setting objects or None if not present
443
- """
444
- # Check if we have dialout settings
445
  if "dialout_settings" in self.body:
446
  dialout_settings = self.body["dialout_settings"]
447
-
448
- # Convert to list if it's an object (for backward compatibility)
449
  if isinstance(dialout_settings, dict):
450
  return [dialout_settings]
451
  elif isinstance(dialout_settings, list):
452
  return dialout_settings
453
-
454
  return None
455
 
456
  def get_dialin_settings(self) -> Optional[Dict[str, Any]]:
457
- """Extract dialin settings from the body.
458
-
459
- Handles both camelCase and snake_case variations of fields for backward compatibility,
460
- but normalizes to snake_case for internal usage.
461
-
462
- Returns:
463
- Dictionary containing dialin settings or None if not present
464
- """
465
  raw_dialin_settings = self.body.get("dialin_settings")
466
  if not raw_dialin_settings:
467
  return None
468
-
469
- # Normalize dialin settings to handle different case variations
470
- # Prioritize snake_case (call_id, call_domain) but fall back to camelCase (callId, callDomain)
471
  dialin_settings = {
472
  "call_id": raw_dialin_settings.get("call_id") or raw_dialin_settings.get("callId"),
473
- "call_domain": raw_dialin_settings.get("call_domain")
474
- or raw_dialin_settings.get("callDomain"),
475
  "to": raw_dialin_settings.get("to") or raw_dialin_settings.get("To"),
476
  "from": raw_dialin_settings.get("from") or raw_dialin_settings.get("From"),
477
  }
478
-
479
  return dialin_settings
480
 
481
- # Bot prompt helper functions - no defaults provided, just return what's in the body
482
-
483
  def get_prompt(self, prompt_name: str) -> Optional[str]:
484
- """Retrieve the prompt text for a given prompt name.
485
-
486
- Args:
487
- prompt_name: The name of the prompt to retrieve.
488
-
489
- Returns:
490
- The prompt string corresponding to the provided name, or None if not configured.
491
- """
492
  prompts = self.body.get("prompts", [])
493
  for prompt in prompts:
494
  if prompt.get("name") == prompt_name:
@@ -496,41 +237,21 @@ class CallConfigManager:
496
  return None
497
 
498
  def get_transfer_mode(self) -> Optional[str]:
499
- """Get transfer mode from the body.
500
-
501
- Returns:
502
- Transfer mode string or None if not configured
503
- """
504
  if "call_transfer" in self.body:
505
  return self.body["call_transfer"].get("mode")
506
  return None
507
 
508
  def get_speak_summary(self) -> Optional[bool]:
509
- """Get speak summary from the body.
510
-
511
- Returns:
512
- Boolean indicating if summary should be spoken or None if not configured
513
- """
514
  if "call_transfer" in self.body:
515
  return self.body["call_transfer"].get("speakSummary")
516
  return None
517
 
518
  def get_store_summary(self) -> Optional[bool]:
519
- """Get store summary from the body.
520
-
521
- Returns:
522
- Boolean indicating if summary should be stored or None if not configured
523
- """
524
  if "call_transfer" in self.body:
525
  return self.body["call_transfer"].get("storeSummary")
526
  return None
527
 
528
  def is_test_mode(self) -> bool:
529
- """Check if running in test mode.
530
-
531
- Returns:
532
- Boolean indicating if test mode is enabled
533
- """
534
  if "voicemail_detection" in self.body:
535
  return bool(self.body["voicemail_detection"].get("testInPrebuilt"))
536
  if "call_transfer" in self.body:
@@ -542,67 +263,46 @@ class CallConfigManager:
542
  return False
543
 
544
  def is_voicemail_detection_enabled(self) -> bool:
545
- """Check if voicemail detection is enabled in the body.
546
-
547
- Returns:
548
- Boolean indicating if voicemail detection is enabled
549
- """
550
  return bool(self.body.get("voicemail_detection"))
551
 
552
  def customize_prompt(self, prompt: str, customer_name: Optional[str] = None) -> str:
553
- """Insert customer name into prompt template if available.
554
-
555
- Args:
556
- prompt: The prompt template containing optional {customer_name} placeholders
557
- customer_name: Optional customer name to insert
558
-
559
- Returns:
560
- Customized prompt with customer name inserted
561
- """
562
  if customer_name and prompt:
563
  return prompt.replace("{customer_name}", customer_name)
564
  return prompt
565
 
566
  def create_system_message(self, content: str) -> Dict[str, str]:
567
- """Create a properly formatted system message.
568
-
569
- Args:
570
- content: The message content
571
-
572
- Returns:
573
- Dictionary with role and content for the system message
574
- """
575
  return {"role": "system", "content": content}
576
 
577
  def create_user_message(self, content: str) -> Dict[str, str]:
578
- """Create a properly formatted user message.
579
-
580
- Args:
581
- content: The message content
582
-
583
- Returns:
584
- Dictionary with role and content for the user message
585
- """
586
  return {"role": "user", "content": content}
587
 
588
- def get_customer_info_suffix(
589
- self, customer_name: Optional[str] = None, preposition: str = "for"
590
- ) -> str:
591
- """Create a consistent customer info suffix.
592
-
593
- Args:
594
- customer_name: Optional customer name
595
- preposition: Preposition to use before the name (e.g., "for", "to", "")
596
-
597
- Returns:
598
- String with formatted customer info suffix
599
- """
600
  if not customer_name:
601
  return ""
602
-
603
- # Add a space before the preposition if it's not empty
604
  space_prefix = " " if preposition else ""
605
- # For non-empty prepositions, add a space after it
606
  space_suffix = " " if preposition else ""
607
-
608
  return f"{space_prefix}{preposition}{space_suffix}{customer_name}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  #
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
  #
 
 
 
 
 
 
 
6
  import json
7
  import os
8
+ import time
9
  from typing import Any, Dict, List, Optional
 
10
  from loguru import logger
11
 
 
12
  class CallFlowState:
 
 
13
  def __init__(self):
 
14
  self.dialed_operator = False
15
  self.operator_connected = False
16
  self.current_operator_index = 0
17
  self.operator_dialout_settings = []
18
  self.summary_finished = False
 
 
19
  self.voicemail_detected = False
20
  self.human_detected = False
21
  self.voicemail_message_left = False
 
 
22
  self.call_terminated = False
23
  self.participant_left_early = False
24
+ self.silence_prompt_count = 0 # Track silence prompts
25
 
 
26
  def set_operator_dialed(self):
 
27
  self.dialed_operator = True
28
 
29
  def set_operator_connected(self):
 
30
  self.operator_connected = True
 
31
  self.summary_finished = False
32
 
33
  def set_operator_disconnected(self):
 
34
  self.operator_connected = False
35
  self.summary_finished = False
36
 
37
  def set_summary_finished(self):
 
38
  self.summary_finished = True
39
 
40
  def set_operator_dialout_settings(self, settings):
 
41
  self.operator_dialout_settings = settings
42
  self.current_operator_index = 0
43
 
44
  def get_current_dialout_setting(self):
45
+ if not self.operator_dialout_settings or self.current_operator_index >= len(self.operator_dialout_settings):
 
 
 
46
  return None
47
  return self.operator_dialout_settings[self.current_operator_index]
48
 
49
  def move_to_next_operator(self):
 
50
  self.current_operator_index += 1
51
  return self.get_current_dialout_setting()
52
 
 
53
  def set_voicemail_detected(self):
 
54
  self.voicemail_detected = True
55
  self.human_detected = False
56
 
57
  def set_human_detected(self):
 
58
  self.human_detected = True
59
  self.voicemail_detected = False
60
 
61
  def set_voicemail_message_left(self):
 
62
  self.voicemail_message_left = True
63
 
 
64
  def set_call_terminated(self):
 
65
  self.call_terminated = True
66
 
67
  def set_participant_left_early(self):
 
68
  self.participant_left_early = True
69
 
70
+ def increment_silence_prompts(self):
71
+ self.silence_prompt_count += 1
72
 
73
+ def reset_silence_prompts(self):
74
+ self.silence_prompt_count = 0
75
 
76
+ class SessionManager:
77
  def __init__(self):
78
+ self.session_ids = {"operator": None, "customer": None, "bot": None}
79
+ self.session_id_refs = {"operator": [None], "customer": [None], "bot": [None]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  self.call_flow_state = CallFlowState()
81
 
82
  def set_session_id(self, participant_type, session_id):
 
 
 
 
 
 
83
  if participant_type in self.session_ids:
84
  self.session_ids[participant_type] = session_id
 
 
85
  if participant_type in self.session_id_refs:
86
  self.session_id_refs[participant_type][0] = session_id
87
 
88
  def get_session_id(self, participant_type):
 
 
 
 
 
 
 
 
89
  return self.session_ids.get(participant_type)
90
 
91
  def get_session_id_ref(self, participant_type):
 
 
 
 
 
 
 
 
92
  return self.session_id_refs.get(participant_type)
93
 
94
  def is_participant_type(self, session_id, participant_type):
 
 
 
 
 
 
 
 
 
95
  return self.session_ids.get(participant_type) == session_id
96
 
97
  def reset_participant(self, participant_type):
 
 
 
 
 
98
  if participant_type in self.session_ids:
99
  self.session_ids[participant_type] = None
 
100
  if participant_type in self.session_id_refs:
101
  self.session_id_refs[participant_type][0] = None
 
 
102
  if participant_type == "operator":
103
  self.call_flow_state.set_operator_disconnected()
104
 
 
105
  class CallConfigManager:
 
 
106
  def __init__(self, body_data: Dict[str, Any] = None):
 
 
 
 
 
107
  self.body = body_data or {}
108
+ self.dial_in_from_number = os.environ.get("HF_DIAL_IN_FROM_NUMBER", "+10000000001")
109
+ self.dial_out_to_number = os.environ.get("HF_DIAL_OUT_TO_NUMBER", "+10000000002")
110
+ self.operator_number = os.environ.get("HF_OPERATOR_NUMBER", "+10000000003")
 
 
 
 
111
  self._initialize_maps()
112
  self._build_reverse_lookup_maps()
113
 
114
  def _initialize_maps(self):
 
 
115
  self.CUSTOMER_MAP = {
116
+ "Dominic": {"phoneNumber": self.dial_in_from_number},
117
+ "Stewart": {"phoneNumber": self.dial_out_to_number},
118
+ "James": {"phoneNumber": "+10000000000", "callerId": "james-caller-id-uuid", "sipUri": "sip:[email protected]"},
119
+ "Sarah": {"sipUri": "sip:[email protected]"},
120
+ "Michael": {"phoneNumber": "+16505557890", "callerId": "michael-caller-id-uuid"},
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
 
 
122
  self.CUSTOMER_TO_OPERATOR_MAP = {
123
+ "Dominic": ["Yunyoung", "Maria"],
124
  "Stewart": "Yunyoung",
125
  "James": "Yunyoung",
126
  "Sarah": "Jennifer",
127
  "Michael": "Paul",
 
128
  "Default": "Yunyoung",
129
  }
 
 
130
  self.OPERATOR_CONTACT_MAP = {
131
+ "Paul": {"phoneNumber": "+12345678904", "callerId": "paul-caller-id-uuid"},
132
+ "Yunyoung": {"phoneNumber": self.operator_number},
133
+ "Maria": {"sipUri": "sip:[email protected]"},
 
 
 
 
 
 
 
134
  "Jennifer": {"phoneNumber": "+14155559876", "callerId": "jennifer-caller-id-uuid"},
135
+ "Default": {"phoneNumber": self.operator_number},
 
 
136
  }
137
 
138
  def _build_reverse_lookup_maps(self):
 
139
  self._PHONE_TO_CUSTOMER_MAP = {}
140
  self._SIP_TO_CUSTOMER_MAP = {}
 
141
  for customer_name, contact_info in self.CUSTOMER_MAP.items():
142
  if "phoneNumber" in contact_info:
143
  self._PHONE_TO_CUSTOMER_MAP[contact_info["phoneNumber"]] = customer_name
 
146
 
147
  @classmethod
148
  def from_json_string(cls, json_string: str):
 
 
 
 
 
 
 
 
 
 
 
149
  body_data = json.loads(json_string)
150
  return cls(body_data)
151
 
152
  def find_customer_by_contact(self, contact_info: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
153
  if contact_info in self._PHONE_TO_CUSTOMER_MAP:
154
  return self._PHONE_TO_CUSTOMER_MAP[contact_info]
 
 
155
  if contact_info in self._SIP_TO_CUSTOMER_MAP:
156
  return self._SIP_TO_CUSTOMER_MAP[contact_info]
 
157
  return None
158
 
159
  def get_customer_name(self, phone_number: str) -> Optional[str]:
 
 
 
 
 
 
 
 
 
160
  return self.find_customer_by_contact(phone_number)
161
 
162
  def get_operators_for_customer(self, customer_name: Optional[str]) -> List[str]:
 
 
 
 
 
 
 
 
 
163
  if not customer_name or customer_name not in self.CUSTOMER_TO_OPERATOR_MAP:
164
  return ["Default"]
 
165
  operators = self.CUSTOMER_TO_OPERATOR_MAP[customer_name]
 
166
  if isinstance(operators, str):
167
  return [operators]
168
  return operators
169
 
170
  def get_operator_dialout_settings(self, operator_name: str) -> Dict[str, str]:
 
 
 
 
 
 
 
 
 
171
  return self.OPERATOR_CONTACT_MAP.get(operator_name, self.OPERATOR_CONTACT_MAP["Default"])
172
 
173
+ def get_dialout_settings_for_caller(self, from_number: Optional[str] = None) -> List[Dict[str, str]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  if not from_number:
 
175
  return [self.get_operator_dialout_settings("Default")]
 
 
176
  customer_name = self.get_customer_name(from_number)
 
 
177
  operator_names = self.get_operators_for_customer(customer_name)
 
 
178
  return [self.get_operator_dialout_settings(name) for name in operator_names]
179
 
180
  def get_caller_info(self) -> Dict[str, Optional[str]]:
 
 
 
 
 
181
  raw_dialin_settings = self.body.get("dialin_settings")
182
  if not raw_dialin_settings:
183
  return {"caller_number": None, "dialed_number": None}
 
 
184
  dialed_number = raw_dialin_settings.get("To") or raw_dialin_settings.get("to")
185
  caller_number = raw_dialin_settings.get("From") or raw_dialin_settings.get("from")
 
186
  return {"caller_number": caller_number, "dialed_number": dialed_number}
187
 
188
  def get_caller_number(self) -> Optional[str]:
 
 
 
 
 
189
  return self.get_caller_info()["caller_number"]
190
 
191
  async def start_dialout(self, transport, dialout_settings=None):
 
 
 
 
 
 
 
 
 
 
192
  settings = dialout_settings or self.get_dialout_settings()
193
  if not settings:
194
  logger.warning("No dialout settings available")
195
  return
 
196
  for setting in settings:
197
  if "phoneNumber" in setting:
198
  logger.info(f"Dialing number: {setting['phoneNumber']}")
199
  if "callerId" in setting:
200
  logger.info(f"with callerId: {setting['callerId']}")
201
+ await transport.start_dialout({"phoneNumber": setting["phoneNumber"], "callerId": setting["callerId"]})
 
 
202
  else:
203
  logger.info("with no callerId")
204
  await transport.start_dialout({"phoneNumber": setting["phoneNumber"]})
 
209
  logger.warning(f"Unknown dialout setting format: {setting}")
210
 
211
  def get_dialout_settings(self) -> Optional[List[Dict[str, Any]]]:
 
 
 
 
 
 
212
  if "dialout_settings" in self.body:
213
  dialout_settings = self.body["dialout_settings"]
 
 
214
  if isinstance(dialout_settings, dict):
215
  return [dialout_settings]
216
  elif isinstance(dialout_settings, list):
217
  return dialout_settings
 
218
  return None
219
 
220
  def get_dialin_settings(self) -> Optional[Dict[str, Any]]:
 
 
 
 
 
 
 
 
221
  raw_dialin_settings = self.body.get("dialin_settings")
222
  if not raw_dialin_settings:
223
  return None
 
 
 
224
  dialin_settings = {
225
  "call_id": raw_dialin_settings.get("call_id") or raw_dialin_settings.get("callId"),
226
+ "call_domain": raw_dialin_settings.get("call_domain") or raw_dialin_settings.get("callDomain"),
 
227
  "to": raw_dialin_settings.get("to") or raw_dialin_settings.get("To"),
228
  "from": raw_dialin_settings.get("from") or raw_dialin_settings.get("From"),
229
  }
 
230
  return dialin_settings
231
 
 
 
232
  def get_prompt(self, prompt_name: str) -> Optional[str]:
 
 
 
 
 
 
 
 
233
  prompts = self.body.get("prompts", [])
234
  for prompt in prompts:
235
  if prompt.get("name") == prompt_name:
 
237
  return None
238
 
239
  def get_transfer_mode(self) -> Optional[str]:
 
 
 
 
 
240
  if "call_transfer" in self.body:
241
  return self.body["call_transfer"].get("mode")
242
  return None
243
 
244
  def get_speak_summary(self) -> Optional[bool]:
 
 
 
 
 
245
  if "call_transfer" in self.body:
246
  return self.body["call_transfer"].get("speakSummary")
247
  return None
248
 
249
  def get_store_summary(self) -> Optional[bool]:
 
 
 
 
 
250
  if "call_transfer" in self.body:
251
  return self.body["call_transfer"].get("storeSummary")
252
  return None
253
 
254
  def is_test_mode(self) -> bool:
 
 
 
 
 
255
  if "voicemail_detection" in self.body:
256
  return bool(self.body["voicemail_detection"].get("testInPrebuilt"))
257
  if "call_transfer" in self.body:
 
263
  return False
264
 
265
  def is_voicemail_detection_enabled(self) -> bool:
 
 
 
 
 
266
  return bool(self.body.get("voicemail_detection"))
267
 
268
  def customize_prompt(self, prompt: str, customer_name: Optional[str] = None) -> str:
 
 
 
 
 
 
 
 
 
269
  if customer_name and prompt:
270
  return prompt.replace("{customer_name}", customer_name)
271
  return prompt
272
 
273
  def create_system_message(self, content: str) -> Dict[str, str]:
 
 
 
 
 
 
 
 
274
  return {"role": "system", "content": content}
275
 
276
  def create_user_message(self, content: str) -> Dict[str, str]:
 
 
 
 
 
 
 
 
277
  return {"role": "user", "content": content}
278
 
279
+ def get_customer_info_suffix(self, customer_name: Optional[str] = None, preposition: str = "for") -> str:
 
 
 
 
 
 
 
 
 
 
 
280
  if not customer_name:
281
  return ""
 
 
282
  space_prefix = " " if preposition else ""
 
283
  space_suffix = " " if preposition else ""
 
284
  return f"{space_prefix}{preposition}{space_suffix}{customer_name}"
285
+
286
+ async def log_call_summary(self, start_time: float, session_manager: SessionManager, caller_number: str, dialed_number: str, customer_name: str):
287
+ """Log call summary to a file."""
288
+ end_time = time.time()
289
+ duration = end_time - start_time
290
+ silence_prompts = session_manager.call_flow_state.silence_prompt_count
291
+ summary = {
292
+ "call_start_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(start_time)),
293
+ "call_end_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(end_time)),
294
+ "duration_seconds": round(duration, 2),
295
+ "caller_number": caller_number,
296
+ "dialed_number": dialed_number,
297
+ "customer_name": customer_name,
298
+ "silence_prompts_triggered": silence_prompts,
299
+ "call_terminated_by_bot": session_manager.call_flow_state.call_terminated,
300
+ "participant_left_early": session_manager.call_flow_state.participant_left_early
301
+ }
302
+ log_file = "/home/user/call_summary.log"
303
+ try:
304
+ with open(log_file, "a") as f:
305
+ f.write(json.dumps(summary) + "\n")
306
+ logger.info(f"Call summary logged to {log_file}")
307
+ except Exception as e:
308
+ logger.error(f"Failed to log call summary: {e}")