Voice AI Agents for IP Telephony: Integration and Latency Optimization
You deployed a voice AI agent, but calls drop, latency exceeds one second, and operators complain about echo? This is typical when the SIP stack poorly interfaces with the LLM pipeline. We solve this at the media bridge level—decoding RTP, resampling to 16 kHz for STT, passing the transcript to the LLM, synthesizing the response, and returning it to RTP with minimal latency. Our approach has been proven in 30+ projects, with p99 latency not exceeding 800 ms.
Why Latency Is Critical in Voice AI Agents
Every 100 ms of delay reduces sales conversion by 1-2% (CCW Digital, 2023). If response time exceeds 1.5 s, callers start repeating phrases or hang up. The problem often lies in suboptimal audio processing: using STT at 8 kHz, lack of parallel chunk processing, or synchronous TTS. We optimize each stage: using deep jitter buffers, asynchronous WebSocket, and hardware G.711 decoding. As a result, operator cost savings reach 30-50%. For a typical 100-agent call center, this translates to annual savings of $150,000–$250,000.
How We Build a Media Bridge Between SIP and AI
Consider a real case: a client with FreePBX (Asterisk 20) wanted to automate incoming calls. We deployed a media gateway using Kamailio + Rtpengine, which accepts RTP, converts PCMU/PCMA to PCM 16 kHz, and sends it via WebSocket to the STT engine (Deepgram Nova-2). The LLM response (Mistral 7B via vLLM) is converted to speech using Azure TTS and returned to RTP. The entire cycle takes 700-750 ms—40% faster than a typical solution with a single synchronous API call. Stack: async Python with asyncio, audioop for decoding, and asynchronous AMI.
import asyncio
import audioop
import wave
from typing import Optional
import websockets
class SIPAudioBridge:
"""
Bridge between SIP RTP stream and WebSocket for AI agent.
Receives PCMU/PCMA audio from Asterisk, transmits PCM 16kHz to STT.
"""
SAMPLE_RATE_RTP = 8000 # Telephony standard
SAMPLE_RATE_AI = 16000 # Deepgram Nova-2 requires 16kHz
CODEC_PCMU = 0 # G.711 μ-law
CODEC_PCMA = 8 # G.711 A-law
def ulaw_to_pcm(self, ulaw_data: bytes) -> bytes:
"""Decode G.711 μ-law (PCMU) to PCM"""
return audioop.ulaw2lin(ulaw_data, 2) # 2 = 16-bit
def alaw_to_pcm(self, alaw_data: bytes) -> bytes:
"""Decode G.711 A-law (PCMA) to PCM"""
return audioop.alaw2lin(alaw_data, 2)
def resample_8k_to_16k(self, pcm_8k: bytes) -> bytes:
"""Upsample 8kHz to 16kHz for STT models"""
resampled, _ = audioop.ratecv(
pcm_8k,
2, # 16-bit
1, # mono
self.SAMPLE_RATE_RTP,
self.SAMPLE_RATE_AI,
None
)
return resampled
async def stream_rtp_to_stt(self, rtp_socket,
stt_websocket,
codec: int = CODEC_PCMU,
chunk_ms: int = 20):
"""
Stream RTP audio to STT WebSocket.
chunk_ms: chunk size in milliseconds (standard 20ms)
"""
rtp_chunk_size = int(self.SAMPLE_RATE_RTP * 2 * chunk_ms / 1000)
while True:
rtp_data, addr = rtp_socket.recvfrom(4096)
# RTP header — first 12 bytes
rtp_payload = rtp_data[12:]
if codec == self.CODEC_PCMU:
pcm = self.ulaw_to_pcm(rtp_payload)
elif codec == self.CODEC_PCMA:
pcm = self.alaw_to_pcm(rtp_payload)
else:
continue
pcm_16k = self.resample_8k_to_16k(pcm)
await stt_websocket.send(pcm_16k)
class AsteriskAMIConnector:
"""
Integration with Asterisk via Asterisk Manager Interface (AMI).
AMI allows call control: originate, hangup, transfer, monitor.
"""
def __init__(self, host: str, port: int,
username: str, password: str):
self.host = host
self.port = port
self.username = username
self.password = password
self.reader: Optional[asyncio.StreamReader] = None
self.writer: Optional[asyncio.StreamWriter] = None
async def connect(self):
self.reader, self.writer = await asyncio.open_connection(
self.host, self.port
)
# Skip greeting
await self.reader.readline()
# Authentication
await self._send_action({
"Action": "Login",
"Username": self.username,
"Secret": self.password,
})
async def _send_action(self, action: dict) -> dict:
"""Send AMI action and get response"""
message = "\r\n".join(
f"{k}: {v}" for k, v in action.items()
) + "\r\n\r\n"
self.writer.write(message.encode())
await self.writer.drain()
response = {}
while True:
line = (await self.reader.readline()).decode().strip()
if not line:
break
if ": " in line:
key, value = line.split(": ", 1)
response[key] = value
return response
async def originate_call_to_ai(self, phone_number: str,
ai_context: str,
caller_id: str = "AI Agent") -> dict:
"""
Initiate a call with AI agent via Asterisk.
Connects the caller to an extension handled by AI agent.
"""
return await self._send_action({
"Action": "Originate",
"Channel": f"SIP/trunk/{phone_number}",
"Context": "ai-agent",
"Exten": "s",
"Priority": 1,
"CallerID": caller_id,
"Variable": f"AI_CONTEXT={ai_context}",
"Async": "true",
"Timeout": 30000,
})
async def transfer_to_queue(self, channel: str,
queue_name: str,
agent_context: str) -> dict:
"""Transfer call to a live agent queue"""
return await self._send_action({
"Action": "Redirect",
"Channel": channel,
"Exten": queue_name,
"Context": "queues",
"Priority": 1,
"ExtraChannel": "",
})
class CallRecordingManager:
"""Manage AI agent call recordings"""
def __init__(self, storage_backend: str = "s3"):
self.storage = storage_backend
def generate_dial_plan_snippet(self, ai_extension: str = "7000") -> str:
"""
Asterisk dialplan snippet for routing to AI agent.
"""
return f"""
; extensions.conf — AI Agent routing
[ai-agent]
exten => s,1,NoOp(AI Agent handling call)
same => n,Set(CALL_ID=${{UNIQUEID}})
same => n,Set(CALLER_NUM=${{CALLERID(num)}})
same => n,Record(/var/spool/asterisk/recordings/${{CALL_ID}}.wav,0,300)
same => n,AGI(ai-agent-bridge.py,${{CALL_ID}},${{CALLER_NUM}})
same => n,GotoIf(${{AGENT_ESCALATE}}?escalate)
same => n,Hangup()
same => n(escalate),Queue(support-agents,t,,,300)
[inbound]
exten => _+7XXXXXXXXXX,1,Goto(ai-agent,s,1)
"""
def save_call_metadata(self, call_id: str,
transcript: list[dict],
metadata: dict) -> dict:
"""Save call transcript and metadata"""
record = {
"call_id": call_id,
"timestamp": metadata.get("start_time"),
"duration_seconds": metadata.get("duration"),
"caller_number": metadata.get("caller"),
"resolution": metadata.get("resolution"),
"escalated": metadata.get("escalated", False),
"transcript": transcript,
"tool_calls_made": metadata.get("tools_used", []),
}
# In production: save to S3 + write to PostgreSQL
return {"status": "saved", "record_id": f"CALL-{call_id}"}
How to Integrate an AI Agent with Asterisk in 5 Steps
- Audit current infrastructure: Check SIP traffic, codecs, load. Determine connection point (SBC/media gateway).
- Deploy media gateway: Set up Kamailio + Rtpengine, configure transcoding and WebSocket bridge.
- Integrate with PBX: Use AMI (Asterisk Manager Interface) or AGI (Asterisk Gateway Interface) for call control. Create dialplan for AI extension.
- Configure LLM agent: Deploy the model (Mistral 7B with vLLM), connect RAG for knowledge base access, set up guardrails.
- Test and optimize: Simulate 50+ concurrent calls, measure p99 latency, calibrate STT/TTS.
Example log from a live call (excerpt)
[TIMESTAMP] CALL-4521: Incoming call from +7-XXX-XXX-XX-XX
[TIMESTAMP] STT: chunk received, processing...
[TIMESTAMP] STT: duration 0.320s, text: "Hello, I'm interested in..."
[TIMESTAMP] LLM: response generated in 0.250s
[TIMESTAMP] TTS: audio synthesized in 0.180s
[TIMESTAMP] RTP: audio sent to caller. Total latency: 0.750s
Dialplan and Routing
Key routing scenarios in IP-PBX:
- Inbound calls: All calls go to the AI agent first. On escalation, transfer to an ACD queue with context (CRM data + conversation summary via screen pop).
- Outbound campaigns: AI agent initiates calls via AMI Originate on schedule. Progressive dialer: next call starts when the previous finishes.
- Hybrid routing: By time of day: all calls to AI at night, skill-based routing with AI overflow during the day.
Comparison: Typical Solution vs Our Architecture
| Parameter | Typical Solution | Our Architecture |
|---|---|---|
| STT frequency | 8 kHz | 16 kHz (Deepgram Nova-2) |
| Chunk processing | Synchronous 50 ms | Asynchronous 20 ms |
| TTS caching | None | Partial (greeting phrases) |
| G.711 decoding | On AI side | Hardware via Kamailio |
| p99 latency | 1.2-1.5 s | 0.7-0.8 s |
| Operator cost reduction | - | 30-50% |
Infrastructure Requirements
| Component | Minimum Requirements | Production |
|---|---|---|
| SBC/Media Gateway | Kamailio / FreeSWITCH | Kamailio + Rtpengine |
| IP PBX | Asterisk 20+ / 3CX | FreePBX Enterprise |
| STT latency | < 300ms | Deepgram Nova-2 self-hosted |
| TTS latency | < 200ms | Azure TTS / ElevenLabs |
| Bandwidth | 100 kbps/call G.711 | 80 kbps G.729 with transcoding |
| Call recording | Local | S3 + WORM policy |
What's Included in the Work
- Architectural audit of current telephony (SIP traffic, load, codecs).
- Development and deployment of media bridge (WebSocket + async STT/TTS).
- Integration with your PBX: Asterisk/FreePBX/3CX via AMI or AGI.
- Configuration of LLM agent (RAG, few-shot, guardrails).
- Load testing (simulation of 50+ concurrent calls).
- Operations and monitoring documentation, training for your engineers.
- Warranty support for one month after launch.
Timelines and Cost Estimate
A typical project takes 10 to 16 weeks—from audit to acceptance testing. Cost is calculated individually based on infrastructure complexity, call volume, and required functionality. Typical project cost ranges from $50,000 to $150,000 depending on scope. We are ready to assess your project after a brief discussion. Get a consultation for your project—we will evaluate the architecture and propose a solution. Contact us for a demonstration of the AI agent working on your scenario.
Over 5 years, we have implemented AI telephony in 30+ companies, guaranteeing latency below 800 ms and full compatibility with your equipment.







