Build Your First Voice Agent using Twilio

View as Markdown

Overview

This guide shows you how to build a real-time voice agent that answers phone calls. It uses Twilio for telephony, Pipecat to orchestrate the real-time audio pipeline, and Sarvam AI for speech-to-text, the LLM, and text-to-speech. It’s a great starting point for IVR replacements, customer support lines, and conversational phone agents in Indian languages.

What You’ll Build

A voice agent that can:

  • Answer inbound phone calls on a Twilio number
  • Listen to callers speaking, in multiple Indian languages
  • Understand and process what they say
  • Respond back in a natural-sounding voice, over the phone

Quick Overview

  1. Get API keys (Twilio, Sarvam)
  2. Install packages: pip install "pipecat-ai[websocket,sarvam]" python-dotenv
  3. Create a .env file with your API keys
  4. Write about 80 lines of Python code
  5. Point a Twilio phone number at your agent using TwiML
  6. Call your Twilio number

Quick Start

1. Prerequisites

  • Python 3.9 or higher
  • ngrok, to expose your local server during development
  • Accounts and keys from:
    • Twilio: an Account SID, an Auth Token, and a phone number with voice capability
    • Sarvam AI: an API key from your dashboard

2. Install Dependencies

$pip install "pipecat-ai[websocket,sarvam]" python-dotenv loguru

3. Create Environment File

Create a file named .env in your project folder:

SARVAM_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Then replace each placeholder with your real credentials from the Sarvam dashboard and the Twilio Console.

TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN aren’t optional here. Pipecat’s Twilio transport uses them to authenticate with Twilio’s REST API and to hang up the call cleanly when your agent ends the conversation.

4. Write Your Agent

Create agent.py:

1import os
2from dotenv import load_dotenv
3from loguru import logger
4from pipecat.frames.frames import LLMRunFrame
5from pipecat.pipeline.pipeline import Pipeline
6from pipecat.pipeline.runner import PipelineRunner
7from pipecat.pipeline.task import PipelineParams, PipelineTask
8from pipecat.processors.aggregators.llm_context import LLMContext
9from pipecat.processors.aggregators.llm_response_universal import (
10 LLMContextAggregatorPair,
11)
12from pipecat.runner.types import RunnerArguments
13from pipecat.runner.utils import create_transport
14from pipecat.services.sarvam.stt import SarvamSTTService
15from pipecat.services.sarvam.tts import SarvamTTSService
16from pipecat.services.sarvam.llm import SarvamLLMService
17from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
18
19load_dotenv(override=True)
20
21async def bot(runner_args: RunnerArguments):
22 """Main bot entry point."""
23
24 # create_transport auto-detects Twilio's WebSocket handshake and builds
25 # the matching TwilioFrameSerializer, using TWILIO_ACCOUNT_SID and
26 # TWILIO_AUTH_TOKEN from the environment.
27 transport = await create_transport(
28 runner_args,
29 {
30 "twilio": lambda: FastAPIWebsocketParams(
31 audio_in_enabled=True, audio_out_enabled=True
32 ),
33 },
34 )
35
36 # Initialize AI services
37 stt = SarvamSTTService(api_key=os.getenv("SARVAM_API_KEY"))
38 tts = SarvamTTSService(api_key=os.getenv("SARVAM_API_KEY"))
39 llm = SarvamLLMService(
40 api_key=os.getenv("SARVAM_API_KEY"),
41 settings=SarvamLLMService.Settings(model="sarvam-105b"),
42 )
43
44 # Set up conversation context
45 messages = [
46 {
47 "role": "system",
48 "content": "You are a friendly AI phone assistant. Keep your responses brief and conversational.",
49 },
50 ]
51 context = LLMContext(messages)
52 context_aggregator = LLMContextAggregatorPair(context)
53
54 # Build pipeline
55 pipeline = Pipeline(
56 [
57 transport.input(),
58 stt,
59 context_aggregator.user(),
60 llm,
61 tts,
62 transport.output(),
63 context_aggregator.assistant(),
64 ]
65 )
66
67 # Twilio Media Streams send and receive 8kHz mono audio
68 task = PipelineTask(
69 pipeline,
70 params=PipelineParams(
71 audio_in_sample_rate=8000,
72 audio_out_sample_rate=8000,
73 ),
74 )
75
76 @transport.event_handler("on_client_connected")
77 async def on_client_connected(transport, client):
78 logger.info("Caller connected")
79 messages.append(
80 {"role": "system", "content": "Greet the caller and briefly introduce yourself."}
81 )
82 await task.queue_frames([LLMRunFrame()])
83
84 @transport.event_handler("on_client_disconnected")
85 async def on_client_disconnected(transport, client):
86 logger.info("Caller disconnected")
87 await task.cancel()
88
89 runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
90 await runner.run(task)
91
92if __name__ == "__main__":
93 from pipecat.runner.run import main
94 main()

5. Configure Twilio to Reach Your Agent

