intellema-vdk 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (28) hide show
  1. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/MANIFEST.in +1 -0
  2. {intellema_vdk-0.1.0/intellema_vdk.egg-info → intellema_vdk-0.2.0}/PKG-INFO +102 -1
  3. intellema_vdk-0.2.0/README.md +174 -0
  4. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk/__init__.py +2 -5
  5. intellema_vdk-0.2.0/intellema_vdk/retell_lib/import_phone_number.py +73 -0
  6. intellema_vdk-0.2.0/intellema_vdk/retell_lib/retell_client.py +248 -0
  7. intellema_vdk-0.2.0/intellema_vdk/speech_lib/__init__.py +2 -0
  8. intellema_vdk-0.2.0/intellema_vdk/speech_lib/stt_client.py +108 -0
  9. intellema_vdk-0.2.0/intellema_vdk/speech_lib/tts_streamer.py +188 -0
  10. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0/intellema_vdk.egg-info}/PKG-INFO +102 -1
  11. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk.egg-info/SOURCES.txt +4 -5
  12. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk.egg-info/requires.txt +6 -0
  13. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/pyproject.toml +8 -2
  14. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/requirements.txt +6 -0
  15. intellema_vdk-0.1.0/README.md +0 -79
  16. intellema_vdk-0.1.0/intellema_vdk/livekit_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  17. intellema_vdk-0.1.0/intellema_vdk/livekit_lib/__pycache__/client.cpython-312.pyc +0 -0
  18. intellema_vdk-0.1.0/intellema_vdk/retell_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  19. intellema_vdk-0.1.0/intellema_vdk/retell_lib/__pycache__/retell_client.cpython-312.pyc +0 -0
  20. intellema_vdk-0.1.0/intellema_vdk/retell_lib/retell_client.py +0 -190
  21. intellema_vdk-0.1.0/tests/test_retell_hybrid.py +0 -71
  22. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/LICENSE +0 -0
  23. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk/livekit_lib/__init__.py +0 -0
  24. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk/livekit_lib/client.py +0 -0
  25. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk/retell_lib/__init__.py +0 -0
  26. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk.egg-info/dependency_links.txt +0 -0
  27. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/intellema_vdk.egg-info/top_level.txt +0 -0
  28. {intellema_vdk-0.1.0 → intellema_vdk-0.2.0}/setup.cfg +0 -0
@@ -2,3 +2,4 @@ include README.md
2
2
  include requirements.txt
3
3
  include LICENSE
4
4
  recursive-include intellema_vdk *
5
+ recursive-exclude intellema_vdk/agent_api *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: intellema-vdk
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Voice Development Kit for different Voice Agent Platforms
5
5
  Author: Intellema
6
6
  License: MIT License
@@ -37,6 +37,12 @@ Requires-Dist: boto3>=1.28.0
37
37
  Requires-Dist: twilio
38
38
  Requires-Dist: retell-sdk
39
39
  Requires-Dist: requests
40
+ Requires-Dist: openai
41
+ Requires-Dist: httpx
42
+ Requires-Dist: pyaudio
43
+ Requires-Dist: together
44
+ Requires-Dist: langchain-openai
45
+ Requires-Dist: langchain-core
40
46
  Dynamic: license-file
41
47
 
42
48
  # Intellema VDK
@@ -100,6 +106,73 @@ from intellema_vdk import start_outbound_call
100
106
  await start_outbound_call("livekit", phone_number="+1...")
