intellema-vdk 0.1.0__tar.gz → 0.2.1__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.
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/MANIFEST.in +1 -0
- {intellema_vdk-0.1.0/intellema_vdk.egg-info → intellema_vdk-0.2.1}/PKG-INFO +102 -1
- intellema_vdk-0.2.1/README.md +174 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk/__init__.py +2 -5
- intellema_vdk-0.2.1/intellema_vdk/__pycache__/__init__.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/livekit_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk/livekit_lib/__pycache__/client.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/retell_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/retell_lib/__pycache__/retell_client.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/retell_lib/import_phone_number.py +73 -0
- intellema_vdk-0.2.1/intellema_vdk/retell_lib/retell_client.py +248 -0
- intellema_vdk-0.2.1/intellema_vdk/speech_lib/__init__.py +2 -0
- intellema_vdk-0.2.1/intellema_vdk/speech_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/speech_lib/__pycache__/stt_client.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/speech_lib/__pycache__/tts_streamer.cpython-312.pyc +0 -0
- intellema_vdk-0.2.1/intellema_vdk/speech_lib/stt_client.py +110 -0
- intellema_vdk-0.2.1/intellema_vdk/speech_lib/tts_streamer.py +188 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1/intellema_vdk.egg-info}/PKG-INFO +102 -1
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk.egg-info/SOURCES.txt +8 -1
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk.egg-info/requires.txt +6 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/pyproject.toml +8 -2
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/requirements.txt +6 -0
- intellema_vdk-0.1.0/README.md +0 -79
- intellema_vdk-0.1.0/intellema_vdk/livekit_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- intellema_vdk-0.1.0/intellema_vdk/retell_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- intellema_vdk-0.1.0/intellema_vdk/retell_lib/__pycache__/retell_client.cpython-312.pyc +0 -0
- intellema_vdk-0.1.0/intellema_vdk/retell_lib/retell_client.py +0 -190
- intellema_vdk-0.1.0/tests/test_retell_hybrid.py +0 -71
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/LICENSE +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk/livekit_lib/__init__.py +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk/livekit_lib/client.py +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk/retell_lib/__init__.py +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk.egg-info/dependency_links.txt +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/intellema_vdk.egg-info/top_level.txt +0 -0
- {intellema_vdk-0.1.0 → intellema_vdk-0.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: intellema-vdk
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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
|
"""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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")
|
|
Binary file
|