From 179ddbea7d4bfaf2a3ab23f0492e4ae0ffc40296 Mon Sep 17 00:00:00 2001 From: chadbailey59 Date: Mon, 27 Jan 2025 12:21:30 -0600 Subject: [PATCH] Add dialout to the Daily phone example (#998) * added dialout to daily phone example * cleanup * cleanup * pre-commit hook * Fix typo * More explicit README instructions --------- Co-authored-by: Mark Backman --- examples/README.md | 2 +- examples/dialin-chatbot/README.md | 85 ---------- .../.dockerignore | 0 .../.gitignore | 0 .../Dockerfile | 0 examples/phone-chatbot/README.md | 149 ++++++++++++++++++ .../bot_daily.py | 53 +++++-- .../bot_runner.py | 31 ++-- .../bot_twilio.py | 0 .../env.example | 0 .../fly.example.toml | 0 .../image.png | Bin .../requirements.txt | 0 13 files changed, 212 insertions(+), 108 deletions(-) delete mode 100644 examples/dialin-chatbot/README.md rename examples/{dialin-chatbot => phone-chatbot}/.dockerignore (100%) rename examples/{dialin-chatbot => phone-chatbot}/.gitignore (100%) rename examples/{dialin-chatbot => phone-chatbot}/Dockerfile (100%) create mode 100644 examples/phone-chatbot/README.md rename examples/{dialin-chatbot => phone-chatbot}/bot_daily.py (57%) rename examples/{dialin-chatbot => phone-chatbot}/bot_runner.py (89%) rename examples/{dialin-chatbot => phone-chatbot}/bot_twilio.py (100%) rename examples/{dialin-chatbot => phone-chatbot}/env.example (100%) rename examples/{dialin-chatbot => phone-chatbot}/fly.example.toml (100%) rename examples/{dialin-chatbot => phone-chatbot}/image.png (100%) rename examples/{dialin-chatbot => phone-chatbot}/requirements.txt (100%) diff --git a/examples/README.md b/examples/README.md index 7c73f833d..01150b450 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,7 +39,7 @@ Next, follow the steps in the README for each demo. | [Translation Chatbot](translation-chatbot) | Listens for user speech, then translates that speech to Spanish and speaks the translation back. Demonstrates multi-participant use-cases. | Deepgram, Azure, OpenAI, Daily, Daily Prebuilt UI | | [Moondream Chatbot](moondream-chatbot) | Demonstrates how to add vision capabilities to GPT4. **Note: works best with a GPU** | Deepgram, ElevenLabs, OpenAI, Moondream, Daily, Daily Prebuilt UI | | [Patient intake](patient-intake) | A chatbot that can call functions in response to user input. | Deepgram, ElevenLabs, OpenAI, Daily, Daily Prebuilt UI | -| [Dialin Chatbot](dialin-chatbot) | A chatbot that connects to an incoming phone call from Daily or Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio | +| [Phone Chatbot](phone-chatbot) | A chatbot that connects to PSTN/SIP phone calls, powered by Daily or Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio | | [Twilio Chatbot](twilio-chatbot) | A chatbot that connects to an incoming phone call from Twilio. | Deepgram, ElevenLabs, OpenAI, Daily, Twilio | | [studypal](studypal) | A chatbot to have a conversation about any article on the web | | | [WebSocket Chatbot Server](websocket-server) | A real-time websocket server that handles audio streaming and bot interactions with speech-to-text and text-to-speech capabilities. | Cartesia, Deepgram, OpenAI, Websockets | diff --git a/examples/dialin-chatbot/README.md b/examples/dialin-chatbot/README.md deleted file mode 100644 index 3af299e8b..000000000 --- a/examples/dialin-chatbot/README.md +++ /dev/null @@ -1,85 +0,0 @@ -
pipecat -
- -# Dialin example - -Example project that demonstrates how to add phone number dialin to your Pipecat bots. We include examples for both Daily (`bot_daily.py`) and Twilio (`bot_twilio.py`), depending on who you want to use as a phone vendor. - -- 🔁 Transport: Daily WebRTC -- 💬 Speech-to-Text: Deepgram via Daily transport -- 🤖 LLM: GPT4-o / OpenAI -- 🔉 Text-to-Speech: ElevenLabs - -#### Should I use Daily or Twilio as a vendor? - -If you're starting from scratch, using Daily to provision phone numbers alongside Daily as a transport offers some convenience (such as automatic call forwarding.) - -If you already have Twilio numbers and workflows that you want to connect to your Pipecat bots, there is some additional configuration required (you'll need to create a `on_dialin_ready` and use the Twilio client to trigger the forward.) - -You can read more about this, as well as see respective walkthroughs in our docs. - -## Setup - -```shell -# Install the requirements -pip install -r requirements.txt - -# Setup your env -mv env.example .env -``` - -## Using Daily numbers - -Run `bot_runner.py` to handle incoming HTTP requests: - -`python bot_runner.py --host localhost` - -Then target the following URL: - -`POST /daily_start_bot` - -For more configuration options, please consult Daily's API documentation. - - -## Using Twilio numbers - -As above, but target the following URL: - -`POST /twilio_start_bot` - -For more configuration options, please consult Twilio's API documentation. - -## Deployment example - -A Dockerfile is included in this demo for convenience. Here is an example of how to build and deploy your bot to [fly.io](https://fly.io). - -*Please note: This demo spawns agents as subprocesses for convenience / demonstration purposes. You would likely not want to do this in production as it would limit concurrency to available system resources. For more information on how to deploy your bots using VMs, refer to the Pipecat documentation.* - -### Build the docker image - -`docker build -t tag:project .` - -### Launch the fly project - -`mv fly.example.toml fly.toml` - -`fly launch` (using the included fly.toml) - -### Setup your secrets on Fly - -Set the necessary secrets (found in `env.example`) - -`fly secrets set DAILY_API_KEY=... OPENAI_API_KEY=... ELEVENLABS_API_KEY=... ELEVENLABS_VOICE_ID=...` - -If you're using Twilio as a number vendor: - -`fly secrets set TWILIO_ACCOUNT_SID=... TWILIO_AUTH_TOKEN=...` - -### Deploy! - -`fly deploy` - -## Need to do something more advanced? - -This demo covers the basics of bot telephony. If you want to know more about working with PSTN / SIP, please ping us on [Discord](https://discord.gg/pipecat). \ No newline at end of file diff --git a/examples/dialin-chatbot/.dockerignore b/examples/phone-chatbot/.dockerignore similarity index 100% rename from examples/dialin-chatbot/.dockerignore rename to examples/phone-chatbot/.dockerignore diff --git a/examples/dialin-chatbot/.gitignore b/examples/phone-chatbot/.gitignore similarity index 100% rename from examples/dialin-chatbot/.gitignore rename to examples/phone-chatbot/.gitignore diff --git a/examples/dialin-chatbot/Dockerfile b/examples/phone-chatbot/Dockerfile similarity index 100% rename from examples/dialin-chatbot/Dockerfile rename to examples/phone-chatbot/Dockerfile diff --git a/examples/phone-chatbot/README.md b/examples/phone-chatbot/README.md new file mode 100644 index 000000000..2d0fd096a --- /dev/null +++ b/examples/phone-chatbot/README.md @@ -0,0 +1,149 @@ +
pipecat +
+ +# Phone Chatbot + +Example project that demonstrates how to add phone funtionality to your Pipecat bots. We include examples for Daily (`bot_daily.py`) dial-in and dial-out, and Twilio (`bot_twilio.py`) dial-in, depending on who you want to use as a phone vendor. + +- 🔁 Transport: Daily WebRTC +- 💬 Speech-to-Text: Deepgram via Daily transport +- 🤖 LLM: GPT4-o / OpenAI +- 🔉 Text-to-Speech: ElevenLabs + +#### Should I use Daily or Twilio as a vendor? + +If you're starting from scratch, using Daily to provision phone numbers alongside Daily as a transport offers some convenience (such as automatic call forwarding.) + +If you already have Twilio numbers and workflows that you want to connect to your Pipecat bots, there is some additional configuration required (you'll need to create a `on_dialin_ready` and use the Twilio client to trigger the forward.) + +You can read more about this, as well as see respective walkthroughs in our docs. + +## Setup + +1. Create and activate a virtual environment: + ```shell + python3 -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` +2. Install requirements: + ```shell + pip install -r requirements.txt + ``` +3. Copy env.example to .env and configure: + ```shell + cp env.example .env + ``` +4. Install [ngrok](https://ngrok.com/) so your local server can receive requests from Daily's servers. + +## Using Daily numbers + +### Running the example + +To run either the dial-in or dial-out example, follow these steps to get started: + +1. Run `bot_runner.py` to handle incoming HTTP requests: + + ```shell + python bot_runner.py --host localhost + ``` + +2. Start ngrok running in a terminal window: + + ```shell + ngrok http --domain yourdomain.ngrok.app 8000 + ``` + +3. In a different terminal window, run the Daily bot file: + ```shell + python bot_daily.py + ``` + +### Dial-in + +To dial-in to the bot, you will need to enable dial-in for your Daily domain. Follow [this guide](https://docs.daily.co/guides/products/dial-in-dial-out/dialin-pinless#provisioning-sip-interconnect-and-pinless-dialin-workflow) to set up your domain. + +Note: For the `room_creation_api` property, point at your ngrok hostname: `"room_creation_api": "https://yourdomain.ngrok.app/daily_start_bot"`. + +Once your domain is configured, receiving a phone call at a number associated with your Daily account will result in a POST to the `/daily_start_bot` endpoint, which will start a bot session. + +### Dial-out + +For the bot to dial out to a number, make a POST request to `/daily_start_bot` and include the dial-out phone number in the body of the request as `dialoutNumber`. + +For example: + +```shell +url -X "POST" "http://localhost:7860/daily_start_bot" \ + -H 'Content-Type: application/json; charset=utf-8' \ + -d $'{ + "dialoutNumber": "+12125551234" +}' +``` + +### More information + +For more configuration options, please consult [Daily's API documentation](https://docs.daily.co). + +## Using Twilio numbers + +### Running the example + +Follow these steps to get started: + +1. Run `bot_runner.py` to handle incoming HTTP requests: + + ```shell + python bot_runner.py --host localhost + ``` + +2. Start ngrok running in a terminal window: + + ```shell + ngrok http --domain yourdomain.ngrok.app 8000 + ``` + +3. In a different terminal window, run the Daily bot file: + ```shell + python bot_twilio.py + ``` + +As above, but target the following URL: + +`POST /twilio_start_bot` + +For more configuration options, please consult Twilio's API documentation. + +## Deployment example + +A Dockerfile is included in this demo for convenience. Here is an example of how to build and deploy your bot to [fly.io](https://fly.io). + +_Please note: This demo spawns agents as subprocesses for convenience / demonstration purposes. You would likely not want to do this in production as it would limit concurrency to available system resources. For more information on how to deploy your bots using VMs, refer to the Pipecat documentation._ + +### Build the docker image + +`docker build -t tag:project .` + +### Launch the fly project + +`mv fly.example.toml fly.toml` + +`fly launch` (using the included fly.toml) + +### Setup your secrets on Fly + +Set the necessary secrets (found in `env.example`) + +`fly secrets set DAILY_API_KEY=... OPENAI_API_KEY=... ELEVENLABS_API_KEY=... ELEVENLABS_VOICE_ID=...` + +If you're using Twilio as a number vendor: + +`fly secrets set TWILIO_ACCOUNT_SID=... TWILIO_AUTH_TOKEN=...` + +### Deploy! + +`fly deploy` + +## Need to do something more advanced? + +This demo covers the basics of bot telephony. If you want to know more about working with PSTN / SIP, please ping us on [Discord](https://discord.gg/pipecat)! diff --git a/examples/dialin-chatbot/bot_daily.py b/examples/phone-chatbot/bot_daily.py similarity index 57% rename from examples/dialin-chatbot/bot_daily.py rename to examples/phone-chatbot/bot_daily.py index b3e4a31b7..81913e87a 100644 --- a/examples/dialin-chatbot/bot_daily.py +++ b/examples/phone-chatbot/bot_daily.py @@ -25,12 +25,11 @@ daily_api_key = os.getenv("DAILY_API_KEY", "") daily_api_url = os.getenv("DAILY_API_URL", "https://api.daily.co/v1") -async def main(room_url: str, token: str, callId: str, callDomain: str): - # diallin_settings are only needed if Daily's SIP URI is used +async def main(room_url: str, token: str, callId: str, callDomain: str, dialout_number: str | None): + # 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. - diallin_settings = DailyDialinSettings(call_id=callId, call_domain=callDomain) - + dialin_settings = DailyDialinSettings(call_id=callId, call_domain=callDomain) transport = DailyTransport( room_url, token, @@ -38,7 +37,7 @@ async def main(room_url: str, token: str, callId: str, callDomain: str): DailyParams( api_url=daily_api_url, api_key=daily_api_key, - dialin_settings=diallin_settings, + dialin_settings=dialin_settings, audio_in_enabled=True, audio_out_enabled=True, camera_out_enabled=False, @@ -58,7 +57,7 @@ async def main(room_url: str, token: str, callId: str, callDomain: str): 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 'Oh, hello! Who dares dial me at this hour?!'.", + "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 'Oh, hello! I'm a friendly chatbot. How can I help you?'.", }, ] @@ -78,10 +77,41 @@ async def main(room_url: str, token: str, callId: str, callDomain: str): task = PipelineTask(pipeline, 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()]) + if dialout_number: + logger.debug("dialout number detected; doing dialout") + + # Configure some handlers for dialing out + @transport.event_handler("on_joined") + async def on_joined(transport, data): + logger.debug(f"Joined; starting dialout to: {dialout_number}") + await transport.start_dialout({"phoneNumber": dialout_number}) + + @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_answered") + async def on_dialout_answered(transport, data): + logger.debug(f"Dial-out answered: {data}") + + @transport.event_handler("on_first_participant_joined") + async def on_first_participant_joined(transport, participant): + await transport.capture_participant_transcription(participant["id"]) + # unlike the dialin case, for the dialout case, the caller will speak first. Presumably + # they will answer the phone and say "Hello?" Since we've captured their transcript, + # That will put a frame into the pipeline and prompt an LLM completion, which is how the + # bot will then greet the user. + + else: + logger.debug("no dialout number; assuming dialin") + + # Different handlers for dialin + @transport.event_handler("on_first_participant_joined") + async def on_first_participant_joined(transport, participant): + await transport.capture_participant_transcription(participant["id"]) + # For the dialin case, we want the bot to answer the phone and greet the user. We + # can prompt the bot to speak by putting the context into the pipeline. + await task.queue_frames([context_aggregator.user().get_context_frame()]) @transport.event_handler("on_participant_left") async def on_participant_left(transport, participant, reason): @@ -98,6 +128,7 @@ if __name__ == "__main__": parser.add_argument("-t", type=str, help="Token") parser.add_argument("-i", type=str, help="Call ID") parser.add_argument("-d", type=str, help="Call Domain") + parser.add_argument("-o", type=str, help="Dialout number", default=None) config = parser.parse_args() - asyncio.run(main(config.u, config.t, config.i, config.d)) + asyncio.run(main(config.u, config.t, config.i, config.d, config.o)) diff --git a/examples/dialin-chatbot/bot_runner.py b/examples/phone-chatbot/bot_runner.py similarity index 89% rename from examples/dialin-chatbot/bot_runner.py rename to examples/phone-chatbot/bot_runner.py index 050ad62b9..ebdd1e3ea 100644 --- a/examples/dialin-chatbot/bot_runner.py +++ b/examples/phone-chatbot/bot_runner.py @@ -73,24 +73,27 @@ action using the Twilio Client library. """ -async def _create_daily_room(room_url, callId, callDomain=None, vendor="daily"): +async def _create_daily_room(room_url, callId, callDomain=None, dialoutNumber=None, vendor="daily"): if not room_url: - params = DailyRoomParams( - properties=DailyRoomProperties( - # Note: these are the default values, except for the display name - sip=DailyRoomSipParams( - display_name="dialin-user", video=False, sip_mode="dial-in", num_endpoints=1 - ) + # Create base properties with SIP settings + properties = DailyRoomProperties( + sip=DailyRoomSipParams( + display_name="dialin-user", video=False, sip_mode="dial-in", num_endpoints=1 ) ) + # Only enable dialout if dialoutNumber is provided + if dialoutNumber: + properties.enable_dialout = True + + params = DailyRoomParams(properties=properties) + print(f"Creating new room...") room: DailyRoomObject = await daily_helpers["rest"].create_room(params=params) else: # Check passed room URL exist (we assume that it already has a sip set up!) try: - print(f"Joining existing room: {room_url}") room: DailyRoomObject = await daily_helpers["rest"].get_room_from_url(room_url) except Exception: raise HTTPException(status_code=500, detail=f"Room not found: {room_url}") @@ -107,6 +110,8 @@ async def _create_daily_room(room_url, callId, callDomain=None, vendor="daily"): # Note: this is mostly for demonstration purposes (refer to 'deployment' in docs) if vendor == "daily": bot_proc = f"python3 -m bot_daily -u {room.url} -t {token} -i {callId} -d {callDomain}" + if dialoutNumber: + bot_proc += f" -o {dialoutNumber}" else: bot_proc = f"python3 -m bot_twilio -u {room.url} -t {token} -i {callId} -s {room.config.sip_endpoint}" @@ -179,11 +184,15 @@ async def daily_start_bot(request: Request) -> JSONResponse: return JSONResponse({"test": True}) callId = data.get("callId", None) callDomain = data.get("callDomain", None) + dialoutNumber = data.get("dialoutNumber", None) except Exception: - raise HTTPException(status_code=500, detail="Missing properties 'callId' or 'callDomain'") + raise HTTPException( + status_code=500, detail="Missing properties 'callId', 'callDomain', or 'dialoutNumber'" + ) - print(f"CallId: {callId}, CallDomain: {callDomain}") - room: DailyRoomObject = await _create_daily_room(room_url, callId, callDomain, "daily") + room: DailyRoomObject = await _create_daily_room( + room_url, callId, callDomain, dialoutNumber, "daily" + ) # Grab a token for the user to join with return JSONResponse({"room_url": room.url, "sipUri": room.config.sip_endpoint}) diff --git a/examples/dialin-chatbot/bot_twilio.py b/examples/phone-chatbot/bot_twilio.py similarity index 100% rename from examples/dialin-chatbot/bot_twilio.py rename to examples/phone-chatbot/bot_twilio.py diff --git a/examples/dialin-chatbot/env.example b/examples/phone-chatbot/env.example similarity index 100% rename from examples/dialin-chatbot/env.example rename to examples/phone-chatbot/env.example diff --git a/examples/dialin-chatbot/fly.example.toml b/examples/phone-chatbot/fly.example.toml similarity index 100% rename from examples/dialin-chatbot/fly.example.toml rename to examples/phone-chatbot/fly.example.toml diff --git a/examples/dialin-chatbot/image.png b/examples/phone-chatbot/image.png similarity index 100% rename from examples/dialin-chatbot/image.png rename to examples/phone-chatbot/image.png diff --git a/examples/dialin-chatbot/requirements.txt b/examples/phone-chatbot/requirements.txt similarity index 100% rename from examples/dialin-chatbot/requirements.txt rename to examples/phone-chatbot/requirements.txt