Remove Twilio from phone-chatbot directory

This commit is contained in:
Mark Backman
2025-05-02 22:18:34 -04:00
parent 38bd55e518
commit 0679bb217d
5 changed files with 126 additions and 364 deletions

View File

@@ -235,10 +235,10 @@ For incoming calls from customers, Daily will send a webhook to your `/start` en
```json ```json
{ {
"From": "+CALLERS_PHONE", "From": "+CALLERS_PHONE",
"To": "$PURCHASED_PHONE", "To": "$PURCHASED_PHONE",
"callId": "callid-read-only-string", "callId": "callid-read-only-string",
"callDomain": "callDomain-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 ```json
{ {
"config": { "config": {
"prompts": [ "prompts": [
{ {
"name": "call_transfer_initial_prompt", "name": "call_transfer_initial_prompt",
"text": "Your custom prompt here" "text": "Your custom prompt here"
}, },
{ {
"name": "call_transfer_prompt", "name": "call_transfer_prompt",
"text": "Your custom prompt here" "text": "Your custom prompt here"
}, },
{ {
"name": "call_transfer_finished_prompt", "name": "call_transfer_finished_prompt",
"text": "Your custom prompt here" "text": "Your custom prompt here"
}, },
{ {
"name": "voicemail_detection_prompt", "name": "voicemail_detection_prompt",
"text": "Your custom prompt here" "text": "Your custom prompt here"
}, },
{ {
"name": "voicemail_prompt", "name": "voicemail_prompt",
"text": "Your custom prompt here" "text": "Your custom prompt here"
}, },
{ {
"name": "human_conversation_prompt", "name": "human_conversation_prompt",
"text": "Your custom prompt here" "text": "Your custom prompt here"
} }
], ],
"dialin_settings": { "dialin_settings": {
"From": "+CALLERS_PHONE", "From": "+CALLERS_PHONE",
"To": "$PURCHASED_PHONE", "To": "$PURCHASED_PHONE",
"callId": "callid-read-only-string", "callId": "callid-read-only-string",
"callDomain": "callDomain-read-only-string" "callDomain": "callDomain-read-only-string"
}, },
"dialout_settings": [ "dialout_settings": [
{ {
"phoneNumber": "+12345678910", "phoneNumber": "+12345678910",
"callerId": "caller-id-uuid", "callerId": "caller-id-uuid",
"sipUri": "sip:maria@example.com" "sipUri": "sip:maria@example.com"
} }
], ],
"call_transfer": { "call_transfer": {
"mode": "dialout", "mode": "dialout",
"speakSummary": true, "speakSummary": true,
"storeSummary": false, "storeSummary": false,
"operatorNumber": "+12345678910", "operatorNumber": "+12345678910",
"testInPrebuilt": false "testInPrebuilt": false
}, },
"voicemail_detection": { "voicemail_detection": {
"testInPrebuilt": true "testInPrebuilt": true
}, },
"simple_dialin": { "simple_dialin": {
"testInPrebuilt": true "testInPrebuilt": true
}, },
"simple_dialout": { "simple_dialout": {
"testInPrebuilt": true "testInPrebuilt": true
} }
} }
} }
``` ```
@@ -393,19 +393,19 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"dialin_settings": { "dialin_settings": {
"from": "+12345678901", "from": "+12345678901",
"to": "+19876543210", "to": "+19876543210",
"call_id": "call-id-string", "call_id": "call-id-string",
"call_domain": "domain-string" "call_domain": "domain-string"
}, },
"call_transfer": { "call_transfer": {
"mode": "dialout", "mode": "dialout",
"speakSummary": true, "speakSummary": true,
"operatorNumber": "+12345678910" "operatorNumber": "+12345678910"
} }
} }
} }
``` ```
@@ -413,14 +413,14 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"call_transfer": { "call_transfer": {
"mode": "dialout", "mode": "dialout",
"speakSummary": true, "speakSummary": true,
"operatorNumber": "+12345678910", "operatorNumber": "+12345678910",
"testInPrebuilt": true "testInPrebuilt": true
} }
} }
} }
``` ```
@@ -428,11 +428,11 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"voicemail_detection": { "voicemail_detection": {
"testInPrebuilt": true "testInPrebuilt": true
} }
} }
} }
``` ```
@@ -440,16 +440,16 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"dialout_settings": [ "dialout_settings": [
{ {
"phoneNumber": "+12345678910" "phoneNumber": "+12345678910"
} }
], ],
"voicemail_detection": { "voicemail_detection": {
"testInPrebuilt": false "testInPrebuilt": false
} }
} }
} }
``` ```
@@ -457,15 +457,15 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"dialin_settings": { "dialin_settings": {
"from": "+12345678901", "from": "+12345678901",
"to": "+19876543210", "to": "+19876543210",
"call_id": "call-id-string", "call_id": "call-id-string",
"call_domain": "domain-string" "call_domain": "domain-string"
}, },
"simple_dialin": {} "simple_dialin": {}
} }
} }
``` ```
@@ -473,11 +473,11 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"simple_dialin": { "simple_dialin": {
"testInPrebuilt": true "testInPrebuilt": true
} }
} }
} }
``` ```
@@ -485,14 +485,14 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"dialout_settings": [ "dialout_settings": [
{ {
"phoneNumber": "+12345678910" "phoneNumber": "+12345678910"
} }
], ],
"simple_dialout": {} "simple_dialout": {}
} }
} }
``` ```
@@ -500,37 +500,14 @@ The following table shows which feature combinations are supported when making r
```json ```json
{ {
"config": { "config": {
"simple_dialout": { "simple_dialout": {
"testInPrebuilt": true "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 ## Deployment
See Pipecat Cloud deployment docs for how to deploy this example: https://docs.pipecat.daily.co/agents/deploy See Pipecat Cloud deployment docs for how to deploy this example: https://docs.pipecat.daily.co/agents/deploy

View File

@@ -20,8 +20,7 @@ from bot_runner_helpers import (
from dotenv import load_dotenv from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, PlainTextResponse from fastapi.responses import JSONResponse
from twilio.twiml.voice_response import VoiceResponse
from pipecat.transports.services.helpers.daily_rest import ( from pipecat.transports.services.helpers.daily_rest import (
DailyRESTHelper, 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}") 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 ----------------- # # ----------------- API Setup ----------------- #
@@ -180,47 +153,6 @@ app.add_middleware(
# ----------------- API Endpoints ----------------- # # ----------------- 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") @app.post("/start")
async def handle_start_request(request: Request) -> JSONResponse: async def handle_start_request(request: Request) -> JSONResponse:
"""Unified endpoint to handle bot configuration for different scenarios.""" """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) room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", None)
try: try:
# Check if this is form data (from Twilio) or JSON data = await request.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()
# Handle webhook test # Handle webhook test
if "test" in data: if "test" in data:
@@ -298,14 +216,6 @@ async def handle_start_request(request: Request) -> JSONResponse:
return JSONResponse(response) return JSONResponse(response)
except json.JSONDecodeError: 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") raise HTTPException(status_code=400, detail="Invalid JSON in request body")
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"Request processing error: {str(e)}") raise HTTPException(status_code=400, detail=f"Request processing error: {str(e)}")

View File

@@ -1,122 +0,0 @@
#
# Copyright (c) 20242025, 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"<Response><Dial><Sip>{sipUri}</Sip></Dial></Response>"
)
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))

View File

@@ -5,8 +5,6 @@ DEEPGRAM_API_KEY=
OPENAI_API_KEY= OPENAI_API_KEY=
GOOGLE_API_KEY GOOGLE_API_KEY
CARTESIA_API_KEY= CARTESIA_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
DIAL_IN_FROM_NUMBER= DIAL_IN_FROM_NUMBER=
DIAL_OUT_TO_NUMBER= DIAL_OUT_TO_NUMBER=
OPERATOR_NUMBER= OPERATOR_NUMBER=

View File

@@ -2,5 +2,4 @@ pipecat-ai[daily,cartesia,deepgram,openai,google,silero]
fastapi==0.115.6 fastapi==0.115.6
uvicorn uvicorn
python-dotenv python-dotenv
twilio
python-multipart python-multipart