refactor: telephony integration

This commit is contained in:
Sabiha Khan 2025-10-13 17:55:10 +05:30
parent b9d1720d94
commit a01f2df7ea
26 changed files with 1583 additions and 28 deletions

View file

@ -33,8 +33,12 @@ STACK_AUTH_PROJECT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
STACK_SECRET_SERVER_KEY="ssk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
STACK_PUBLISHABLE_CLIENT_KEY="pck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Twilio Configuration
TWILIO_ACCOUNT_SID="SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Telephony Configuration
# Provider selection (default: twilio, future options: vonage, plivo, etc.)
TELEPHONY_PROVIDER=twilio
# Twilio Configuration (when TELEPHONY_PROVIDER=twilio)
TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_FROM_NUMBER="+1234567890"

View file

@ -62,7 +62,7 @@ class OrganizationConfigurationKey(Enum):
DISPOSITION_CODE_MAPPING = "DISPOSITION_CODE_MAPPING"
DISPOSITION_MESSAGE_TEMPLATE = "DISPOSITION_MESSAGE_TEMPLATE"
CONCURRENT_CALL_LIMIT = "CONCURRENT_CALL_LIMIT"
TWILIO_CONFIGURATION = "TWILIO_CONFIGURATION"
TWILIO_CONFIGURATION = "TWILIO_CONFIGURATION" # TODO: Rename to TELEPHONY_CONFIGURATION
class WorkflowStatus(Enum):

View file

@ -11,7 +11,8 @@ from api.routes.rtc_offer import router as rtc_offer_router
from api.routes.s3_signed_url import router as s3_router
from api.routes.service_keys import router as service_keys_router
from api.routes.superuser import router as superuser_router
from api.routes.twilio import router as twilio_router
from api.routes.telephony import router as telephony_router
from api.routes.twilio import router as twilio_router # TODO: Remove after migrating workflow_run_cost.py
from api.routes.user import router as user_router
from api.routes.webrtc_signaling import router as webrtc_signaling_router
from api.routes.workflow import router as workflow_router
@ -21,7 +22,8 @@ router = APIRouter(
responses={404: {"description": "Not found"}},
)
router.include_router(twilio_router)
router.include_router(telephony_router) # New generic telephony routes
router.include_router(twilio_router) # TODO: Remove after migrating workflow_run_cost.py
router.include_router(rtc_offer_router)
router.include_router(superuser_router)
router.include_router(workflow_router)

View file

@ -14,6 +14,7 @@ from api.services.configuration.masking import is_mask_of, mask_key
router = APIRouter(prefix="/organizations", tags=["organizations"])
# TODO: Make endpoints provider-agnostic
@router.get("/telephony-config", response_model=TelephonyConfigurationResponse)
async def get_telephony_configuration(user: UserModel = Depends(get_user)):
"""Get telephony configuration for the user's organization with masked sensitive fields."""
@ -22,7 +23,7 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value, # TODO: Use TELEPHONY_CONFIGURATION
)
if not config or not config.value:
@ -53,7 +54,7 @@ async def save_telephony_configuration(
# Fetch existing configuration to handle masked values
existing_config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value, # TODO: Use TELEPHONY_CONFIGURATION
)
# Build new configuration

315
api/routes/telephony.py Normal file
View file

