intellema-vdk 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- intellema_vdk/__init__.py +67 -10
- intellema_vdk/config.py +14 -0
- intellema_vdk/providers/__init__.py +35 -0
- intellema_vdk/providers/livekit/__init__.py +19 -0
- intellema_vdk/providers/livekit/client.py +612 -0
- intellema_vdk/providers/livekit/exceptions.py +23 -0
- intellema_vdk/providers/protocols.py +33 -0
- intellema_vdk/providers/retell/__init__.py +17 -0
- intellema_vdk/providers/retell/client.py +468 -0
- intellema_vdk/providers/retell/exceptions.py +19 -0
- intellema_vdk/{retell_lib → providers/retell}/import_phone_number.py +1 -1
- intellema_vdk/stt/__init__.py +17 -0
- intellema_vdk/stt/client.py +482 -0
- intellema_vdk/stt/exceptions.py +19 -0
- intellema_vdk/tts/__init__.py +15 -0
- intellema_vdk/tts/__pycache__/__init__.cpython-312.pyc +0 -0
- intellema_vdk/tts/__pycache__/client.cpython-312.pyc +0 -0
- intellema_vdk/tts/__pycache__/exceptions.cpython-312.pyc +0 -0
- intellema_vdk/tts/__pycache__/providers.cpython-312.pyc +0 -0
- intellema_vdk/tts/client.py +541 -0
- intellema_vdk/tts/exceptions.py +15 -0
- intellema_vdk/tts/providers.py +293 -0
- intellema_vdk/utils/logger_config.py +41 -0
- intellema_vdk-0.2.2.dist-info/METADATA +311 -0
- intellema_vdk-0.2.2.dist-info/RECORD +29 -0
- {intellema_vdk-0.2.0.dist-info → intellema_vdk-0.2.2.dist-info}/WHEEL +1 -1
- intellema_vdk/livekit_lib/__init__.py +0 -3
- intellema_vdk/livekit_lib/client.py +0 -280
- intellema_vdk/retell_lib/retell_client.py +0 -248
- intellema_vdk/speech_lib/__init__.py +0 -2
- intellema_vdk/speech_lib/stt_client.py +0 -108
- intellema_vdk/speech_lib/tts_streamer.py +0 -188
- intellema_vdk-0.2.0.dist-info/METADATA +0 -221
- intellema_vdk-0.2.0.dist-info/RECORD +0 -14
- /intellema_vdk/{retell_lib/__init__.py → stt/providers.py} +0 -0
- {intellema_vdk-0.2.0.dist-info → intellema_vdk-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {intellema_vdk-0.2.0.dist-info → intellema_vdk-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import os # Used for operating system dependent functionality, like file paths.
|
|
2
|
+
import logging # Used for logging events.
|
|
3
|
+
import time # Used for time-related tasks.
|
|
4
|
+
import uuid # Used for generating unique identifiers.
|
|
5
|
+
import subprocess # Used for installing packages
|
|
6
|
+
import sys # Used for sys operations
|
|
7
|
+
from typing import List, Optional, Any, Dict, TYPE_CHECKING # Used for type hinting.
|
|
8
|
+
|
|
9
|
+
# Lazy imports - only load when RetellManager is instantiated
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from twilio.rest import Client as TwilioClient
|
|
12
|
+
from retell import Retell, APIError
|
|
13
|
+
import boto3
|
|
14
|
+
else:
|
|
15
|
+
TwilioClient = None
|
|
16
|
+
Retell = None
|
|
17
|
+
APIError = None
|
|
18
|
+
boto3 = None
|
|
19
|
+
|
|
20
|
+
# Import requests - should be available as a core dependency
|
|
21
|
+
import requests # Used for making HTTP requests.
|
|
22
|
+
|
|
23
|
+
from ...config import get_env # Used to get environment variables.
|
|
24
|
+
from ..protocols import VoiceProvider # Protocol for voice providers.
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
RetellConfigurationError,
|
|
27
|
+
RetellPhoneNumberError,
|
|
28
|
+
RetellCallError,
|
|
29
|
+
RetellAPIError,
|
|
30
|
+
RetellError
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Setup logger for this module.
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RetellManager(VoiceProvider):
|
|
38
|
+
"""Manages voice calls using the Retell and Twilio APIs.
|
|
39
|
+
|
|
40
|
+
This class provides a high-level interface for making outbound calls,
|
|
41
|
+
managing phone numbers with Retell, and handling call recordings. It uses
|
|
42
|
+
Twilio for the underlying telephony infrastructure.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
twilio_account_sid (str): The Twilio account SID.
|
|
46
|
+
twilio_auth_token (str): The Twilio auth token.
|
|
47
|
+
twilio_number (str): The Twilio phone number to use for calls.
|
|
48
|
+
retell_api_key (str): The Retell API key.
|
|
49
|
+
retell_agent_id (str): The ID of the Retell agent to use.
|
|
50
|
+
twilio_client (TwilioClient): The Twilio API client.
|
|
51
|
+
retell_client (Retell): The Retell API client.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> async def main():
|
|
55
|
+
... retell_manager = VoiceClient("retell")
|
|
56
|
+
... # Import a phone number (run once per number)
|
|
57
|
+
... # retell_manager.import_phone_number()
|
|
58
|
+
... call_id = await retell_manager.start_outbound_call(
|
|
59
|
+
... phone_number="+15551234567",
|
|
60
|
+
... prompt_content="Hello from Retell!"
|
|
61
|
+
... )
|
|
62
|
+
... print(f"Call started with ID: {call_id}")
|
|
63
|
+
... # Wait for the call to connect and then end it.
|
|
64
|
+
... await asyncio.sleep(30)
|
|
65
|
+
... await retell_manager.delete_room(call_id)
|
|
66
|
+
...
|
|
67
|
+
>>> if __name__ == "__main__":
|
|
68
|
+
... import asyncio
|
|
69
|
+
... asyncio.run(main())
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
"""Initializes the RetellManager.
|
|
74
|
+
|
|
75
|
+
Retrieves configuration from environment variables and initializes
|
|
76
|
+
the Twilio and Retell API clients.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
RetellConfigurationError: If required environment variables are missing.
|
|
80
|
+
"""
|
|
81
|
+
# Lazy import dependencies - only install when actually used
|
|
82
|
+
global TwilioClient, Retell, APIError, boto3
|
|
83
|
+
|
|
84
|
+
if TwilioClient is None:
|
|
85
|
+
try:
|
|
86
|
+
from twilio.rest import Client as _TwilioClient
|
|
87
|
+
TwilioClient = _TwilioClient
|
|
88
|
+
except ImportError:
|
|
89
|
+
print("Twilio SDK is not installed. Installing now...")
|
|
90
|
+
print("Run: pip install intellema-vdk[retell]")
|
|
91
|
+
try:
|
|
92
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "twilio>=8.0.0"])
|
|
93
|
+
from twilio.rest import Client as _TwilioClient
|
|
94
|
+
TwilioClient = _TwilioClient
|
|
95
|
+
print("✓ Twilio SDK installed successfully!")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise RetellConfigurationError(
|
|
98
|
+
"Failed to install twilio. Please install manually:\n"
|
|
99
|
+
" pip install intellema-vdk[retell]\n"
|
|
100
|
+
"or:\n"
|
|
101
|
+
" pip install twilio>=8.0.0"
|
|
102
|
+
) from e
|
|
103
|
+
|
|
104
|
+
if Retell is None:
|
|
105
|
+
try:
|
|
106
|
+
from retell import Retell as _Retell, APIError as _APIError
|
|
107
|
+
Retell = _Retell
|
|
108
|
+
APIError = _APIError
|
|
109
|
+
except ImportError:
|
|
110
|
+
print("Retell SDK is not installed. Installing now...")
|
|
111
|
+
try:
|
|
112
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "retell-sdk>=2.0.0"])
|
|
113
|
+
from retell import Retell as _Retell, APIError as _APIError
|
|
114
|
+
Retell = _Retell
|
|
115
|
+
APIError = _APIError
|
|
116
|
+
print("✓ Retell SDK installed successfully!")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise RetellConfigurationError(
|
|
119
|
+
"Failed to install retell-sdk. Please install manually:\n"
|
|
120
|
+
" pip install intellema-vdk[retell]\n"
|
|
121
|
+
"or:\n"
|
|
122
|
+
" pip install retell-sdk>=2.0.0"
|
|
123
|
+
) from e
|
|
124
|
+
|
|
125
|
+
if boto3 is None:
|
|
126
|
+
try:
|
|
127
|
+
import boto3 as _boto3
|
|
128
|
+
boto3 = _boto3
|
|
129
|
+
except ImportError:
|
|
130
|
+
print("boto3 (AWS SDK) is not installed. Installing now...")
|
|
131
|
+
print("This is required for AWS services like Polly TTS.")
|
|
132
|
+
try:
|
|
133
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "boto3>=1.28.0"])
|
|
134
|
+
import boto3 as _boto3
|
|
135
|
+
boto3 = _boto3
|
|
136
|
+
print("✓ boto3 installed successfully!")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise RetellConfigurationError(
|
|
139
|
+
"Failed to install boto3. Please install manually:\n"
|
|
140
|
+
" pip install boto3>=1.28.0"
|
|
141
|
+
) from e
|
|
142
|
+
|
|
143
|
+
self.twilio_account_sid = get_env("TWILIO_ACCOUNT_SID")
|
|
144
|
+
self.twilio_auth_token = get_env("TWILIO_AUTH_TOKEN")
|
|
145
|
+
self.twilio_number = get_env("TWILIO_PHONE_NUMBER")
|
|
146
|
+
self.retell_api_key = get_env("RETELL_API_KEY")
|
|
147
|
+
self.retell_agent_id = get_env("RETELL_AGENT_ID")
|
|
148
|
+
|
|
149
|
+
if not all([self.twilio_account_sid, self.twilio_auth_token, self.twilio_number, self.retell_api_key, self.retell_agent_id]):
|
|
150
|
+
raise RetellConfigurationError(
|
|
151
|
+
"Missing necessary environment variables for RetellManager")
|
|
152
|
+
|
|
153
|
+
self.twilio_client = TwilioClient(
|
|
154
|
+
self.twilio_account_sid, self.twilio_auth_token)
|
|
155
|
+
self.retell_client = Retell(api_key=self.retell_api_key)
|
|
156
|
+
|
|
157
|
+
def import_phone_number(self, termination_uri: Optional[str] = None, outbound_agent_id: Optional[str] = None, inbound_agent_id: Optional[str] = None, nickname: Optional[str] = None, sip_trunk_auth_username: Optional[str] = None, sip_trunk_auth_password: Optional[str] = None) -> Any:
|
|
158
|
+
"""Imports and registers a Twilio phone number with Retell.
|
|
159
|
+
|
|
160
|
+
This is a necessary step before making outbound calls with the number.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
termination_uri (Optional[str]): The Twilio SIP trunk termination URI
|
|
164
|
+
(e.g., "yourtrunk.pstn.twilio.com").
|
|
165
|
+
outbound_agent_id (Optional[str]): The agent ID for outbound calls.
|
|
166
|
+
Defaults to `self.retell_agent_id`.
|
|
167
|
+
inbound_agent_id (Optional[str]): The agent ID for inbound calls.
|
|
168
|
+
nickname (Optional[str]): A nickname for the phone number.
|
|
169
|
+
sip_trunk_auth_username (Optional[str]): Username for SIP trunk
|
|
170
|
+
authentication.
|
|
171
|
+
sip_trunk_auth_password (Optional[str]): Password for SIP trunk
|
|
172
|
+
authentication.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Any: The phone number registration response from Retell.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
RetellAPIError: If the Retell API returns an error.
|
|
179
|
+
RetellPhoneNumberError: For other unexpected errors.
|
|
180
|
+
"""
|
|
181
|
+
import_kwargs: Dict[str, Any] = {
|
|
182
|
+
"phone_number": self.twilio_number,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if termination_uri:
|
|
186
|
+
import_kwargs["termination_uri"] = termination_uri
|
|
187
|
+
if sip_trunk_auth_username and sip_trunk_auth_password:
|
|
188
|
+
import_kwargs["sip_trunk_auth_username"] = sip_trunk_auth_username
|
|
189
|
+
import_kwargs["sip_trunk_auth_password"] = sip_trunk_auth_password
|
|
190
|
+
if outbound_agent_id:
|
|
191
|
+
import_kwargs["outbound_agent_id"] = outbound_agent_id
|
|
192
|
+
elif self.retell_agent_id:
|
|
193
|
+
import_kwargs["outbound_agent_id"] = self.retell_agent_id
|
|
194
|
+
if inbound_agent_id:
|
|
195
|
+
import_kwargs["inbound_agent_id"] = inbound_agent_id
|
|
196
|
+
if nickname:
|
|
197
|
+
import_kwargs["nickname"] = nickname
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
response = self.retell_client.phone_number.import_(**import_kwargs)
|
|
201
|
+
logger.info(
|
|
202
|
+
f"✓ Phone number {self.twilio_number} successfully imported to Retell!")
|
|
203
|
+
logger.info(f" Phone Number: {response.phone_number}")
|
|
204
|
+
logger.info(f" Type: {response.phone_number_type}")
|
|
205
|
+
if hasattr(response, 'outbound_agent_id') and response.outbound_agent_id:
|
|
206
|
+
logger.info(f" Outbound Agent: {response.outbound_agent_id}")
|
|
207
|
+
if hasattr(response, 'inbound_agent_id') and response.inbound_agent_id:
|
|
208
|
+
logger.info(f" Inbound Agent: {response.inbound_agent_id}")
|
|
209
|
+
return response
|
|
210
|
+
except APIError as e:
|
|
211
|
+
logger.error(
|
|
212
|
+
f"Retell API error importing phone number: {e}", exc_info=True)
|
|
213
|
+
raise RetellAPIError(
|
|
214
|
+
f"Retell API error importing phone number: {e}") from e
|
|
215
|
+
except Exception as e:
|
|
216
|
+
logger.error(
|
|
217
|
+
f"An unexpected error occurred while importing phone number: {e}", exc_info=True)
|
|
218
|
+
raise RetellPhoneNumberError(
|
|
219
|
+
f"An unexpected error occurred while importing phone number: {e}") from e
|
|
220
|
+
|
|
221
|
+
async def start_outbound_call(self, phone_number: str, prompt_content: Optional[str] = None, call_id: Optional[str] = None) -> str:
|
|
222
|
+
"""Initiates an outbound call via Retell AI.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
phone_number (str): The destination phone number.
|
|
226
|
+
prompt_content (Optional[str]): A prompt to override the agent's default.
|
|
227
|
+
call_id (Optional[str]): A metadata ID for the call.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
str: The Retell call ID.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
RetellCallError: If the call fails to start.
|
|
234
|
+
"""
|
|
235
|
+
call_kwargs: Dict[str, Any] = {
|
|
236
|
+
"from_number": self.twilio_number,
|
|
237
|
+
"to_number": phone_number,
|
|
238
|
+
"override_agent_id": self.retell_agent_id,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if call_id:
|
|
242
|
+
call_kwargs["metadata"] = {"call_id": call_id}
|
|
243
|
+
|
|
244
|
+
# Set up dynamic variables for custom prompts.
|
|
245
|
+
if prompt_content:
|
|
246
|
+
enhanced_prompt = (
|
|
247
|
+
f"{prompt_content}\n\n"
|
|
248
|
+
"SYSTEM INSTRUCTION: Continually check the dynamic variable {{force_end}}. "
|
|
249
|
+
"If {{force_end}} == 'true', you must IMMEDIATELY say goodbye and "
|
|
250
|
+
"activate your end_call tool to terminate the call."
|
|
251
|
+
)
|
|
252
|
+
call_kwargs["retell_llm_dynamic_variables"] = {
|
|
253
|
+
"prompt_content": enhanced_prompt,
|
|
254
|
+
"force_end": "false"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
call_response = self.retell_client.call.create_phone_call(
|
|
259
|
+
**call_kwargs)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
raise RetellCallError(
|
|
262
|
+
f"Failed to start outbound call: {e}") from e
|
|
263
|
+
|
|
264
|
+
logger.info("Call created successfully!")
|
|
265
|
+
logger.info(f"Retell Call ID: {call_response.call_id}")
|
|
266
|
+
logger.info(f"Call Status: {call_response.call_status}")
|
|
267
|
+
|
|
268
|
+
return call_response.call_id
|
|
269
|
+
|
|
270
|
+
async def delete_room(self, call_id: str) -> None:
|
|
271
|
+
"""Ends the call by updating a dynamic variable.
|
|
272
|
+
|
|
273
|
+
This method signals the agent to end the call by setting the `force_end`
|
|
274
|
+
dynamic variable to "true".
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
call_id (str): The Retell call ID.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
RetellCallError: If the call cannot be ended.
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
call_data = self.retell_client.call.retrieve(call_id)
|
|
284
|
+
logger.info(f"Current call status: {call_data.call_status}")
|
|
285
|
+
|
|
286
|
+
if call_data.call_status in ['registered', 'ongoing', 'dialing']:
|
|
287
|
+
logger.info(f"Triggering end for Retell call {call_id}...")
|
|
288
|
+
self.retell_client.call.update(
|
|
289
|
+
call_id,
|
|
290
|
+
override_dynamic_variables={"force_end": "true"}
|
|
291
|
+
)
|
|
292
|
+
logger.info("✓ force_end override sent to Retell API")
|
|
293
|
+
else:
|
|
294
|
+
logger.info(f"Call already ended: {call_data.call_status}")
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Error ending call {call_id}: {e}")
|
|
298
|
+
raise RetellCallError(f"Error ending call {call_id}: {e}") from e
|
|
299
|
+
|
|
300
|
+
async def start_stream(self, call_id: str, rtmp_urls: List[str]) -> None:
|
|
301
|
+
"""Not supported for Retell phone calls.
|
|
302
|
+
|
|
303
|
+
Retell deprecated their Audio WebSocket API at the end of 2024.
|
|
304
|
+
For phone calls, audio is managed internally by Retell and cannot
|
|
305
|
+
be accessed externally for streaming.
|
|
306
|
+
|
|
307
|
+
Use `start_recording()` to retrieve recordings after the call ends.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
call_id (str): The Retell Call ID.
|
|
311
|
+
rtmp_urls (List[str]): Not used.
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
NotImplementedError: Always raised as streaming is not supported.
|
|
315
|
+
"""
|
|
316
|
+
raise NotImplementedError(
|
|
317
|
+
"Real-time audio streaming is not supported for Retell phone calls. "
|
|
318
|
+
"Retell deprecated their Audio WebSocket API. "
|
|
319
|
+
"Use start_recording() to retrieve recordings after the call ends."
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def start_recording(self, call_id: str, output_filepath: Optional[str] = None, upload_to_s3: bool = True, wait_for_completion: bool = True) -> str:
|
|
324
|
+
"""Retrieves the recording from a Retell call.
|
|
325
|
+
|
|
326
|
+
Retell automatically records calls. This method polls for the recording
|
|
327
|
+
URL to become available, then downloads and optionally uploads to S3.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
call_id (str): The Retell Call ID.
|
|
331
|
+
output_filepath (Optional[str]): A filename for the recording.
|
|
332
|
+
upload_to_s3 (bool): If True, uploads the recording to S3.
|
|
333
|
+
wait_for_completion (bool): If True, waits for the recording to
|
|
334
|
+
be available before proceeding.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
str: The recording URL from Retell.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
RetellError: If the recording fails or cannot be downloaded.
|
|
341
|
+
RetellConfigurationError: If S3 upload is requested but not configured.
|
|
342
|
+
"""
|
|
343
|
+
logger.info(f"Retrieving recording for call: {call_id}")
|
|
344
|
+
|
|
345
|
+
# Poll for call to end and recording to be available.
|
|
346
|
+
recording_url = None
|
|
347
|
+
max_attempts = 60 # 5 minutes max wait (5s intervals)
|
|
348
|
+
attempts = 0
|
|
349
|
+
|
|
350
|
+
if wait_for_completion:
|
|
351
|
+
logger.info("Waiting for call to end and recording to be available...")
|
|
352
|
+
while attempts < max_attempts:
|
|
353
|
+
try:
|
|
354
|
+
call_data = self.retell_client.call.retrieve(call_id)
|
|
355
|
+
|
|
356
|
+
if call_data.call_status == 'ended':
|
|
357
|
+
if hasattr(call_data, 'recording_url') and call_data.recording_url:
|
|
358
|
+
recording_url = call_data.recording_url
|
|
359
|
+
logger.info("Recording is available.")
|
|
360
|
+
break
|
|
361
|
+
else:
|
|
362
|
+
logger.info("Call ended but recording not yet available...")
|
|
363
|
+
elif call_data.call_status == 'error':
|
|
364
|
+
raise RetellError(f"Call failed with error status")
|
|
365
|
+
else:
|
|
366
|
+
logger.info(f"Call status: {call_data.call_status}, waiting...")
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.warning(f"Error checking call status: {e}")
|
|
370
|
+
|
|
371
|
+
time.sleep(5)
|
|
372
|
+
attempts += 1
|
|
373
|
+
|
|
374
|
+
if not recording_url:
|
|
375
|
+
raise RetellError(
|
|
376
|
+
f"Recording not available after {max_attempts * 5} seconds. "
|
|
377
|
+
"Ensure the call has ended and recording is enabled for the agent."
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
# Just try to get the recording URL once
|
|
381
|
+
call_data = self.retell_client.call.retrieve(call_id)
|
|
382
|
+
if hasattr(call_data, 'recording_url') and call_data.recording_url:
|
|
383
|
+
recording_url = call_data.recording_url
|
|
384
|
+
else:
|
|
385
|
+
return call_id # Return call_id, recording not yet available
|
|
386
|
+
|
|
387
|
+
logger.info(f"Recording URL: {recording_url}")
|
|
388
|
+
|
|
389
|
+
if not upload_to_s3:
|
|
390
|
+
return recording_url
|
|
391
|
+
|
|
392
|
+
# Download recording from Retell.
|
|
393
|
+
logger.info(f"Downloading recording from: {recording_url}")
|
|
394
|
+
response = requests.get(recording_url)
|
|
395
|
+
if response.status_code != 200:
|
|
396
|
+
raise RetellError(
|
|
397
|
+
f"Failed to download recording: {response.status_code} {response.text}")
|
|
398
|
+
|
|
399
|
+
# Upload to S3.
|
|
400
|
+
access_key = get_env("AWS_ACCESS_KEY_ID")
|
|
401
|
+
secret_key = get_env("AWS_SECRET_ACCESS_KEY")
|
|
402
|
+
bucket = get_env("AWS_S3_BUCKET")
|
|
403
|
+
region = get_env("AWS_REGION")
|
|
404
|
+
|
|
405
|
+
if not access_key or not secret_key or not bucket:
|
|
406
|
+
raise RetellConfigurationError(
|
|
407
|
+
"AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, "
|
|
408
|
+
"AWS_S3_BUCKET) are required for S3 upload."
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Determine file extension from URL or default to .wav (Retell uses .wav)
|
|
412
|
+
filename = output_filepath if output_filepath else f"{call_id}-{uuid.uuid4().hex[:6]}.wav"
|
|
413
|
+
|
|
414
|
+
s3 = boto3.client(
|
|
415
|
+
's3',
|
|
416
|
+
aws_access_key_id=access_key,
|
|
417
|
+
aws_secret_access_key=secret_key,
|
|
418
|
+
region_name=region
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
logger.info(f"Uploading to S3: s3://{bucket}/{filename}")
|
|
422
|
+
s3.put_object(Bucket=bucket, Key=filename, Body=response.content)
|
|
423
|
+
logger.info(f"Upload complete: s3://{bucket}/{filename}")
|
|
424
|
+
|
|
425
|
+
# Save locally as well.
|
|
426
|
+
local_dir = "recordings"
|
|
427
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
428
|
+
local_path = os.path.join(local_dir, filename)
|
|
429
|
+
with open(local_path, 'wb') as f:
|
|
430
|
+
f.write(response.content)
|
|
431
|
+
logger.info(f"Recording saved locally: {local_path}")
|
|
432
|
+
|
|
433
|
+
return recording_url
|
|
434
|
+
|
|
435
|
+
async def mute_participant(self, call_id: str, identity: str, track_sid: str, muted: bool) -> None:
|
|
436
|
+
"""Mutes the participant on the Twilio call.
|
|
437
|
+
|
|
438
|
+
Note: Twilio's REST API does not support muting 1:1 calls directly.
|
|
439
|
+
This method will raise NotImplementedError.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
call_id (str): The Retell call ID.
|
|
443
|
+
identity (str): Unused.
|
|
444
|
+
track_sid (str): Unused.
|
|
445
|
+
muted (bool): True to mute, False to unmute.
|
|
446
|
+
"""
|
|
447
|
+
logger.warning("Twilio REST API does not support muting 1:1 calls directly.")
|
|
448
|
+
raise NotImplementedError("Twilio REST API does not support muting 1:1 calls directly.")
|
|
449
|
+
|
|
450
|
+
async def kick_participant(self, call_id: str, identity: str) -> None:
|
|
451
|
+
"""Ends the call.
|
|
452
|
+
|
|
453
|
+
This is an alias for `delete_room`.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
call_id (str): The call ID.
|
|
457
|
+
identity (str): Unused in this context.
|
|
458
|
+
"""
|
|
459
|
+
await self.delete_room(call_id)
|
|
460
|
+
|
|
461
|
+
async def send_alert(self, call_id: str, message: str, participant_identity: Optional[str] = None) -> None:
|
|
462
|
+
"""Not supported by this provider.
|
|
463
|
+
|
|
464
|
+
Raises:
|
|
465
|
+
NotImplementedError: This method is not implemented.
|
|
466
|
+
"""
|
|
467
|
+
raise NotImplementedError(
|
|
468
|
+
"send_alert is not currently supported in RetellManager")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class RetellError(Exception):
|
|
2
|
+
"""Base exception for all Retell-related errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class RetellConfigurationError(RetellError):
|
|
6
|
+
"""Raised when configuration (API keys, secrets) is missing or invalid."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class RetellAPIError(RetellError):
|
|
10
|
+
"""Raised when the Retell API returns an error response."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class RetellPhoneNumberError(RetellError):
|
|
14
|
+
"""Raised when there are issues importing or managing phone numbers."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class RetellCallError(RetellError):
|
|
18
|
+
"""Raised when starting, updating, or ending a call fails."""
|
|
19
|
+
pass
|
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
# Add the project root to the python path so we can import intellema_vdk
|
|
5
5
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
6
6
|
|
|
7
|
-
from intellema_vdk
|
|
7
|
+
from intellema_vdk import RetellManager
|
|
8
8
|
|
|
9
9
|
def import_twilio_number():
|
|
10
10
|
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client import STTManager
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
STTError,
|
|
4
|
+
STTConfigurationError,
|
|
5
|
+
STTFileError,
|
|
6
|
+
STTTranscriptionError,
|
|
7
|
+
STTAgentError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"STTManager",
|
|
12
|
+
"STTError",
|
|
13
|
+
"STTConfigurationError",
|
|
14
|
+
"STTFileError",
|
|
15
|
+
"STTTranscriptionError",
|
|
16
|
+
"STTAgentError",
|
|
17
|
+
]
|