Hoppa till navigering

Anpassad LLM-integrering

Driv en Twilio-telefonagent med din egen LLM med hjälp av Speech Engine SDK.

Översikt

ElevenAgents inbyggda Twilio-integrering omfattar användningsfallet där ElevenLabs är värd för LLM:en. Använd den här guiden när du behöver full kontroll över LLM-hjärnan på din egen server — din egen modell, RAG-pipeline, routning av funktionsanrop eller annat resonemang på serversidan — och agenten fortfarande finns på ett Twilio-telefonnummer.

Den anpassade LLM-delen levereras av Speech Engine SDK, som öppnar en WebSocket mellan ElevenLabs och din server så att din LLM kan strömma svar medan samtalet pågår. Twilio-delen använder Media Streams för att vidarebefordra samtalsljud till agenten.

Arkitektur

Speech Engine SDK exponerar två WebSocket-slutpunkter i agentens konversationssystem:

  • Brain WebSocket körs på din server. ElevenLabs ansluter till den för att leverera transkript och ta emot LLM-genererad text.
  • Conversation WebSocket körs på ElevenLabs. Klienter ansluter till den för att skicka in ljud och ta emot syntetiserat ljud. Twilio-bryggan ansluter via en signerad URL och vidarebefordrar μ-law-ljud i båda riktningarna.

Eftersom Twilio Media Streams och Speech Engine båda använder ulaw_8000 vidarebefordrar bryggan base64-kodat ljud utan omkodning.

loop [Conversation] Dial number POST /incoming-call TwiML <Connect><Stream> WebSocket /media-stream Open conversation WebSocket (signed URL) Speak media event (μ-law base64) user_audio_chunk user_transcript agent_response (streamed) audio event (μ-law base64) media event Play audio Caller Twilio Bridge Server ElevenLabs (conversation WS) Brain Server

Bryggan och brain-servern kan köras i samma process om det passar — exemplet nedan kombinerar dem.

När du ska använda det här mönstret

Både den här guiden och den inbyggda Twilio-integreringen placerar en agent på ett Twilio-telefonnummer. Skillnaden är vem som äger LLM:en:

  • Inbyggd integrering: ElevenLabs är värd för LLM:en och du konfigurerar den via agenten. Enklare.
  • Anpassad LLM via Speech Engine SDK (den här guiden): du är värd för LLM:en på din egen server. Full kontroll över modellen, RAG, funktionsanrop och affärslogik. Fler rörliga delar.

Om din LLM-logik ryms inom standardkonfigurationen för agenten bör du använda den inbyggda integreringen. Använd den här guiden när din brain behöver köra kod i din egen infrastruktur.

Det här mönstret använder Speech Engine SDK, som använder en WebSocket-anslutning för att kommunicera mellan din server och ElevenLabs API. Du kan också använda guiden Custom LLM, som använder en OpenAI-kompatibel HTTP-slutpunkt i stället för Speech Engine SDK.

Den största skillnaden mellan de två är WebSockets jämfört med HTTP-förfrågningar. Med WebSockets upprätthåller du en enda anslutning i stället för att upprätta en ny HTTP-anslutning för varje tur, vilket kan minska fördröjningen.

Förutsättningar

  • Ett Twilio-konto och ett telefonnummer med röstfunktioner.
  • En Speech Engine-resurs. Följ snabbstartsguiden för Speech Engine för att skapa en och lära dig mönstret för brain-servern.
  • En offentlig HTTPS-tunnel (t.ex. ngrok). Twilio ringer upp din brygga via det offentliga internet.
  • Python 3.9+ eller Node.js 18+.

Konfigurera agenten för μ-law-ljud

Twilio Media Streams använder 8 kHz μ-law-ljud. Konfigurera Speech Engine så att den tar emot och skickar ut samma format, så att bryggan inte behöver omkoda.

import asyncio
import os
from elevenlabs import AsyncElevenLabs
elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
async def update_engine():
await elevenlabs.speech_engine.update(
speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
asr={"user_input_audio_format": "ulaw_8000"},
tts={
"model_id": "eleven_flash_v2",
"agent_output_audio_format": "ulaw_8000",
},
speech_engine={
"request_headers": {"x-api-key": os.environ["SHARED_SECRET"]},
},
)
asyncio.run(update_engine())

eleven_flash_v2 håller fördröjningen för text-till-tal låg, vilket är viktigt i ett telefonsamtal. Blocket request_headers instruerar ElevenLabs att inkludera x-api-key: <shared-secret> i varje WebSocket-anslutning till brain-servern — brain-servern kontrollerar headern för att säkerställa att endast din Speech Engine kan nå den.

