Azure Communication Services

ACS Call Automation के ज़रिए यूज़र्स को उस फ़ोन नंबर पर कॉल करने दें, जिसका जवाब आपका ElevenLabs एजेंट देता है।

अवलोकन

इस तरीके से आपके एजेंट को एक फ़ोन नंबर मिलता है। कॉलर इस पर डायल करता है, Azure Communication Services (ACS) दो-तरफ़ा मीडिया स्ट्रीमिंग के साथ कॉल का जवाब देता है, और एक छोटा ब्रिज मानक एजेंट WebSocket प्रोटोकॉल का इस्तेमाल करके ACS और ElevenLabs एजेंट के बीच PCM ऑडियो रिले करता है। यह कॉन्टैक्ट सेंटर / IVR पैटर्न है — SIP ट्रंकिंग डिप्लॉयमेंट जैसा ही सेटअप, जिसमें ACS कैरियर है।

यह Teams से भी दो तरीकों से जुड़ता है: Calling Plan वाला Teams यूज़र सीधे ACS नंबर डायल कर सकता है, या आप नंबर के आगे Teams Phone Extensibility लगा सकते हैं, ताकि Teams रिसोर्स अकाउंट पर आने वाली कॉल ACS पर रूट हों।

ACS केवल कुछ सीमित देशों में PSTN नंबर प्रोविज़न करता है। अगर आपके क्षेत्र में नंबर उपलब्ध नहीं है, तो SIP ट्रंकिंग वाले SIP प्रोवाइडर का इस्तेमाल करें, या Graph calling bot इस्तेमाल करें।

यह कैसे काम करता है

एक कॉलर ACS नंबर डायल करता है; ACS, Event Grid के ज़रिए ब्रिज को IncomingCall भेजता है, जो दो-तरफ़ा PCM 16k मीडिया स्ट्रीमिंग के साथ जवाब देता है और उसे WebSocket पर ElevenLabs एजेंट तक रिले करता है
इनबाउंड कॉल → ACS → ब्रिज → ElevenLabs

दोनों लेग पर ऑडियो PCM 16 kHz मोनो होता है (एजेंट का इनपुट/आउटपुट फ़ॉर्मैट pcm_16000 है), इसलिए यह बिना री-सैंपलिंग के base64 के रूप में पास होता है।

ब्रिज ये रूट एक्सपोज़ करता है:

रूटउद्देश्य
POST /api/incomingCallEvent Grid वेबहुक: सब्सक्रिप्शन सत्यापित करता है, फिर मीडिया स्ट्रीमिंग के साथ answer_call
POST /api/callbacksCall Automation लाइफ़साइकल इवेंट (CallConnected, CallDisconnected, AddParticipant*)
GET|WS /wsACS मीडिया-स्ट्रीमिंग सॉकेट ↔ ElevenLabs
POST /api/outboundCallवैकल्पिक: आउटबाउंड कॉल करें, जो जवाब देने वाले को एजेंट से जोड़ती है

ज़रूरतें

  1. एक पेड Azure सब्सक्रिप्शन (MCA / EA / Pay-As-You-Go) — मुफ़्त/ट्रायल/स्पॉन्सरशिप सब्सक्रिप्शन नंबर नहीं खरीद सकते।
  2. एक Azure Communication Services रिसोर्स।
  3. सार्वजनिक WebSocket के साथ ब्रिज के लिए एक HTTPS होस्ट (Azure Container Apps, App Service, या VM)।
  4. दोनों लेग पर PCM 16000 Hz पर सेट एक ElevenLabs एजेंट: Voice टैब में TTS आउटपुट फ़ॉर्मैट और Advanced टैब में यूज़र इनपुट ऑडियो फ़ॉर्मैट।

अनुमतियां और रोल

स्कोपरोल / अनुमतिकारण
Azure RBACरिसोर्स ग्रुप पर ContributorACS रिसोर्स, Container App और Event Grid सब्सक्रिप्शन बनाना
Azure सब्सक्रिप्शनसब्सक्रिप्शन पर Owner या Contributorफ़ोन नंबर खरीदना (वरना खरीदने का विकल्प बंद रहता है)
बिलिंगMCA / EA / Pay-As-You-Go सब्सक्रिप्शन प्रकारमुफ़्त, ट्रायल, स्पॉन्सरशिप और Dev सब्सक्रिप्शन नंबर नहीं खरीद सकते

