Hoppa till navigering

Azure Communication Services

Låt användare ringa ett telefonnummer som din ElevenLabs-agent svarar på via ACS Call Automation.

Översikt

Med den här metoden får din agent ett telefonnummer. En uppringare ringer numret, Azure Communication Services (ACS) svarar med dubbelriktad mediestreaming, och en liten brygga vidarebefordrar PCM-ljud mellan ACS och ElevenLabs-agenten via standardprotokollet för agent-WebSocket. Det är kontaktcenter-/IVR-mönstret — samma upplägg som en SIP trunking-distribution, med ACS som operatör.

Den ansluter också till Teams på två sätt: en Teams-användare med Calling Plan kan ringa ACS-numret direkt, eller så kan du lägga Teams Phone Extensibility framför numret så att samtal till ett Teams-resurskonto dirigeras till ACS.

ACS tillhandahåller PSTN-nummer endast i en begränsad uppsättning länder. Om ett nummer inte är tillgängligt i din region använder du i stället en SIP-leverantör med SIP trunking, eller Graph-samtalsboten.

Så fungerar det

En uppringare ringer ACS-numret; ACS skickar IncomingCall via Event Grid till bryggan, som svarar med dubbelriktad PCM 16k-mediastreaming och vidarebefordrar den till ElevenLabs-agenten via en WebSocket
Inkommande samtal → ACS → brygga → ElevenLabs

Ljudet är PCM 16 kHz mono i båda riktningarna (agentens in-/utdataformat är pcm_16000), så det skickas vidare som base64 utan omsampling.

Bryggan exponerar följande rutter:

RuttSyfte
POST /api/incomingCallEvent Grid-webhook: validerar prenumerationen och kör sedan answer_call med mediestreaming
POST /api/callbacksCall Automation-livscykelhändelser (CallConnected, CallDisconnected, AddParticipant*)
GET|WS /wsACS-mediaströmssocket ↔ ElevenLabs
POST /api/outboundCallValfritt: ring ett utgående samtal som kopplar mottagaren till agenten

Krav

  1. En betald Azure-prenumeration (MCA / EA / Pay-As-You-Go) — kostnadsfria prov- och sponsringsprenumerationer kan inte köpa nummer.
  2. En Azure Communication Services-resurs.
  3. En HTTPS-värd för bryggan med en offentlig WebSocket (Azure Container Apps, App Service eller en VM).
  4. En ElevenLabs-agent inställd på PCM 16000 Hz i båda riktningarna: TTS-utdataformat på fliken Voice, och ljudformat för användarindata på fliken Advanced.

Behörigheter och roller

OmfattningRoll / behörighetVarför
Azure RBACContributor för resursgruppenskapa ACS-resursen, Container App och Event Grid-prenumerationen
Azure-prenumerationOwner eller Contributor för prenumerationenköpa telefonnummer (köpalternativet är annars inaktiverat)
FaktureringPrenumerationstyp MCA / EA / Pay-As-You-Gokostnadsfria, prov-, sponsrings- och Dev-prenumerationer kan inte köpa nummer

Med Contributor (inte Owner) kan az containerapp up inte skapa rolltilldelningen för ACR-pull med hanterad identitet. Aktivera i stället registeradministratörsanvändaren och koppla den — se varningen i steg 2.

Steg 1 — Etablera ACS-resursen och numret

RG=my-rg
# Register providers (once)
az provider register -n Microsoft.Communication --wait
az provider register -n Microsoft.EventGrid --wait
# Create the ACS resource
az communication create --name my-acs --resource-group $RG \
--location global --data-location unitedstates

Köp ett nummer i resursen (Portal → din ACS-resurs → Phone numbers → Get, eller phone-numbers SDK). För en agent som svarar på samtal räcker ett nummer med inbound calling; lägg till stöd för outbound om du även vill använda /api/outboundCall.

Vyn Phone numbers för ACS-resursen visar aktiva nummer med deras
samtalsfunktioner

Telefonnummer i ACS-resursen — kolumnen Calling visar riktningen för varje nummer

För att verifiera från CLI:t (kräver az extension add --name communication) och hämta anslutningssträngen som bryggan använder som ACS_CONNECTION_STRING:

CONN=$(az communication list-key -n my-acs -g $RG --query primaryConnectionString -o tsv)
az communication phonenumber list --connection-string "$CONN" --query "[].phoneNumber"

Steg 2 — Distribuera bryggan

Bryggan är en liten Flask- + flask-sock-app som använder azure-communication-callautomation. Kärnan i det inkommande flödet:

