Skip to content

Commit d93f0b4

Browse files
a692570Nash0x7E2cursoragent
authored
Add Telnyx media streaming plugin (#594)
* Add Telnyx media streaming plugin * Address Telnyx media stream review * docs(telnyx): record example implementation plan * feat(telnyx): add runnable phone examples * test(telnyx): cover example setup helpers * docs(telnyx): document phone example setup * docs: surface Telnyx phone examples * docs(telnyx): remove implementation plan artifact * fix(telnyx): address CodeRabbit review on phone examples Move example helpers into the plugin package, verify Telnyx webhook signatures, redact PII from logs, and harden async cleanup in the runnable inbound/outbound examples. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(telnyx): treat malformed webhook input as verification failures Map invalid timestamps, payloads, and key material to TelnyxWebhookVerificationError instead of leaking parse errors. Co-authored-by: Cursor <cursoragent@cursor.com> * style(telnyx): apply ruff formatting to example helpers and tests Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: a692570 <a692570@users.noreply.github.com> Co-authored-by: Neevash Ramdial (Nash) <mail@neevash.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f98c82a commit d93f0b4

19 files changed

Lines changed: 2679 additions & 4 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ agent = Agent(
7373
| **Video Processing** | Pluggable processor pipeline for YOLO, Roboflow, or custom PyTorch/ONNX models before/after LLM calls. |
7474
| **Turn Detection** | Natural conversation flow with VAD, diarization, and smart turn-taking. |
7575
| **Tool Calling & MCP** | Execute code and APIs mid-conversation — Linear issues, weather, telephony, or any MCP server. |
76-
| **Phone Integration** | Inbound and outbound voice calls via Twilio with bidirectional audio streaming. |
76+
| **Phone Integration** | Inbound and outbound voice calls via Twilio or Telnyx with bidirectional audio streaming. |
7777
| **RAG** | Retrieval-augmented generation with TurboPuffer/Qdrant vector search or Gemini FileSearch. |
7878
| **Memory** | Agents recall context across turns and sessions via Stream Chat. |
7979
| **Text Back-channel** | Message the agent silently during a call — coaching overlays, silent instructions, etc. |
@@ -95,7 +95,7 @@ agent = Agent(
9595

9696
**Turn Detection:** [Vogent](https://visionagents.ai/integrations/vogent) · [Smart Turn](https://visionagents.ai/integrations/smart-turn)
9797

98-
**Other:** [Twilio](https://github.com/GetStream/Vision-Agents/tree/main/examples/03_phone_and_rag_example) · [TurboPuffer](https://visionagents.ai/guides/rag)
98+
**Other:** [Twilio](https://github.com/GetStream/Vision-Agents/tree/main/examples/03_phone_and_rag_example) · [Telnyx](https://github.com/GetStream/Vision-Agents/tree/main/plugins/telnyx/examples) · [TurboPuffer](https://visionagents.ai/guides/rag)
9999

100100
## Documentation
101101

@@ -115,7 +115,7 @@ Check out the full docs at [VisionAgents.ai](https://visionagents.ai/).
115115
| <br><h3>Realtime Coaching and Video Understanding</h3>Power interactive coaching flows with live pose tracking and processor pipelines for frame-by-frame understanding.<br><br>• Real-time pose tracking<br>• Actionable coaching feedback<br>• Video processor pipeline support<br><br> [>Source Code and tutorial](https://github.com/GetStream/Vision-Agents/tree/main/examples/02_golf_coach_example) | <img src="assets/demo_gifs/golf.gif" width="320" alt="Realtime Coaching Demo"> |
116116
| <br><h3>Video Restyling and Avatars</h3>Use models like Decart Lucy to build virtual try-ons, stylized scenes, or give your agents a visual identity.<br><br>• Real-time video restyling<br>• Virtual try-on experiences<br>• Avatar-like visual presence<br><br> [>Source Code and tutorial](https://github.com/GetStream/Vision-Agents/tree/main/plugins/decart/example) | <img src="assets/demo_gifs/mirage.gif" width="320" alt="Video Restyling Demo"> |
117117
| <br><h3>Custom Video Models (Roboflow, YOLO, and More)</h3>Train and run custom computer vision models for security monitoring, moderation, and other domain-specific workflows.<br><br>• Bring your own CV models<br>• Real-time moderation pipelines<br>• Security and detection use cases<br><br> [>Source Code and tutorial](https://github.com/GetStream/Vision-Agents/tree/main/examples/11_moderation_example) | <img src="assets/demo_gifs/security_camera.gif" width="320" alt="Custom Video Models Demo"> |
118-
| <br><h3>Tools, MCP, and Phone Calling</h3>Connect external APIs and services so agents can validate data and take real-world actions during live conversations.<br><br>• MCP and function calling support<br>• Twilio-based phone workflows<br>• Real-time fraud response automation<br><br> [>Phone + RAG example](https://github.com/GetStream/Vision-Agents/tree/main/examples/03_phone_and_rag_example) · [>Fraud workflow example](https://github.com/GetStream/Vision-Agents/tree/main/plugins/openai/examples/nemotron_example) | <img src="assets/demo_gifs/fraud_detection.gif" width="320" alt="Tools and Phone Demo"> |
118+
| <br><h3>Tools, MCP, and Phone Calling</h3>Connect external APIs and services so agents can validate data and take real-world actions during live conversations.<br><br>• MCP and function calling support<br>• Twilio and Telnyx phone workflows<br>• Real-time fraud response automation<br><br> [>Phone + RAG example](https://github.com/GetStream/Vision-Agents/tree/main/examples/03_phone_and_rag_example) · [>Telnyx phone examples](https://github.com/GetStream/Vision-Agents/tree/main/plugins/telnyx/examples) · [>Fraud workflow example](https://github.com/GetStream/Vision-Agents/tree/main/plugins/openai/examples/nemotron_example) | <img src="assets/demo_gifs/fraud_detection.gif" width="320" alt="Tools and Phone Demo"> |
119119

120120
## Community Highlights
121121

agents-core/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ nvidia = ["vision-agents-plugins-nvidia"]
6262
openai = ["vision-agents-plugins-openai"]
6363
roboflow = ["vision-agents-plugins-roboflow"]
6464
smart_turn = ["vision-agents-plugins-smart-turn"]
65+
telnyx = ["vision-agents-plugins-telnyx"]
6566
ultralytics = ["vision-agents-plugins-ultralytics"]
6667
wizper = ["vision-agents-plugins-wizper"]
6768
xai = ["vision-agents-plugins-xai"]

plugins/telnyx/README.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Telnyx Plugin
2+
3+
Telnyx plugin for Vision Agents enabling inbound and outbound phone calls with
4+
real-time bidirectional media streaming.
5+
6+
## Features
7+
8+
- **Media Streaming**: Bidirectional audio streaming via Telnyx Media Streaming
9+
- **Call Control**: Support for programmable inbound and outbound phone calls
10+
- **Call Registry**: Track active calls with metadata, stream objects, and
11+
validation tokens
12+
- **Audio Conversion**: PCMU, PCMA, and L16 RTP payload conversion
13+
- **WebSocket Management**: Handle Telnyx WebSocket media events
14+
- **Stream Bridge**: Attach a Telnyx phone participant to a Stream call
15+
16+
## Installation
17+
18+
```bash
19+
uv add "vision-agents[telnyx]"
20+
# or directly
21+
uv add vision-agents-plugins-telnyx
22+
```
23+
24+
## Usage
25+
26+
```python
27+
from vision_agents.plugins import telnyx
28+
29+
# Create a call registry to track active calls
30+
registry = telnyx.CallRegistry()
31+
32+
# Register a call from your Telnyx webhook handler
33+
call = registry.create(
34+
call_control_id="v2:abc123",
35+
webhook_data={"data": {"payload": {"from": "+15551234567"}}},
36+
)
37+
38+
# Create a media stream for the WebSocket connection
39+
stream = telnyx.MediaStream(websocket)
40+
await stream.accept()
41+
42+
# Associate stream with call
43+
call.telnyx_stream = stream
44+
45+
# Run the stream until Telnyx sends a stop event
46+
await stream.run()
47+
```
48+
49+
## Examples
50+
51+
See [examples/](examples/) for minimal inbound and outbound Telnyx phone
52+
examples.
53+
54+
```bash
55+
# Outbound call
56+
uv run plugins/telnyx/examples/outbound_call.py \
57+
--setup-telnyx \
58+
--from +15551234567 \
59+
--to +15557654321
60+
61+
# Inbound call server
62+
uv run plugins/telnyx/examples/inbound_call.py \
63+
--setup-telnyx \
64+
--phone-number +15551234567
65+
```
66+
67+
Telnyx phone calls require a Call Control App. The Call Control App is where
68+
Telnyx sends call webhooks such as `call.initiated`, `call.answered`, and
69+
`call.hangup`. It is also the `connection_id` used by the outbound Dial API. A
70+
forwarding-only phone-number connection is not enough for media streaming through
71+
this plugin.
72+
73+
With `--setup-telnyx`, the examples create a temporary Call Control App and
74+
delete it on normal shutdown. The inbound example also routes the Telnyx number
75+
to the temporary app and restores the previous routing on shutdown.
76+
77+
Without `--setup-telnyx`, the examples validate the common setup requirements:
78+
79+
- `TELNYX_CALL_CONTROL_APP_ID` exists and is active
80+
- the Call Control App webhook URL matches `https://<NGROK_URL>/telnyx/events`
81+
- inbound phone numbers are routed to the Call Control App
82+
- restricted accounts verify outbound destination numbers before dialing
83+
84+
## Components
85+
86+
### TelnyxCall
87+
88+
Dataclass representing an active call session:
89+
90+
```python
91+
@dataclass
92+
class TelnyxCall:
93+
call_control_id: str
94+
token: str
95+
webhook_data: Optional[dict[str, Any]]
96+
telnyx_stream: Optional[TelnyxMediaStream]
97+
stream_call: Optional[Any]
98+
started_at: datetime
99+
ended_at: Optional[datetime]
100+
101+
# Convenience properties from Telnyx webhook payloads
102+
from_number: Optional[str]
103+
to_number: Optional[str]
104+
call_status: Optional[str]
105+
```
106+
107+
### TelnyxCallRegistry
108+
109+
In-memory registry for managing active calls:
110+
111+
```python
112+
registry = telnyx.CallRegistry()
113+
registry.create(call_control_id, webhook_data=webhook_data) # Register new call
114+
registry.get(call_control_id) # Look up call
115+
registry.require(call_control_id) # Look up or raise
116+
registry.validate(call_control_id, token) # Validate media URL token
117+
registry.remove(call_control_id) # Remove and mark ended
118+
registry.list_active() # List active calls
119+
```
120+
121+
### TelnyxMediaStream
122+
123+
Manages Telnyx Media Streaming WebSocket connections:
124+
125+
```python
126+
stream = telnyx.MediaStream(websocket)
127+
await stream.accept()
128+
129+
# Access the audio track for publishing
130+
stream.audio_track # AudioStreamTrack matching the Telnyx media format
131+
132+
# Send audio back to Telnyx when bidirectional RTP streaming is enabled
133+
await stream.send_audio(pcm_data)
134+
135+
# Run until the stream ends
136+
await stream.run()
137+
```
138+
139+
To send audio back to the call, start Telnyx streaming with
140+
`stream_bidirectional_mode=rtp`. The plugin supports PCMU and PCMA at 8 kHz, and
141+
L16 at 16 kHz.
142+
143+
Use `attach_phone_to_call` to bridge audio between a Telnyx media stream and a
144+
Stream call:
145+
146+
```python
147+
await telnyx.attach_phone_to_call(stream_call, stream, user_id="phone-user")
148+
```
149+
150+
## Audio Utilities
151+
152+
```python
153+
from vision_agents.plugins.telnyx import (
154+
TELNYX_DEFAULT_SAMPLE_RATE,
155+
TELNYX_L16_SAMPLE_RATE,
156+
l16_to_pcm,
157+
pcma_to_pcm,
158+
pcm_to_l16,
159+
pcm_to_pcma,
160+
pcm_to_pcmu,
161+
pcm_to_telnyx_payload,
162+
pcmu_to_pcm,
163+
telnyx_payload_to_pcm,
164+
)
165+
166+
pcm = pcmu_to_pcm(payload)
167+
payload = pcm_to_pcmu(pcm)
168+
```
169+
170+
## Configuration
171+
172+
| Parameter | Description | Default |
173+
|------------------------------|--------------------------------------|---------|
174+
| `TELNYX_DEFAULT_SAMPLE_RATE` | Telnyx PCMU and PCMA sample rate | `8000` |
175+
| `TELNYX_L16_SAMPLE_RATE` | Telnyx L16 bidirectional sample rate | `16000` |
176+
177+
## Environment Variables
178+
179+
- `TELNYX_API_KEY`: Your Telnyx API key for Call Control API requests.
180+
- `TELNYX_PHONE_NUMBER`: Telnyx caller ID or inbound number, in E.164 format.
181+
You can also pass this as `--from` or `--phone-number`.
182+
- `NGROK_URL`: Public HTTPS hostname that forwards to your local example server.
183+
The examples can also auto-detect a local ngrok tunnel.
184+
- `TELNYX_CALL_CONTROL_APP_ID`: Existing Call Control App ID. Required only when
185+
running without `--setup-telnyx`.
186+
- `TELNYX_PHONE_NUMBER_ID`: Telnyx phone number resource ID. Required for
187+
inbound only when running without `--setup-telnyx`.
188+
189+
## Dependencies
190+
191+
- vision-agents
192+
- numpy
193+
- fastapi

plugins/telnyx/examples/README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Telnyx Phone Examples
2+
3+
Minimal inbound and outbound phone examples for the Telnyx plugin. These examples
4+
use Telnyx Call Control, Telnyx Media Streaming, Stream, and Gemini Realtime.
5+
6+
## Requirements
7+
8+
Create a `.env` file at the repo root or export these variables:
9+
10+
```bash
11+
STREAM_API_KEY=
12+
STREAM_API_SECRET=
13+
GOOGLE_API_KEY=
14+
TELNYX_API_KEY=
15+
TELNYX_PUBLIC_KEY=
16+
```
17+
18+
`TELNYX_PUBLIC_KEY` is the Base64 Ed25519 public key from the Telnyx Mission
19+
Control Portal. The examples verify webhook signatures before handling events.
20+
21+
You also need a Telnyx phone number. You can pass it to the examples with
22+
`--from` or `--phone-number`, or set:
23+
24+
```bash
25+
TELNYX_PHONE_NUMBER=+15551234567
26+
```
27+
28+
Start ngrok before running either example:
29+
30+
```bash
31+
ngrok http 8000
32+
```
33+
34+
The examples auto-detect the local ngrok HTTPS tunnel. You can also set
35+
`NGROK_URL=example.ngrok-free.app` or pass `--ngrok-url`.
36+
37+
## Quick Start
38+
39+
Use `--setup-telnyx` for the most direct local development flow. The example
40+
creates a temporary Telnyx Call Control App with webhook URL
41+
`https://<NGROK_URL>/telnyx/events` and deletes it on normal shutdown.
42+
43+
Outbound:
44+
45+
```bash
46+
uv run plugins/telnyx/examples/outbound_call.py \
47+
--setup-telnyx \
48+
--from +15551234567 \
49+
--to +15557654321
50+
```
51+
52+
Inbound:
53+
54+
```bash
55+
uv run plugins/telnyx/examples/inbound_call.py \
56+
--setup-telnyx \
57+
--phone-number +15551234567
58+
```
59+
60+
For inbound calls, `--setup-telnyx` also routes the Telnyx number to the
61+
temporary Call Control App and restores the previous routing on normal shutdown.
62+
63+
Restricted Telnyx accounts may only call verified destination numbers. The
64+
outbound example checks that by default. If your account is unrestricted, you can
65+
skip that preflight:
66+
67+
```bash
68+
uv run plugins/telnyx/examples/outbound_call.py \
69+
--setup-telnyx \
70+
--from +15551234567 \
71+
--to +15557654321 \
72+
--skip-verified-destination-check
73+
```
74+
75+
## Manual Telnyx Setup
76+
77+
If you do not want the examples to create or route Telnyx resources, configure
78+
Telnyx yourself and omit `--setup-telnyx`.
79+
80+
1. Create a Telnyx Call Control App.
81+
2. Set the app webhook URL to:
82+
83+
```text
84+
https://<NGROK_URL>/telnyx/events
85+
```
86+
87+
3. For inbound calls, route your Telnyx phone number to that Call Control App.
88+
4. For outbound calls, make sure the Call Control App has an outbound voice
89+
profile that can call your target country.
90+
5. If your Telnyx account is restricted, verify the destination phone number in
91+
Telnyx before running the outbound example.
92+
93+
Then set:
94+
95+
```bash
96+
TELNYX_CALL_CONTROL_APP_ID=
97+
TELNYX_PHONE_NUMBER=+15551234567
98+
NGROK_URL=example.ngrok-free.app
99+
```
100+
101+
Inbound manual setup also needs the Telnyx phone number resource ID:
102+
103+
```bash
104+
TELNYX_PHONE_NUMBER_ID=
105+
```
106+
107+
Run manually configured outbound:
108+
109+
```bash
110+
uv run plugins/telnyx/examples/outbound_call.py --to +15557654321
111+
```
112+
113+
Run manually configured inbound:
114+
115+
```bash
116+
uv run plugins/telnyx/examples/inbound_call.py
117+
```
118+
119+
## Why Call Control Is Required
120+
121+
Telnyx media streaming for programmable calls is configured through a Call
122+
Control App. A regular phone-number connection, including a forwarding-only
123+
connection, is not enough for these examples because the app needs a webhook URL
124+
where Telnyx can send call events and receive answer/dial commands.
125+
126+
For outbound calls, the Call Control App ID is passed to the Telnyx Dial API as
127+
`connection_id`.
128+
129+
For inbound calls, the Telnyx phone number must be routed to the same Call
130+
Control App so Telnyx sends `call.initiated` webhooks to this example.
131+
132+
## Common Setup Errors
133+
134+
- `Invalid value for connection_id`: the Call Control App ID is missing,
135+
invalid, or inactive.
136+
- Webhook URL mismatch: update the Call Control App webhook URL to the current
137+
ngrok URL, or use `--setup-telnyx`.
138+
- Inbound number not routed: assign the Telnyx phone number to the Call Control
139+
App, or use `--setup-telnyx`.
140+
- Destination not verified: verify the `--to` number in Telnyx or use an
141+
unrestricted account.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Minimal Telnyx phone examples for Vision Agents."""

0 commit comments

Comments
 (0)