Contributor के तहत (Owner नहीं), az containerapp up मैनेज्ड-आइडेंटिटी ACR pull रोल असाइनमेंट नहीं बना सकता। इसके बजाय registry admin यूज़र सक्षम करें और उसे अटैच करें — चरण 2 में चेतावनी देखें।

चरण 1 — ACS रिसोर्स और नंबर प्रोविज़न करें

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

रिसोर्स में नंबर खरीदें (Portal → आपका ACS रिसोर्स → Phone numbers → Get, या phone-numbers SDK)। कॉल का जवाब देने वाले एजेंट के लिए inbound calling वाला नंबर काफ़ी है; अगर आप /api/outboundCall भी चाहते हैं, तो outbound क्षमता जोड़ें।

ACS रिसोर्स का Phone numbers पेज, जिसमें सक्रिय नंबर उनकी कॉलिंग क्षमताओं के साथ सूचीबद्ध हैं

ACS रिसोर्स के फ़ोन नंबर — Calling कॉलम हर नंबर की दिशा दिखाता है

CLI से सत्यापित करने के लिए (az extension add --name communication ज़रूरी है), और ब्रिज द्वारा 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"

चरण 2 — ब्रिज डिप्लॉय करें

ब्रिज azure-communication-callautomation का उपयोग करने वाला एक छोटा Flask + flask-sock ऐप है। इनबाउंड फ़्लो का मुख्य भाग:

bridge.py (excerpt)
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"})

/ws सॉकेट पर PCM16 को दोनों दिशाओं में रिले करें: ACS AudioData फ़्रेम को ElevenLabs में {"user_audio_chunk": "<base64>"} के रूप में फ़ॉरवर्ड करें, और एजेंट का ऑडियो वापस {"Kind":"AudioData","AudioData":{"Data":"<base64>"},"StopAudio":null} के रूप में भेजें। ACS का पहला फ़्रेम AudioMetadata होता है (नेगोशिएट किया गया फ़ॉर्मैट) — इसे लॉग करें और अनदेखा करें। ElevenLabs वाला भाग मानक एजेंट WebSocket प्रोटोकॉल है।

ACS हर दिशा के लिए अलग JSON केसिंग इस्तेमाल करता है: इसके भेजे गए इनबाउंड फ़्रेम camelCase (kind, audioData.data) में होते हैं, जबकि इसके अपेक्षित आउटबाउंड फ़्रेम PascalCase (Kind, AudioData.Data, StopAudio) में होते हैं। दोनों केस अलग रखें — नीचे दिया रिले इसे ही दर्शाता है।

bridge.py — media relay
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

यह रिले जानबूझकर न्यूनतम रखा गया है। प्रोडक्शन के लिए लॉगिंग, रिकनेक्शन और व्यवस्थित टियरडाउन जोड़ें। पूरा मैसेज रेफ़रेंस WebSocket दस्तावेज़ में है।

EL_WS एक पब्लिक एजेंट से कनेक्ट होता है। प्राइवेट एजेंट के लिए, ब्रिज से सर्वर-साइड एक अल्पकालिक साइन किया हुआ URL मांगें — आपकी API key के साथ GET /v1/convai/conversation/get-signed-url?agent_id=... — और इसके बजाय लौटे हुए URL से कनेक्ट करें। डेटा रेज़िडेंसी के लिए ELEVENLABS_ORIGIN को अपने रेज़िडेंसी होस्ट (wss://api.eu.residency.elevenlabs.io, .in., या .sg.) पर सेट करें — साइन किए गए URL अनुरोध मेल खाते https:// होस्ट का उपयोग करते हैं।

Azure Container Apps पर डिप्लॉय करें और सार्वजनिक FQDN कैप्चर करें:

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)

फिर ऐप पर BRIDGE_PUBLIC_HOST=$FQDN और ACS कनेक्शन स्ट्रिंग (सीक्रेट के रूप में) सेट करें।

Contributor के तहत (Owner नहीं), az containerapp up मैनेज्ड-आइडेंटिटी ACR pull रोल नहीं बना सकता। registry admin यूज़र सक्षम करें (az acr update --admin-enabled true) और इसे az containerapp registry set के साथ अटैच करें, फिर az containerapp update --image ... चलाएं।

चरण 3 — IncomingCall को ब्रिज पर रूट करें

ACS रिसोर्स पर एक Event Grid सब्सक्रिप्शन बनाएं, जो IncomingCall को ब्रिज पर पोस्ट करे। ब्रिज का वैलिडेशन हैंडशेक (ऊपर) सब्सक्रिप्शन अपने-आप पूरा कर देता है।

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