Bygg bryggservern

Bryggan har tre rutter:

  • POST /incoming-call — Twilio-webhook. Returnerar TwiML som instruerar Twilio att öppna en Media Stream till /media-stream.
  • GET /media-stream — Twilio Media Streams WebSocket. Vidarebefordrar ljud till och från Speech Engine Conversation WebSocket.
  • GET /ws — Brain WebSocket. ElevenLabs ansluter hit när en konversation startar. Kör standardservern engine.serve() / engine.attach().
1

Installera beroenden

pip install "elevenlabs" "aiohttp" "twilio" "python-dotenv"
2

Skapa en signerad URL för Speech Engine

Bryggan begär en signerad URL varje gång ett nytt samtal kommer in. URL:en innehåller Speech Engine-ID:t och en engångssignatur, så bryggan behöver aldrig den råa API-nyckeln.

from elevenlabs import AsyncElevenLabs
elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
async def signed_url() -> str:
response = await elevenlabs.conversational_ai.conversations.get_signed_url(
agent_id=os.environ["SPEECH_ENGINE_ID"],
)
return response.signed_url
3

Servera TwiML-svaret

När ett samtal kommer in skickar Twilio en POST-förfrågan till /incoming-call. Svaret är TwiML som öppnar en Media Stream till bryggans egen /media-stream WebSocket.

from aiohttp import web
from twilio.request_validator import RequestValidator
validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
async def incoming_call(request: web.Request) -> web.Response:
form = await request.post()
signature = request.headers.get("X-Twilio-Signature", "")
url = str(request.url)
if not validator.validate(url, dict(form), signature):
return web.Response(status=403, text="forbidden")
host = request.headers.get("X-Forwarded-Host") or request.host
twiml = (
'<?xml version="1.0" encoding="UTF-8"?>'
"<Response><Connect>"
f'<Stream url="wss://{host}/media-stream"/>'
"</Connect></Response>"
)
return web.Response(text=twiml, content_type="text/xml")

RequestValidator (Python) och twilio.webhook({ validate: true }) (Node) kontrollerar headern X-Twilio-Signature mot TWILIO_AUTH_TOKEN. Utan validering kan vem som helst på det offentliga internet skicka en POST-förfrågan till /incoming-call och debitera samtal på ditt konto.

4

Koppla ihop Media Stream

Media Stream är en WebSocket som skickar en sekvens av JSON-händelser: connected, start, media (ljudets nyttolast) och stop. Bryggan öppnar en Speech Engine Conversation WebSocket vid start och vidarebefordrar ljud i båda riktningarna tills strömmen stängs.

import asyncio
import json
import aiohttp
from aiohttp import web
async def media_stream(request: web.Request) -> web.WebSocketResponse:
twilio_ws = web.WebSocketResponse()
await twilio_ws.prepare(request)
stream_sid: str | None = None
el_session: aiohttp.ClientSession | None = None
el_ws: aiohttp.ClientWebSocketResponse | None = None
pump_task: asyncio.Task | None = None
async def pump_el_to_twilio(el: aiohttp.ClientWebSocketResponse):
async for msg in el:
if msg.type != aiohttp.WSMsgType.TEXT:
continue
event = json.loads(msg.data)
etype = event.get("type")
if etype == "audio":
await twilio_ws.send_str(json.dumps({
"event": "media",
"streamSid": stream_sid,
"media": {"payload": event["audio_event"]["audio_base_64"]},
}))
elif etype == "interruption":
await twilio_ws.send_str(json.dumps({
"event": "clear",
"streamSid": stream_sid,
}))
elif etype == "ping":
event_id = event.get("ping_event", {}).get("event_id")
await el.send_str(json.dumps({
"type": "pong", "event_id": event_id,
}))
try:
async for msg in twilio_ws:
if msg.type != aiohttp.WSMsgType.TEXT:
continue
event = json.loads(msg.data)
if event["event"] == "start":
stream_sid = event["start"]["streamSid"]
el_session = aiohttp.ClientSession()
el_ws = await el_session.ws_connect(await signed_url())
await el_ws.send_str(json.dumps({
"type": "conversation_initiation_client_data",
}))
pump_task = asyncio.create_task(pump_el_to_twilio(el_ws))
elif event["event"] == "media" and el_ws is not None:
await el_ws.send_str(json.dumps({
"user_audio_chunk": event["media"]["payload"],
}))
elif event["event"] == "stop":
break
finally:
if pump_task:
pump_task.cancel()
if el_ws and not el_ws.closed:
await el_ws.close()
if el_session and not el_session.closed:
await el_session.close()
return twilio_ws