@ -0,0 +1,315 @@
"""
Generic telephony routes that work with any telephony provider.
"""
import json
import random
from datetime import UTC, datetime
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, WebSocket
from loguru import logger
from pydantic import BaseModel
from starlette.responses import HTMLResponse
from api.db import db_client
from api.db.models import UserModel
from api.enums import WorkflowRunMode
from api.services.auth.depends import get_user
from api.services.campaign.call_dispatcher import campaign_call_dispatcher
from api.services.campaign.campaign_event_publisher import get_campaign_event_publisher
from api.services.pipecat.run_pipeline import run_pipeline_twilio
from api.services.telephony.factory import get_telephony_provider
from api.utils.tunnel import TunnelURLProvider
from pipecat.utils.context import set_current_run_id
router = APIRouter(prefix="/telephony")
class InitiateCallRequest(BaseModel):
workflow_id: int
workflow_run_id: int | None = None
class StatusCallbackRequest(BaseModel):
"""Generic status callback that can handle different providers"""
# Common fields
call_id: str
status: str
from_number: Optional[str] = None
to_number: Optional[str] = None
direction: Optional[str] = None
duration: Optional[str] = None
# Provider-specific fields stored as extra
extra: dict = {}
@classmethod
def from_twilio(cls, data: dict):
"""Convert Twilio callback to generic format"""
return cls(
call_id=data.get("CallSid", ""),
status=data.get("CallStatus", ""),
from_number=data.get("From"),
to_number=data.get("To"),
direction=data.get("Direction"),
duration=data.get("CallDuration") or data.get("Duration"),
extra=data
)
@router.post("/initiate-call")
async def initiate_call(
request: InitiateCallRequest, user: UserModel = Depends(get_user)
):
"""Initiate a call using the configured telephony provider."""
# Get the telephony provider for the organization
provider = await get_telephony_provider(user.selected_organization_id)
# Validate provider is configured
if not provider.validate_config():
raise HTTPException(
status_code=400,
detail="telephony_not_configured",
)
user_configuration = await db_client.get_user_configurations(user.id)
workflow_run_id = request.workflow_run_id
if not workflow_run_id:
workflow_run_name = f"WR-TEL-{random.randint(1000, 9999)}"
workflow_run = await db_client.create_workflow_run(
workflow_run_name,
request.workflow_id,
WorkflowRunMode.TWILIO.value, # TODO: Make this provider-agnostic
initial_context={
"phone_number": user_configuration.test_phone_number,
},
user_id=user.id,
)
workflow_run_id = workflow_run.id
else:
workflow_run = await db_client.get_workflow_run(workflow_run_id, user.id)
if not workflow_run:
raise HTTPException(status_code=400, detail="Workflow run not found")
workflow_run_name = workflow_run.name
if not user_configuration.test_phone_number:
raise HTTPException(status_code=400, detail="Test phone number not set")
# Construct webhook URL
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
webhook_url = (
f"https://{backend_endpoint}/api/v1/telephony/twiml"
f"?workflow_id={request.workflow_id}"
f"&user_id={user.id}"
f"&workflow_run_id={workflow_run_id}"
f"&organization_id={user.selected_organization_id}"
)
# Initiate call via provider
await provider.initiate_call(
to_number=user_configuration.test_phone_number,
webhook_url=webhook_url,
workflow_run_id=workflow_run_id,
)
return {
"message": f"Call initiated successfully with run name {workflow_run_name}"
}
@router.post("/twiml", include_in_schema=False)
async def handle_twiml_webhook(
workflow_id: int,
user_id: int,
workflow_run_id: int,
organization_id: int
):
"""
Handle initial webhook from telephony provider.
Returns provider-specific response (e.g., TwiML for Twilio).
"""
# Get provider for organization - exactly like original gets TwilioService
provider = await get_telephony_provider(organization_id)
# Generate provider-specific response (TwiML for Twilio)
response_content = await provider.get_webhook_response(
workflow_id, user_id, workflow_run_id
)
# Return exactly like original - HTMLResponse with application/xml
return HTMLResponse(content=response_content, media_type="application/xml")
@router.websocket("/ws/{workflow_id}/{user_id}/{workflow_run_id}")
async def websocket_endpoint(
websocket: WebSocket, workflow_id: int, user_id: int, workflow_run_id: int
):
"""WebSocket endpoint for real-time call handling - matches original Twilio implementation."""
await websocket.accept()
try:
# "connected" (ignore)
msg = json.loads(await websocket.receive_text())
if msg.get("event") != "connected":
raise RuntimeError("Expected connected message first")
# "start" this has everything we need
start_msg = await websocket.receive_text()
# set the run context
set_current_run_id(workflow_run_id)
logger.debug(f"Received start message: {start_msg}")
start_msg = json.loads(start_msg)
if start_msg.get("event") != "start":
raise RuntimeError("Expected start message second")
try:
stream_sid = start_msg["start"]["streamSid"]
call_sid = start_msg["start"]["callSid"]
except KeyError:
logger.error(
"Missing callSID and streamSID in start message. Closing connection."
)
await websocket.close(code=4400, reason="Missing or bad start message")
return
# Run your Pipecat bot
await run_pipeline_twilio(
websocket, stream_sid, call_sid, workflow_id, workflow_run_id, user_id
)
except Exception as e:
logger.error(f"Error in Twilio WebSocket connection: {e}")
await websocket.close(1011, "Internal server error")
@router.post("/status-callback/{workflow_run_id}")
async def handle_status_callback(
workflow_run_id: int,
request: Request,
x_twilio_signature: Optional[str] = Header(None),
):
"""Handle status callbacks from telephony providers."""
# Parse form data
form_data = await request.form()
callback_data = dict(form_data)
logger.info(
f"[run {workflow_run_id}] Received status callback: {json.dumps(callback_data)}"
)
# Get workflow run to find organization
workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id)
if not workflow_run:
logger.warning(f"Workflow run {workflow_run_id} not found for status callback")
return {"status": "ignored", "reason": "workflow_run_not_found"}
# Get provider for verification (if signature provided)
if x_twilio_signature:
# Get organization from workflow run
workflow = await db_client.get_workflow_by_id(workflow_run.workflow_id)
if workflow:
provider = await get_telephony_provider(workflow.organization_id)
# Verify signature
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
full_url = f"https://{backend_endpoint}/api/v1/telephony/status-callback/{workflow_run_id}"
is_valid = await provider.verify_webhook_signature(
full_url, callback_data, x_twilio_signature
)
if not is_valid:
logger.warning(f"Invalid status callback signature for run {workflow_run_id}")
return {"status": "error", "reason": "invalid_signature"}
# Convert provider-specific callback to generic format
# (Currently assumes Twilio format, will be extended for other providers)
status_update = StatusCallbackRequest.from_twilio(callback_data)
# Process the status update
await _process_status_update(workflow_run_id, status_update, workflow_run)
return {"status": "success"}
async def _process_status_update(
workflow_run_id: int,
status: StatusCallbackRequest,
workflow_run: any
):
"""Process status updates from telephony providers."""
# Log the status callback
twilio_callback_logs = workflow_run.logs.get("twilio_status_callbacks", [])
twilio_callback_log = {
"status": status.status,
"timestamp": datetime.now(UTC).isoformat(),
"call_id": status.call_id,
"duration": status.duration,
**status.extra # Include provider-specific data
}
twilio_callback_logs.append(twilio_callback_log)
# Update workflow run logs
await db_client.update_workflow_run(
run_id=workflow_run_id,
logs={"twilio_status_callbacks": twilio_callback_logs},
)
# Handle call completion
if status.status == "completed":
logger.info(
f"[run {workflow_run_id}] Call completed with duration: {status.duration}s"
)
# Release concurrent slot if this was a campaign call
if workflow_run.campaign_id:
await campaign_call_dispatcher.release_call_slot(workflow_run_id)
# Mark workflow run as completed
await db_client.update_workflow_run(
run_id=workflow_run_id, is_completed=True
)
# Publish campaign event if applicable
if workflow_run.campaign_id:
publisher = await get_campaign_event_publisher()
await publisher.publish_call_completed(
campaign_id=workflow_run.campaign_id,
workflow_run_id=workflow_run_id,
queued_run_id=workflow_run.queued_run_id,
call_duration=int(status.duration) if status.duration else 0,
)
elif status.status in ["failed", "busy", "no-answer", "canceled"]:
logger.warning(f"[run {workflow_run_id}] Call failed with status: {status.status}")
# Release concurrent slot for terminal statuses if this was a campaign call
if workflow_run.campaign_id:
await campaign_call_dispatcher.release_call_slot(workflow_run_id)
# Check if retry is needed for campaign calls (busy/no-answer)
if status.status in ["busy", "no-answer"] and workflow_run.campaign_id:
publisher = await get_campaign_event_publisher()
await publisher.publish_retry_needed(
workflow_run_id=workflow_run_id,
reason=status.status.replace("-", "_"), # Convert no-answer to no_answer
campaign_id=workflow_run.campaign_id,
queued_run_id=workflow_run.queued_run_id,
)
# Mark workflow run as completed with failure tags
call_tags = workflow_run.gathered_context.get("call_tags", []) if workflow_run.gathered_context else []
call_tags.extend(["not_connected", f"telephony_{status.status.lower()}"])
await db_client.update_workflow_run(
run_id=workflow_run_id,
is_completed=True,
gathered_context={"call_tags": call_tags}
)