101
107
  ```
102
108
 
109
+ ## Speech To Text (STT)
110
+
111
+ The `STTManager` class provides an interface for transcribing audio files using OpenAI's Whisper model and optionally posting the transcribed text to a specified agent API.
112
+
113
+ ### Usage
114
+
115
+ Here's how to use the `STTManager` to transcribe an audio file and post the result:
116
+ Ensure to set OPENAI_API_KEY and AGENT_API_URL in your `.env` file.
117
+
118
+ ```python
119
+ import asyncio
120
+ from intellema_vdk import STTManager
121
+
122
+ async def main():
123
+ # 1- Initialize the STTManager
124
+ stt_manager = STTManager()
125
+
126
+ try:
127
+ # 2- Transcribe an audio file and post the result to your agent API URL (if provided)
128
+ # Replace "path/to/your/audio.mp3" with the actual file path
129
+ transcript = await stt_manager.transcribe_and_post("path/to/your/audio.mp3")
130
+ print(f"Transcription: {transcript}")
131
+
132
+ except FileNotFoundError:
133
+ print("The audio file was not found.")
134
+ except Exception as e:
135
+ print(f"An error occurred: {e}")
136
+ finally:
137
+ # 3- Clean up
138
+ await stt_manager.close()
139
+
140
+ if __name__ == "__main__":
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ ## TTS Streaming
145
+
146
+ The `TTSStreamer` class provides low-latency text-to-speech streaming using Together AI's inference engine. It enables real-time voice synthesis from streaming LLM responses.
147
+
148
+ ### Running the Sample implementation
149
+
150
+ We provide a ready-to-use sample that connects LangChain (OpenAI) with the TTS Streamer.
151
+
152
+ 1. **Configure Keys**: Ensure `OPENAI_API_KEY` and `TOGETHER_API_KEY` are set in your `.env`.
153
+ 2. **Run the script**:
154
+ ```bash
155
+ python sample_implementation.py
156
+ ```
157
+
158
+ ### Library Usage
159
+
160
+ You can integrate the streamer into your own loops:
161
+
162
+ ```python
163
+ from intellema_vdk import TTSStreamer
164
+
165
+ # 1. Initialize per turn
166
+ tts = TTSStreamer()
167
+
168
+ # 2. Feed text chunks as they are generated
169
+ for chunk in llm_response_stream:
170
+ tts.feed(chunk)
171
+
172
+ # 3. Flush and clean up
173
+ tts.flush()
174
+ tts.close()
175
+ ```
103
176
 
104
177
  ## Configuration
105
178
 
@@ -115,6 +188,34 @@ TWILIO_AUTH_TOKEN=your-token
115
188
  TWILIO_PHONE_NUMBER=your-number
116
189
  RETELL_API_KEY=your-retell-key
117
190
  RETELL_AGENT_ID=your-agent-id