सब्सक्रिप्शन ACS रिसोर्स के Events पेज में दिखाई देता है:

ACS रिसोर्स का Events पेज, जिसमें Microsoft.Communication.IncomingCall से फ़िल्टर किया गया acs-incomingcall वेबहुक सब्सक्रिप्शन सूचीबद्ध है

ACS रिसोर्स → Events → Event Subscriptions

नंबर डायल करें — एजेंट जवाब देगा।

इसे Teams से कनेक्ट करना

  • सीधा डायल: Teams Phone + Calling Plan वाला Teams यूज़र ACS नंबर को किसी भी बाहरी नंबर की तरह डायल कर सकता है।
  • Teams रिसोर्स अकाउंट (TPE): Teams Phone Extensibility के साथ Teams रिसोर्स अकाउंट को ACS रिसोर्स से बाइंड करें, ताकि रिसोर्स अकाउंट पर कॉल वही IncomingCall → ब्रिज फ़्लो ट्रिगर करें।

कॉल का अंत

जब एजेंट बातचीत समाप्त करता है (जैसे अपने End Call टूल से), ElevenLabs WebSocket बंद कर देता है। ACS लेग को हैंग अप करें, ताकि कॉलर डेड लाइन पर न रहे:

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

किसी व्यक्ति को वॉर्म ट्रांसफ़र

ElevenLabs के नेटिव ट्रांसफ़र टूल केवल तब लागू होते हैं जब टेलीफ़ोनी ElevenLabs के पास हो। इसलिए यहां एजेंट एक कस्टम क्लाइंट टूल (जैसे transfer_to_human) ट्रिगर करता है, जिसे आपका ब्रिज ब्लाइंड ट्रांसफ़र के बजाय add_participant के साथ लाइव कॉल में व्यक्ति को जोड़कर (वॉर्म) संभालता है:

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 /api/callbacks पर AddParticipantSucceeded / AddParticipantFailed कॉलबैक भेजता है। एजेंट को client_tool_result लौटाएं, ताकि वह अपनी हैंडऑफ़ लाइन बोल सके। एजेंट-साइड कॉन्फ़िगरेशन के लिए सिस्टम टूल्स देखें।

टूल ट्रिगर होते ही ट्रांसफ़र गार्ड सेट करें (add_participant कॉल करने से पहले), वरना तेज़ EL WebSocket क्लोज़ हैंगअप के साथ रेस कर सकता है और व्यक्ति के जुड़ने से पहले कॉल ड्रॉप हो सकती है।

समस्या निवारण

पुष्टि करें कि Event Grid सब्सक्रिप्शन प्रोविज़न हुआ है (provisioningState: Succeeded) और ब्रिज के /api/incomingCall ने वैलिडेशन इको लौटाया है। पुष्टि करें कि नंबर में इनबाउंड कॉलिंग है और वह उसी ACS रिसोर्स में है जिस पर सब्सक्रिप्शन है। सब्सक्रिप्शन के Filters टैब में इवेंट प्रकारों में Incoming Call शामिल होना चाहिए:

इवेंट सब्सक्रिप्शन का Filters टैब, जिसमें इवेंट प्रकार Incoming Call पर फ़िल्टर किया गया है

Event subscription → Filters → Incoming Call

कुछ गंतव्यों (जैसे भारत) के लिए ACS आउटबाउंड प्रतिबंधित या रुक-रुक कर काम करता है। समर्थित गंतव्य इस्तेमाल करें या व्यक्ति के लेग के आगे SIP/Operator नंबर लगाएं। ब्रिज लॉजिक प्रभावित नहीं होता — यह आउटबाउंड लेग पर कैरियर-लेवल विफलता है।

दोनों पक्ष PCM 16 kHz मोनो होने चाहिए। एजेंट के इनपुट/आउटपुट फ़ॉर्मैट को pcm_16000 पर सेट करें; ब्रिज conversation_initiation_metadata से नेगोशिएट किया फ़ॉर्मैट लॉग करता है।

नंबर खरीदने के लिए पेड सब्सक्रिप्शन प्रकार (MCA/EA/PAYG) चाहिए। अगर ACS आपके देश में नंबर नहीं देता, तो इसके बजाय SIP प्रोवाइडर इस्तेमाल करें।

उपयोगी लिंक