Files
pipecat/examples/phone-chatbot/daily-pstn-dial-in/bot.py

220 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""simple_dialin.py.
Daily PSTN Dial-in Bot.
"""
import argparse
import asyncio
import json
import os
import sys
from dotenv import load_dotenv
from loguru import logger
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.cartesia.tts import CartesiaTTSService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.services.daily import DailyDialinSettings, DailyParams, DailyTransport
# Setup logging
load_dotenv()
logger.remove(0)
logger.add(sys.stderr, level="DEBUG")
daily_api_key = os.getenv("DAILY_API_KEY", "")
daily_api_url = os.getenv("DAILY_API_URL", "https://api.daily.co/v1")
async def run_bot(
room_url: str,
token: str,
body: dict,
) -> None:
"""Run the voice bot with the given parameters.
Args:
room_url: The Daily room URL
token: The Daily room token
body: Body passed to the bot from the webhook
"""
# ------------ CONFIGURATION AND SETUP ------------
logger.info(f"Starting bot with room: {room_url}")
logger.info(f"Token: {token}")
logger.info(f"Body: {body}")
# Parse the body to get the dial-in settings
body_data = json.loads(body)
# Check if the body contains dial-in settings
logger.debug(f"Body data: {body_data}")
if not all([body_data.get("callId"), body_data.get("callDomain")]):
logger.error("Call ID and Call Domain are required in the body.")
return None
call_id = body_data.get("callId")
call_domain = body_data.get("callDomain")
logger.debug(f"Call ID: {call_id}")
logger.debug(f"Call Domain: {call_domain}")
if not call_id or not call_domain:
logger.error("Call ID and Call Domain are required for dial-in.")
sys.exit(1)
daily_dialin_settings = DailyDialinSettings(call_id=call_id, call_domain=call_domain)
logger.debug(f"Dial-in settings: {daily_dialin_settings}")
transport_params = DailyParams(
api_url=daily_api_url,
api_key=daily_api_key,
dialin_settings=daily_dialin_settings,
audio_in_enabled=True,
audio_out_enabled=True,
video_out_enabled=False,
vad_analyzer=SileroVADAnalyzer(),
transcription_enabled=True,
)
logger.debug("setup transport params")
# Initialize transport with Daily
transport = DailyTransport(
room_url,
token,
"Simple Dial-in Bot",
transport_params,
)
logger.debug("setup transport")
# Initialize TTS
tts = CartesiaTTSService(
api_key=os.getenv("CARTESIA_API_KEY", ""),
voice_id="b7d50908-b17c-442d-ad8d-810c63997ed9", # Use Helpful Woman voice by default
)
logger.debug("setup tts")
# ------------ LLM AND CONTEXT SETUP ------------
# Set up the system instruction for the LLM
# Initialize LLM
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
logger.debug("setup llm")
# Initialize LLM context with system prompt
messages = [
{
"role": "system",
"content": (
"You are a friendly phone assistant. Your responses will be read aloud, "
"so keep them concise and conversational. Avoid special characters or "
"formatting. Begin by greeting the caller and asking how you can help them today."
),
},
]
# Setup the conversational context
context = OpenAILLMContext(messages)
logger.debug("setup context")
context_aggregator = llm.create_context_aggregator(context)
logger.debug("setup context aggregator")
# ------------ PIPELINE SETUP ------------
# Build the pipeline
pipeline = Pipeline(
[
transport.input(), # Transport user input
context_aggregator.user(), # User responses
llm, # LLM
tts, # TTS
transport.output(), # Transport bot output
context_aggregator.assistant(), # Assistant spoken responses
]
)
logger.debug("setup pipeline")
# Create pipeline task
task = PipelineTask(
pipeline,
params=PipelineParams(
enable_metrics=True,
enable_usage_metrics=True,
),
)
logger.debug("setup task")
# ------------ EVENT HANDLERS ------------
@transport.event_handler("on_first_participant_joined")
async def on_first_participant_joined(transport, participant):
logger.debug(f"First participant joined: {participant['id']}")
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):
logger.debug(f"Participant left: {participant}, reason: {reason}")
await task.cancel()
@transport.event_handler("on_dialin_ready")
async def on_dialin_ready(transport, cdata):
logger.debug(f"Dial-in ready: {cdata}")
@transport.event_handler("on_dialin_connected")
async def on_dialin_connected(transport, data):
logger.debug(f"Dial-in connected: {data}")
@transport.event_handler("on_dialin_stopped")
async def on_dialin_stopped(transport, data):
logger.debug(f"Dial-in stopped: {data}")
@transport.event_handler("on_dialin_error")
async def on_dialin_error(transport, data):
logger.error(f"Dial-in error: {data}")
# If there is an error, the bot should leave the call
# This may be also handled in on_participant_left with
# await task.cancel()
@transport.event_handler("on_dialin_warning")
async def on_dialin_warning(transport, data):
logger.warning(f"Dial-in warning: {data}")
# Run the pipeline
runner = PipelineRunner()
await runner.run(task)
async def main():
"""Parse command line arguments and run the bot."""
parser = argparse.ArgumentParser(description="Simple Dial-in Bot")
parser.add_argument("-u", "--url", type=str, help="Daily room URL")
parser.add_argument("-t", "--token", type=str, help="Daily room token")
parser.add_argument("-b", "--body", type=str, help="JSON configuration string")
args = parser.parse_args()
logger.debug(f"url: {args.url}")
logger.debug(f"token: {args.token}")
logger.debug(f"body: {args.body}")
if not all([args.url, args.token, args.body]):
logger.error("All arguments (-u, -t, -b) are required")
parser.print_help()
sys.exit(1)
await run_bot(args.url, args.token, args.body)
if __name__ == "__main__":
asyncio.run(main())