From a01f2df7ea3509ba81fa3744f9f4b84e56e55893 Mon Sep 17 00:00:00 2001 From: Sabiha Khan Date: Mon, 13 Oct 2025 17:55:10 +0530 Subject: [PATCH] refactor: telephony integration --- api/.env.example | 8 +- api/enums.py | 2 +- api/routes/main.py | 6 +- api/routes/organization.py | 5 +- api/routes/telephony.py | 315 ++++++++++++++++++ api/routes/twilio.py | 3 + api/schemas/telephony_config.py | 1 + api/services/campaign/call_dispatcher.py | 36 +- api/services/telephony/README.md | 167 ++++++++++ api/services/telephony/base.py | 103 ++++++ api/services/telephony/factory.py | 120 +++++++ api/services/telephony/providers/__init__.py | 1 + .../telephony/providers/twilio_provider.py | 151 +++++++++ api/services/telephony/twilio.py | 3 + api/tasks/workflow_run_cost.py | 3 + docs/docs.json | 18 +- docs/integrations/overview.mdx | 50 +++ docs/integrations/telephony/custom.mdx | 184 ++++++++++ docs/integrations/telephony/overview.mdx | 115 +++++++ docs/integrations/telephony/twilio.mdx | 127 +++++++ docs/integrations/telephony/webhooks.mdx | 93 ++++++ ui/package-lock.json | 4 +- ui/src/app/configure-telephony/page.tsx | 1 + .../components/WorkflowHeader.tsx | 5 +- ui/src/client/sdk.gen.ts | 28 +- ui/src/client/types.gen.ts | 62 ++++ 26 files changed, 1583 insertions(+), 28 deletions(-) create mode 100644 api/routes/telephony.py create mode 100644 api/services/telephony/README.md create mode 100644 api/services/telephony/base.py create mode 100644 api/services/telephony/factory.py create mode 100644 api/services/telephony/providers/__init__.py create mode 100644 api/services/telephony/providers/twilio_provider.py create mode 100644 docs/integrations/overview.mdx create mode 100644 docs/integrations/telephony/custom.mdx create mode 100644 docs/integrations/telephony/overview.mdx create mode 100644 docs/integrations/telephony/twilio.mdx create mode 100644 docs/integrations/telephony/webhooks.mdx diff --git a/api/.env.example b/api/.env.example index 8803380..6fd14fe 100644 --- a/api/.env.example +++ b/api/.env.example @@ -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" diff --git a/api/enums.py b/api/enums.py index 7e7e9c6..690a89a 100644 --- a/api/enums.py +++ b/api/enums.py @@ -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): diff --git a/api/routes/main.py b/api/routes/main.py index 9c6b647..f418512 100644 --- a/api/routes/main.py +++ b/api/routes/main.py @@ -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) diff --git a/api/routes/organization.py b/api/routes/organization.py index 8b30860..4bf055f 100644 --- a/api/routes/organization.py +++ b/api/routes/organization.py @@ -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 diff --git a/api/routes/telephony.py b/api/routes/telephony.py new file mode 100644 index 0000000..009088f --- /dev/null +++ b/api/routes/telephony.py @@ -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} + ) \ No newline at end of file diff --git a/api/routes/twilio.py b/api/routes/twilio.py index 5532e0e..5a3ce3a 100644 --- a/api/routes/twilio.py +++ b/api/routes/twilio.py @@ -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 diff --git a/api/schemas/telephony_config.py b/api/schemas/telephony_config.py index 907a8c4..aa49c93 100644 --- a/api/schemas/telephony_config.py +++ b/api/schemas/telephony_config.py @@ -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.""" diff --git a/api/services/campaign/call_dispatcher.py b/api/services/campaign/call_dispatcher.py index 6488ae2..f42281f 100644 --- a/api/services/campaign/call_dispatcher.py +++ b/api/services/campaign/call_dispatcher.py @@ -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( diff --git a/api/services/telephony/README.md b/api/services/telephony/README.md new file mode 100644 index 0000000..d243803 --- /dev/null +++ b/api/services/telephony/README.md @@ -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) \ No newline at end of file diff --git a/api/services/telephony/base.py b/api/services/telephony/base.py new file mode 100644 index 0000000..c321a45 --- /dev/null +++ b/api/services/telephony/base.py @@ -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 \ No newline at end of file diff --git a/api/services/telephony/factory.py b/api/services/telephony/factory.py new file mode 100644 index 0000000..dc31c23 --- /dev/null +++ b/api/services/telephony/factory.py @@ -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}") \ No newline at end of file diff --git a/api/services/telephony/providers/__init__.py b/api/services/telephony/providers/__init__.py new file mode 100644 index 0000000..5c8985e --- /dev/null +++ b/api/services/telephony/providers/__init__.py @@ -0,0 +1 @@ +# Telephony provider implementations \ No newline at end of file diff --git a/api/services/telephony/providers/twilio_provider.py b/api/services/telephony/providers/twilio_provider.py new file mode 100644 index 0000000..6912c68 --- /dev/null +++ b/api/services/telephony/providers/twilio_provider.py @@ -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""" + + + + + +""" + return twiml_content \ No newline at end of file diff --git a/api/services/telephony/twilio.py b/api/services/telephony/twilio.py index ea7dd2f..eec5d4d 100644 --- a/api/services/telephony/twilio.py +++ b/api/services/telephony/twilio.py @@ -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 diff --git a/api/tasks/workflow_run_cost.py b/api/tasks/workflow_run_cost.py index ad5844c..7021a42 100644 --- a/api/tasks/workflow_run_cost.py +++ b/api/tasks/workflow_run_cost.py @@ -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") diff --git a/docs/docs.json b/docs/docs.json index 6f6384a..3efe483 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -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" ] } ] diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx new file mode 100644 index 0000000..745e5d2 --- /dev/null +++ b/docs/integrations/overview.mdx @@ -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. + + + Configure telephony providers like Twilio, Vonage, and Plivo for voice communication + + +### 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 \ No newline at end of file diff --git a/docs/integrations/telephony/custom.mdx b/docs/integrations/telephony/custom.mdx new file mode 100644 index 0000000..75bba0d --- /dev/null +++ b/docs/integrations/telephony/custom.mdx @@ -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. \ No newline at end of file diff --git a/docs/integrations/telephony/overview.mdx b/docs/integrations/telephony/overview.mdx new file mode 100644 index 0000000..39cde21 --- /dev/null +++ b/docs/integrations/telephony/overview.mdx @@ -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 + + + + Industry-leading cloud communications platform with global reach + + {/* Additional providers can be added in the future by implementing the TelephonyProvider interface */} + + Build your own telephony provider integration + + + +## 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 + + + + - Verify credentials are correctly configured + - Check phone number format (must include country code) + - Ensure webhook URLs are publicly accessible + - Review provider-specific error logs + + + + - Check network bandwidth and latency + - Verify audio codec compatibility + - Review WebSocket connection stability + - Ensure proper audio format (MULAW for Twilio) + + + + - Confirm auth tokens match between provider and configuration + - Verify webhook URL matches exactly (including parameters) + - Check for proxy or load balancer modifications + + + +## 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) \ No newline at end of file diff --git a/docs/integrations/telephony/twilio.mdx b/docs/integrations/telephony/twilio.mdx new file mode 100644 index 0000000..f6926bb --- /dev/null +++ b/docs/integrations/telephony/twilio.mdx @@ -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: + + + +## 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 + + + + 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 + + + + 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 + ``` + + + +### 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 + + + + Ensure phone numbers include country code in E.164 format: `+1234567890` + + + + - Verify Account SID and Auth Token are correct + - Check for extra spaces in credentials + - Ensure credentials haven't been regenerated in Twilio Console + + + + - Confirm your Auth Token matches exactly + - Verify the webhook URL matches what Twilio sends + - Check if you're behind a proxy that modifies requests + + + + - Verify WebSocket connection is established + - Check firewall rules for WebSocket traffic + - Ensure audio pipeline is configured correctly + + + +## 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 \ No newline at end of file diff --git a/docs/integrations/telephony/webhooks.mdx b/docs/integrations/telephony/webhooks.mdx new file mode 100644 index 0000000..78e2ec9 --- /dev/null +++ b/docs/integrations/telephony/webhooks.mdx @@ -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 + + + + + + +``` + +### 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 + + + + - Verify your domain/tunnel URL is publicly accessible + - Check firewall rules allow incoming HTTPS traffic + - Test with `curl` from external network + + + + - Check WebSocket upgrade headers are preserved + - Verify no timeout on load balancer/proxy + - Monitor for memory/CPU constraints + + + + - Verify workflow_run_id is included in URL + - Check provider console for webhook errors + - Review webhook retry logs + + \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index bedab21..460f9b0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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", diff --git a/ui/src/app/configure-telephony/page.tsx b/ui/src/app/configure-telephony/page.tsx index b6c09bf..7ab3b39 100644 --- a/ui/src/app/configure-telephony/page.tsx +++ b/ui/src/app/configure-telephony/page.tsx @@ -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; diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx index d12fdb2..d334e16 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowHeader.tsx @@ -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}` }, }); diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts index a01e7d9..27734e6 100644 --- a/ui/src/client/sdk.gen.ts +++ b/ui/src/client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch'; import { client as _heyApiClient } from './client.gen'; -import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HealthApiV1HealthGetData,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitiateCallApiV1TwilioInitiateCallPostData, InitiateCallApiV1TwilioInitiateCallPostError, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, OfferApiV1PipecatRtcOfferPostData, OfferApiV1PipecatRtcOfferPostError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; +import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostData, HandleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPostError, HealthApiV1HealthGetData,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, InitiateCallApiV1TwilioInitiateCallPostData, InitiateCallApiV1TwilioInitiateCallPostError, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, OfferApiV1PipecatRtcOfferPostData, OfferApiV1PipecatRtcOfferPostError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; export type Options = ClientOptions & { /** @@ -19,6 +19,32 @@ export type Options; }; +/** + * Initiate Call + * Initiate a call using the configured telephony provider. + */ +export const initiateCallApiV1TelephonyInitiateCallPost = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/api/v1/telephony/initiate-call', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); +}; + +/** + * Handle Status Callback + * Handle status callbacks from telephony providers. + */ +export const handleStatusCallbackApiV1TelephonyStatusCallbackWorkflowRunIdPost = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/api/v1/telephony/status-callback/{workflow_run_id}', + ...options + }); +}; + /** * Initiate Call */ diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index 39b7473..18cb355 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -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?: {