View file

@ -1,3 +1,6 @@
# TODO: Remove this entire file after migrating workflow_run_cost.py to use telephony abstraction
# All endpoints here are deprecated - use /api/v1/telephony/* instead
import json
import random
from datetime import UTC, datetime

View file

@ -2,6 +2,7 @@ from typing import List
from pydantic import BaseModel, Field
# TODO: Make schemas provider-agnostic
class TwilioConfigurationRequest(BaseModel):
"""Request schema for Twilio configuration."""

View file

@ -9,7 +9,9 @@ from api.db import db_client
from api.db.models import QueuedRunModel, WorkflowRunModel
from api.enums import OrganizationConfigurationKey, WorkflowRunMode
from api.services.campaign.rate_limiter import rate_limiter
from api.services.telephony.twilio import TwilioService
from api.services.telephony.factory import get_telephony_provider
from api.services.telephony.base import TelephonyProvider
from api.utils.tunnel import TunnelURLProvider
class CampaignCallDispatcher:
@ -18,9 +20,9 @@ class CampaignCallDispatcher:
def __init__(self):
self.default_concurrent_limit = 20
def get_twilio_service(self, organization_id: int) -> TwilioService:
"""Get TwilioService instance for specific organization"""
return TwilioService(organization_id)
async def get_telephony_provider(self, organization_id: int) -> TelephonyProvider:
"""Get telephony provider instance for specific organization"""
return await get_telephony_provider(organization_id)
async def get_org_concurrent_limit(self, organization_id: int) -> int:
"""Get the concurrent call limit for an organization."""
@ -219,19 +221,25 @@ class CampaignCallDispatcher:
},
)
# Initiate call via Twilio
# Initiate call via telephony provider
try:
twilio_service = self.get_twilio_service(campaign.organization_id)
call_result = await twilio_service.initiate_call(
provider = await self.get_telephony_provider(campaign.organization_id)
# Construct webhook URL with parameters
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
webhook_url = (
f"https://{backend_endpoint}/api/v1/telephony/twiml"
f"?workflow_id={campaign.workflow_id}"
f"&user_id={campaign.created_by}"
f"&workflow_run_id={workflow_run.id}"
f"&campaign_id={campaign.id}"
f"&organization_id={campaign.organization_id}"
)
call_result = await provider.initiate_call(
to_number=phone_number,
webhook_url=webhook_url,
workflow_run_id=workflow_run.id,
url_args={
"workflow_id": campaign.workflow_id,
"user_id": campaign.created_by,
"workflow_run_id": workflow_run.id,
"campaign_id": campaign.id,
"organization_id": campaign.organization_id,
},
)
logger.info(

View file

@ -0,0 +1,167 @@
# Telephony Provider Implementation
This module implements the telephony provider abstraction for Dograh AI. For user-facing documentation, see the [Mintlify docs](https://docs.dograh.com/integrations/telephony/overview).
## Architecture
```
Business Logic → TelephonyProvider (Interface) → Concrete Provider (Twilio, Vonage, etc.)
```
## Developer Quick Reference
### Using the Provider in Code
```python
from api.services.telephony.factory import get_telephony_provider
# Get provider based on environment/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
├── twilio.py # Legacy TwilioService (backward compat)
└── README.md # This file
```
## Implementing a New Provider
See the [Custom Provider Guide](https://docs.dograh.com/integrations/telephony/custom) 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. Add environment variable support in `factory.py`
4. Write unit tests
5. Update documentation
## Key Interfaces
```python
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` handles configuration from two sources:
1. **OSS Mode** (default): Environment variables
```python
TELEPHONY_PROVIDER=twilio
TWILIO_ACCOUNT_SID=xxx
TWILIO_AUTH_TOKEN=xxx
TWILIO_FROM_NUMBER=+1234567890
```
2. **SaaS Mode**: Database configuration per organization
```python
# Loaded from organization_configuration table
key: "TWILIO_CONFIGURATION"
value: {"account_sid": "xxx", "auth_token": "xxx", "from_numbers": [...]}
```
## Testing
### Unit Testing with Mock Provider
```python
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:
```bash
# Set test credentials
export TELEPHONY_PROVIDER=twilio
export TWILIO_ACCOUNT_SID=test_sid
export TWILIO_AUTH_TOKEN=test_token
export TWILIO_FROM_NUMBER=+15005550006 # Twilio test number
# Run integration tests
pytest tests/integration/test_telephony.py
```
## Migration Notes
### From Direct TwilioService Usage
Old code:
```python
from api.services.telephony.twilio import TwilioService
service = TwilioService(org_id)
await service.initiate_call(...)
```
New code:
```python
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 environment variables or database configuration
3. **Signature Verification**: Ensure auth tokens match between provider and config
4. **WebSocket Issues**: Verify audio format compatibility (MULAW for Twilio)
## Related Documentation
- [User Documentation](https://docs.dograh.com/integrations/telephony/overview)
- [Twilio Integration](https://docs.dograh.com/integrations/telephony/twilio)
- [Custom Providers](https://docs.dograh.com/integrations/telephony/custom)
- [Webhooks Guide](https://docs.dograh.com/integrations/telephony/webhooks)

View file

@ -0,0 +1,103 @@
"""
Base telephony provider interface for abstracting telephony services.
This allows easy switching between different providers (Twilio, Vonage, etc.)
while keeping business logic decoupled from specific implementations.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
class TelephonyProvider(ABC):
"""
Abstract base class for telephony providers.
All telephony providers must implement these core methods.
"""
@abstractmethod
async def initiate_call(
self,
to_number: str,
webhook_url: str,
workflow_run_id: Optional[int] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""
Initiate an outbound call.
Args:
to_number: The destination phone number
webhook_url: The URL to receive call events
workflow_run_id: Optional workflow run ID for tracking
**kwargs: Provider-specific additional parameters
Returns:
Dict containing call details (provider-specific format)
"""
pass
@abstractmethod
async def get_call_status(self, call_id: str) -> Dict[str, Any]:
"""
Get the current status of a call.
Args:
call_id: The provider-specific call identifier
Returns:
Dict containing call status information
"""
pass
@abstractmethod
async def get_available_phone_numbers(self) -> List[str]:
"""
Get list of available phone numbers for this provider.
Returns:
List of phone numbers that can be used for outbound calls
"""
pass
@abstractmethod
def validate_config(self) -> bool:
"""
Validate that the provider is properly configured.
Returns:
True if configuration is valid, False otherwise
"""
pass
@abstractmethod
async def verify_webhook_signature(
self, url: str, params: Dict[str, Any], signature: str
) -> bool:
"""
Verify webhook signature for security.
Args:
url: The webhook URL
params: The webhook parameters
signature: The signature to verify
Returns:
True if signature is valid, False otherwise
"""
pass
@abstractmethod
async def get_webhook_response(
self, workflow_id: int, user_id: int, workflow_run_id: int
) -> str:
"""
Generate the initial webhook response for starting a call session.
Args:
workflow_id: The workflow ID
user_id: The user ID
workflow_run_id: The workflow run ID
Returns:
Provider-specific response (e.g., TwiML for Twilio)
"""
pass

View file

@ -0,0 +1,120 @@
"""
Factory for creating telephony providers.
Handles configuration loading from environment (OSS) or database (SaaS).
The providers themselves don't know or care where config comes from.
"""
import os
from typing import Any, Dict, Optional
from loguru import logger
from api.db import db_client
from api.enums import OrganizationConfigurationKey
from api.services.telephony.base import TelephonyProvider
from api.services.telephony.providers.twilio_provider import TwilioProvider
async def load_telephony_config(organization_id: Optional[int] = None) -> Dict[str, Any]:
"""
Load telephony configuration from appropriate source.
Args:
organization_id: Organization ID for database config (SaaS mode)
None for environment config (OSS mode)
Returns:
Configuration dictionary with provider type and credentials
"""
if organization_id:
# SaaS mode: Load from database
logger.debug(f"Loading telephony config from database for org {organization_id}")
# TODO: Use TELEPHONY_CONFIGURATION
twilio_config = await db_client.get_configuration(
organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
if twilio_config and twilio_config.value:
# TODO: Get provider from config
return {
"provider": "twilio",
"account_sid": twilio_config.value.get("account_sid"),
"auth_token": twilio_config.value.get("auth_token"),
"from_numbers": twilio_config.value.get("from_numbers", [])
}
raise ValueError(f"No telephony configuration found for organization {organization_id}")
else:
# OSS mode: Load from environment variables
logger.debug("Loading telephony config from environment variables")
provider = os.getenv("TELEPHONY_PROVIDER", "twilio").lower()
if provider == "twilio":
# Load Twilio config from env
account_sid = os.getenv("TWILIO_ACCOUNT_SID")
auth_token = os.getenv("TWILIO_AUTH_TOKEN")
from_number = os.getenv("TWILIO_FROM_NUMBER")
if not all([account_sid, auth_token, from_number]):
raise ValueError(
"Missing Twilio configuration. Please set TWILIO_ACCOUNT_SID, "
"TWILIO_AUTH_TOKEN, and TWILIO_FROM_NUMBER environment variables."
)
return {
"provider": "twilio",
"account_sid": account_sid,
"auth_token": auth_token,
"from_numbers": [from_number] if isinstance(from_number, str) else from_number
}
# Future providers can be added here
# elif provider == "vonage":
# return {
# "provider": "vonage",
# "api_key": os.getenv("VONAGE_API_KEY"),
# "api_secret": os.getenv("VONAGE_API_SECRET"),
# "from_numbers": [os.getenv("VONAGE_FROM_NUMBER")]
# }
else:
raise ValueError(f"Unknown telephony provider: {provider}")
async def get_telephony_provider(
organization_id: Optional[int] = None
) -> TelephonyProvider:
"""
Factory function to create telephony providers.
Args:
organization_id: Organization ID for SaaS mode (optional)
Returns:
Configured telephony provider instance
Raises:
ValueError: If provider type is unknown or configuration is invalid
"""
# Load configuration from appropriate source
config = await load_telephony_config(organization_id)
provider_type = config.get("provider", "twilio")
logger.info(f"Creating {provider_type} telephony provider")
# Create provider instance with configuration
# Provider doesn't know or care if config came from env or database
if provider_type == "twilio":
return TwilioProvider(config)
# Future providers can be added here
# elif provider_type == "vonage":
# return VonageProvider(config)
# elif provider_type == "plivo":
# return PlivoProvider(config)
else:
raise ValueError(f"Unknown telephony provider: {provider_type}")

View file

@ -0,0 +1 @@
# Telephony provider implementations

View file

@ -0,0 +1,151 @@
"""
Twilio implementation of the TelephonyProvider interface.
"""
import random
from typing import Any, Dict, List, Optional
import aiohttp
from loguru import logger
from twilio.request_validator import RequestValidator
from api.services.telephony.base import TelephonyProvider
from api.utils.tunnel import TunnelURLProvider
class TwilioProvider(TelephonyProvider):
"""
Twilio implementation of TelephonyProvider.
Accepts configuration and works the same regardless of OSS/SaaS mode.
"""
def __init__(self, config: Dict[str, Any]):
"""
Initialize TwilioProvider with configuration.
Args:
config: Dictionary containing:
- account_sid: Twilio Account SID
- auth_token: Twilio Auth Token
- from_numbers: List of phone numbers to use
"""
self.account_sid = config.get("account_sid")
self.auth_token = config.get("auth_token")
self.from_numbers = config.get("from_numbers", [])
# Handle both single number (string) and multiple numbers (list)
if isinstance(self.from_numbers, str):
self.from_numbers = [self.from_numbers]
self.base_url = f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}"
async def initiate_call(
self,
to_number: str,
webhook_url: str,
workflow_run_id: Optional[int] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""
Initiate an outbound call via Twilio.
"""
if not self.validate_config():
raise ValueError("Twilio provider not properly configured")
endpoint = f"{self.base_url}/Calls.json"
# Select a random phone number
from_number = random.choice(self.from_numbers)
logger.info(f"Selected phone number {from_number} for outbound call")
# Prepare call data
data = {
"To": to_number,
"From": from_number,
"Url": webhook_url
}
# Add status callback if workflow_run_id provided
if workflow_run_id:
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
callback_url = f"https://{backend_endpoint}/api/v1/telephony/status-callback/{workflow_run_id}"
data.update({
"StatusCallback": callback_url,
"StatusCallbackEvent": ["initiated", "ringing", "answered", "completed"],
"StatusCallbackMethod": "POST"
})
# Add any additional kwargs
data.update(kwargs)
# Make the API request
async with aiohttp.ClientSession() as session:
auth = aiohttp.BasicAuth(self.account_sid, self.auth_token)
async with session.post(endpoint, data=data, auth=auth) as response:
if response.status != 201:
error_data = await response.json()
raise Exception(f"Failed to initiate call: {error_data}")
return await response.json()
async def get_call_status(self, call_id: str) -> Dict[str, Any]:
"""
Get the current status of a Twilio call.
"""
if not self.validate_config():
raise ValueError("Twilio provider not properly configured")
endpoint = f"{self.base_url}/Calls/{call_id}.json"
async with aiohttp.ClientSession() as session:
auth = aiohttp.BasicAuth(self.account_sid, self.auth_token)
async with session.get(endpoint, auth=auth) as response:
if response.status != 200:
error_data = await response.json()
raise Exception(f"Failed to get call status: {error_data}")
return await response.json()
async def get_available_phone_numbers(self) -> List[str]:
"""
Get list of available Twilio phone numbers.
"""
return self.from_numbers
def validate_config(self) -> bool:
"""
Validate Twilio configuration.
"""
return bool(
self.account_sid and
self.auth_token and
self.from_numbers
)
async def verify_webhook_signature(
self, url: str, params: Dict[str, Any], signature: str
) -> bool:
"""
Verify Twilio webhook signature for security.
"""
if not self.auth_token:
return False
validator = RequestValidator(self.auth_token)
return validator.validate(url, params, signature)
async def get_webhook_response(
self, workflow_id: int, user_id: int, workflow_run_id: int
) -> str:
"""
Generate TwiML response for starting a call session.
"""
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
twiml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{backend_endpoint}/api/v1/telephony/ws/{workflow_id}/{user_id}/{workflow_run_id}"></Stream>
</Connect>
<Pause length="40"/>
</Response>"""
return twiml_content

View file

@ -1,3 +1,6 @@
# TODO: Remove this file after migrating workflow_run_cost.py to use telephony abstraction
# Deprecated - use api/services/telephony/providers/twilio_provider.py instead
import random
from typing import Any, Dict, List, Optional
from urllib.parse import urlencode

View file

@ -40,6 +40,9 @@ async def calculate_workflow_run_cost(ctx, workflow_run_id: int):
logger.warning("Workflow not found for workflow run")
raise Exception("Workflow not found")
# TODO: Migrate to use telephony provider abstraction
# provider = await get_telephony_provider(workflow.organization_id)
# call_info = await provider.get_call_status(twilio_call_sid)
twilio_service = TwilioService(workflow.organization_id)
call_info = await twilio_service.get_call(twilio_call_sid)
# Twilio returns price as a string with negative value (e.g., "-0.0085")

View file

@ -38,11 +38,25 @@
"features/campaigns",
"features/looptalk"
]
}
]
},
{
"tab": "Integrations",
"groups": [
{
"group": "Overview",
"pages": [
"integrations/overview"
]
},
{
"group": "Telephony Integrations",
"group": "Telephony",
"pages": [
"telephony/twilio"
"integrations/telephony/overview",
"integrations/telephony/twilio",
"integrations/telephony/webhooks",
"integrations/telephony/custom"
]
}
]

View file

@ -0,0 +1,50 @@
---
title: "Integrations Overview"
description: "Connect Dograh AI with external services and platforms"
---
## Introduction
Dograh AI provides a flexible integration architecture that allows you to connect with various external services and platforms. Our integration system is designed with modularity and extensibility in mind, making it easy to add new providers while maintaining backward compatibility.
## Integration Categories
### Telephony Providers
Connect your voice agents with telephony services to make and receive calls.
<Card title="Telephony Providers" href="/integrations/telephony/overview">
Configure telephony providers like Twilio, Vonage, and Plivo for voice communication
</Card>
### Future Integration Categories
The integration architecture is designed to support additional categories in the future, such as storage services, analytics platforms, and CRM systems.
## Architecture Principles
Our integration system follows these core principles:
- **Provider Abstraction**: All integrations implement a common interface, making it easy to switch between providers
- **Configuration Flexibility**: Support for both environment-based (OSS) and database-based (SaaS) configuration
- **Backward Compatibility**: New integrations don't break existing implementations
- **Secure by Default**: All credentials are encrypted and never exposed in logs or UI
## Getting Started
1. Choose the integration category you need
2. Follow the provider-specific setup guide
3. Configure credentials through the UI or environment variables
4. Test your integration with the provided verification tools
## Best Practices
- Store credentials securely using environment variables (OSS) or database configuration (SaaS)
- Test integrations in a development environment before production deployment
- Use the provider abstraction to maintain clean separation between business logic and provider specifics
- Monitor integration health through application logs
## Need Help?
- Check provider-specific documentation for detailed setup instructions
- Visit our [GitHub Issues](https://github.com/dograh-hq/dograh/issues) for community support
- Join our [Slack community](https://join.slack.com/t/dograh-ai/shared_invite/zt-2u29h3bkm-RrkJ2f2B5lvTVZo0ZQ1MMA) for assistance

View file

@ -0,0 +1,184 @@
---
title: "Custom Telephony Provider"
description: "Build your own telephony provider integration for Dograh AI"
---
## Overview
Dograh AI's telephony abstraction layer allows you to integrate any telephony service by implementing the `TelephonyProvider` interface.
## Provider Interface
All telephony providers must implement this abstract base class:
```python
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
class TelephonyProvider(ABC):
"""Abstract base class for telephony providers."""
@abstractmethod
async def initiate_call(
self,
to_number: str,
webhook_url: str,
workflow_run_id: Optional[int] = None,
**kwargs: Any
) -> Dict[str, Any]:
"""Initiate an outbound call."""
pass
@abstractmethod
async def get_call_status(self, call_id: str) -> Dict[str, Any]:
"""Get current status of a call."""
pass
@abstractmethod
async def get_available_phone_numbers(self) -> List[str]:
"""Get list of available phone numbers."""
pass
@abstractmethod
def validate_config(self) -> bool:
"""Validate provider configuration."""
pass
@abstractmethod
async def verify_webhook_signature(
self, url: str, params: Dict[str, Any], signature: str
) -> bool:
"""Verify webhook signature for security."""
pass
@abstractmethod
async def get_webhook_response(
self, workflow_id: int, user_id: int, workflow_run_id: int
) -> str:
"""Generate initial webhook response."""
pass
```
## Implementation Guide
### 1. Create Your Provider
Create a new file in `api/services/telephony/providers/`:
```python
# api/services/telephony/providers/your_provider.py
from typing import Any, Dict, List, Optional
from api.services.telephony.base import TelephonyProvider
class YourProvider(TelephonyProvider):
"""Your custom telephony provider implementation."""
def __init__(self, config: Dict[str, Any]):
"""Initialize with configuration dictionary."""
# Extract your provider-specific configuration
self.api_key = config.get("api_key")
self.api_secret = config.get("api_secret")
self.from_number = config.get("from_numbers", [""])[0]
def validate_config(self) -> bool:
"""Check if all required configuration is present."""
return bool(self.api_key and self.api_secret and self.from_number)
async def initiate_call(
self,
to_number: str,
webhook_url: str,
workflow_run_id: Optional[int] = None,
**kwargs: Any
) -> Dict[str, Any]:
"""Start an outbound call using your provider's API."""
# Implement your provider's call initiation logic
pass
# Implement other required methods...
```
### 2. Register in Factory
Update `api/services/telephony/factory.py` to include your provider:
```python
from api.services.telephony.providers.your_provider import YourProvider
async def get_telephony_provider(
organization_id: Optional[int] = None
) -> TelephonyProvider:
"""Factory function to get appropriate telephony provider."""
config = await load_telephony_config(organization_id)
provider_type = config.get("provider", "twilio").lower()
if provider_type == "twilio":
return TwilioProvider(config)
elif provider_type == "your_provider":
return YourProvider(config)
else:
raise ValueError(f"Unknown telephony provider: {provider_type}")
```
### 3. Add Configuration Support
For OSS deployment (environment variables):
```bash
# .env
TELEPHONY_PROVIDER=your_provider
YOUR_PROVIDER_API_KEY=your_api_key
YOUR_PROVIDER_API_SECRET=your_api_secret
YOUR_PROVIDER_FROM_NUMBER=+1234567890
```
Update the configuration loader in `factory.py`:
```python
if provider == "your_provider":
return {
"provider": "your_provider",
"api_key": os.getenv("YOUR_PROVIDER_API_KEY"),
"api_secret": os.getenv("YOUR_PROVIDER_API_SECRET"),
"from_numbers": [os.getenv("YOUR_PROVIDER_FROM_NUMBER")]
}
```
## Audio Format Considerations
Different providers use different audio formats. Twilio uses MULAW at 8000 Hz encoded in Base64. Your provider may differ, so ensure proper audio format conversion in your WebSocket handler.
## Testing
Create unit tests for your provider:
```python
# tests/test_your_provider.py
import pytest
from api.services.telephony.providers.your_provider import YourProvider
@pytest.mark.asyncio
async def test_validate_config():
config = {
"api_key": "test_key",
"api_secret": "test_secret",
"from_numbers": ["+1234567890"]
}
provider = YourProvider(config)
assert provider.validate_config() is True
```
## Best Practices
1. **Error Handling**: Implement robust error handling with meaningful messages
2. **Logging**: Use `loguru.logger` for consistent logging
3. **Async Operations**: All I/O operations should be async
4. **Configuration Validation**: Validate config on initialization
5. **Security**: Always verify webhook signatures
## Reference Implementation
See the Twilio provider implementation at `api/services/telephony/providers/twilio_provider.py` for a complete example.

View file

@ -0,0 +1,115 @@
---
title: "Telephony Integration"
description: "Connect voice agents with telephony providers for inbound and outbound calls"
---
## Overview
Dograh AI's telephony integration system provides a unified interface for connecting with various telephony providers. This abstraction layer allows you to easily switch between providers without changing your application logic.
## Supported Providers
<CardGroup cols={2}>
<Card title="Twilio" href="/integrations/telephony/twilio">
Industry-leading cloud communications platform with global reach
</Card>
{/* Additional providers can be added in the future by implementing the TelephonyProvider interface */}
<Card title="Custom Provider" href="/integrations/telephony/custom">
Build your own telephony provider integration
</Card>
</CardGroup>
## Architecture
The telephony integration system uses a provider abstraction pattern that ensures consistency across different services:
```python
# All providers implement this interface
class TelephonyProvider:
async def initiate_call(to_number, webhook_url, ...)
async def get_call_status(call_id)
async def verify_webhook_signature(url, params, signature)
# ... more methods
```
## Configuration Methods
### OSS Deployment (Environment Variables)
For self-hosted deployments, configure your telephony provider using environment variables:
```bash
# .env file
TELEPHONY_PROVIDER=twilio # Required to specify which provider to use
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_FROM_NUMBER=+1234567890
```
### SaaS Deployment (Database Configuration)
For cloud deployments, configure providers through the web interface:
1. Navigate to **Settings** → **Integrations** → **Telephony**
2. Select your provider
3. Enter credentials
4. Test connection
## Common Features
The telephony integration in Dograh AI supports:
- **Outbound Calls**: Initiate calls to any phone number
- **Call Status Tracking**: Monitor call lifecycle events (initiated, ringing, answered, completed, failed)
- **WebSocket Streaming**: Real-time audio streaming for voice agents
- **Webhook Authentication**: Secure webhook signature verification
## API Endpoints
The telephony system exposes these unified endpoints:
| Endpoint | Method | Description |
|----------|---------|-------------|
| `/api/v1/telephony/initiate-call` | POST | Start an outbound call |
| `/api/v1/telephony/status-callback/{id}` | POST | Receive call status updates |
| `/api/v1/telephony/twiml` | POST | Handle initial webhook |
| `/api/v1/telephony/ws/{id}` | WebSocket | Real-time audio streaming |
## Implementation Status
- **Twilio**: ✅ Fully implemented and tested
- **Custom Providers**: The abstraction layer supports adding new providers by implementing the `TelephonyProvider` interface
- **API Endpoints**: All telephony operations use the unified `/api/v1/telephony/*` endpoints:
- `/api/v1/telephony/initiate-call` - Start outbound calls
- `/api/v1/telephony/status-callback/{id}` - Receive call status updates
- `/api/v1/telephony/ws/{workflow_id}/{user_id}/{workflow_run_id}` - WebSocket for audio streaming
## Troubleshooting
<AccordionGroup>
<Accordion title="Calls not connecting">
- Verify credentials are correctly configured
- Check phone number format (must include country code)
- Ensure webhook URLs are publicly accessible
- Review provider-specific error logs
</Accordion>
<Accordion title="Audio quality issues">
- Check network bandwidth and latency
- Verify audio codec compatibility
- Review WebSocket connection stability
- Ensure proper audio format (MULAW for Twilio)
</Accordion>
<Accordion title="Webhook signature validation failing">
- Confirm auth tokens match between provider and configuration
- Verify webhook URL matches exactly (including parameters)
- Check for proxy or load balancer modifications
</Accordion>
</AccordionGroup>
## Next Steps
- [Set up your first telephony provider](/integrations/telephony/twilio)
- [Build a custom provider integration](/integrations/telephony/custom)
- [Configure webhooks and callbacks](/integrations/telephony/webhooks)

View file

@ -0,0 +1,127 @@
---
title: "Twilio Integration"
description: "Configure Twilio for voice communication in Dograh AI"
---
## Overview
Twilio is a cloud communications platform that enables voice calling, messaging, and video capabilities. Dograh AI's Twilio integration provides seamless connectivity for your voice agents.
## Prerequisites
Before setting up Twilio integration, you'll need:
- A [Twilio account](https://www.twilio.com/try-twilio)
- Account SID and Auth Token from your Twilio Console
- At least one Twilio phone number
- Dograh AI instance running and accessible
## Video Tutorial
Watch this step-by-step guide to set up Twilio with Dograh AI:
<iframe
className="w-full aspect-video rounded-xl"
src="https://www.tella.tv/video/cmgbvzkrt00jk0clacu16blm3/embed?b=1&title=1&a=1&loop=0&t=0&muted=0&wt=1"
title="Dograh Twilio Setup"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
## Configuration
### Step 1: Get Twilio Credentials
1. Log in to your [Twilio Console](https://console.twilio.com/)
2. Find your **Account SID** and **Auth Token** on the dashboard
3. Navigate to **Phone Numbers** → **Manage** → **Active Numbers**
4. Copy your phone number(s)
### Step 2: Configure in Dograh AI
<Tabs>
<Tab title="Web Interface (SaaS)">
1. Navigate to **Settings** → **Integrations** → **Telephony**
2. Select **Twilio** as your provider
3. Enter your credentials:
- Account SID
- Auth Token
- Phone Numbers (comma-separated if multiple)
4. Click **Test Connection**
5. Save configuration
</Tab>
<Tab title="Environment Variables (OSS)">
Add these variables to your `.env` file:
```bash
# Telephony Configuration
TELEPHONY_PROVIDER=twilio # Specifies Twilio as the telephony provider
TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_FROM_NUMBER="+1234567890"
# For multiple numbers, use comma separation:
# TWILIO_FROM_NUMBER="+1234567890,+0987654321"
```
Restart your Dograh AI services:
```bash
docker-compose restart api
```
</Tab>
</Tabs>
### Step 3: Test Your Configuration
1. Create a test workflow
2. Click "Test Call" to verify connection
3. Check call logs for successful connection
## How It Works
### Outbound Calling
When you initiate a call through Dograh AI:
1. The system selects a phone number from your configured pool
2. Twilio places the call to your recipient
3. Once connected, audio streams through WebSocket for real-time voice interaction
4. Call status updates are tracked throughout the lifecycle
## Campaign Features
When using Twilio with campaigns:
- **Rate Limiting**: Enforced per organization to prevent overwhelming
- **Automatic Retry**: Failed calls (busy/no-answer) are retried automatically
- **Concurrent Call Management**: System manages call slots to optimize throughput
## Troubleshooting
<AccordionGroup>
<Accordion title="Invalid phone number error">
Ensure phone numbers include country code in E.164 format: `+1234567890`
</Accordion>
<Accordion title="Authentication failed">
- Verify Account SID and Auth Token are correct
- Check for extra spaces in credentials
- Ensure credentials haven't been regenerated in Twilio Console
</Accordion>
<Accordion title="Webhook signature validation failing">
- Confirm your Auth Token matches exactly
- Verify the webhook URL matches what Twilio sends
- Check if you're behind a proxy that modifies requests
</Accordion>
<Accordion title="No audio on calls">
- Verify WebSocket connection is established
- Check firewall rules for WebSocket traffic
- Ensure audio pipeline is configured correctly
</Accordion>
</AccordionGroup>
## Best Practices
- Store credentials securely in environment variables (OSS) or database (SaaS)
- Test your configuration with a single call before running campaigns
- Monitor Twilio Console for usage and billing

View file

@ -0,0 +1,93 @@
---
title: "Webhooks and Callbacks"
description: "How Dograh AI handles telephony webhooks and audio streaming"
---
## Overview
Dograh AI uses webhooks to communicate with telephony providers for call events and audio streaming. Webhooks are automatically configured when you initiate calls.
## Webhook Types
### 1. Initial Call Webhook
When a call is initiated, the telephony provider requests instructions.
**Endpoint**: `/api/v1/telephony/twiml`
**Purpose**: Returns provider-specific instructions (TwiML for Twilio)
**Example Response**:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://your-domain/api/v1/telephony/ws/123/456/789" />
</Connect>
</Response>
```
### 2. Status Callback
Receives call lifecycle events.
**Endpoint**: `/api/v1/telephony/status-callback/{workflow_run_id}`
**Events Tracked**:
- `initiated` - Call request received
- `ringing` - Call is ringing
- `answered` - Call was answered
- `completed` - Call ended normally
- `busy` - Line was busy
- `no-answer` - Call not answered
- `failed` - Call failed
### 3. WebSocket Audio Stream
Real-time audio streaming for voice interaction.
**Endpoint**: `/api/v1/telephony/ws/{workflow_id}/{user_id}/{workflow_run_id}`
## How It Works
Dograh AI automatically:
1. Constructs webhook URLs based on your deployment
2. Passes them to the telephony provider when initiating calls
3. Verifies webhook signatures for security
4. Processes status updates to track call lifecycle
5. Manages WebSocket connections for audio streaming
## Local Development
For local development, use the built-in Cloudflare tunnel:
```yaml
# docker-compose.yml includes:
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate --url http://api:8000
```
The tunnel URL is automatically detected and used for webhooks.
## Troubleshooting
<AccordionGroup>
<Accordion title="Webhook URL not accessible">
- Verify your domain/tunnel URL is publicly accessible
- Check firewall rules allow incoming HTTPS traffic
- Test with `curl` from external network
</Accordion>
<Accordion title="WebSocket connection dropping">
- Check WebSocket upgrade headers are preserved
- Verify no timeout on load balancer/proxy
- Monitor for memory/CPU constraints
</Accordion>
<Accordion title="Status callbacks not received">
- Verify workflow_run_id is included in URL
- Check provider console for webhook errors
- Review webhook retry logs
</Accordion>
</AccordionGroup>

4
ui/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "ui",
"version": "0.1.0",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ui",
"version": "0.1.0",
"version": "1.1.0",
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@hey-api/client-fetch": "^0.10.0",

View file

@ -25,6 +25,7 @@ import {
} from "@/components/ui/select";
import { useAuth } from "@/lib/auth";
// TODO: Make UI provider-agnostic
interface TelephonyConfigForm {
provider: string;
account_sid: string;

View file

@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { PhoneInput } from 'react-international-phone';
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TwilioInitiateCallPost } from '@/client/sdk.gen';
import { getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet, initiateCallApiV1TelephonyInitiateCallPost } from '@/client/sdk.gen';
import { WorkflowError } from '@/client/types.gen';
import { FlowEdge, FlowNode } from "@/components/flow/types";
import { OnboardingTooltip } from '@/components/onboarding/OnboardingTooltip';
@ -117,6 +117,7 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
});
// If no configuration exists, show configure dialog
// Check if Twilio is configured (currently the only supported provider)
if (configResponse.error || !configResponse.data?.twilio) {
setConfigureDialogOpen(true);
return;
@ -151,7 +152,7 @@ const WorkflowHeader = ({ isDirty, workflowName, rfInstance, onRun, workflowId,
}
// Configuration exists, proceed with call initiation
const response = await initiateCallApiV1TwilioInitiateCallPost({
const response = await initiateCallApiV1TelephonyInitiateCallPost({
body: { workflow_id: workflowId },
headers: { 'Authorization': `Bearer ${accessToken}` },
});

File diff suppressed because one or more lines are too long

View file

@ -621,6 +621,68 @@ export type WorkflowTemplateResponse = {
created_at: string;
};
export type InitiateCallApiV1TelephonyInitiateCallPostData = {
body: InitiateCallRequest;
headers?: {
authorization?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/telephony/initiate-call';
};
export type InitiateCallApiV1TelephonyInitiateCallPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type InitiateCallApiV1TelephonyInitiateCallPostError = InitiateCallApiV1TelephonyInitiateCallPostErrors[keyof InitiateCallApiV1TelephonyInitiateCallPostErrors];
export type InitiateCallApiV1TelephonyInitiateCallPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostData = {
body?: never;
headers?: {
'x-twilio-signature'?: string | null;
};
path: {
workflow_run_id: number;
};
query?: never;
url: '/api/v1/telephony/status-callback/{workflow_run_id}';
};
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostError = HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors[keyof HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostErrors];
export type HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type InitiateCallApiV1TwilioInitiateCallPostData = {
body: InitiateCallRequest;
headers?: {