bridge.py (utdrag)
from azure.communication.callautomation import (
CallAutomationClient, MediaStreamingOptions, StreamingTransportType,
MediaStreamingContentType, MediaStreamingAudioChannelType, AudioFormat,
)
@app.route("/api/incomingCall", methods=["POST"])
def incoming_call():
for event in request.get_json():
# Event Grid subscription validation handshake
if event.get("eventType") == "Microsoft.EventGrid.SubscriptionValidationEvent":
return jsonify({"validationResponse": event["data"]["validationCode"]})
if event.get("eventType") == "Microsoft.Communication.IncomingCall":
client = CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING)
client.answer_call(
incoming_call_context=event["data"]["incomingCallContext"],
callback_url=f"https://{HOST}/api/callbacks",
media_streaming=MediaStreamingOptions(
transport_url=f"wss://{HOST}/ws",
transport_type=StreamingTransportType.WEBSOCKET,
content_type=MediaStreamingContentType.AUDIO,
audio_channel_type=MediaStreamingAudioChannelType.MIXED,
start_media_streaming=True,
enable_bidirectional=True,
audio_format=AudioFormat.PCM16_K_MONO,
),
)
return jsonify({"status": "ok"})

På /ws-socketen vidarebefordrar du PCM16 i båda riktningarna: skicka vidare ACS-ramar med AudioData till ElevenLabs som {"user_audio_chunk": "<base64>"}, och skicka tillbaka agentens ljud som {"Kind":"AudioData","AudioData":{"Data":"<base64>"},"StopAudio":null}. Den första ramen ACS skickar är AudioMetadata (det förhandlade formatet) — logga den och ignorera den. ElevenLabs-sidan använder standardprotokollet för agent-WebSocket.

ACS använder olika JSON-storlek på bokstäverna per riktning: inkommande ramar som tjänsten skickar använder camelCase (kind, audioData.data), medan utgående ramar som den förväntar sig använder PascalCase (Kind, AudioData.Data, StopAudio). Håll de två formaten åtskilda — vidarebefordringen nedan speglar detta.

bridge.py — mediavidarebefordring
import asyncio, json, os, queue, threading, websockets
from flask_sock import Sock
sock = Sock(app)
AGENT_ID = os.environ["ELEVENLABS_AGENT_ID"]
# US default; data residency: wss://api.eu.residency.elevenlabs.io, .in., or .sg.
EL_ORIGIN = os.environ.get("ELEVENLABS_ORIGIN", "wss://api.elevenlabs.io")
EL_WS = f"{EL_ORIGIN}/v1/convai/conversation?agent_id={AGENT_ID}"
@sock.route("/ws")
def media_stream(ws):
loop = asyncio.new_event_loop()
el = {"ws": None}
to_acs = queue.Queue() # outbound frames; only this handler thread touches `ws`
async def el_session():
async with websockets.connect(EL_WS) as elws:
el["ws"] = elws
await elws.send(json.dumps({"type": "conversation_initiation_client_data"}))
async for msg in elws:
data = json.loads(msg)
kind = data.get("type")
if kind == "audio": # agent audio -> caller
b64 = data["audio_event"]["audio_base_64"]
to_acs.put({"Kind": "AudioData", "AudioData": {"Data": b64}, "StopAudio": None})
elif kind == "ping":
await elws.send(json.dumps({"type": "pong", "event_id": data["ping_event"]["event_id"]}))
elif kind == "interruption": # barge-in
to_acs.put({"Kind": "StopAudio", "AudioData": None, "StopAudio": {}})
threading.Thread(target=lambda: loop.run_until_complete(el_session()), daemon=True).start()
# Keep all ACS-socket I/O on this one thread: receive with a short timeout,
# then drain any audio the ElevenLabs thread queued. Sending from the other
# thread would race flask-sock and corrupt the stream.
try:
while True:
raw = ws.receive(timeout=0.02) # None when no frame arrived this tick
if raw:
evt = json.loads(raw)
if evt.get("kind") == "AudioData" and el["ws"]: # caller audio -> agent
asyncio.run_coroutine_threadsafe(
el["ws"].send(json.dumps({"user_audio_chunk": evt["audioData"]["data"]})), loop)
while not to_acs.empty():
ws.send(json.dumps(to_acs.get_nowait()))
except Exception:
pass # ACS socket closed

Den här vidarebefordringen är avsiktligt minimal. För produktion bör du lägga till loggning, återanslutning och ordnad nedstängning. Den fullständiga meddelandereferensen finns i WebSocket- dokumentationen.