Twilio needs a public wss:// URL to stream call audio to. For local development, expose your agent with ngrok.

Start ngrok:

$ngrok http 7860

ngrok prints a forwarding URL that looks like https://xxxx.ngrok-free.app. You’ll turn this into a wss:// URL in the next step.

The domain suffix ngrok assigns varies per account — you may see ngrok-free.app, ngrok-free.dev, or the older ngrok.io, regardless of whether you’re on macOS, Windows, or Linux. Always use whatever URL ngrok actually prints, not the placeholder shown here.

Free ngrok URLs change every time you restart ngrok. If your agent stops receiving calls, check whether the URL in your TwiML Bin still matches the one ngrok is currently printing.

Create a TwiML Bin:

TwiML Bins aren’t in the Twilio Console’s left sidebar by default. The fastest way to reach them is to click the search bar at the top of the console and type “TwiML Bins”.

  1. Open the Twilio Console and search for TwiML Bins using the search bar at the top (or find it under Developer Tools in the sidebar)
  2. Click Create new TwiML Bin, give it a friendly name, and paste the following, replacing the domain with your own ngrok URL:
1<?xml version="1.0" encoding="UTF-8"?>
2<Response>
3 <Connect>
4 <Stream url="wss://xxxx.ngrok-free.app/ws" />
5 </Connect>
6</Response>
  1. Click Create to save the bin

Assign it to your phone number:

  1. Go to Numbers & Senders, then Phone Numbers, and select your number
  2. Open its configuration/voice settings tab
  3. Find the setting for what happens when a call comes in, and set it to use your TwiML Bin (Twilio has labeled this option TwiML, TwiML Bin, or similar depending on your console version)
  4. Save

Twilio has redesigned this page’s tabs and section names more than once (you may see Configuration, Configure, or Configuration Details; Voice & Fax, Voice Configuration, or Voice and emergency calling). The setting you want is always the one controlling what happens when a call comes in. If you can’t find it, Twilio’s own Getting Started with TwiML Bins guide has current screenshots.

6. Run Your Agent

$python agent.py --transport twilio

This starts a local FastAPI server, via Pipecat’s development runner, that accepts Twilio’s Media Streams WebSocket connection at /ws.

7. Test Your Agent

Call your Twilio phone number from any phone. Your voice agent will pick up and start the conversation.


Customization Examples

Example 1: Hindi Voice Agent

1stt = SarvamSTTService(
2 api_key=os.getenv("SARVAM_API_KEY"),
3 settings=SarvamSTTService.Settings(
4 model="saaras:v3",
5 language="hi-IN", # Hindi
6 ),
7 mode="transcribe",
8)
9
10tts = SarvamTTSService(
11 api_key=os.getenv("SARVAM_API_KEY"),
12 settings=SarvamTTSService.Settings(
13 model="bulbul:v3",
14 voice="simran", # Or: priya, ishita, kavya, aditya, anand, rohan
15 target_language_code="hi-IN",
16 ),
17)
18
19llm = SarvamLLMService(
20 api_key=os.getenv("SARVAM_API_KEY"),
21 settings=SarvamLLMService.Settings(model="sarvam-105b"),
22)

language, voice, and target_language_code are Settings fields, not constructor keyword arguments. Passing them directly to SarvamSTTService(...) or SarvamTTSService(...) raises a TypeError on current versions of pipecat-ai. Only mode (STT) and api_key stay outside settings.

Example 2: Tamil Voice Agent

1stt = SarvamSTTService(
2 api_key=os.getenv("SARVAM_API_KEY"),
3 settings=SarvamSTTService.Settings(
4 model="saaras:v3",
5 language="ta-IN",
6 ),
7 mode="transcribe",
8)
9
10tts = SarvamTTSService(
11 api_key=os.getenv("SARVAM_API_KEY"),
12 settings=SarvamTTSService.Settings(
13 model="bulbul:v3",
14 voice="shubh",
15 target_language_code="ta-IN",
16 ),
17)
18
19llm = SarvamLLMService(
20 api_key=os.getenv("SARVAM_API_KEY"),
21 settings=SarvamLLMService.Settings(model="sarvam-105b"),
22)

Example 3: Multilingual Agent (Auto-detect)

1# Auto-detect the caller's language
2stt = SarvamSTTService(
3 api_key=os.getenv("SARVAM_API_KEY"),
4 settings=SarvamSTTService.Settings(
5 model="saaras:v3",
6 language="unknown", # Auto-detects language
7 ),
8 mode="transcribe",
9)
10
11tts = SarvamTTSService(
12 api_key=os.getenv("SARVAM_API_KEY"),
13 settings=SarvamTTSService.Settings(
14 model="bulbul:v3",
15 voice="anand",
16 target_language_code="en-IN",
17 ),
18)
19
20llm = SarvamLLMService(
21 api_key=os.getenv("SARVAM_API_KEY"),
22 settings=SarvamLLMService.Settings(model="sarvam-105b"),
23)

