TAC Voice Live on Azure Container Apps¶
Complete guide for deploying Twilio Agent Connect (TAC) with Azure AI Foundry Voice Live on Azure Container Apps.
Table of Contents¶
Overview¶
This deployment runs a voice-only AI agent using:
- Twilio — Voice via ConversationRelay (STT/TTS), plus optional Conversation Memory / Conversation Orchestrator
- Azure AI Foundry Voice Live — Low-latency LLM inference via WebSocket (pre-existing; not provisioned by this Bicep)
- TAC (Twilio Agent Connect) — Integration middleware
Voice Live manages conversation state server-side. The TAC server acts as a bridge between Twilio's ConversationRelay and Voice Live's WebSocket API, operating in text-only mode (modalities: ["text"]) because ConversationRelay handles STT/TTS.
Memory retrieval is opt-in — the sample voice_live/basic.py does not enable it. Set VoiceChannelConfig(memory_mode="always") to have TAC pull from the Conversation Memory (with fallback to Conversation Orchestrator) before each utterance.
Architecture¶
graph LR
Customer([Customer])
subgraph Twilio["Twilio"]
CRelay[ConversationRelay]
Memory[Conversation Memory]
end
subgraph Bicep["Azure — provisioned by Bicep"]
CA[Container App<br/>TAC Server]
end
subgraph Existing["Azure — pre-existing"]
VoiceLive[Azure AI Foundry<br/>Voice Live]
end
Customer <--> CRelay
CRelay <-->|voice text| CA
CA <-->|WebSocket<br/>text-only| VoiceLive
CA -.->|optional| Memory
Flow:
- Twilio Phone receives call →
POST /twiml→ response TwiML contains<ConversationRelay>→ ConversationRelay handles STT/TTS and opens a WebSocket to/wsfor bidirectional text. - TAC opens a Voice Live WebSocket (text-only mode) per call; Voice Live manages conversation state server-side.
- Memory retrieval is opt-in (
VoiceChannelConfig(memory_mode="always")); disabled in the sample.
Azure Services¶
Provisioned by this Bicep¶
| Service | Purpose |
|---|---|
| Container Apps | Container runtime with built-in ingress, TLS, and auto-scaling |
| Container Registry | Docker image storage (Basic SKU) |
| Log Analytics | Application logs (30-day retention) |
Required pre-existing¶
| Service | Purpose |
|---|---|
| Azure AI Foundry with Voice Live enabled | Low-latency LLM inference via WebSocket. Pass the endpoint and API key as Bicep parameters. |
Prerequisites¶
- Azure Developer CLI (
azd) - Azure CLI (
az) installed and logged in - Docker installed and running
- Python 3.10+ with
pip - Azure subscription with:
- Azure AI Foundry resource with Voice Live enabled
- Permission to create Container Apps and Container Registry
- Twilio account with:
- Auth Token
- API Key and Secret
- Phone number
- Conversation Configuration ID from Conversation Orchestrator
Deployment¶
Step 1: Configure Environment¶
cd deploy/voice_live_container_apps
cp .env.template .env
Edit .env and fill in your Twilio and Azure credentials.
Step 2: Deploy¶
azd env new my-tac-voice-live
azd env set --file .env
azd up
azd env set --file .env loads your .env into the azd environment so azd up
can resolve every Bicep parameter without prompting. Omit it only if you're
happy answering the prompts on first run.
This automatically:
- Deploys all Azure infrastructure (Container Registry, Container App)
- Builds and pushes the Docker image to ACR (dependencies installed from PyPI)
- Configures the Container App with the image and FQDN
Step 3: Configure Twilio Webhooks¶
After deployment completes, the app URL is printed. Use it to configure Twilio:
Voice (Phone Numbers):
- Go to Twilio Console > Phone Numbers > Active Numbers
- Select your phone number
- Set Voice URL:
https://<FQDN>/twiml(POST)
Step 4: Test¶
Make a phone call to your Twilio phone number.
View logs:
az containerapp logs show \
--name <environmentName>-app \
--resource-group rg-<environmentName> \
--type console \
--follow
Redeploying After Code Changes¶
Re-run azd up — it will rebuild the Docker image, push it to ACR, and update
the Container App. Infrastructure deployments are idempotent, so unchanged
resources are not re-created.
azd up
Teardown¶
azd down --purge
Manual Deployment (without azd)
### Step 0: Build Docker Image# Run from the deploy/ directory (parent of voice_live_container_apps)
cd deploy
docker build -t tac-voice-live:latest -f voice_live_container_apps/Dockerfile .
az group create \
--name tac-voice-live-rg \
--location eastus2
cd deploy/voice_live_container_apps
az deployment group create \
--resource-group tac-voice-live-rg \
--template-file infra/main.bicep \
--parameters \
environmentName=tacvoicelive \
twilioTacAuthToken=YOUR_AUTH_TOKEN \
twilioTacApiKey=YOUR_API_KEY \
twilioTacApiToken=YOUR_API_TOKEN \
twilioTacPhoneNumber=YOUR_PHONE_NUMBER \
twilioTacConversationConfigurationId=YOUR_CONFIGURATION_ID \
azureVoiceLiveEndpoint=YOUR_VOICE_LIVE_ENDPOINT \
azureVoiceLiveApiKey=YOUR_VOICE_LIVE_API_KEY
ACR_LOGIN_SERVER=$(az deployment group show \
--resource-group tac-voice-live-rg \
--name main \
--query 'properties.outputs.acrLoginServer.value' -o tsv)
az acr login --name ${ACR_LOGIN_SERVER%%.*}
docker tag tac-voice-live:latest $ACR_LOGIN_SERVER/tac-voice-live:latest
docker push $ACR_LOGIN_SERVER/tac-voice-live:latest
FQDN=$(az deployment group show \
--resource-group tac-voice-live-rg \
--name main \
--query 'properties.outputs.containerAppFqdn.value' -o tsv)
az deployment group create \
--resource-group tac-voice-live-rg \
--template-file infra/main.bicep \
--parameters \
environmentName=tacvoicelive \
containerImageName=$ACR_LOGIN_SERVER/tac-voice-live:latest \
twilioTacVoicePublicDomain=$FQDN \
twilioTacAuthToken=YOUR_AUTH_TOKEN \
twilioTacApiKey=YOUR_API_KEY \
twilioTacApiToken=YOUR_API_TOKEN \
twilioTacPhoneNumber=YOUR_PHONE_NUMBER \
twilioTacConversationConfigurationId=YOUR_CONFIGURATION_ID \
azureVoiceLiveEndpoint=YOUR_VOICE_LIVE_ENDPOINT \
azureVoiceLiveApiKey=YOUR_VOICE_LIVE_API_KEY
echo "https://$FQDN"