Integrate conversational AI assistants with Twilio to create seamless voice experiences.
This demo illustrates how to integrate Neuphonic’s agent features with Twilio to create an interactive
and intelligent phone voice agent capable of handling both inbound and outbound phone calls.
In this section, you will learn how to set up a server that manages both incoming and outgoing calls.
The server will interact with Twilio to handle incoming audio and forward responses back to the user’s
phone using Twilio’s services.
Looking for a complete example? Check out this demo on GitHub which you can run instantly.
1
Initialize the Project
Create a folder to hold the python project.
Copy
mkdir twilio-agentcd twilio-agent
2
Create a Virtual Environment
Create a virtual environment using your desired environment manager.
Create and configure your .env file with the following secrets
Copy
TWILIO_AUTH_TOKEN=TWILIO_ACCOUNT_SID=# The base url of this server, e.g., if using ngrok it would similar to 123d-45-678-912-3.ngrok-free.appSERVER_BASE_URL=FROM_NUMBER=TO_NUMBER=NEUPHONIC_API_KEY=
TWILIO_AUTH_TOKEN and TWILIO_ACCOUNT_SID: Available in your Twilio portal
FROM_NUMBER: Your Twilio-purchased phone number
TO_NUMBER: Your personal phone number for receiving test calls
Leave SERVER_BASE_URL blank for now - we’ll fill this in later.
5
Create the Project Files
Create an app.py and make_outbound_call.py file with the following code
app.py
Copy
from dotenv import load_dotenvfrom starlette.websockets import WebSocketStateimport loggingimport jsonimport base64from pyneuphonic import Neuphonic, WebsocketEvents, AgentConfigfrom pyneuphonic.models import APIResponse, TTSResponseimport osfrom twilio.twiml.voice_response import VoiceResponse, Connectfrom fastapi.responses import HTMLResponsefrom fastapi import ( FastAPI, APIRouter, WebSocket, WebSocketDisconnect, Request)load_dotenv(override=True)app = FastAPI()router = APIRouter()@app.get('/ping')def ping(): return {'message': 'pong'}@app.api_route("/twilio/inbound_call", methods=["GET", "POST"])async def handle_incoming_call(request: Request): """Handle incoming call and return TwiML response.""" response = VoiceResponse() connect = Connect() connect.stream(url=f'wss://{os.getenv("SERVER_BASE_URL")}/twilio/agent') response.append(connect) return HTMLResponse(content=str(response), media_type="application/xml")@app.websocket('/twilio/agent')async def agent_websocket( websocket: WebSocket,): """Handles the WebSocket connection for a Twilio agent, allowing real-time voice conversation.""" await websocket.accept() stream_sid = None client = Neuphonic(api_key=os.getenv('NEUPHONIC_API_KEY')) neuphonic_agent_websocket = client.agents.AsyncWebsocketClient() async def on_message(message: APIResponse[TTSResponse]): """Handles messages that are returned from the Neuphonic server to the websocket client.""" if stream_sid is not None and message.data.type == 'audio_response': # Forward audio to the users's phone await websocket.send_json( { 'event': 'media', 'streamSid': stream_sid, 'media': { 'payload': base64.b64encode(message.data.audio).decode('utf-8') }, } ) elif message.data.type == 'user_transcript': logging.info(f'user_transcript: {message.data.text}') elif message.data.type == 'llm_response': logging.info(f'llm_response: {message.data.text}') elif message.data.type == 'stop_audio_response': # If the user interrupts then stop playing any currently queued audio if stream_sid is not None: await websocket.send_json( { 'event': 'clear', 'streamSid': stream_sid, } ) neuphonic_agent_websocket.on(WebsocketEvents.MESSAGE, on_message) await neuphonic_agent_websocket.open( agent_config=AgentConfig( incoming_sampling_rate=8000, return_sampling_rate=8000, incoming_encoding='pcm_mulaw', return_encoding='pcm_mulaw', voice_id='<VOICE_ID>', ) ) try: while websocket.client_state == WebSocketState.CONNECTED: message = await websocket.receive() if message is None or message['type'] == 'websocket.disconnect': continue data = json.loads(message['text']) if data['event'] == 'connected': logging.info(f'Connected Message received: {message}') if data['event'] == 'start': stream_sid = data['start']['streamSid'] if data['event'] == 'media': # Send incoming audio to the Neuphonic agent await neuphonic_agent_websocket.send( {'audio': data['media']['payload']} ) if data['event'] == 'closed': logging.info(f'Closed Message received: {message}') break except WebSocketDisconnect as e: logging.error(f'WebSocketDisconnect: {e}') except Exception as e: logging.error(f'Error occured: {e}') finally: await neuphonic_agent_websocket.close()if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
make_outbound_call.py
Copy
import osfrom twilio.rest import Clientfrom dotenv import load_dotenvload_dotenv(override=True)def make_outbound_call(): # Initialize the Twilio client with account credentials client = Client(os.getenv('TWILIO_ACCOUNT_SID'), os.getenv('TWILIO_AUTH_TOKEN')) # Construct the TwiML message to connect the call to the WebSocket endpoint twiml_message = f'<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="wss://{os.getenv("SERVER_BASE_URL")}/twilio/agent" /></Connect></Response>' # Create the call with the specified parameters call = client.calls.create( twiml=twiml_message, to=os.getenv('TO_NUMBER'), from_=os.getenv('FROM_NUMBER'), record=True, ) # Print the call SID print(call.sid)if __name__ == '__main__': # Execute the make_outbound_call function if the script is run directly make_outbound_call()
Copy the displayed URL (excluding the https:// prefix) and add it to your .env file as SERVER_BASE_URL. It will look something like 123d-45-678-912-3.ngrok-free.app.
Note that this is an ephemeral address, every time you restart the ngrok service using the above command, you will be given a new URL.
7
Start the Server
Start the FastAPI server using the following command:
Copy
python app.py
Your server is now running and ready to accept incoming calls and make outgoing calls.
8
Initiate an Outbound Call
Initiate a call from your Twilio number FROM_NUMBER to your personal number TO_NUMBER
using
Copy
python make_outbound_call.py
Enjoy chatting away!
Looking for a complete example? Check out this demo on GitHub which you can run instantly.
1
Initialize the Project
Create a folder to hold the python project.
Copy
mkdir twilio-agentcd twilio-agent
2
Create a Virtual Environment
Create a virtual environment using your desired environment manager.
Create and configure your .env file with the following secrets
Copy
TWILIO_AUTH_TOKEN=TWILIO_ACCOUNT_SID=# The base url of this server, e.g., if using ngrok it would similar to 123d-45-678-912-3.ngrok-free.appSERVER_BASE_URL=FROM_NUMBER=TO_NUMBER=NEUPHONIC_API_KEY=
TWILIO_AUTH_TOKEN and TWILIO_ACCOUNT_SID: Available in your Twilio portal
FROM_NUMBER: Your Twilio-purchased phone number
TO_NUMBER: Your personal phone number for receiving test calls
Leave SERVER_BASE_URL blank for now - we’ll fill this in later.
5
Create the Project Files
Create an app.py and make_outbound_call.py file with the following code
app.py
Copy
from dotenv import load_dotenvfrom starlette.websockets import WebSocketStateimport loggingimport jsonimport base64from pyneuphonic import Neuphonic, WebsocketEvents, AgentConfigfrom pyneuphonic.models import APIResponse, TTSResponseimport osfrom twilio.twiml.voice_response import VoiceResponse, Connectfrom fastapi.responses import HTMLResponsefrom fastapi import ( FastAPI, APIRouter, WebSocket, WebSocketDisconnect, Request)load_dotenv(override=True)app = FastAPI()router = APIRouter()@app.get('/ping')def ping(): return {'message': 'pong'}@app.api_route("/twilio/inbound_call", methods=["GET", "POST"])async def handle_incoming_call(request: Request): """Handle incoming call and return TwiML response.""" response = VoiceResponse() connect = Connect() connect.stream(url=f'wss://{os.getenv("SERVER_BASE_URL")}/twilio/agent') response.append(connect) return HTMLResponse(content=str(response), media_type="application/xml")@app.websocket('/twilio/agent')async def agent_websocket( websocket: WebSocket,): """Handles the WebSocket connection for a Twilio agent, allowing real-time voice conversation.""" await websocket.accept() stream_sid = None client = Neuphonic(api_key=os.getenv('NEUPHONIC_API_KEY')) neuphonic_agent_websocket = client.agents.AsyncWebsocketClient() async def on_message(message: APIResponse[TTSResponse]): """Handles messages that are returned from the Neuphonic server to the websocket client.""" if stream_sid is not None and message.data.type == 'audio_response': # Forward audio to the users's phone await websocket.send_json( { 'event': 'media', 'streamSid': stream_sid, 'media': { 'payload': base64.b64encode(message.data.audio).decode('utf-8') }, } ) elif message.data.type == 'user_transcript': logging.info(f'user_transcript: {message.data.text}') elif message.data.type == 'llm_response': logging.info(f'llm_response: {message.data.text}') elif message.data.type == 'stop_audio_response': # If the user interrupts then stop playing any currently queued audio if stream_sid is not None: await websocket.send_json( { 'event': 'clear', 'streamSid': stream_sid, } ) neuphonic_agent_websocket.on(WebsocketEvents.MESSAGE, on_message) await neuphonic_agent_websocket.open( agent_config=AgentConfig( incoming_sampling_rate=8000, return_sampling_rate=8000, incoming_encoding='pcm_mulaw', return_encoding='pcm_mulaw', voice_id='<VOICE_ID>', ) ) try: while websocket.client_state == WebSocketState.CONNECTED: message = await websocket.receive() if message is None or message['type'] == 'websocket.disconnect': continue data = json.loads(message['text']) if data['event'] == 'connected': logging.info(f'Connected Message received: {message}') if data['event'] == 'start': stream_sid = data['start']['streamSid'] if data['event'] == 'media': # Send incoming audio to the Neuphonic agent await neuphonic_agent_websocket.send( {'audio': data['media']['payload']} ) if data['event'] == 'closed': logging.info(f'Closed Message received: {message}') break except WebSocketDisconnect as e: logging.error(f'WebSocketDisconnect: {e}') except Exception as e: logging.error(f'Error occured: {e}') finally: await neuphonic_agent_websocket.close()if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
make_outbound_call.py
Copy
import osfrom twilio.rest import Clientfrom dotenv import load_dotenvload_dotenv(override=True)def make_outbound_call(): # Initialize the Twilio client with account credentials client = Client(os.getenv('TWILIO_ACCOUNT_SID'), os.getenv('TWILIO_AUTH_TOKEN')) # Construct the TwiML message to connect the call to the WebSocket endpoint twiml_message = f'<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="wss://{os.getenv("SERVER_BASE_URL")}/twilio/agent" /></Connect></Response>' # Create the call with the specified parameters call = client.calls.create( twiml=twiml_message, to=os.getenv('TO_NUMBER'), from_=os.getenv('FROM_NUMBER'), record=True, ) # Print the call SID print(call.sid)if __name__ == '__main__': # Execute the make_outbound_call function if the script is run directly make_outbound_call()
Copy the displayed URL (excluding the https:// prefix) and add it to your .env file as SERVER_BASE_URL. It will look something like 123d-45-678-912-3.ngrok-free.app.
Note that this is an ephemeral address, every time you restart the ngrok service using the above command, you will be given a new URL.
7
Start the Server
Start the FastAPI server using the following command:
Copy
python app.py
Your server is now running and ready to accept incoming calls and make outgoing calls.
8
Initiate an Outbound Call
Initiate a call from your Twilio number FROM_NUMBER to your personal number TO_NUMBER
using
Copy
python make_outbound_call.py
Enjoy chatting away!
The JavaScript guide is coming soon! Contact us at support@neuphonic.com if you need help
in the meantime.