191
+ TOGETHER_API_KEY=your-together-key
192
+ OPENAI_API_KEY=your-openai-key
193
+ AGENT_API_URL=https://your-agent-api.com/endpoint
118
194
  ```
119
195
 
196
+ ## Retell Setup
197
+
198
+ **Important:** Before initiating calls with Retell, you must register your Twilio phone number with Retell. This binds your agent to the number and allows Retell to handle the call flow.
199
+
200
+ You can register your number in two ways:
201
+
202
+ 1. **Using the Helper Script:**
203
+ We provide an interactive script to guide you through the process:
204
+ ```bash
205
+ python import_phone_number.py
206
+ ```
207
+
208
+ 2. **Programmatically:**
209
+ ```python
210
+ from intellema_vdk.retell_lib.retell_client import RetellManager
211
+
212
+ manager = RetellManager()
213
+ # Optional: Pass termination_uri if you have a SIP trunk
214
+ manager.import_phone_number(nickname="My Twilio Number")
215
+ ```
216
+
217
+ ## Notes
218
+
219
+ - **Retell `delete_room` Limitation**: The `delete_room` method for Retell relies on updating dynamic variables during the conversation loop. As a result, it **only works if the user speaks something** which triggers the agent to check the variable and terminate the call.
220
+
120
221
 
@@ -0,0 +1,174 @@
1
+ # Intellema VDK
2
+
3
+ Intellema VDK is a unified Voice Development Kit designed to simplify the integration and management of various voice agent platforms. It provides a consistent, factory-based API to interact with providers like LiveKit and Retell AI, enabling developers to build scalable voice applications with ease. Whether you need real-time streaming, outbound calling, or participant management, Intellema VDK abstracts the complexity into a single, intuitive interface.
4
+
5
+ ## Features
6
+
7
+ - **Room Management**: Create and delete rooms dynamically.
8
+ - **Participant Management**: Generate tokens, kick users, and mute tracks.
9
+ - **SIP Outbound Calling**: Initiate calls to phone numbers via SIP trunks.
10
+ - **Streaming & Recording**: Stream to RTMP destinations and record room sessions directly to AWS S3.
11
+ - **Real-time Alerts**: Send data packets (alerts) to participants.
12
+
13
+ ## Prerequisites
14
+
15
+ - Python 3.8+
16
+ - A SIP Provider (for outbound calls)
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install intellema-vdk
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Unified Wrapper (Factory Pattern)
27
+
28
+ The recommended way to use the library is via the `VoiceClient` factory:
29
+
30
+ ```python
31
+ import asyncio
32
+ from intellema_vdk import VoiceClient
33
+
34
+ async def main():
35
+ # 1. Initialize the client
36
+ client = VoiceClient("livekit")
37
+
38
+ # 2. Use methods directly
39
+ call_id = await client.start_outbound_call(
40
+ phone_number="+15551234567",
41
+ prompt_content="Hello from LiveKit"
42
+ )
43
+
44
+ # 3. Clean API calls
45
+ await client.mute_participant(call_id, "user-1", "track-1", True)
46
+ await client.close()
47
+
48
+ if __name__ == "__main__":
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ ### Convenience Function
53
+
54
+ For quick one-off calls, you can still use the helper:
55
+
56
+ ```python
57
+ from intellema_vdk import start_outbound_call
58
+
59
+ await start_outbound_call("livekit", phone_number="+1...")
60
+ ```
61
+
62
+ ## Speech To Text (STT)
63
+
64
+ The `STTManager` class provides an interface for transcribing audio files using OpenAI's Whisper model and optionally posting the transcribed text to a specified agent API.
65
+
66
+ ### Usage
67
+
68
+ Here's how to use the `STTManager` to transcribe an audio file and post the result:
69
+ Ensure to set OPENAI_API_KEY and AGENT_API_URL in your `.env` file.
70
+
71
+ ```python
72
+ import asyncio
73
+ from intellema_vdk import STTManager
74
+
75
+ async def main():
76
+ # 1- Initialize the STTManager
77
+ stt_manager = STTManager()
78
+
79
+ try:
80
+ # 2- Transcribe an audio file and post the result to your agent API URL (if provided)
81
+ # Replace "path/to/your/audio.mp3" with the actual file path
82
+ transcript = await stt_manager.transcribe_and_post("path/to/your/audio.mp3")
83
+ print(f"Transcription: {transcript}")
84
+
85
+ except FileNotFoundError:
86
+ print("The audio file was not found.")
87
+ except Exception as e:
88
+ print(f"An error occurred: {e}")
89
+ finally:
90
+ # 3- Clean up
91
+ await stt_manager.close()
92
+
93
+ if __name__ == "__main__":
94
+ asyncio.run(main())
95
+ ```
96
+
97
+ ## TTS Streaming
98
+
99
+ The `TTSStreamer` class provides low-latency text-to-speech streaming using Together AI's inference engine. It enables real-time voice synthesis from streaming LLM responses.
100
+
101
+ ### Running the Sample implementation
102
+
103
+ We provide a ready-to-use sample that connects LangChain (OpenAI) with the TTS Streamer.
104
+
105
+ 1. **Configure Keys**: Ensure `OPENAI_API_KEY` and `TOGETHER_API_KEY` are set in your `.env`.
106
+ 2. **Run the script**:
107
+ ```bash
108
+ python sample_implementation.py
109
+ ```
110
+
111
+ ### Library Usage
112
+
113
+ You can integrate the streamer into your own loops:
114
+
115
+ ```python
116
+ from intellema_vdk import TTSStreamer
117
+
118
+ # 1. Initialize per turn
119
+ tts = TTSStreamer()
120
+
121
+ # 2. Feed text chunks as they are generated
122
+ for chunk in llm_response_stream:
123
+ tts.feed(chunk)
124
+
125
+ # 3. Flush and clean up
126
+ tts.flush()
127
+ tts.close()
128
+ ```
129
+
130
+ ## Configuration
131
+
132
+ Create a `.env` file in the root directory:
133
+
134
+ ```bash
135
+ LIVEKIT_URL=wss://your-livekit-domain.com
136
+ LIVEKIT_API_KEY=your-key
137
+ LIVEKIT_API_SECRET=your-secret
138
+ SIP_OUTBOUND_TRUNK_ID=your-trunk-id
139
+ TWILIO_ACCOUNT_SID=your-sid
140
+ TWILIO_AUTH_TOKEN=your-token
141
+ TWILIO_PHONE_NUMBER=your-number
142
+ RETELL_API_KEY=your-retell-key
143
+ RETELL_AGENT_ID=your-agent-id
144
+ TOGETHER_API_KEY=your-together-key
145
+ OPENAI_API_KEY=your-openai-key
146
+ AGENT_API_URL=https://your-agent-api.com/endpoint
147
+ ```
148
+
149
+ ## Retell Setup
150
+
151
+ **Important:** Before initiating calls with Retell, you must register your Twilio phone number with Retell. This binds your agent to the number and allows Retell to handle the call flow.
152
+
153
+ You can register your number in two ways:
154
+
155
+ 1. **Using the Helper Script:**
156
+ We provide an interactive script to guide you through the process:
157
+ ```bash
158
+ python import_phone_number.py
159
+ ```
160
+
161
+ 2. **Programmatically:**
162
+ ```python
163
+ from intellema_vdk.retell_lib.retell_client import RetellManager
164
+
165
+ manager = RetellManager()
166
+ # Optional: Pass termination_uri if you have a SIP trunk
167
+ manager.import_phone_number(nickname="My Twilio Number")
168
+ ```
169
+
170
+ ## Notes
171
+
172
+ - **Retell `delete_room` Limitation**: The `delete_room` method for Retell relies on updating dynamic variables during the conversation loop. As a result, it **only works if the user speaks something** which triggers the agent to check the variable and terminate the call.
173
+
174
+
@@ -1,12 +1,9 @@
1
1
  from typing import Optional, List, Any
