dograh/api/services/telephony
Sabiha Khan ebeffdbc40
fix(ari): pre-register ext channel id and defer bridge to its StasisS… (#284)
* fix(ari): pre-register ext channel id and defer bridge to its StasisStart

Two race conditions in the inbound ARI flow could leave a call silent:

1. Bridging both channels immediately after creating the ext media leg
   raced against the ext channel entering the Stasis application; slow
   chan_websocket handshakes produced "Channel not in Stasis application"
   422 errors on addChannel.

2. Asterisk could fire StasisStart for the ext channel before the
   externalMedia POST response returned, so _is_ext_channel returned
   False and the event was dropped as an unknown outbound call.

Fixes:
- Generate the ext channel id as dograh-ext-<uuid> client-side and pass
  it to Asterisk via the channelId query param. Mark the ext channel,
  set its channel->run mapping, register the pending bridge entry, and
  persist gathered_context.ext_channel_id all before the POST.
- Defer the bridge to a new _complete_bridge_after_ext_ready handler
  triggered by the ext channel's own StasisStart. Both channels are
  guaranteed in Stasis by then, so addChannel cannot 422.
- On POST failure or channelId mismatch, roll back the pending entry
  and ERROR loudly.

* fix: replace in-memory dict with redis storage
2026-05-13 18:33:34 +05:30
..
providers feat: verify telnyx webhook signature optionally (#279) 2026-05-12 19:47:28 +05:30
__init__.py feat: refactor telephony to support multiple telephony configurations (#251) 2026-04-29 11:39:57 +05:30
ari_manager.py fix(ari): pre-register ext channel id and defer bridge to its StasisS… (#284) 2026-05-13 18:33:34 +05:30
base.py feat: agent stream for cloudonix OPBX (#261) 2026-05-02 15:53:58 +05:30
call_transfer_manager.py feat: tansfer calls with aasterisk (#171) 2026-03-05 09:28:05 +05:30
factory.py feat: agent stream for cloudonix OPBX (#261) 2026-05-02 15:53:58 +05:30
README.md feat: add vonage telephony (#35) 2025-10-27 15:58:20 +05:30
registry.py feat: agent stream for cloudonix OPBX (#261) 2026-05-02 15:53:58 +05:30
status_processor.py feat: add logs in campaigns for failure or pausing (#265) 2026-05-05 19:23:50 +05:30
transfer_event_protocol.py feat(telephony/telnyx): add call transfer via conference bridge (#274) 2026-05-12 13:44:39 +05:30

Telephony Provider Implementation

This module implements the telephony provider abstraction for Dograh AI. For user-facing documentation, see the Mintlify docs.

Architecture

Business Logic → TelephonyProvider (Interface) → Concrete Provider (Twilio, Vonage, etc.)

Developer Quick Reference

Using the Provider in Code

from api.services.telephony.factory import get_telephony_provider

# Get provider based on organization config
provider = await get_telephony_provider(organization_id)

# Initiate a call
result = await provider.initiate_call(
    to_number="+1987654321",
    webhook_url="https://your-app.com/webhook",
    workflow_run_id=123
)

File Structure

telephony/
├── __init__.py
├── base.py              # Abstract TelephonyProvider interface
├── factory.py           # Provider creation and config loading
├── providers/
│   ├── __init__.py
│   ├── twilio_provider.py  # Twilio implementation
│   └── vonage_provider.py  # Vonage implementation
├── twilio.py           # Legacy (removed, use factory instead)
└── README.md           # This file

Implementing a New Provider

See the Custom Provider Guide in the documentation for detailed implementation instructions.

Quick checklist:

  1. Create providers/your_provider.py implementing TelephonyProvider
  2. Update factory.py to include your provider
  3. Write unit tests
  4. Update documentation

Key Interfaces

class TelephonyProvider(ABC):
    @abstractmethod
    async def initiate_call(self, to_number: str, webhook_url: str, workflow_run_id: Optional[int] = None, **kwargs: Any) -> Dict[str, Any]
    
    @abstractmethod
    async def get_call_status(self, call_id: str) -> Dict[str, Any]
    
    @abstractmethod
    async def get_available_phone_numbers(self) -> List[str]
    
    @abstractmethod
    def validate_config(self) -> bool
    
    @abstractmethod
    async def verify_webhook_signature(self, url: str, params: Dict[str, Any], signature: str) -> bool
    
    @abstractmethod
    async def get_webhook_response(self, workflow_id: int, user_id: int, workflow_run_id: int) -> str

Configuration Loading

The factory.py loads configuration from the database:

Both Saas and OSS Modes: Database configuration via UI

# Loaded from organization_configuration table
key: "TELEPHONY_CONFIGURATION"
value: {
    "provider": "twilio",  # or "vonage"
    "account_sid": "xxx",  # for Twilio
    "auth_token": "xxx",   # for Twilio
    "application_id": "xxx",  # for Vonage
    "private_key": "xxx",     # for Vonage
    "from_numbers": [...]
}

Testing

Unit Testing with Mock Provider

class MockProvider(TelephonyProvider):
    async def initiate_call(self, to_number, webhook_url, **kwargs):
        return {"call_id": "mock_123", "status": "initiated"}
    
    async def get_call_status(self, call_id):
        return {"call_id": call_id, "status": "completed"}
    
    # Implement other required methods...

# In tests
@patch('api.services.telephony.factory.get_telephony_provider')
async def test_call_initiation(mock_get_provider):
    mock_get_provider.return_value = MockProvider()
    # Test your business logic

Integration Testing

Run against actual providers in development:

  1. Configure your provider through the UI:

    • Navigate to Settings → Integrations → Telephony
    • Select your provider (Twilio or Vonage)
    • Enter test credentials
    • Save configuration
  2. Run integration tests:

pytest tests/integration/test_telephony.py

Migration Notes

From Direct TwilioService Usage

Old code:

from api.services.telephony.twilio import TwilioService
service = TwilioService(org_id)
await service.initiate_call(...)

New code:

from api.services.telephony.factory import get_telephony_provider
provider = await get_telephony_provider(org_id)
await provider.initiate_call(...)

Backward Compatibility

  • Old /api/v1/twilio/* endpoints still work (redirect to /api/v1/telephony/*)
  • TwilioService class remains for legacy code
  • Database configuration key TWILIO_CONFIGURATION unchanged

Common Issues

  1. Import Error: Always import from factory, not directly from providers
  2. Config Not Found: Check database configuration via UI
  3. Signature Verification: Ensure auth tokens match between provider and config
  4. WebSocket Issues: Verify audio format compatibility (MULAW for Twilio)