Add Vonage support to telephony runner utilities

This commit is contained in:
Mark Backman
2025-12-18 22:33:08 -05:00
parent 6c75d6f24a
commit d3780f19f7
2 changed files with 69 additions and 12 deletions

View File

@@ -0,0 +1 @@
- Updated the development runner utility to support parsing `VonageFrameSerializer` websocket messages and handling/passing the to and from numbers via the `RunnerArguments`.

View File

@@ -88,6 +88,11 @@ def _detect_transport_type_from_message(message_data: dict) -> str:
logger.trace("Auto-detected: EXOTEL") logger.trace("Auto-detected: EXOTEL")
return "exotel" return "exotel"
# Vonage detection
if message_data.get("event") == "websocket:connected" and "content-type" in message_data:
logger.trace("Auto-detected: VONAGE")
return "vonage"
logger.trace("Auto-detection failed - unknown format") logger.trace("Auto-detection failed - unknown format")
return "unknown" return "unknown"
@@ -135,6 +140,16 @@ async def parse_telephony_websocket(websocket: WebSocket):
"to": str, "to": str,
} }
- Vonage::
{
"content_type": str, # e.g., "audio/l16;rate=16000"
"from": str,
"to": str,
"call_uuid": str,
"conversation_uuid": str,
}
Example usage:: Example usage::
transport_type, call_data = await parse_telephony_websocket(websocket) transport_type, call_data = await parse_telephony_websocket(websocket)
@@ -153,17 +168,23 @@ async def parse_telephony_websocket(websocket: WebSocket):
except json.JSONDecodeError: except json.JSONDecodeError:
first_message = {} first_message = {}
# Second message # Try auto-detection on first message
second_message_raw = await start_data.__anext__()
logger.trace(f"Second message: {second_message_raw}")
try:
second_message = json.loads(second_message_raw)
except json.JSONDecodeError:
second_message = {}
# Try auto-detection on both messages
detected_type_first = _detect_transport_type_from_message(first_message) detected_type_first = _detect_transport_type_from_message(first_message)
detected_type_second = _detect_transport_type_from_message(second_message)
# Vonage only sends one text message at start, then binary audio
# For other providers, read second message
if detected_type_first != "vonage":
second_message_raw = await start_data.__anext__()
logger.debug(f"Second message: {second_message_raw}")
try:
second_message = json.loads(second_message_raw)
except json.JSONDecodeError:
second_message = {}
detected_type_second = _detect_transport_type_from_message(second_message)
else:
second_message = {}
detected_type_second = "unknown"
# Use the successful detection # Use the successful detection
if detected_type_first != "unknown": if detected_type_first != "unknown":
@@ -219,6 +240,21 @@ async def parse_telephony_websocket(websocket: WebSocket):
"custom_parameters": start_data.get("custom_parameters", ""), "custom_parameters": start_data.get("custom_parameters", ""),
} }
elif transport_type == "vonage":
# Vonage sends websocket:connected event with content-type
content_type = call_data_raw.get("content-type", "audio/l16;rate=16000")
call_data = {
"content_type": content_type,
}
# Extract phone numbers and call info from websocket state (set by server.py)
if hasattr(websocket, "state") and hasattr(websocket.state, "vonage_call_data"):
vonage_data = websocket.state.vonage_call_data
call_data["from"] = vonage_data.get("from", "")
call_data["to"] = vonage_data.get("to", "")
call_data["call_uuid"] = vonage_data.get("call_uuid", "")
call_data["conversation_uuid"] = vonage_data.get("conversation_uuid", "")
else: else:
call_data = {} call_data = {}
@@ -465,10 +501,24 @@ async def _create_telephony_transport(
stream_sid=call_data["stream_id"], stream_sid=call_data["stream_id"],
call_sid=call_data["call_id"], call_sid=call_data["call_id"],
) )
elif transport_type == "vonage":
from pipecat.serializers.vonage import VonageFrameSerializer
content_type = call_data.get("content_type", "audio/l16;rate=16000")
sample_rate = 16000 # Default
if "rate=" in content_type:
try:
sample_rate = int(content_type.split("rate=")[1].split(";")[0])
except (ValueError, IndexError):
logger.warning(f"Could not parse sample rate from {content_type}, using 16000")
params.serializer = VonageFrameSerializer(
params=VonageFrameSerializer.InputParams(vonage_sample_rate=sample_rate)
)
else: else:
raise ValueError( raise ValueError(
f"Unsupported telephony provider: {transport_type}. " f"Unsupported telephony provider: {transport_type}. "
f"Supported providers: twilio, telnyx, plivo, exotel" f"Supported providers: twilio, telnyx, plivo, exotel, vonage"
) )
return FastAPIWebsocketTransport(websocket=websocket, params=params) return FastAPIWebsocketTransport(websocket=websocket, params=params)
@@ -485,7 +535,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", "exotel" Keys should be: "daily", "webrtc", "twilio", "telnyx", "plivo", "exotel", "vonage"
Values should be functions that return transport parameters when called. Values should be functions that return transport parameters when called.
Returns: Returns:
@@ -532,6 +582,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
), ),
"vonage": 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)