"""Twilio + Daily voice bot implementation.""" 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.cartesia.tts import CartesiaTTSService from pipecat.services.openai.llm import OpenAILLMService from pipecat.transports.services.daily import DailyParams, DailyTransport # Setup logging load_dotenv() logger.remove(0) logger.add(sys.stderr, level="DEBUG") # Initialize Twilio client twilio_client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN")) async def run_bot(room_url: str, token: str, call_id: str, sip_uri: str) -> None: """Run the voice bot with the given parameters. Args: room_url: The Daily room URL token: The Daily room token call_id: The Twilio call ID sip_uri: The Daily SIP URI for forwarding the call """ logger.info(f"Starting bot with room: {room_url}") logger.info(f"SIP endpoint: {sip_uri}") call_already_forwarded = False # Setup the Daily transport transport = DailyTransport( room_url, token, "Phone Bot", DailyParams( audio_in_enabled=True, audio_out_enabled=True, transcription_enabled=True, vad_analyzer=SileroVADAnalyzer(), ), ) # Setup TTS service tts = CartesiaTTSService( api_key=os.getenv("CARTESIA_API_KEY"), voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady ) # Setup LLM service llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) # 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) context_aggregator = llm.create_context_aggregator(context) # Build the pipeline pipeline = Pipeline( [ transport.input(), context_aggregator.user(), llm, tts, transport.output(), context_aggregator.assistant(), ] ) # Create the pipeline task task = PipelineTask( pipeline, params=PipelineParams( allow_interruptions=True # Enable barge-in so callers can interrupt the bot ), ) # Handle participant joining @transport.event_handler("on_first_participant_joined") async def on_first_participant_joined(transport, participant): logger.info(f"First participant joined: {participant['id']}") await transport.capture_participant_transcription(participant["id"]) await task.queue_frames([context_aggregator.user().get_context_frame()]) # Handle participant leaving @transport.event_handler("on_participant_left") async def on_participant_left(transport, participant, reason): logger.info(f"Participant left: {participant['id']}, reason: {reason}") await task.cancel() # Handle call ready to forward @transport.event_handler("on_dialin_ready") async def on_dialin_ready(transport, cdata): nonlocal call_already_forwarded # We only want to forward the call once # The on_dialin_ready event will be triggered for each sip endpoint provisioned if call_already_forwarded: logger.warning("Call already forwarded, ignoring this event.") return logger.info(f"Forwarding call {call_id} to {sip_uri}") try: # Update the Twilio call with TwiML to forward to the Daily SIP endpoint twilio_client.calls(call_id).update( twiml=f"{sip_uri}" ) logger.info("Call forwarded successfully") call_already_forwarded = True except Exception as e: logger.error(f"Failed to forward call: {str(e)}") raise @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="Daily + Twilio Voice Bot") parser.add_argument("-u", type=str, required=True, help="Daily room URL") parser.add_argument("-t", type=str, required=True, help="Daily room token") parser.add_argument("-i", type=str, required=True, help="Twilio call ID") parser.add_argument("-s", type=str, required=True, help="Daily SIP URI") args = parser.parse_args() # Validate required arguments if not all([args.u, args.t, args.i, args.s]): logger.error("All arguments (-u, -t, -i, -s) are required") parser.print_help() sys.exit(1) await run_bot(args.u, args.t, args.i, args.s) if __name__ == "__main__": asyncio.run(main())