EL_WS ansluter till en offentlig agent. För en privat agent ska bryggan begära en kortlivad signerad URL på serversidan — GET /v1/convai/conversation/get-signed-url?agent_id=... med din API- nyckel — och ansluta till den returnerade URL:en i stället. För data- residens anger du ELEVENLABS_ORIGIN till din residensvärd (wss://api.eu.residency.elevenlabs.io, .in. eller .sg.) — begäranden om signerade URL:er använder motsvarande https://-värd.

Distribuera till Azure Container Apps och hämta den offentliga FQDN:en:

az containerapp up --name acs-el-bridge --resource-group $RG \
--source . --ingress external --target-port 8080 \
--env-vars ELEVENLABS_AGENT_ID=$AGENT_ID \
ELEVENLABS_ORIGIN=wss://api.elevenlabs.io
FQDN=$(az containerapp show -n acs-el-bridge -g $RG \
--query properties.configuration.ingress.fqdn -o tsv)

Ange sedan BRIDGE_PUBLIC_HOST=$FQDN och ACS-anslutningssträngen (som en hemlighet) i appen.

Med Contributor (inte Owner) kan az containerapp up inte skapa ACR-pullrollen för hanterad identitet. Aktivera registeradministratörsanvändaren (az acr update --admin-enabled true) och koppla den med az containerapp registry set, sedan az containerapp update --image ....

Steg 3 — Dirigera IncomingCall till bryggan

Skapa en Event Grid-prenumeration på ACS-resursen som skickar IncomingCall till bryggan. Bryggans valideringshandshake (ovan) slutför prenumerationen automatiskt.

ACS_ID=$(az communication show -n my-acs -g $RG --query id -o tsv)
az eventgrid event-subscription create \
--name acs-incomingcall \
--source-resource-id "$ACS_ID" \
--endpoint "https://$FQDN/api/incomingCall" \
--endpoint-type webhook \
--included-event-types Microsoft.Communication.IncomingCall
# Verify — should print "Succeeded"
az eventgrid event-subscription show --name acs-incomingcall \
--source-resource-id "$ACS_ID" --query provisioningState -o tsv

Prenumerationen visas under ACS-resursens blad Events:

Bladet Events för ACS-resursen listar webhook-prenumerationen acs-incomingcall, filtrerad
till
Microsoft.Communication.IncomingCall

ACS-resurs → Events → Event Subscriptions

Ring numret — agenten svarar.

Anslut till Teams

  • Direktuppringning: En Teams-användare med Teams Phone + Calling Plan kan ringa ACS-numret som vilket externt nummer som helst.
  • Teams-resurskonto (TPE): Koppla ett Teams-resurskonto till ACS-resursen med Teams Phone Extensibility, så att samtal till resurskontot utlöser samma flöde IncomingCall → brygga.

Avsluta samtalet

När agenten avslutar samtalet (till exempel med verktyget End Call) stänger ElevenLabs WebSocket-anslutningen. Lägg på ACS-samtalet så att uppringaren inte lämnas på en död linje:

CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING) \
.get_call_connection(call_connection_id).hang_up(is_for_everyone=True)

Varm överföring till en människa

ElevenLabs inbyggda överföringsverktyg gäller endast när ElevenLabs äger telefonin, så här utlöser agenten ett anpassat klientverktyg (till exempel transfer_to_human) som din brygga hanterar genom att lägga till människan i det aktiva samtalet med add_participant (varm överföring) i stället för en blind överföring:

conn = client.get_call_connection(call_connection_id)
conn.add_participant(
PhoneNumberIdentifier(human_number),
source_caller_id_number=PhoneNumberIdentifier(your_outbound_number),
invitation_timeout=30,
)
# then mute the bot and skip the end-of-call hangup so the human's leg survives

ACS skickar callbacks för AddParticipantSucceeded / AddParticipantFailed till /api/callbacks. Returnera ett client_tool_result till agenten så att den kan säga sin överlämningsfras. Se systemverktyg för konfiguration på agentsidan.

Ställ in överföringsskyddet direkt när verktyget utlöses (innan du anropar add_participant), annars kan en snabb stängning av EL- WebSocket ansluta till nedläggningen och avbryta samtalet innan människan hinner ansluta.

Felsökning

Kontrollera att Event Grid-prenumerationen har etablerats (provisioningState: Succeeded) och att bryggans /api/incomingCall returnerade valideringsekot. Kontrollera att numret har inkommande samtalsfunktion och finns i samma ACS-resurs som prenumerationen. På prenumerationens flik Filters måste händelsetyperna innehålla Incoming Call:

Fliken Filters för händelseprenumerationen med händelsetypen filtrerad till Incoming
Call

Händelseprenumeration → Filters → Incoming Call

ACS utgående samtal till vissa destinationer (t.ex. Indien) är begränsade eller intermittenta. Använd en destination som stöds, eller lägg ett SIP-/Operator-nummer framför den mänskliga delen. Brygglogiken påverkas inte — det är ett fel hos operatören på den utgående delen.

Båda sidor måste använda PCM 16 kHz mono. Ställ in agentens in-/utdataformat på pcm_16000; bryggan loggar det förhandlade formatet från conversation_initiation_metadata.

Köp av nummer kräver en betald prenumerationstyp (MCA/EA/PAYG). Om ACS inte erbjuder nummer i ditt land använder du i stället en SIP-leverantör.

Användbara länkar