Händelsen interruption från Speech Engine utlöser en clear-händelse på Twilio-strömmen, som kasserar buffrat ljud så att avbrott fungerar smidigt. Händelsen ping besvaras med pong för att hålla Conversation WebSocket aktiv.

5

Kör brain-servern parallellt

Brain-servern är standardservern för Speech Engine som visas i snabbstartsguiden. Det enda tillägget är kontrollen av den delade hemligheten vid WebSocket-uppgraderingen — acceptera anslutningen endast om x-api-key matchar värdet som du angav i Speech Engine.

import os
from elevenlabs import AsyncElevenLabs
elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
SHARED_SECRET = os.environ["SHARED_SECRET"]
async def brain_ws(request: web.Request) -> web.WebSocketResponse:
if request.headers.get("x-api-key") != SHARED_SECRET:
return web.Response(status=401, text="unauthorized")
ws = web.WebSocketResponse()
await ws.prepare(request)
engine = await elevenlabs.speech_engine.get(os.environ["SPEECH_ENGINE_ID"])
session = engine.create_session(ws)
async def on_transcript(transcript):
# Replace this with your own LLM call; see the quickstart.
await session.send_response("Hello, you've reached the demo.")
session.on("user_transcript", on_transcript)
await session.run()
return ws
def make_app() -> web.Application:
app = web.Application()
app.router.add_post("/incoming-call", incoming_call)
app.router.add_get("/media-stream", media_stream)
app.router.add_get("/ws", brain_ws)
return app
if __name__ == "__main__":
web.run_app(make_app(), port=3001)

Se snabbstartsguiden för Speech Engine för den fullständiga implementeringen av on_transcript, inklusive ett LLM-anrop och strömmat svar.

Peka Twilio mot bryggan

1

Starta bryggan och en offentlig tunnel

ngrok http 3001
python bridge.py

Notera den https://-URL som ngrok skriver ut — Twilio skickar en POST-förfrågan till den.

2

Uppdatera Speech Engine ws_url

Ange speech_engine.ws_url till den offentliga WebSocket-URL:en för din brain-slutpunkt så att ElevenLabs vet var den ska ansluta.

await elevenlabs.speech_engine.update(
speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
speech_engine={"ws_url": "wss://abc123.ngrok.io/ws"},
)
3

Konfigurera Twilio-numret

Öppna telefonnumrets Voice Configuration i Twilio-konsolen:

  • A call comes in: Webhook
  • URL: https://abc123.ngrok.io/incoming-call
  • HTTP method: POST

Om numret är kopplat till en Elastic SIP Trunk ska du koppla bort det först — ett Twilio-nummer dirigeras antingen till en trunk eller till en webhook, inte båda.

4

Ring numret

Ring numret från valfri telefon. Agenten svarar; tala i samtalet så bör du höra agenten svara. När felsökningsloggning är aktiverad loggar bryggan samtalets SID, konversations-ID och ljudformat för varje tur.

Produktionsöverväganden

  • Webhook-validering: validera alltid X-Twilio-Signature på /incoming-call. Exemplet ovan använder Twilios hjälpbibliotek; hoppa inte över detta steg.
  • Delad hemlighet: tillämpa den delade hemligheten på brain WebSocket. Utan den kan vem som helst som gissar din ngrok-URL ansluta och utge sig för att vara ElevenLabs.
  • Stabil värd: URL:er i ngroks kostnadsfria nivå ändras vid varje omstart. Använd en reserverad ngrok-domän eller ett riktigt värdnamn så att du inte behöver uppdatera Speech Engine ws_url och Twilio-webhooken efter varje omstart.
  • Fördröjning: varje samtal lägger till två nätverkshopp utöver LLM:ens tid till första token. Använd en modell med låg fördröjning och strömma svar för att hålla den upplevda fördröjningen låg.
  • En eller två processer: exemplet placerar bryggan och brain-servern på samma port så att en enda ngrok-tunnel täcker allt. I produktion kan du dela upp dem på två tjänster så länge båda har en offentlig URL.
  • Promptinjektion: talad inmatning från ett telefonsamtal är otillförlitlig användarinmatning. Validera transkript innan de påverkar verktygsanrop eller databasskrivningar.

Nästa steg