Add Exotel support to the development runner

This commit is contained in:
Mark Backman
2025-08-12 13:01:25 -04:00
parent 5f8433476c
commit a40327305c
3 changed files with 57 additions and 10 deletions

View File

@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added Exotel support to Pipecat's development runner. You can now connect
using the runner with `uv run bot.py -t exotel` and an ngrok connection to
HTTP port 7860.
- Added `enable_direct_mode` argument to `FrameProcessor`. The direct mode is
for processors which require very little I/O or compute resources, that is
processors that can perform their task almost immediately. These type of

View File

@@ -53,7 +53,7 @@ Supported transports:
- Daily - Creates rooms and tokens, runs bot as participant
- WebRTC - Provides local WebRTC interface with prebuilt UI
- Telephony - Handles webhook and WebSocket connections for Twilio, Telnyx, Plivo
- Telephony - Handles webhook and WebSocket connections for Twilio, Telnyx, Plivo, Exotel
To run locally:
@@ -62,6 +62,7 @@ To run locally:
- Daily (server): `python bot.py -t daily`
- Daily (direct, testing only): `python bot.py -d`
- Telephony: `python bot.py -t twilio -x your_username.ngrok.io`
- Exotel: `python bot.py -t exotel` (no proxy needed, but ngrok connection to HTTP 7860 is required)
"""
import argparse
@@ -168,7 +169,7 @@ def _create_server_app(
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host)
elif transport_type == "daily":
_setup_daily_routes(app)
elif transport_type in ["twilio", "telnyx", "plivo"]:
elif transport_type in ["twilio", "telnyx", "plivo", "exotel"]:
_setup_telephony_routes(app, transport_type, proxy)
else:
logger.warning(f"Unknown transport type: {transport_type}")
@@ -333,7 +334,7 @@ def _setup_daily_routes(app: FastAPI):
def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
"""Set up telephony-specific routes."""
# XML response templates
# XML response templates (Exotel doesn't use XML webhooks)
XML_TEMPLATES = {
"twilio": f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
@@ -358,9 +359,18 @@ def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
@app.post("/")
async def start_call():
"""Handle telephony webhook and return XML response."""
logger.debug(f"POST {transport_type.upper()} XML")
xml_content = XML_TEMPLATES.get(transport_type, "<Response></Response>")
return HTMLResponse(content=xml_content, media_type="application/xml")
if transport_type == "exotel":
# Exotel doesn't use POST webhooks - redirect to proper documentation
logger.debug("POST Exotel endpoint - not used")
return {
"error": "Exotel doesn't use POST webhooks",
"websocket_url": f"wss://{proxy}/ws",
"note": "Configure the WebSocket URL above in your Exotel App Bazaar Voicebot Applet",
}
else:
logger.debug(f"POST {transport_type.upper()} XML")
xml_content = XML_TEMPLATES.get(transport_type, "<Response></Response>")
return HTMLResponse(content=xml_content, media_type="application/xml")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
@@ -436,7 +446,7 @@ def main():
Args:
--host: Server host address (default: localhost)
--port: Server port (default: 7860)
-t/--transport: Transport type (daily, webrtc, twilio, telnyx, plivo)
-t/--transport: Transport type (daily, webrtc, twilio, telnyx, plivo, exotel)
-x/--proxy: Public proxy hostname for telephony webhooks
--esp32: Enable SDP munging for ESP32 compatibility (requires --host with IP address)
-d/--direct: Connect directly to Daily room (automatically sets transport to daily)
@@ -451,7 +461,7 @@ def main():
"-t",
"--transport",
type=str,
choices=["daily", "webrtc", "twilio", "telnyx", "plivo"],
choices=["daily", "webrtc", "twilio", "telnyx", "plivo", "exotel"],
default="webrtc",
help="Transport type",
)

View File

@@ -77,6 +77,17 @@ def _detect_transport_type_from_message(message_data: dict) -> str:
logger.trace("Auto-detected: PLIVO")
return "plivo"
# Exotel detection
if (
message_data.get("event") == "start"
and "start" in message_data
and "stream_sid" in message_data.get("start", {})
and "call_sid" in message_data.get("start", {})
and "account_sid" in message_data.get("start", {})
):
logger.trace("Auto-detected: EXOTEL")
return "exotel"
logger.trace("Auto-detection failed - unknown format")
return "unknown"
@@ -91,6 +102,7 @@ async def parse_telephony_websocket(websocket: WebSocket):
- Twilio: {"stream_id": str, "call_id": str}
- Telnyx: {"stream_id": str, "call_control_id": str, "outbound_encoding": str}
- Plivo: {"stream_id": str, "call_id": str}
- Exotel: {"stream_id": str, "call_id": str, "account_sid": str}
Example usage::
@@ -160,6 +172,14 @@ async def parse_telephony_websocket(websocket: WebSocket):
"call_id": start_data.get("callId"),
}
elif transport_type == "exotel":
start_data = call_data_raw.get("start", {})
call_data = {
"stream_id": start_data.get("stream_sid"),
"call_id": start_data.get("call_sid"),
"account_sid": start_data.get("account_sid"),
}
else:
call_data = {}
@@ -379,10 +399,17 @@ async def _create_telephony_transport(
auth_id=os.getenv("PLIVO_AUTH_ID", ""),
auth_token=os.getenv("PLIVO_AUTH_TOKEN", ""),
)
elif transport_type == "exotel":
from pipecat.serializers.exotel import ExotelFrameSerializer
params.serializer = ExotelFrameSerializer(
stream_sid=call_data["stream_id"],
call_sid=call_data["call_id"],
)
else:
raise ValueError(
f"Unsupported telephony provider: {transport_type}. "
f"Supported providers: twilio, telnyx, plivo"
f"Supported providers: twilio, telnyx, plivo, exotel"
)
return FastAPIWebsocketTransport(websocket=websocket, params=params)
@@ -399,7 +426,7 @@ async def create_transport(
Args:
runner_args: Arguments from the runner.
transport_params: Dict mapping transport names to parameter factory functions.
Keys should be: "daily", "webrtc", "twilio", "telnyx", "plivo"
Keys should be: "daily", "webrtc", "twilio", "telnyx", "plivo", "exotel"
Values should be functions that return transport parameters when called.
Returns:
@@ -440,6 +467,12 @@ async def create_transport(
vad_analyzer=SileroVADAnalyzer(),
# add_wav_header and serializer will be set automatically
),
"exotel": lambda: FastAPIWebsocketParams(
audio_in_enabled=True,
audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(),
# add_wav_header and serializer will be set automatically
),
}
transport = await create_transport(runner_args, transport_params)