dograh/api/services/telephony/providers/telnyx/transport.py
Sabiha Khan 4a6752e62b
feat(telephony/telnyx): add call transfer via conference bridge (#274)
Conference-based transfer for Telnyx with a two-step flow:
- transfer_call dials the destination with a per-transfer webhook URL.
- On call.answered, the webhook seeds a conference with the destination's
  live call_control_id and publishes DESTINATION_ANSWERED.
- TelnyxConferenceStrategy joins the caller into the conference on
  pipeline teardown (EndTaskReason.TRANSFER_CALL).
- On post-answer destination hangup, the webhook hangs up the caller —
  Telnyx's create_conference doesn't accept end_conference_on_exit on
  the seed leg, so we tear down the bridge ourselves.

TransferContext gains optional workflow_run_id (for webhook→provider
resolution in multi-config orgs) and conference_id (set on answer,
rd by the strategy).

Also fixes the transfer tool's provider lookup to go through
get_telephony_provider_for_run instead of the deprecated org-default
shim, which was returning the wrong provider in multi-config orgs.
2026-05-12 13:44:39 +05:30

68 lines
2.3 KiB
Python

"""Telnyx transport factory."""
from fastapi import WebSocket
from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.audio_mixer import build_audio_out_mixer
from api.services.telephony.factory import load_credentials_for_transport
from .serializers import TelnyxFrameSerializer
from .strategies import TelnyxConferenceStrategy, TelnyxHangupStrategy
async def create_transport(
websocket: WebSocket,
workflow_run_id: int,
audio_config: AudioConfig,
organization_id: int,
*,
ambient_noise_config: dict | None = None,
telephony_configuration_id: int | None = None,
stream_id: str,
call_control_id: str,
encoding: str = "PCMU",
):
"""Create a transport for Telnyx connections."""
config = await load_credentials_for_transport(
organization_id, telephony_configuration_id, expected_provider="telnyx"
)
api_key = config.get("api_key")
if not api_key:
raise ValueError(
f"Incomplete Telnyx configuration for organization {organization_id}"
)
# Pipecat's TelnyxFrameSerializer names its params from the call's POV,
# not Dograh's: ``inbound_encoding`` is what we *send into the call*
# (Dograh → Telnyx), and ``outbound_encoding`` is what we *receive out of
# the call* (Telnyx → Dograh).
serializer = TelnyxFrameSerializer(
stream_id=stream_id,
call_control_id=call_control_id,
api_key=api_key,
inbound_encoding="PCMU", # Dograh → Telnyx; matches stream_bidirectional_codec
outbound_encoding=encoding, # Telnyx → Dograh; from media_format.encoding
transfer_strategy=TelnyxConferenceStrategy(),
hangup_strategy=TelnyxHangupStrategy(),
)
mixer = await build_audio_out_mixer(
audio_config.transport_out_sample_rate, ambient_noise_config
)
return FastAPIWebsocketTransport(
websocket=websocket,
params=FastAPIWebsocketParams(
audio_in_enabled=True,
audio_out_enabled=True,
audio_in_sample_rate=audio_config.transport_in_sample_rate,
audio_out_sample_rate=audio_config.transport_out_sample_rate,
audio_out_mixer=mixer,
serializer=serializer,
),
)