Développeurs
Connecter des agents IA à des appels téléphoniques avec l’API vocale Sinch v2
Connecter un agent IA à un appel téléphonique en direct implique de faire le pont entre le réseau téléphonique et l’agent. Cela peut impliquer une intégration SIP ou un chemin média brut via un WebSocket, en plus de la reconnaissance vocale, de la synthèse vocale et de la gestion des tours de parole entre l’appelant et l’agent. Cela représente une grande quantité d’infrastructure de téléphonie et de média à construire et à exploiter autour d’un agent IA.
API vocale Sinch v2 vous décharge de ce travail. Sa destination Voice Relay connecte un appel en direct à votre point de terminaison WebSocket, Sinch exécute la reconnaissance vocale et la synthèse vocale, et votre application échange du texte brut. L’API vocale v2 ajoute également Voice Streams, un chemin audio bidirectionnel brut pour les cas où vous souhaitez gérer le média vous-même.
Elle est en prévisualisation publique, donc les fonctionnalités, les limites, la documentation et le comportement peuvent encore changer et aucun SLA ne s’applique, mais elle est ouverte pour les tests, l’évaluation et le trafic de production initial. J’ai exécuté l’ensemble du chemin de bout en bout avec le tutoriel d’exemple : un agent LangChain répondant à un véritable appel téléphonique, interruptions incluses.
Pourquoi Voice Relay est important
Un agent IA commence généralement comme une application textuelle. Il accepte un message, appelle un modèle et renvoie une réponse textuelle. Voice Relay déplace la frontière audio dans la plateforme Sinch, ce qui vous permet de conserver l’agent, ses outils, ses prompts et son état, et de travailler avec du texte plutôt qu’avec de l’audio brut. Les interruptions sont incluses, de sorte que les appelants peuvent intervenir pendant la lecture d’une réponse.
Cela fait de Voice Relay un point d’entrée pratique pour un système de réception IA, un bot de démonstration de produit et un centre d’assistance interne.
Le modèle de l’API vocale v2
L’API vocale v2 organise une interaction autour de trois ressources :
- Une session regroupe les appels et les connexions liés et reste active jusqu’à la fin de ses appels associés.
- Un appel (call) représente la connexion d’un participant ou d’une participante, telle qu’un segment téléphonique, SIP ou de streaming.
- Un pont (bridge) connecte les appels au sein d’une session lorsque plusieurs personnes doivent communiquer.
Le SVAML, le Sinch Voice API Markup Language (langage de balisage de l’API vocale Sinch), décrit ce qui se passe pendant l’interaction. Une commande dial peut créer un segment d’appel. Des événements imbriqués peuvent définir ce qui se passe lorsque l’appel aboutit, est occupé, expire ou échoue. Un webhook peut prendre le relais lorsque l’application doit prendre une décision dynamique.
Ce modèle est important pour les agents IA car la conversation et l’appel sont liés, mais ils ne sont pas la même chose. L’agent IA gère la conversation. Le SVAML et l’API vocale gèrent le flux d’appels. Cette séparation vous permet d’ajouter un transfert vers un agent humain, un autre segment d’appel ou un chemin média différent sans placer toute cette logique dans le prompt du modèle.
Ce dont vous aurez besoin
- Un compte Sinch avec un accès à l’API vocale v2, via le tableau de bord Sinch Build
- Un identifiant de projet Sinch, un identifiant de clé d’accès et un secret de clé d’accès
- Un numéro virtuel Sinch activé
- Une ID de service de l’API vocale v2, à partir de Voix > Voix programmable > Services dans le tableau de bord
- Python 3.10 ou une version plus récente
- Une clé API pour OpenAI, Anthropic Claude ou Google Gemini
ngrokavec un jeton d’authentification enregistré, ou un autre moyen d’exposer un serveur local viawss://
Ce tutoriel utilise le sinch-voice-tutorials dépôt. Son tutoriel 4.1-voice-relay est un serveur WebSocket Python soutenu par LangChain, qui lit sa configuration à partir de variables d’environnement et prend en charge OpenAI, Anthropic Claude et Google Gemini.
Connecter un agent avec Voice Relay
Clonez le dépôt des tutoriels et accédez à l’exemple Voice Relay :
git clone https://github.com/sinch/sinch-voice-tutorials.git
cd sinch-voice-tutorials/4.1-voice-relay
Créez un environnement et installez les dépendances de l’exemple :
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Les commandes ici concernent macOS et Linux. Sur Windows, utilisez python au lieu de python3 et activez avec .venv\Scripts\activate.
Copiez la configuration d’exemple :
cp .env.example .env
Définissez un fournisseur et sa clé API dans .env :
PROVIDER=openai
API_KEY=YOUR_LLM_API_KEY
Le fichier .env.example du dépôt utilise openai comme fournisseur par défaut. L’exemple prend également en charge claude pour Anthropic Claude et gemini pour Google Gemini, et requirements.txt installe les paquets pour les trois. Par conséquent, changer de fournisseur implique de modifier PROVIDER et API_KEY. Vous pouvez également définir MODEL, TEMPERATURE, MAX_TOKENS et PORT, dont les valeurs par défaut s’affichent dans le journal de démarrage ci-dessous, ainsi que GREETING pour changer le Hello! par lequel l’agent commence.
Démarrez le serveur local :
python server.py
Le serveur indique le prompt système qu’il a chargé, puis son fournisseur et les paramètres de son modèle, et enfin l’adresse locale sur laquelle il écoute :
[*] Loaded system prompt (3424 chars)
[*] Provider: openai Model: gpt-4o Temp: 0.7 MaxTokens: 1024
[*] Agent Relay listening on ws://0.0.0.0:8765
Vérifiez cette sortie avant de passer un appel. Une clé de fournisseur manquante ou une dépendance cassée s’affiche ici plutôt qu’au beau milieu d’une conversation.
Le comportement de l’agent provient de system_prompt.md. Modifiez ce fichier pour changer le persona, le domaine ou les instructions, puis redémarrez server.py car le prompt est chargé au démarrage.
Sinch établit une connexion entrante vers votre serveur relais, il a donc besoin d’une adresse publique. Laissez server.py en cours d’exécution dans le premier terminal et ouvrez-en un second. Si vous n’avez pas encore ngrok, installez-le et enregistrez un jeton d’authentification à partir de la page Your Authtoken dans le tableau de bord de ngrok. Le niveau gratuit couvre ce tutoriel, et ngrok refuse de démarrer un tunnel tant qu’un jeton n’est pas configuré :
brew install ngrok
ngrok config add-authtoken YOUR_NGROK_AUTHTOKEN
ngrok http 8765
ngrok prend le contrôle du terminal et affiche un tableau d’état. La ligne importante est l’adresse de transfert (Forwarding) :
Forwarding https://YOUR_NGROK_ID.ngrok-free.dev -> http://localhost:8765
Prenez cette adresse, changez uniquement le schéma en wss:// et utilisez-la comme point de terminaison de Voice Relay :
ngrok address: https://YOUR_NGROK_ID.ngrok-free.dev
Sinch endpoint: wss://YOUR_NGROK_ID.ngrok-free.dev
Cette valeur wss:// va dans la configuration du service Sinch, et non dans le serveur relais, qui reste sur le port local 8765. Laissez server.py et ngrok en cours d’exécution à partir d’ici.
La destination minimale de Voice Relay ressemble à ceci :
{
"type": "VOICE_RELAY",
"voiceRelay": {
"endpoint": "wss://YOUR_NGROK_ID.ngrok-free.dev",
"ttsVoice": "Tiffany",
"sttLanguage": "en-US"
}
}
| Champ | Requis | Notes |
endpoint | Oui | L’adresse wss:// à laquelle Sinch se connecte |
ttsVoice | Oui | Tiffany correspond à ce tutoriel. Consultez le référence des voix prises en charge avant de la modifier |
sttLanguage | Oui | Une balise de langue BCP-47 |
enableInterruptions | Non | La valeur par défaut est true |
callHeaders | Non | Jusqu’à 16 paires clé/valeur, chaque clé et valeur comportant 255 caractères ou moins |
Votre service a besoin du numéro avant que tout cela n’ait de l’importance. Dans le tableau de bord, ouvrez le service, accédez à « Voice channels », et choisissez « Configure » sur la ligne « Phone », puis « Add numbers » et sélectionnez votre numéro virtuel. Un numéro appartient à un seul service à la fois. Par conséquent, s’il est déjà associé à un autre service, le tableau de bord vous demande de confirmer la réaffectation, et les appels entrants vers ce numéro suivront le nouveau service à partir de ce moment-là.
Une fois le numéro en place, acheminez l’appel en attribuant au service un comportement d’appel statique ou un webhook qui renvoie le SVAML. La configuration statique effectue cinq actions :
- Répondre à l’appel entrant.
- Ajouter le segment entrant à
main-bridge. - Créer un segment d’appel avec une destination
VOICE_RELAY. - Ajouter le segment relais au même
main-bridgelors de la réponse. - Raccrocher le segment relais à la fin du segment entrant.
server.py et ngrok occupent tous deux leurs terminaux maintenant, ouvrez donc un troisième terminal pour cette étape. Renseignez les cinq valeurs en haut de la requête et exécutez-la. Si vous préférez utiliser le tableau de bord, collez plutôt le corps SVAML interne de static.txt dans son éditeur de comportement d’appel prédéfini.
export PROJECT_ID=YOUR_PROJECT_ID
export KEY_ID=YOUR_ACCESS_KEY_ID
export SERVICE_ID=YOUR_SERVICE_ID
export VOICE_RELAY_ENDPOINT=wss://YOUR_NGROK_ID.ngrok-free.dev
# prompts without echoing, so the secret stays out of shell history
printf 'Access key secret: '
read -rs KEY_SECRET
echo
export KEY_SECRET
curl -X PATCH \
-u "$KEY_ID:$KEY_SECRET" \
"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/services/$SERVICE_ID" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"callBehavior": {
"type": "STATIC",
"static": {
"callName": "caller",
"commands": [
{ "command": "answer" },
{ "command": "bridgeCall", "bridgeName": "main-bridge" },
{
"command": "dial",
"callName": "voice_relay_call",
"to": {
"type": "VOICE_RELAY",
"voiceRelay": {
"endpoint": "$VOICE_RELAY_ENDPOINT",
"ttsVoice": "Tiffany",
"sttLanguage": "en-US"
}
},
"events": {
"onAnswer": [
{ "command": "bridgeCall", "bridgeName": "main-bridge" }
]
}
}
],
"events": {
"onHangup": [
{ "command": "hangup", "callName": "voice_relay_call" }
]
}
}
}
}
JSON
Une requête PATCH réussie renvoie le service mis à jour, ce qui vous permet de relire votre point de terminaison à partir de la réponse et de confirmer qu’il a été pris en compte :
{
"serviceId": "YOUR_SERVICE_ID",
"projectId": "YOUR_PROJECT_ID",
"name": "voice-relay-test",
"isDefault": true,
"callBehavior": {
"type": "STATIC",
"static": {
"callName": "caller",
"commands": [
{ "command": "answer" },
{ "command": "bridgeCall", "bridgeName": "main-bridge" },
{
"command": "dial",
"to": {
"type": "VOICE_RELAY",
"voiceRelay": {
"endpoint": "wss://YOUR_NGROK_ID.ngrok-free.dev",
"ttsVoice": "Tiffany",
"sttLanguage": "en-US"
}
},
"events": {
"onAnswer": [
{ "command": "bridgeCall", "bridgeName": "main-bridge" }
]
},
"callName": "voice_relay_call"
}
],
"events": {
"onHangup": [
{ "command": "hangup", "callName": "voice_relay_call" }
]
}
}
}
}
Le corps SVAML interne est également disponible dans static.txt.
Résultat attendu
Appelez le numéro Sinch associé au service. Vous devriez entendre Hello!, ou le message d’accueil que vous avez défini avec GREETING. Posez ensuite une question et écoutez la réponse de l’agent.
Le terminal du serveur consigne les deux sens du WebSocket, et ce journal est la chose la plus utile à l’écran lors d’un premier appel. Le mien ressemblait à ceci, réduit à un seul échange :
[+] Connected: ('127.0.0.1', 65438)
WS << {"callHeaders":{},"callId":"01M25YACD3G61GH4Y0NQNWR4WD","interruptionsEnabled":true,...}
connect callId=01M25YACD3G61GH4Y0NQNWR4WD serviceId=YOUR_SERVICE_ID
WS >> {"command": "answer"}
WS >> {"command": "text", "text": "Hello!", "isLast": true, "isInterruptible": true}
WS << {"batchSequence":0,"command":"textPlaybackStart"}
WS << {"batchSequence":0,"command":"textPlaybackStop"}
WS << {"reason":"speech-detected","command":"interrupt"}
WS << {"sttLanguage":"en-US","text":"Hi.","isCorrection":false,"command":"text"}
STT → 'Hi.'
LLM ← 'Hey there! How can I assist you today? If it involves voice tech or a good dad j'…
WS >> {"command": "text", "text": "Hey there! How can I assist you today?...", "isLast": true}
WS << {"batchSequence":1,"command":"textPlaybackStart"}
WS << {"batchSequence":1,"command":"textPlaybackStop"}
[-] Connection closed (1006):
[-] Session ended callId=01M25YACD3G61GH4Y0NQNWR4WD
Deux éléments de ce journal méritent d’être connus avant votre premier appel. Le message connect contient interruptionsEnabled: true, ce qui vous permet de confirmer la valeur par défaut pour l’interruption plutôt que de la tirer de la référence de configuration. De plus, la connexion se ferme avec le code 1006 lorsque l’appelant raccroche, ce qui est interprété comme une fermeture anormale, mais correspond ici à un raccrochage normal.
Un interrupt avec reason=speech-detected apparaît à chaque tour, car Sinch en envoie un chaque fois qu’il entend l’appelant. C’est sa position qui vous donne une indication : après textPlaybackStop, il marque simplement le début du tour de l’appelant, tandis qu’entre textPlaybackStart et textPlaybackStop, il s’agit d’une intervention. Lorsque j’ai parlé pendant une réponse, la lecture s’est arrêtée et la transcription suivante correspondait à ce que j’avais dit par-dessus :
WS << {"batchSequence":5,"command":"textPlaybackStart"}
WS << {"reason":"speech-detected","command":"interrupt"}
WS << {"batchSequence":5,"command":"textPlaybackStop"}
WS << {"sttLanguage":"en-US","text":"I'm going to stop you right there.",...,"command":"text"}
Il s’agit d’une interface vocale devant un agent IA basé sur du texte, sans que vous ayez à traiter une seule trame audio vous-même.
Où se situe votre application
Sinch et le serveur WebSocket échangent un petit protocole JSON, une commande par message, et server.py le gère pour vous. Les corrections de transcription sont la partie qu’il vaut la peine de comprendre avant de vous appuyer sur l’exemple.
Sinch envoie une première transcription, puis peut en envoyer une corrigée pour le même énoncé avec isCorrection: true, et le texte corrigé inclut le texte précédent au lieu de remplacer uniquement la partie modifiée. Cela s’est produit une fois en trois appels : Thank you. a été suivi de Thank you.\nOh, that's nice., et les deux ont été envoyés au modèle, de sorte que l’agent a répondu deux fois au même énoncé. Décidez si vous souhaitez attendre une correction, annuler la requête en cours ou ignorer complètement les corrections.
La limite de l’application ressemble à ceci :
Parole de l'appelant
|
Reconnaissance vocale Sinch
|
Message texte Voice Relay
|
Agent IA
|
Réponse textuelle Voice Relay
|
Synthèse vocale Sinch
|
L'appelant entend la réponse
Voice Relay ou streaming audio brut ?
| Choisir | Quand |
| Voice Relay | Votre agent accepte du texte et renvoie du texte, et Sinch gère la reconnaissance et la synthèse vocales (STT et TTS). |
| Voice Streams | Vous avez besoin d’audio brut et possédez le pipeline STT/TTS. |
Voice Streams convient à un pipeline vocal personnalisé, à un processeur audio spécialisé ou à un fournisseur dont le protocole nécessite un accès direct à l’audio. Cela signifie également que vous avez davantage la main sur le comportement en cas de latence et d’échec.
Considérations de production
Les interactions vocales rendent la latence visible. Une réponse lente du modèle crée un silence pour l’appelant. Mesurez donc le temps écoulé entre l’événement vocal entrant et la première réponse, ainsi que jusqu’à la réponse complète. Utilisez un modèle et un prompt adaptés à une conversation téléphonique plutôt que de vous concentrer uniquement sur l’optimisation de la qualité maximale de la réponse.
L’exemple du dépôt attend la réponse complète du modèle avant de renvoyer quoi que ce soit, de sorte que l’appelant n’entend rien tant que le modèle n’a pas fini de générer. C’est la première chose à vérifier si les pauses semblent longues.
L’exemple conserve également l’historique des conversations en mémoire pour chaque connexion WebSocket. C’est suffisant pour un premier test, mais il ne s’agit pas d’un stockage de conversation durable. Décidez de quel état appartient à un appel, ce qui appartient à un profil client et ce qui doit survivre à une reconnexion.
Les interruptions nécessitent une gestion explicite. Si l’appelant parle pendant qu’une réponse est lue, vous pouvez recevoir un événement d’interruption alors qu’une requête LLM est toujours en cours d’exécution. Annulez la requête lorsque cela est possible, ou marquez chaque réponse avec un ID de tour (turn ID) et éliminez les sorties obsolètes avant de les renvoyer à Sinch. Ce même ID de tour gère les transcriptions corrigées, qui arrivent sous la forme d’un second message text pour un énoncé que vous avez déjà envoyé au modèle.
L’exemple local nécessite également une gestion des échecs plus robuste avant d’être utilisé en production :
- Envoyer une courte réponse de repli lorsque le fournisseur du modèle échoue.
- Persister l’état de la conversation lorsque l’appel doit survivre au redémarrage d’un processus.
- Maintenir le point de terminaison WebSocket disponible pendant toute la durée de l’appel.
- Consigner les identifiants d’appel, de session, de connexion et de minutage du modèle sans enregistrer inutilement le contenu sensible de la conversation.
- Utiliser les identifiants client OAuth 2.0 pour l’accès à l’API en production. L’authentification de base est utile pour les tests initiaux.
Résolution de problèmes
Vérifiez la configuration du fournisseur avant de passer un appel. Une clé LLM incorrecte ou manquante se traduit par un appel silencieux plutôt que par une erreur, car le modèle n’est appelé qu’au premier tour. Confirmez donc que PROVIDER et API_KEY correspondent dans .env.
L’URL gratuite de ngrok change lorsque le tunnel redémarre. Mettez à jour endpoint dans la configuration du service de l’API vocale v2 chaque fois que cela se produit, sans quoi Sinch ne pourra pas joindre le serveur actuel. Il n’est pas nécessaire de redémarrer server.py pour cela.
Statut de la prévisualisation
Les conditions de la prévisualisation autorisent les tests, l’évaluation, l’utilisation commerciale initiale et le trafic en direct, et indiquent que la plateforme est conçue pour supporter des volumes de niveau de production. Le service est fourni tel quel et selon la disponibilité. Des limites d’utilisation peuvent s’appliquer, et aucun SLA ne s’applique. Sinch peut ajouter des fonctionnalités, et des changements majeurs (breaking changes) ne peuvent pas être exclus avant la disponibilité générale.
Prochaines étapes
Voice Relay est l’une des voies d’accès à l’API vocale v2. Le même modèle de plateforme prend également en charge :
- Alertes vocales sortantes avec synthèse vocale
- Détection de répondeur
- Appels par lots avec espacement des appels
- Enregistrement des appels et transcription
- Numéros masqués
- Connexions SIP
- Streaming audio brut
- Contrôle des appels en direct via SVAML et webhooks
Le Le dépôt des tutoriels de l’API vocale Sinch v2 contient des exemples fonctionnels pour ces parcours. Commencez par le tutoriel 4.1-voice-relay pour un agent textuel, puis passez à 4.2-stream-audio lorsque vous avez besoin d’un accès direct au flux audio.
L’API vocale v2 offre à un agent textuel existant une voie vers une interaction vocale en direct sans transformer cet agent en un système de traitement audio. Clonez 4.1-voice-relay, pointez system_prompt.md vers le prompt de votre propre agent, et appelez le numéro.