2
- import os
3
- from dotenv import load_dotenv
4
-
5
- # Load environment variables
6
- load_dotenv()
7
2
 
8
3
  from .livekit_lib.client import LiveKitManager
9
4
  from .retell_lib.retell_client import RetellManager
5
+ from .speech_lib.stt_client import STTManager
6
+ from .speech_lib.tts_streamer import TTSStreamer
10
7
 
11
8
  def VoiceClient(provider: str, **kwargs) -> Any:
12
9
  """
@@ -0,0 +1,73 @@
1
+ import os
2
+ import sys
3
+
4
+ # Add the project root to the python path so we can import intellema_vdk
5
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
6
+
7
+ from intellema_vdk.retell_lib.retell_client import RetellManager
8
+
9
+ def import_twilio_number():
10
+ """
11
+ Import your Twilio phone number to Retell.
12
+ This is required before you can make outbound calls using Retell.
13
+ """
14
+ try:
15
+ manager = RetellManager()
16
+
17
+ print("=== Retell Phone Number Import ===\n")
18
+ print(f"Phone Number to import: {manager.twilio_number}")
19
+ print(f"Agent ID to bind: {manager.retell_agent_id}\n")
20
+
21
+ # Ask if user has a Twilio SIP trunk
22
+ print("Do you have a Twilio Elastic SIP Trunk configured?")
23
+ print("If you're not sure, you can:")
24
+ print(" 1. Visit: https://console.twilio.com/us1/develop/voice/manage/trunks")
25
+ print(" 2. Or just press Enter to try without it (may not work for some setups)\n")
26
+
27
+ has_trunk = input("Do you have a SIP trunk? (y/n, default: n): ").strip().lower()
28
+
29
+ termination_uri = None
30
+ sip_username = None
31
+ sip_password = None
32
+
33
+ if has_trunk == 'y':
34
+ print("\nEnter your Twilio SIP Trunk Termination URI.")
35
+ print("Format: yourtrunkname.pstn.twilio.com")
36
+ print("You can find this in Twilio Console > Elastic SIP Trunking > Your Trunk > Termination")
37
+ termination_uri = input("Termination URI: ").strip()
38
+
39
+ print("\nDo you use Credential List authentication? (Recommended)")
40
+ has_creds = input("Use credentials? (y/n, default: y): ").strip().lower() or 'y'
41
+
42
+ if has_creds == 'y':
43
+ print("Enter the username/password from your Twilio Credential List:")
44
+ sip_username = input("Username: ").strip()
45
+ sip_password = input("Password: ").strip()
46
+
47
+ # Optional nickname
48
+ nickname = input("\nOptional: Enter a nickname for this number (press Enter to skip): ").strip() or None
49
+
50
+ print(f"\n=== Importing Phone Number ===")
51
+
52
+ response = manager.import_phone_number(
53
+ termination_uri=termination_uri,
54
+ nickname=nickname,
55
+ sip_trunk_auth_username=sip_username,
56
+ sip_trunk_auth_password=sip_password
57
+ )
58
+
59
+ print(f"\n=== Import Successful! ===")
60
+ print(f"You can now use this number to make outbound calls via Retell.")
61
+
62
+ return response
63
+
64
+ except Exception as e:
65
+ print(f"\n✗ Import failed: {e}")
66
+ print(f"\nTroubleshooting:")
67
+ print(f" 1. If you don't have a SIP trunk, you may need to purchase the number through Retell")
68
+ print(f" 2. Visit Retell dashboard: https://app.retellai.com/")
69
+ print(f" 3. Or create a Twilio Elastic SIP Trunk first")
70
+ raise
71
+
72
+ if __name__ == "__main__":
73
+ import_twilio_number()
@@ -0,0 +1,248 @@
1
+ import os
2
+ from typing import List, Optional
3
+ from dotenv import load_dotenv
4
+ from twilio.rest import Client
5
+ from retell import Retell
6
+ import time
7
+ import uuid
8
+ import requests
9
+ import boto3
10
+
11
+ # Load environment variables
12
+ load_dotenv(dotenv_path=".env.local")
13
+ load_dotenv()
14
+
15
+ class RetellManager:
16
+ def __init__(self):
17
+ self.twilio_account_sid = os.getenv("TWILIO_ACCOUNT_SID")
18
+ self.twilio_auth_token = os.getenv("TWILIO_AUTH_TOKEN")
19
+ self.twilio_number = os.getenv("TWILIO_PHONE_NUMBER")
20
+ self.retell_api_key = os.getenv("RETELL_API_KEY")
21
+ self.retell_agent_id = os.getenv("RETELL_AGENT_ID")
22
+
23
+ if not all([self.twilio_account_sid, self.twilio_auth_token, self.twilio_number, self.retell_api_key, self.retell_agent_id]):
24
+ raise ValueError("Missing necessary environment variables for RetellManager")
25
+
26
+ self.twilio_client = Client(self.twilio_account_sid, self.twilio_auth_token)
27
+ self.retell_client = Retell(api_key=self.retell_api_key)
28
+
29
+ def import_phone_number(self, termination_uri: str = None, outbound_agent_id: str = None, inbound_agent_id: str = None, nickname: str = None, sip_trunk_auth_username: str = None, sip_trunk_auth_password: str = None):
30
+ """
31
+ Import/register your Twilio phone number with Retell.
32
+ This is required before you can make outbound calls using the phone number.
33
+
34
+ Args:
35
+ termination_uri: Twilio SIP trunk termination URI (e.g., "yourtrunk.pstn.twilio.com").
36
+ If not provided, will try to use a default format.
37
+ outbound_agent_id: Agent ID to use for outbound calls. Defaults to self.retell_agent_id.
38
+ inbound_agent_id: Agent ID to use for inbound calls. Defaults to None (no inbound).
39
+ nickname: Optional nickname for the phone number.
40
+ sip_trunk_auth_username: Username for SIP trunk authentication (if using credential list).
41
+ sip_trunk_auth_password: Password for SIP trunk authentication (if using credential list).
42
+
43
+ Returns:
44
+ The phone number registration response from Retell.
45
+ """
46
+ # Build the import kwargs
47
+ import_kwargs = {
48
+ "phone_number": self.twilio_number,
49
+ }
50
+
51
+ # Add termination URI if provided
52
+ if termination_uri:
53
+ import_kwargs["termination_uri"] = termination_uri
54
+
55
+ # Add SIP credentials if provided
56
+ if sip_trunk_auth_username and sip_trunk_auth_password:
57
+ import_kwargs["sip_trunk_auth_username"] = sip_trunk_auth_username
58
+ import_kwargs["sip_trunk_auth_password"] = sip_trunk_auth_password
59
+
60
+ # Set outbound agent (required for outbound calls)
61
+ if outbound_agent_id:
62
+ import_kwargs["outbound_agent_id"] = outbound_agent_id
63
+ elif self.retell_agent_id:
64
+ import_kwargs["outbound_agent_id"] = self.retell_agent_id
65
+
66
+ # Set inbound agent if provided
67
+ if inbound_agent_id:
68
+ import_kwargs["inbound_agent_id"] = inbound_agent_id
69
+
70
+ # Add nickname if provided
71
+ if nickname:
72
+ import_kwargs["nickname"] = nickname
73
+
74
+ try:
75
+ response = self.retell_client.phone_number.import_(**import_kwargs)
76
+ print(f"✓ Phone number {self.twilio_number} successfully imported to Retell!")
77
+ print(f" Phone Number: {response.phone_number}")
78
+ print(f" Type: {response.phone_number_type}")
79
+ if hasattr(response, 'outbound_agent_id') and response.outbound_agent_id:
80
+ print(f" Outbound Agent: {response.outbound_agent_id}")
81
+ if hasattr(response, 'inbound_agent_id') and response.inbound_agent_id:
82
+ print(f" Inbound Agent: {response.inbound_agent_id}")
83
+ return response
84
+ except Exception as e:
85
+ print(f"✗ Error importing phone number: {e}")
86
+ print(f"\nNote: If you're using Twilio, you may need to:")
87
+ print(f" 1. Create an Elastic SIP Trunk in Twilio console")
88
+ print(f" 2. Provide the termination_uri parameter (e.g., 'yourtrunk.pstn.twilio.com')")
89
+ print(f" 3. Or purchase the number directly through Retell dashboard")
90
+ raise
91
+
92
+
93
+ def start_outbound_call(self, phone_number: str, prompt_content: str = None, call_id: str = None) -> str:
94
+ call_kwargs = {
95
+ "from_number": self.twilio_number,
96
+ "to_number": phone_number,
97
+ "override_agent_id": self.retell_agent_id,
98
+ }
99
+
100
+ if call_id:
101
+ call_kwargs["metadata"] = {"call_id": call_id}
102
+
103
+ # Only set up dynamic variables if we have a custom prompt
104
+ if prompt_content:
105
+ enhanced_prompt = (
106
+ f"{prompt_content}\n\n"
107
+ "SYSTEM INSTRUCTION: Continually check the dynamic variable {{force_end}}. "
108
+ "If {{force_end}} == 'true, you must IMMEDIATELY say goodbye and activate your end_call tool to terminate the call."
109
+ )
110
+ call_kwargs["retell_llm_dynamic_variables"] = {
111
+ "prompt_content": enhanced_prompt,
112
+ "force_end": "false"
113
+ }
114
+
115
+ call_response = self.retell_client.call.create_phone_call(**call_kwargs)
116
+
117
+ print(f"Call created successfully!")
118
+ print(f"Retell Call ID: {call_response.call_id}")
119
+ print(f"Call Status: {call_response.call_status}")
120
+
121
+ return call_response.call_id
122
+
123
+ def delete_room(self, call_id: str):
124
+ try:
125
+ call_data = self.retell_client.call.retrieve(call_id)
126
+ print(f"Current call status: {call_data.call_status}")
127
+
128
+ if call_data.call_status in ['registered', 'ongoing', 'dialing']:
129
+ print(f"Triggering end for Retell call {call_id}...")
130
+
131
+ self.retell_client.call.update(
132
+ call_id,
133
+ override_dynamic_variables={"force_end": "true"}
134
+ )
135
+
136
+ print("✓ force_end override sent to Retell API")
137
+ else:
138
+ print(f"Call already ended: {call_data.call_status}")
139
+
140
+ except Exception as e:
141
+ print(f"Error ending call {call_id}: {e}")
142
+ raise
143
+
144
+ def start_stream(self, call_id: str, rtmp_urls: List[str]):
145
+ """
146
+ Starts a Twilio Media Stream.
147
+ Note: Twilio streams are WebSocket-based. If rtmp_urls contains a WSS URL, it will work.
148
+ """
149
+ if not rtmp_urls:
150
+ raise ValueError("No stream URLs provided")
151
+
152
+ self.twilio_client.calls(call_id).streams.create(
153
+ url=rtmp_urls[0]
154
+ )
155
+
156
+ def start_recording(self, call_id: str, output_filepath: Optional[str] = None, upload_to_s3: bool = True, wait_for_completion: bool = True):
157
+ """
158
+ Triggers a recording on the active Twilio call.
159
+
160
+ Args:
161
+ call_id: The Twilio Call SID.
162
+ output_filepath: Optional filename for the recording.
163
+ upload_to_s3: If True, uploads to S3.
164
+ wait_for_completion: If True, waits for recording to finish and then uploads.
165
+
166
+ Returns:
167
+ The Twilio Recording SID.
168
+ """
169
+
170
+ # Start Twilio recording
171
+ recording = self.twilio_client.calls(call_id).recordings.create()
172
+ print(f"Recording started: {recording.sid}")
173
+
174
+ if not wait_for_completion:
175
+ return recording.sid
176
+
177
+ # Poll for recording completion
178
+ print("Waiting for recording to complete...")
179
+ while True:
180
+ rec_status = self.twilio_client.recordings(recording.sid).fetch()
181
+ if rec_status.status == 'completed':
182
+ print("Recording completed.")
183
+ break
184
+ elif rec_status.status in ['failed', 'absent']:
185
+ raise RuntimeError(f"Recording failed with status: {rec_status.status}")
186
+ time.sleep(5)
187
+
188
+ if not upload_to_s3:
189
+ return recording.sid
190
+
191
+ # Download recording from Twilio
192
+ media_url = f"https://api.twilio.com/2010-04-01/Accounts/{self.twilio_account_sid}/Recordings/{recording.sid}.mp3"
193
+ print(f"Downloading recording from: {media_url}")
194
+
195
+ response = requests.get(media_url, auth=(self.twilio_account_sid, self.twilio_auth_token))
196
+ if response.status_code != 200:
197
+ raise RuntimeError(f"Failed to download recording: {response.status_code} {response.text}")
198
+
199
+ # Upload to S3
200
+ access_key = os.getenv("AWS_ACCESS_KEY_ID")
201
+ secret_key = os.getenv("AWS_SECRET_ACCESS_KEY")
202
+ bucket = os.getenv("AWS_S3_BUCKET")
203
+ region = os.getenv("AWS_REGION")
204
+
205
+ if not access_key or not secret_key or not bucket:
206
+ raise ValueError("AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET) are required for S3 upload.")
207
+
208
+ filename = output_filepath if output_filepath else f"{call_id}-{uuid.uuid4().hex[:6]}.mp3"
209
+
210
+ s3 = boto3.client(
211
+ 's3',
212
+ aws_access_key_id=access_key,
213
+ aws_secret_access_key=secret_key,
214
+ region_name=region
215
+ )
216
+
217
+ print(f"Uploading to S3: s3://{bucket}/{filename}")
218
+ s3.put_object(Bucket=bucket, Key=filename, Body=response.content)
219
+ print(f"Upload complete: s3://{bucket}/{filename}")
220
+
221
+ # Also save locally
222
+ local_dir = "recordings"
223
+ os.makedirs(local_dir, exist_ok=True)
224
+ local_path = os.path.join(local_dir, filename)
225
+ with open(local_path, 'wb') as f:
226
+ f.write(response.content)
227
+ print(f"Recording saved locally: {local_path}")
228
+
229
+ return recording.sid
230
+
231
+ def mute_participant(self, call_id: str, identity: str, track_sid: str, muted: bool):
232
+ """
233
+ Mutes the participant on the Twilio call.
234
+ This prevents audio from reaching the Retell AI.
235
+ """
236
+ self.twilio_client.calls(call_id).update(muted=muted)
237
+
238
+ def kick_participant(self, call_id: str, identity: str):
239
+ """
240
+ Alias for delete_room (hangup).
241
+ """
242
+ self.delete_room(call_id)
243
+
244
+ def send_alert(self, call_id: str, message: str, participant_identity: Optional[str] = None):
245
+ """
246
+ Not fully supported in this hybrid model
247
+ """
248
+ raise NotImplementedError("send_alert is not currently supported in RetellManager")
@@ -0,0 +1,2 @@
1
+ from .stt_client import STTManager
2
+ from .tts_streamer import TTSStreamer