diff --git a/examples/phone-chatbot/README.md b/examples/phone-chatbot/README.md index 3d20f504a..83e2c57fd 100644 --- a/examples/phone-chatbot/README.md +++ b/examples/phone-chatbot/README.md @@ -235,10 +235,10 @@ For incoming calls from customers, Daily will send a webhook to your `/start` en ```json { - "From": "+CALLERS_PHONE", - "To": "$PURCHASED_PHONE", - "callId": "callid-read-only-string", - "callDomain": "callDomain-read-only-string" + "From": "+CALLERS_PHONE", + "To": "$PURCHASED_PHONE", + "callId": "callid-read-only-string", + "callDomain": "callDomain-read-only-string" } ``` @@ -266,63 +266,63 @@ When making requests to the `/start` endpoint, the config object can include: ```json { - "config": { - "prompts": [ - { - "name": "call_transfer_initial_prompt", - "text": "Your custom prompt here" - }, - { - "name": "call_transfer_prompt", - "text": "Your custom prompt here" - }, - { - "name": "call_transfer_finished_prompt", - "text": "Your custom prompt here" - }, - { - "name": "voicemail_detection_prompt", - "text": "Your custom prompt here" - }, - { - "name": "voicemail_prompt", - "text": "Your custom prompt here" - }, - { - "name": "human_conversation_prompt", - "text": "Your custom prompt here" - } - ], - "dialin_settings": { - "From": "+CALLERS_PHONE", - "To": "$PURCHASED_PHONE", - "callId": "callid-read-only-string", - "callDomain": "callDomain-read-only-string" - }, - "dialout_settings": [ - { - "phoneNumber": "+12345678910", - "callerId": "caller-id-uuid", - "sipUri": "sip:maria@example.com" - } - ], - "call_transfer": { - "mode": "dialout", - "speakSummary": true, - "storeSummary": false, - "operatorNumber": "+12345678910", - "testInPrebuilt": false - }, - "voicemail_detection": { - "testInPrebuilt": true - }, - "simple_dialin": { - "testInPrebuilt": true - }, - "simple_dialout": { - "testInPrebuilt": true - } - } + "config": { + "prompts": [ + { + "name": "call_transfer_initial_prompt", + "text": "Your custom prompt here" + }, + { + "name": "call_transfer_prompt", + "text": "Your custom prompt here" + }, + { + "name": "call_transfer_finished_prompt", + "text": "Your custom prompt here" + }, + { + "name": "voicemail_detection_prompt", + "text": "Your custom prompt here" + }, + { + "name": "voicemail_prompt", + "text": "Your custom prompt here" + }, + { + "name": "human_conversation_prompt", + "text": "Your custom prompt here" + } + ], + "dialin_settings": { + "From": "+CALLERS_PHONE", + "To": "$PURCHASED_PHONE", + "callId": "callid-read-only-string", + "callDomain": "callDomain-read-only-string" + }, + "dialout_settings": [ + { + "phoneNumber": "+12345678910", + "callerId": "caller-id-uuid", + "sipUri": "sip:maria@example.com" + } + ], + "call_transfer": { + "mode": "dialout", + "speakSummary": true, + "storeSummary": false, + "operatorNumber": "+12345678910", + "testInPrebuilt": false + }, + "voicemail_detection": { + "testInPrebuilt": true + }, + "simple_dialin": { + "testInPrebuilt": true + }, + "simple_dialout": { + "testInPrebuilt": true + } + } } ``` @@ -393,19 +393,19 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "dialin_settings": { - "from": "+12345678901", - "to": "+19876543210", - "call_id": "call-id-string", - "call_domain": "domain-string" - }, - "call_transfer": { - "mode": "dialout", - "speakSummary": true, - "operatorNumber": "+12345678910" - } - } + "config": { + "dialin_settings": { + "from": "+12345678901", + "to": "+19876543210", + "call_id": "call-id-string", + "call_domain": "domain-string" + }, + "call_transfer": { + "mode": "dialout", + "speakSummary": true, + "operatorNumber": "+12345678910" + } + } } ``` @@ -413,14 +413,14 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "call_transfer": { - "mode": "dialout", - "speakSummary": true, - "operatorNumber": "+12345678910", - "testInPrebuilt": true - } - } + "config": { + "call_transfer": { + "mode": "dialout", + "speakSummary": true, + "operatorNumber": "+12345678910", + "testInPrebuilt": true + } + } } ``` @@ -428,11 +428,11 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "voicemail_detection": { - "testInPrebuilt": true - } - } + "config": { + "voicemail_detection": { + "testInPrebuilt": true + } + } } ``` @@ -440,16 +440,16 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "dialout_settings": [ - { - "phoneNumber": "+12345678910" - } - ], - "voicemail_detection": { - "testInPrebuilt": false - } - } + "config": { + "dialout_settings": [ + { + "phoneNumber": "+12345678910" + } + ], + "voicemail_detection": { + "testInPrebuilt": false + } + } } ``` @@ -457,15 +457,15 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "dialin_settings": { - "from": "+12345678901", - "to": "+19876543210", - "call_id": "call-id-string", - "call_domain": "domain-string" - }, - "simple_dialin": {} - } + "config": { + "dialin_settings": { + "from": "+12345678901", + "to": "+19876543210", + "call_id": "call-id-string", + "call_domain": "domain-string" + }, + "simple_dialin": {} + } } ``` @@ -473,11 +473,11 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "simple_dialin": { - "testInPrebuilt": true - } - } + "config": { + "simple_dialin": { + "testInPrebuilt": true + } + } } ``` @@ -485,14 +485,14 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "dialout_settings": [ - { - "phoneNumber": "+12345678910" - } - ], - "simple_dialout": {} - } + "config": { + "dialout_settings": [ + { + "phoneNumber": "+12345678910" + } + ], + "simple_dialout": {} + } } ``` @@ -500,37 +500,14 @@ The following table shows which feature combinations are supported when making r ```json { - "config": { - "simple_dialout": { - "testInPrebuilt": true - } - } + "config": { + "simple_dialout": { + "testInPrebuilt": true + } + } } ``` -## Using Twilio (Alternative) - -To use Twilio for call handling: - -1. Start the bot runner: - - ```shell - python bot_runner.py --host localhost - ``` - -2. Start ngrok: - - ```shell - ngrok http --domain yourdomain.ngrok.app 7860 - ``` - -3. In another terminal, run the Twilio bot: - ```shell - python bot_twilio.py - ``` - -Make requests to `/start_twilio_bot` for Twilio-specific functionality. - ## Deployment See Pipecat Cloud deployment docs for how to deploy this example: https://docs.pipecat.daily.co/agents/deploy diff --git a/examples/phone-chatbot/bot_runner.py b/examples/phone-chatbot/bot_runner.py index 0de27ff8c..0c3d2e65e 100644 --- a/examples/phone-chatbot/bot_runner.py +++ b/examples/phone-chatbot/bot_runner.py @@ -20,8 +20,7 @@ from bot_runner_helpers import ( from dotenv import load_dotenv from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, PlainTextResponse -from twilio.twiml.voice_response import VoiceResponse +from fastapi.responses import JSONResponse from pipecat.transports.services.helpers.daily_rest import ( DailyRESTHelper, @@ -125,32 +124,6 @@ async def start_bot(room_details: Dict[str, str], body: Dict[str, Any], example: raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}") -async def start_twilio_bot(room_details: Dict[str, str], call_id: str) -> bool: - """Start a Twilio bot process with the given configuration. - - Args: - room_details: Room URL, token, and SIP endpoint - call_id: Twilio call ID (CallSid) - - Returns: - Boolean indicating success - """ - room_url = room_details["room"] - token = room_details["token"] - sip_endpoint = room_details["sip_endpoint"] - - # Format command for Twilio bot - bot_proc = f"python3 -m bot_twilio -u {room_url} -t {token} -i {call_id} -s {sip_endpoint}" - print(f"Starting Twilio bot. Room: {room_url}") - - try: - command_parts = shlex.split(bot_proc) - subprocess.Popen(command_parts, bufsize=1, cwd=os.path.dirname(os.path.abspath(__file__))) - return True - except Exception as e: - raise HTTPException(status_code=500, detail=f"Failed to start subprocess: {e}") - - # ----------------- API Setup ----------------- # @@ -180,47 +153,6 @@ app.add_middleware( # ----------------- API Endpoints ----------------- # -@app.post("/twilio_start_bot", response_class=PlainTextResponse) -async def twilio_start_bot(request: Request): - """Handle incoming Twilio webhook calls and start a Twilio bot. - - This endpoint is called directly by Twilio as a webhook when a call is received. - It puts the call on hold with music and starts a bot that will handle the call. - """ - print("POST /twilio_start_bot") - - # Get form data from Twilio webhook - try: - form_data = await request.form() - data = dict(form_data) - except Exception as e: - raise HTTPException(status_code=400, detail=f"Failed to parse Twilio form data: {str(e)}") - - # Get default room URL from environment - room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", None) - - # Extract call ID from Twilio data - call_id = data.get("CallSid") - if not call_id: - raise HTTPException(status_code=400, detail="Missing 'CallSid' in request") - - print(f"CallId: {call_id}") - - # Create Daily room for the Twilio call - room_details = await create_daily_room(room_url, None) # No special config for Twilio rooms - - # Start the Twilio bot - await start_twilio_bot(room_details, call_id) - - # Put the call on hold until the bot is ready to handle it - # The bot will update the call with the SIP URI when it's ready - resp = VoiceResponse() - resp.play( - url="http://com.twilio.sounds.music.s3.amazonaws.com/MARKOVICHAMP-Borghestral.mp3", loop=10 - ) - return str(resp) - - @app.post("/start") async def handle_start_request(request: Request) -> JSONResponse: """Unified endpoint to handle bot configuration for different scenarios.""" @@ -228,21 +160,7 @@ async def handle_start_request(request: Request) -> JSONResponse: room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", None) try: - # Check if this is form data (from Twilio) or JSON - content_type = request.headers.get("content-type", "").lower() - - if "application/x-www-form-urlencoded" in content_type: - # Handle form data from Twilio - form_data = await request.form() - data = dict(form_data) - - # Check for CallSid which indicates this is a Twilio webhook - if "CallSid" in data: - # Redirect to Twilio handler for backward compatibility - return await twilio_start_bot(request) - else: - # Parse JSON request data - data = await request.json() + data = await request.json() # Handle webhook test if "test" in data: @@ -298,14 +216,6 @@ async def handle_start_request(request: Request) -> JSONResponse: return JSONResponse(response) except json.JSONDecodeError: - # Check if this might be form data from Twilio - try: - content_type = request.headers.get("content-type", "").lower() - if "application/x-www-form-urlencoded" in content_type: - return await twilio_start_bot(request) - except Exception: - pass - raise HTTPException(status_code=400, detail="Invalid JSON in request body") except Exception as e: raise HTTPException(status_code=400, detail=f"Request processing error: {str(e)}") diff --git a/examples/phone-chatbot/bot_twilio.py b/examples/phone-chatbot/bot_twilio.py deleted file mode 100644 index 2c3e9a31b..000000000 --- a/examples/phone-chatbot/bot_twilio.py +++ /dev/null @@ -1,122 +0,0 @@ -# -# Copyright (c) 2024–2025, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -import argparse -import asyncio -import os -import sys - -from dotenv import load_dotenv -from loguru import logger -from twilio.rest import Client - -from pipecat.audio.vad.silero import SileroVADAnalyzer -from pipecat.pipeline.pipeline import Pipeline -from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.task import PipelineParams, PipelineTask -from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.services.elevenlabs.tts import ElevenLabsTTSService -from pipecat.services.openai.llm import OpenAILLMService -from pipecat.transports.services.daily import DailyParams, DailyTransport - -load_dotenv(override=True) - -logger.remove(0) -logger.add(sys.stderr, level="DEBUG") - - -twilio_account_sid = os.getenv("TWILIO_ACCOUNT_SID") -twilio_auth_token = os.getenv("TWILIO_AUTH_TOKEN") -twilioclient = Client(twilio_account_sid, twilio_auth_token) - -daily_api_key = os.getenv("DAILY_API_KEY", "") - - -async def main(room_url: str, token: str, callId: str, sipUri: str): - # dialin_settings are only needed if Daily's SIP URI is used - # If you are handling this via Twilio, Telnyx, set this to None - # and handle call-forwarding when on_dialin_ready fires. - transport = DailyTransport( - room_url, - token, - "Chatbot", - DailyParams( - api_key=daily_api_key, - dialin_settings=None, # Not required for Twilio - audio_in_enabled=True, - audio_out_enabled=True, - video_out_enabled=False, - vad_analyzer=SileroVADAnalyzer(), - transcription_enabled=True, - ), - ) - - tts = ElevenLabsTTSService( - api_key=os.getenv("ELEVENLABS_API_KEY", ""), - voice_id=os.getenv("ELEVENLABS_VOICE_ID", ""), - ) - - llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) - - messages = [ - { - "role": "system", - "content": "You are Chatbot, a friendly, helpful robot. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by saying 'Hello! Who dares dial me at this hour?!'.", - }, - ] - - context = OpenAILLMContext(messages) - context_aggregator = llm.create_context_aggregator(context) - - pipeline = Pipeline( - [ - transport.input(), - context_aggregator.user(), - llm, - tts, - transport.output(), - context_aggregator.assistant(), - ] - ) - - task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True)) - - @transport.event_handler("on_first_participant_joined") - async def on_first_participant_joined(transport, participant): - await transport.capture_participant_transcription(participant["id"]) - await task.queue_frames([context_aggregator.user().get_context_frame()]) - - @transport.event_handler("on_participant_left") - async def on_participant_left(transport, participant, reason): - await task.cancel() - - @transport.event_handler("on_dialin_ready") - async def on_dialin_ready(transport, cdata): - # For Twilio, Telnyx, etc. You need to update the state of the call - # and forward it to the sip_uri.. - print(f"Forwarding call: {callId} {sipUri}") - - try: - # The TwiML is updated using Twilio's client library - call = twilioclient.calls(callId).update( - twiml=f"{sipUri}" - ) - except Exception as e: - raise Exception(f"Failed to forward call: {str(e)}") - - runner = PipelineRunner() - await runner.run(task) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Pipecat Simple ChatBot") - parser.add_argument("-u", type=str, help="Room URL") - parser.add_argument("-t", type=str, help="Token") - parser.add_argument("-i", type=str, help="Call ID") - parser.add_argument("-s", type=str, help="SIP URI") - config = parser.parse_args() - - asyncio.run(main(config.u, config.t, config.i, config.s)) diff --git a/examples/phone-chatbot/env.example b/examples/phone-chatbot/env.example index 2ad4e9137..c5e049ead 100644 --- a/examples/phone-chatbot/env.example +++ b/examples/phone-chatbot/env.example @@ -5,8 +5,6 @@ DEEPGRAM_API_KEY= OPENAI_API_KEY= GOOGLE_API_KEY CARTESIA_API_KEY= -TWILIO_ACCOUNT_SID= -TWILIO_AUTH_TOKEN= DIAL_IN_FROM_NUMBER= DIAL_OUT_TO_NUMBER= OPERATOR_NUMBER= \ No newline at end of file diff --git a/examples/phone-chatbot/requirements.txt b/examples/phone-chatbot/requirements.txt index a12a4731b..a70e1113f 100644 --- a/examples/phone-chatbot/requirements.txt +++ b/examples/phone-chatbot/requirements.txt @@ -2,5 +2,4 @@ pipecat-ai[daily,cartesia,deepgram,openai,google,silero] fastapi==0.115.6 uvicorn python-dotenv -twilio python-multipart