Add Exotel support to the development runner
This commit is contained in:
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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
|
- Added `enable_direct_mode` argument to `FrameProcessor`. The direct mode is
|
||||||
for processors which require very little I/O or compute resources, that 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
|
processors that can perform their task almost immediately. These type of
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Supported transports:
|
|||||||
|
|
||||||
- Daily - Creates rooms and tokens, runs bot as participant
|
- Daily - Creates rooms and tokens, runs bot as participant
|
||||||
- WebRTC - Provides local WebRTC interface with prebuilt UI
|
- 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:
|
To run locally:
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@ To run locally:
|
|||||||
- Daily (server): `python bot.py -t daily`
|
- Daily (server): `python bot.py -t daily`
|
||||||
- Daily (direct, testing only): `python bot.py -d`
|
- Daily (direct, testing only): `python bot.py -d`
|
||||||
- Telephony: `python bot.py -t twilio -x your_username.ngrok.io`
|
- 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
|
import argparse
|
||||||
@@ -168,7 +169,7 @@ def _create_server_app(
|
|||||||
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host)
|
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host)
|
||||||
elif transport_type == "daily":
|
elif transport_type == "daily":
|
||||||
_setup_daily_routes(app)
|
_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)
|
_setup_telephony_routes(app, transport_type, proxy)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unknown transport type: {transport_type}")
|
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):
|
def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
|
||||||
"""Set up telephony-specific routes."""
|
"""Set up telephony-specific routes."""
|
||||||
# XML response templates
|
# XML response templates (Exotel doesn't use XML webhooks)
|
||||||
XML_TEMPLATES = {
|
XML_TEMPLATES = {
|
||||||
"twilio": f"""<?xml version="1.0" encoding="UTF-8"?>
|
"twilio": f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Response>
|
<Response>
|
||||||
@@ -358,9 +359,18 @@ def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
|
|||||||
@app.post("/")
|
@app.post("/")
|
||||||
async def start_call():
|
async def start_call():
|
||||||
"""Handle telephony webhook and return XML response."""
|
"""Handle telephony webhook and return XML response."""
|
||||||
logger.debug(f"POST {transport_type.upper()} XML")
|
if transport_type == "exotel":
|
||||||
xml_content = XML_TEMPLATES.get(transport_type, "<Response></Response>")
|
# Exotel doesn't use POST webhooks - redirect to proper documentation
|
||||||
return HTMLResponse(content=xml_content, media_type="application/xml")
|
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")
|
@app.websocket("/ws")
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
@@ -436,7 +446,7 @@ def main():
|
|||||||
Args:
|
Args:
|
||||||
--host: Server host address (default: localhost)
|
--host: Server host address (default: localhost)
|
||||||
--port: Server port (default: 7860)
|
--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
|
-x/--proxy: Public proxy hostname for telephony webhooks
|
||||||
--esp32: Enable SDP munging for ESP32 compatibility (requires --host with IP address)
|
--esp32: Enable SDP munging for ESP32 compatibility (requires --host with IP address)
|
||||||
-d/--direct: Connect directly to Daily room (automatically sets transport to daily)
|
-d/--direct: Connect directly to Daily room (automatically sets transport to daily)
|
||||||
@@ -451,7 +461,7 @@ def main():
|
|||||||
"-t",
|
"-t",
|
||||||
"--transport",
|
"--transport",
|
||||||
type=str,
|
type=str,
|
||||||
choices=["daily", "webrtc", "twilio", "telnyx", "plivo"],
|
choices=["daily", "webrtc", "twilio", "telnyx", "plivo", "exotel"],
|
||||||
default="webrtc",
|
default="webrtc",
|
||||||
help="Transport type",
|
help="Transport type",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,6 +77,17 @@ def _detect_transport_type_from_message(message_data: dict) -> str:
|
|||||||
logger.trace("Auto-detected: PLIVO")
|
logger.trace("Auto-detected: PLIVO")
|
||||||
return "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")
|
logger.trace("Auto-detection failed - unknown format")
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
@@ -91,6 +102,7 @@ async def parse_telephony_websocket(websocket: WebSocket):
|
|||||||
- Twilio: {"stream_id": str, "call_id": str}
|
- Twilio: {"stream_id": str, "call_id": str}
|
||||||
- Telnyx: {"stream_id": str, "call_control_id": str, "outbound_encoding": str}
|
- Telnyx: {"stream_id": str, "call_control_id": str, "outbound_encoding": str}
|
||||||
- Plivo: {"stream_id": str, "call_id": str}
|
- Plivo: {"stream_id": str, "call_id": str}
|
||||||
|
- Exotel: {"stream_id": str, "call_id": str, "account_sid": str}
|
||||||
|
|
||||||
Example usage::
|
Example usage::
|
||||||
|
|
||||||
@@ -160,6 +172,14 @@ async def parse_telephony_websocket(websocket: WebSocket):
|
|||||||
"call_id": start_data.get("callId"),
|
"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:
|
else:
|
||||||
call_data = {}
|
call_data = {}
|
||||||
|
|
||||||
@@ -379,10 +399,17 @@ async def _create_telephony_transport(
|
|||||||
auth_id=os.getenv("PLIVO_AUTH_ID", ""),
|
auth_id=os.getenv("PLIVO_AUTH_ID", ""),
|
||||||
auth_token=os.getenv("PLIVO_AUTH_TOKEN", ""),
|
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:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported telephony provider: {transport_type}. "
|
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)
|
return FastAPIWebsocketTransport(websocket=websocket, params=params)
|
||||||
@@ -399,7 +426,7 @@ async def create_transport(
|
|||||||
Args:
|
Args:
|
||||||
runner_args: Arguments from the runner.
|
runner_args: Arguments from the runner.
|
||||||
transport_params: Dict mapping transport names to parameter factory functions.
|
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.
|
Values should be functions that return transport parameters when called.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -440,6 +467,12 @@ async def create_transport(
|
|||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
# add_wav_header and serializer will be set automatically
|
# 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)
|
transport = await create_transport(runner_args, transport_params)
|
||||||
|
|||||||
Reference in New Issue
Block a user