diff --git a/phone-chatbot/daily-twilio-sip-dial-in/README.md b/phone-chatbot/daily-twilio-sip-dial-in/README.md index 954b06fe..62bbee77 100644 --- a/phone-chatbot/daily-twilio-sip-dial-in/README.md +++ b/phone-chatbot/daily-twilio-sip-dial-in/README.md @@ -1,17 +1,23 @@ -# Daily + Twilio SIP dial-in Voice Bot +# Twilio Phone Number → Daily SIP → Pipecat Bot (Dial-in) -This project demonstrates how to create a voice bot that can receive phone calls via Twilio and use Daily's SIP capabilities to enable voice conversations. +This example shows how to receive inbound phone calls on a **Twilio phone number** and route them through **Daily's SIP infrastructure** to a **Pipecat voice bot**. The `provider="daily"` setting tells Daily to use its own SIP servers for the media path, so no external SIP trunk is required. + +> **Using a Daily phone number instead of Twilio?** See the [`daily-pstn-dial-in`](../daily-pstn-dial-in) example. ## How It Works -1. Twilio receives an incoming call to your phone number -2. Twilio calls your webhook server (`/call` endpoint in `server.py`) -3. The server creates a Daily room with SIP capabilities -4. The server starts the bot process with the room details (locally or via Pipecat Cloud) -5. The caller is put on hold with music (a US ringtone in this example) -6. The bot joins the Daily room and signals readiness -7. Twilio forwards the call to Daily's SIP endpoint -8. The caller and the bot are connected, and the bot handles the conversation +``` +Caller → Twilio Phone Number → Webhook (server.py) → Daily SIP Room → Pipecat Bot +``` + +1. A caller dials your **Twilio phone number** +2. Twilio sends a webhook to your server (`/call` endpoint in `server.py`) +3. The caller hears hold music while the server handles the webhook +4. The server creates a **Daily room** with SIP enabled (`provider="daily"`) +5. The server starts the Pipecat bot (locally or via Pipecat Cloud) +6. The bot joins the Daily room and signals readiness (`dialin-ready` fires) +7. TwiML is invoked that asks Twilio to forward the call to Daily's SIP endpoint +8. The caller and the bot are connected for a voice conversation ## Project Structure @@ -175,6 +181,7 @@ class AgentRequest(BaseModel): token: str call_sid: str sip_uri: str + to_phone: str # Add your custom fields here customer_name: str | None = None account_id: str | None = None @@ -191,6 +198,7 @@ agent_request = AgentRequest( token=sip_config.token, call_sid=call_data.call_sid, sip_uri=sip_config.sip_endpoint, + to_phone=call_data.to_phone, customer_name=customer_info.name, account_id=customer_info.id, ) diff --git a/phone-chatbot/daily-twilio-sip-dial-in/bot.py b/phone-chatbot/daily-twilio-sip-dial-in/bot.py index 04a78271..e422c73e 100644 --- a/phone-chatbot/daily-twilio-sip-dial-in/bot.py +++ b/phone-chatbot/daily-twilio-sip-dial-in/bot.py @@ -99,7 +99,16 @@ async def on_dialin_ready(transport, sip_endpoint): logger.info(f"Forwarding call {request.call_sid} to {request.sip_uri}") try: - twilio_client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN")) + # Use to_phone to select Twilio credentials if you have multiple + # accounts (e.g., US vs EU). For now, we use a single set of credentials. + to_phone = request.to_phone + logger.info(f"Dialing in to {to_phone}") + + account_sid = os.getenv("TWILIO_ACCOUNT_SID") + auth_token = os.getenv("TWILIO_AUTH_TOKEN") + logger.info(f"Using Twilio credentials for {to_phone}") + + twilio_client = Client(account_sid, auth_token) # Update the Twilio call with TwiML to forward to the Daily SIP endpoint twilio_client.calls(request.call_sid).update( diff --git a/phone-chatbot/daily-twilio-sip-dial-in/server.py b/phone-chatbot/daily-twilio-sip-dial-in/server.py index 02485acf..1f3d4e21 100644 --- a/phone-chatbot/daily-twilio-sip-dial-in/server.py +++ b/phone-chatbot/daily-twilio-sip-dial-in/server.py @@ -78,7 +78,9 @@ async def handle_call(request: Request): call_data = await twilio_call_data_from_request(request) - sip_config = await create_daily_room(call_data, request.app.state.http_session) + sip_config = await create_daily_room( + call_data, request.app.state.http_session, sip_provider="daily" + ) # Make sure we have a SIP endpoint. if not sip_config.sip_endpoint: @@ -89,6 +91,7 @@ async def handle_call(request: Request): token=sip_config.token, call_sid=call_data.call_sid, sip_uri=sip_config.sip_endpoint, + to_phone=call_data.to_phone, ) # Start bot locally or in production. diff --git a/phone-chatbot/daily-twilio-sip-dial-in/server_utils.py b/phone-chatbot/daily-twilio-sip-dial-in/server_utils.py index 9a5b4024..ca9bc096 100644 --- a/phone-chatbot/daily-twilio-sip-dial-in/server_utils.py +++ b/phone-chatbot/daily-twilio-sip-dial-in/server_utils.py @@ -5,11 +5,13 @@ # import os +import time import aiohttp from fastapi import HTTPException, Request from loguru import logger from pipecat.runner.daily import DailyRoomConfig, configure +from pipecat.transports.daily.utils import DailyRoomProperties, DailyRoomSipParams from pydantic import BaseModel @@ -41,6 +43,7 @@ class AgentRequest(BaseModel): token: str call_sid: str sip_uri: str + to_phone: str async def twilio_call_data_from_request(request: Request): @@ -67,13 +70,16 @@ async def twilio_call_data_from_request(request: Request): async def create_daily_room( - call_data: TwilioCallData, session: aiohttp.ClientSession + call_data: TwilioCallData, + session: aiohttp.ClientSession, + sip_provider: str | None = None, ) -> DailyRoomConfig: """Create a Daily room configured for PSTN dial-in. Args: call_data: Call data containing caller phone number and call details session: Shared aiohttp session for making HTTP requests + sip_provider: Optional SIP provider name (e.g., "daily") Returns: DailyRoomConfig: Configuration object with room_url and token @@ -82,7 +88,23 @@ async def create_daily_room( HTTPException: If room creation fails """ try: - return await configure(session, sip_caller_phone=call_data.from_phone) + sip_params = DailyRoomSipParams( + display_name=call_data.from_phone, + video=False, + sip_mode="dial-in", + num_endpoints=1, + codecs=None, + provider=sip_provider, + ) + room_props = DailyRoomProperties( + exp=time.time() + 600, # 10 minutes + eject_at_room_exp=True, + sip=sip_params, + enable_dialout=True, + start_video_off=True, + geo="us-east-1", + ) + return await configure(session, room_properties=room_props) except Exception as e: logger.error(f"Error creating Daily room: {e}") raise HTTPException(status_code=500, detail=f"Failed to create Daily room: {e!s}") diff --git a/phone-chatbot/daily-twilio-sip-dial-out/README.md b/phone-chatbot/daily-twilio-sip-dial-out/README.md index e9600b0e..b04d4ec8 100644 --- a/phone-chatbot/daily-twilio-sip-dial-out/README.md +++ b/phone-chatbot/daily-twilio-sip-dial-out/README.md @@ -1,15 +1,21 @@ -# Daily + Twilio SIP dial-out Voice Bot +# Pipecat Bot → Daily SIP → Twilio Phone Number (Dial-out) -This project demonstrates how to create a voice bot that uses Daily's SIP capabilities with Twilio to make outbound calls to phone numbers. +This example shows how a **Pipecat voice bot** can make outbound phone calls through **Daily's SIP infrastructure** to a **Twilio phone number**. The `provider="daily"` setting in the dial-out request tells Daily to use its own SIP servers for the media path. + +> **Using a Daily phone number instead of Twilio?** See the [`daily-pstn-dial-out`](../daily-pstn-dial-out) example — no Twilio SIP domain/TwiML configuration needed. However, Twilio has phone numbers in multiple regions. ## How It Works -1. The server receives a dial-out request with the SIP URI to call -2. The server creates a Daily room with SIP capabilities -3. The server starts the bot process (locally or via Pipecat Cloud based on ENV) -4. The bot joins the room and initiates the dial-out to the specified SIP URI -5. Twilio receives the SIP request and processes it via configured TwiML -6. Twilio rings the number found within the SIP URI +``` +API Request (curl) → server.py → Spins up Pipecat Bot → Daily SIP dial-out → Twilio SIP Domain → Phone +``` + +1. Your server receives a dial-out request with the SIP URI and `provider` +2. The server creates a **Daily room** with dial-out enabled +3. The server starts the Pipecat bot (locally or via Pipecat Cloud) +4. The bot joins the room and initiates the dial-out (`provider="daily"`) +5. **Daily's SIP** sends the call to your **Twilio SIP domain** +6. Twilio processes the call via your TwiML bin or webhook handler and rings the destination number 7. The bot automatically retries on failure (up to 5 attempts) 8. When the call is answered, the bot conducts the conversation @@ -95,6 +101,8 @@ This example is organized to be production-ready and easy to customize: - callerId must be a valid number that you own on [Twilio](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming) - answerOnBridge="true|false" based on your use-case - Save the file. We will use this when creating the SIP domain + +note: `callerid` is hardcoded in the TwiML bin, to set it dynamically, the username field of the SIP URI can be overloaded to contain both the callerId and the destination number. `+1DESTINATION_+1CALLERID`. 4. Create and configure a SIP domain @@ -171,7 +179,8 @@ You'll need two terminal windows open: -H "Content-Type: application/json" \ -d '{ "dialout_settings": { - "sip_uri": "sip:+1234567890@daily.sip.twilio.com" + "sip_uri": "sip:+1234567890@daily.sip.twilio.com", + "provider": "daily" } }' ``` @@ -269,3 +278,5 @@ agent_request = AgentRequest( - Make sure both IP ACLs (0.0.0.0/1 and 128.0.0.0/1) are created and selected - Verify that the TwiML bin has a valid caller ID from your Twilio account - Check that the SIP domain name matches what you're using in the SIP URI + +Note: Setting Daily Provider come with the advantage of using Static IPs, which means you can set a smaller set of IP in the ACLs and have reliable SIP connectivity. diff --git a/phone-chatbot/daily-twilio-sip-dial-out/bot.py b/phone-chatbot/daily-twilio-sip-dial-out/bot.py index cdd42607..6f996875 100644 --- a/phone-chatbot/daily-twilio-sip-dial-out/bot.py +++ b/phone-chatbot/daily-twilio-sip-dial-out/bot.py @@ -51,6 +51,7 @@ def __init__( ): self._transport = transport self._sip_uri = dialout_settings.sip_uri + self._provider = dialout_settings.provider self._max_retries = max_retries self._attempt_count = 0 self._is_successful = False @@ -80,7 +81,10 @@ async def attempt_dialout(self) -> bool: f"Attempting dialout (attempt {self._attempt_count}/{self._max_retries}) to: {self._sip_uri}" ) - await self._transport.start_dialout({"sipUri": self._sip_uri}) + params = {"sipUri": self._sip_uri} + if self._provider: + params["provider"] = self._provider + await self._transport.start_dialout(params) return True def mark_successful(self): @@ -170,6 +174,19 @@ async def on_dialout_answered(transport, data): logger.debug(f"Dial-out answered: {data}") dialout_manager.mark_successful() + @transport.event_handler("on_dialout_connected") + async def on_dialout_connected(transport, data): + logger.debug(f"Dial-out connected: {data}") + + @transport.event_handler("on_dialout_stopped") + async def on_dialout_stopped(transport, data): + logger.debug(f"Dial-out stopped: {data}") + await task.cancel() + + @transport.event_handler("on_dialout_warning") + async def on_dialout_warning(transport, data): + logger.debug(f"Dial-out warning: {data}") + @transport.event_handler("on_dialout_error") async def on_dialout_error(transport, data: Any): logger.error(f"Dial-out error, retrying: {data}") diff --git a/phone-chatbot/daily-twilio-sip-dial-out/server_utils.py b/phone-chatbot/daily-twilio-sip-dial-out/server_utils.py index 32228a7a..c5322ba6 100644 --- a/phone-chatbot/daily-twilio-sip-dial-out/server_utils.py +++ b/phone-chatbot/daily-twilio-sip-dial-out/server_utils.py @@ -13,11 +13,13 @@ """ import os +import time import aiohttp from fastapi import HTTPException, Request from loguru import logger from pipecat.runner.daily import DailyRoomConfig, configure +from pipecat.transports.daily.utils import DailyRoomProperties from pydantic import BaseModel @@ -26,9 +28,11 @@ class DialoutSettings(BaseModel): Attributes: sip_uri: The SIP URI to dial + provider: Optional SIP provider name (e.g., "daily") """ sip_uri: str + provider: str | None = None # Include any custom data here needed for the call @@ -109,7 +113,22 @@ async def create_daily_room( raise HTTPException(status_code=400, detail="Invalid SIP URI") try: - return await configure(session, sip_caller_phone=sip_caller_phone) + # sip_params = DailyRoomSipParams( + # display_name=sip_caller_phone, + # video=False, + # sip_mode="dial-in", + # num_endpoints=1, + # provider="daily" + # ) + room_props = DailyRoomProperties( + exp=time.time() + 600, # 10 minutes + eject_at_room_exp=True, + # sip=sip_params, + enable_dialout=True, + start_video_off=True, + geo="us-east-1", # select a region close to the from_number/pipecat agent + ) + return await configure(session, room_properties=room_props) except Exception as e: logger.error(f"Error creating Daily room: {e}") raise HTTPException(status_code=500, detail=f"Failed to create Daily room: {str(e)}")