Use language="unknown" for support lines where callers might speak any of several languages. Sarvam auto-detects the spoken language per utterance, so the same agent can handle a Hindi caller followed by a Tamil caller without any code changes.

Example 4: Speech-to-English Agent (Saaras)

Saarika transcribes speech to text in the same language. Saaras translates speech directly to English text. Use Saaras when the caller speaks an Indian language but you want to process and respond in English.

1# Caller speaks Hindi, Saaras converts it to English, the LLM processes it,
2# and the agent responds in English.
3
4stt = SarvamSTTService(
5 api_key=os.getenv("SARVAM_API_KEY"),
6 settings=SarvamSTTService.Settings(model="saaras:v3"),
7 mode="translate", # Speech-to-English translation
8)
9
10tts = SarvamTTSService(
11 api_key=os.getenv("SARVAM_API_KEY"),
12 settings=SarvamTTSService.Settings(
13 model="bulbul:v3",
14 voice="aditya",
15 target_language_code="en-IN",
16 ),
17)
18
19llm = SarvamLLMService(
20 api_key=os.getenv("SARVAM_API_KEY"),
21 settings=SarvamLLMService.Settings(model="sarvam-105b"),
22)

Saaras auto-detects the source language (Hindi, Tamil, and so on) and translates spoken content directly to English text, which makes Indian-language speech usable by English-based LLMs.


Available Options

Language Codes

LanguageCode
English (India)en-IN
Hindihi-IN
Bengalibn-IN
Tamilta-IN
Telugute-IN
Gujaratigu-IN
Kannadakn-IN
Malayalamml-IN
Marathimr-IN
Punjabipa-IN
Odiaod-IN
Auto-detectunknown

Speaker Voices (Bulbul v3)

Male (23): Shubh (default), Aditya, Rahul, Rohan, Amit, Dev, Ratan, Varun, Manan, Sumit, Kabir, Aayan, Ashutosh, Advait, Anand, Tarun, Sunny, Mani, Gokul, Vijay, Mohit, Rehan, Soham

Female (14): Ritu, Priya, Neha, Pooja, Simran, Kavya, Ishita, Shreya, Roopa, Tanya, Shruti, Suhani, Kavitha, Rupali

TTS Additional Parameters

You can customize the TTS service with additional parameters:

1tts = SarvamTTSService(
2 api_key=os.getenv("SARVAM_API_KEY"),
3 settings=SarvamTTSService.Settings(
4 model="bulbul:v3",
5 voice="shubh",
6 target_language_code="en-IN",
7 pace=1.0, # Range: 0.5 to 2.0
8 ),
9)

Twilio Media Streams always carry 8kHz mono audio. You don’t need to touch sample_rate on SarvamTTSService for this: Pipecat resamples the TTS output to match the audio_out_sample_rate set in PipelineParams automatically.


Understanding the Call Flow

  1. Caller dials your Twilio number. Twilio answers using the TwiML Bin and opens a WebSocket (Media Stream) to your agent.
  2. Transport Input: Your Pipecat transport receives the caller’s audio over the WebSocket, and hands it to STT.
  3. STT (Speech-to-Text): Converts audio to text using Sarvam’s Saarika or Saaras, and the context aggregator adds it to the conversation context.
  4. LLM: Generates a response using Sarvam.
  5. TTS (Text-to-Speech): Converts the response to audio using Sarvam’s Bulbul.
  6. Transport Output: Streams the audio back over the WebSocket, and Twilio plays it to the caller, while the context aggregator saves the assistant’s response to context.

Pro Tips

  • Use language="unknown" to automatically detect the caller’s language. This works well for multilingual support lines, but transcription accuracy is slightly lower than when you specify the exact language code, so prefer an explicit code whenever you already know which language the caller will use.
  • Sarvam’s models understand code-mixing, so your agent can naturally handle Hinglish, Tanglish, and other mixed languages, which is common on real support calls.
  • Use sarvam-30b for faster responses, or sarvam-105b for more complex conversations.

Log the transcript from context_aggregator if you want post-call analytics. Writing it to a file or database is cheap and won’t slow down the live pipeline.


Troubleshooting

Call connects but there’s no audio. Confirm the TwiML <Stream url> uses wss://, not wss:/ or https://, and that it points at your current ngrok URL. Free ngrok URLs change on every restart.

Call drops immediately. Check that TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN are set correctly in .env. The Twilio serializer needs both to manage the call.

API key errors. Make sure all keys are in your .env file and that the file sits in the same directory as agent.py.

Module not found. Re-run the install command for your operating system (see Step 2 above).

Poor transcription. Try language="unknown" for auto-detection, or set the exact language code (en-IN, hi-IN, and so on) if you already know it.

Choppy or robotic audio. Make sure audio_in_sample_rate and audio_out_sample_rate are both set to 8000 in PipelineParams. Twilio Media Streams don’t support other rates, and a mismatch causes distorted playback.


Additional Resources


Need Help?


Happy Building!