👋 ¿Buscas Sinch Engage? Ahora estás en el sitio web principal de Sinch. Volver a Sinch Engage

Desarrolladores

Conecta agentes de IA a llamadas telefónicas con la API de voz de Sinch v2

Imagen para Conecta agentes de IA a llamadas telefónicas con la API de voz de Sinch v2

Conectar un agente de IA a una llamada telefónica en directo significa enlazar la red telefónica y el agente. Esto puede implicar una integración SIP o una ruta de medios sin procesar a través de un WebSocket, además de conversión de voz a texto, texto a voz y la gestión de los turnos de conversación entre quien llama y el agente. Es mucha infraestructura de telefonía y medios para crear y gestionar en torno a un agente de IA.

La API de voz de Sinch v2 te ahorra todo ese trabajo. Su destino Voice Relay conecta una llamada en directo a tu punto de conexión WebSocket, Sinch ejecuta la conversión de voz a texto y de texto a voz, y tu aplicación intercambia texto sin formato. La API de voz v2 también añade Voice Streams, una ruta de audio bidireccional sin procesar para los casos en los que sí quieres gestionar los medios directamente.

Está en versión de previsualización pública, por lo que las funciones, los límites, la documentación y el comportamiento aún pueden cambiar y no se aplica ningún SLA, pero está abierta para pruebas, evaluación y tráfico inicial de producción. He ejecutado toda la ruta de principio a fin con el ejemplo del tutorial: un agente de LangChain que responde a una llamada telefónica real, interrupciones incluidas.

Por qué es importante Voice Relay

Un agente de IA suele empezar como una aplicación de texto. Acepta un mensaje, llama a un modelo y devuelve una respuesta de texto. Voice Relay mueve el límite de audio a la plataforma de Sinch para que conserves el agente, sus herramientas, prompts y estado, y trabajes con texto en lugar de audio sin procesar. Incluye las interrupciones, por lo que quienes llaman pueden intervenir mientras se reproduce una respuesta.

Esto hace que Voice Relay sea un punto de entrada práctico para funciones de recepción mediante IA, bots de demostración de productos y un centro de asistencia interno.

El modelo de la API de voz v2

La API de voz v2 organiza una interacción en torno a tres recursos:

  • Una sesión agrupa las llamadas y conexiones relacionadas y permanece activa hasta que finalizan las llamadas asociadas.
  • Una llamada representa la conexión de un participante, como un teléfono, SIP o un tramo de streaming.
  • Un puente conecta las llamadas dentro de una sesión cuando varios participantes necesitan comunicarse.

SVAML, el lenguaje de marcado de la API de voz de Sinch, describe lo que ocurre durante la interacción. Un comando dial puede crear un tramo de llamada. Los eventos anidados pueden definir lo que ocurre cuando se responde a la llamada, está ocupada, se agota el tiempo de espera o falla. Un webhook puede tomar el control cuando la aplicación necesite tomar una decisión dinámica.

Este modelo es importante para los agentes de IA porque la conversación y la llamada están relacionadas, pero no son lo mismo. El agente de IA gestiona la conversación. SVAML y la API de voz gestionan los flujos de llamadas. Esta separación te permite añadir una transferencia humana, otro tramo de llamada o una ruta de medios diferente sin poner toda esa lógica dentro del prompt del modelo.

Qué necesitas

  • Una cuenta de Sinch con acceso a la API de voz v2, en el panel de control de Sinch Build
  • Un ID de proyecto de Sinch, un ID de clave de acceso y un secreto de clave de acceso
  • Un número virtual de Sinch activado
  • Una ID de servicio de la API de voz v2, desde Voz > Voz programable > Servicios en el panel de control
  • Python 3.10 o superior
  • Una clave de API para OpenAI, Anthropic Claude o Google Gemini
  • ngrok con un authtoken registrado, u otra forma de exponer un servidor local a través de wss://

El tutorial utiliza el repositorio sinch-voice-tutorials . Su tutorial 4.1-voice-relay es un servidor Python WebSocket respaldado por LangChain, que lee su configuración desde variables de entorno y es compatible con OpenAI, Anthropic Claude y Google Gemini.

Conecta un agente con Voice Relay

Clona el repositorio de tutoriales y ve al ejemplo de Voice Relay:

git clone https://github.com/sinch/sinch-voice-tutorials.git
cd sinch-voice-tutorials/4.1-voice-relay

Crea un entorno e instala las dependencias del ejemplo:

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Estos comandos son para macOS y Linux. En Windows, utiliza python en lugar de python3 y actívalo con .venv\Scripts\activate.

Copia la configuración del ejemplo:

cp .env.example .env

Configura un proveedor y su clave de API en .env:

PROVIDER=openai
API_KEY=YOUR_LLM_API_KEY

El archivo .env.example del repositorio utiliza openai como proveedor predeterminado. El ejemplo también es compatible con claude para Anthropic Claude y gemini para Google Gemini, y requirements.txt instala los paquetes para los tres, por lo que para cambiar de proveedor hay que cambiar PROVIDER y API_KEY. También puedes configurar MODEL, TEMPERATURE, MAX_TOKENS y PORT, cuyos valores predeterminados aparecen en el registro de inicio a continuación, además de GREETING para cambiar el mensaje Hello! con el que inicia el agente.

Inicia el servidor local:

python server.py

El servidor genera un informe del prompt del sistema que ha cargado, luego de los ajustes del proveedor y el modelo, y por último de la dirección local en la que está escuchando:

[*] 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

Comprueba ese resultado antes de realizar una llamada. Si falta una clave del proveedor o hay una dependencia rota, aparecerá aquí en lugar de en mitad de una conversación.

El comportamiento del agente proviene de system_prompt.md. Edita ese archivo para cambiar la persona, el dominio o las instrucciones, y luego reinicia server.py, ya que el prompt se carga al inicio.

Sinch realiza una conexión entrante a tu servidor de retransmisión, por lo que necesita una dirección pública. Deja server.py en ejecución en el primer terminal y abre un segundo. Si aún no tienes ngrok, instálalo y registra un authtoken desde la página Your Authtoken del panel de control de ngrok. El nivel gratuito cubre este tutorial y ngrok no iniciará ningún túnel hasta que se configure un token:

brew install ngrok
ngrok config add-authtoken YOUR_NGROK_AUTHTOKEN
ngrok http 8765

ngrok toma el control del terminal e imprime una tabla de estado. La línea importante es la dirección de reenvío:

Forwarding https://YOUR_NGROK_ID.ngrok-free.dev -> http://localhost:8765

Toma esa dirección, cambia solo el esquema a wss:// y utilízala como punto de conexión de Voice Relay:

ngrok address: https://YOUR_NGROK_ID.ngrok-free.dev
Sinch endpoint: wss://YOUR_NGROK_ID.ngrok-free.dev

Ese valor de wss:// va en la configuración del servicio de Sinch, no en el servidor de retransmisión, que se mantiene en el puerto local 8765. A partir de aquí, deja que tanto server.py como ngrok sigan ejecutándose.

El destino mínimo de Voice Relay tiene este aspecto:

                                

                                    {
  "type": "VOICE_RELAY",
  "voiceRelay": {
    "endpoint": "wss://YOUR_NGROK_ID.ngrok-free.dev",
    "ttsVoice": "Tiffany",
    "sttLanguage": "en-US"
  }
}
                                
                            
Campo Requerido Notas 
endpoint Sí La dirección wss:// a la que se conecta Sinch 
ttsVoice Sí Tiffany coincide con este tutorial. Consulta la referencia de voces compatibles antes de cambiarla 
sttLanguage Sí Una etiqueta de idioma BCP-47 
enableInterruptions No El valor predeterminado es true 
callHeaders No Hasta 16 pares clave/valor, cada clave y valor con 255 caracteres o menos

Tu servicio necesita el número antes de que nada de esto importe. En el panel de control, abre el servicio, ve a Canales de voz y elige Configurar en la fila Teléfono, luego Añadir números y escoge tu número virtual. Un número pertenece a un servicio a la vez, por lo que si ya está en otro servicio, el panel de control te pedirá que confirmes la reasignación, y las llamadas entrantes a ese número seguirán al nuevo servicio a partir de entonces.

Una vez configurado el número, enruta la llamada dando al servicio un comportamiento de llamada estático o un webhook que devuelva SVAML. La configuración estática realiza cinco acciones:

  1. Responder la llamada entrante.
  2. Añadir el tramo entrante a main-bridge.
  3. Crear un tramo de llamada con un destino VOICE_RELAY.
  4. Añadir el tramo de retransmisión al mismo main-bridge cuando responda.
  5. Colgar el tramo de retransmisión cuando finalice el tramo entrante.

Tanto server.py como ngrok están ocupando sus terminales ahora, así que abre un tercero para este paso. Rellena los cinco valores en la parte superior de la solicitud y ejecútala. Si prefieres utilizar el panel de control, pega el cuerpo de SVAML interno de static.txt en el editor de comportamientos de llamada predefinido.

                                

                                    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

                                
                            

Un PATCH exitoso devuelve el servicio actualizado, de modo que puedes leer tu punto de conexión en la respuesta y confirmar que se aplicó:

                                

                                    {
  "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" }
        ]
      }
    }
  }
}
                                
                            

El cuerpo interno de SVAML también está disponible en static.txt.

Aspecto de una ejecución exitosa

Llama al número de Sinch asociado al servicio. Deberías escuchar Hello!, o el saludo que hayas configurado con GREETING. Luego haz una pregunta y escucha la respuesta del agente.

El terminal del servidor registra ambas direcciones del WebSocket y ese registro es lo más útil que verás en pantalla durante una primera llamada. El mío se veía así, recortado a un solo intercambio:

[+] 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! ¿Cómo puedo ayudarte hoy? Si tiene que ver con tecnología de voz o un buen ch'…
WS >> {"command": "text", "text": "Hey there! ¿Cómo puedo ayudarte hoy?...", "isLast": true}
WS << {"batchSequence":1,"command":"textPlaybackStart"}
WS << {"batchSequence":1,"command":"textPlaybackStop"}
[-] Connection closed (1006):
[-] Session ended callId=01M25YACD3G61GH4Y0NQNWR4WD

Vale la pena conocer dos aspectos de ese registro antes de tu primera llamada. El mensaje connect incluye interruptionsEnabled: true, que es donde puedes confirmar el valor predeterminado de interrupción en lugar de tomarlo de la referencia de configuración. Además, la conexión se cierra con el código 1006 cuando quien llama cuelga; esto parece un cierre anormal, pero es el aspecto de un cuelgue normal en este contexto.

Aparece un interrupt con reason=speech-detected en cada turno, ya que Sinch envía uno cada vez que escucha a la persona que llama. Su posición es lo que nos da la información: si aparece después de textPlaybackStop solo marca el inicio del turno de la persona que llama, mientras que si está entre textPlaybackStart y textPlaybackStop se trata de una intervención (barge-in). Cuando hablé por encima de una respuesta, la reproducción se detuvo y la siguiente transcripción reflejó lo que yo había dicho por encima:

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"}

Se trata de una interfaz de voz frente a un agente de IA basado en texto, sin que tú tengas que gestionar ni una sola trama de audio.

Dónde se sitúa tu aplicación

Sinch y el servidor WebSocket intercambian un pequeño protocolo JSON, con un comando por mensaje, y server.py lo gestiona por ti. Las correcciones de las transcripciones son la parte que vale la pena entender antes de seguir construyendo sobre el ejemplo.

Sinch envía una transcripción inicial y luego puede enviar una corregida para la misma declaración con isCorrection: true; el texto corregido incluye el texto anterior en lugar de reemplazar solo la parte modificada. Ocurrió una vez cada tres llamadas: Thank you. fue seguido por Thank you.\nOh, that's nice., y ambos fueron al modelo, por lo que el agente respondió a la misma declaración dos veces. Decide si vas a esperar a una corrección, cancelar la solicitud en curso o ignorar las correcciones por completo.

El límite de la aplicación tiene este aspecto:

Voz de la persona que llama
|
Sinch conversión de voz a texto
|
Mensaje de texto de Voice Relay
|
Agente de IA
|
Respuesta de texto de Voice Relay
|
Sinch texto a voz
|
La persona que llama escucha la respuesta

¿Voice Relay o streaming de audio sin procesar?

Elige Cuándo 
Voice Relay Tu agente acepta texto y devuelve texto, y Sinch gestiona el STT y TTS. 
Voice Streams Necesitas audio sin procesar y controlas el pipeline de STT/TTS. 

Voice Streams es adecuado para un pipeline de voz personalizado, un procesador de audio especializado o un proveedor cuyo protocolo requiera acceso directo al audio. También significa que tienes un mayor control sobre la latencia y el comportamiento ante fallos.

Consideraciones para la producción

Las interacciones de voz hacen visible la latencia. Una respuesta lenta del modelo crea silencios para la persona que llama, por lo que es necesario medir el tiempo transcurrido desde el evento de voz entrante hasta la primera respuesta y hasta la respuesta completa. Utiliza un modelo y un prompt que se adapten a una conversación telefónica en lugar de optimizar solo para obtener la máxima calidad de respuesta.

El ejemplo del repositorio espera a recibir la respuesta completa del modelo antes de devolver nada, por lo que la persona que llama no escucha nada hasta que el modelo ha terminado de generarla. Ese es el primer aspecto a revisar si las pausas parecen muy largas.

El ejemplo también guarda el historial de conversación en la memoria para cada conexión WebSocket. Está bien para una primera prueba, pero no es un almacenamiento duradero para las conversaciones. Decide qué estado pertenece a una llamada, qué pertenece a un cliente y qué debe sobrevivir a una reconexión.

Las interrupciones requieren una gestión explícita. Si la persona que llama habla mientras se reproduce una respuesta, puedes recibir un evento de interrupción cuando aún se está ejecutando una solicitud de LLM. Cancela la solicitud si es posible, o etiqueta cada respuesta con un ID de turno y descarta los resultados obsoletos antes de enviarlos de vuelta a Sinch. El mismo ID de turno gestiona las transcripciones corregidas, que llegan como un segundo mensaje de text para una declaración que ya has enviado al modelo.

El ejemplo local también necesita una gestión de fallos más sólida antes de su uso en producción:

  • Envía una breve respuesta alternativa cuando el proveedor del modelo falle.
  • Mantén el estado de la conversación cuando la llamada deba sobrevivir al reinicio de un proceso.
  • Mantén el punto de conexión WebSocket disponible durante toda la vida útil de la llamada.
  • Registra los identificadores de llamada, sesión, conexión y tiempos del modelo sin registrar innecesariamente contenido sensible de la conversación.
  • Utiliza credenciales de cliente de OAuth 2.0 para el acceso a la API en producción. La autenticación básica resulta útil para las pruebas iniciales.

Solución de problemas

Comprueba la configuración del proveedor antes de realizar una llamada. Una clave de LLM incorrecta o ausente se refleja como una llamada silenciosa en lugar de un error, ya que no se llama al modelo hasta el primer turno, así que confirma que PROVIDER y API_KEY coinciden entre sí en .env.

La URL gratuita de ngrok cambia cuando se reinicia el túnel. Actualiza el endpoint en la configuración del servicio de la API de voz v2 siempre que ocurra, o de lo contrario Sinch no podrá conectarse al servidor actual. Para ello no es necesario reiniciar server.py.

Estado de previsualización

Las condiciones de la previsualización permiten realizar pruebas, evaluaciones, un uso comercial temprano y tráfico en directo, y establecen que la plataforma está diseñada para gestionar volúmenes a nivel de producción. El servicio se proporciona tal cual y según disponibilidad, es posible que se apliquen límites de uso y no se aplica ningún SLA. Sinch podría añadir funcionalidades y no se pueden descartar cambios importantes antes de su disponibilidad general.

Próximos pasos

Voice Relay es una vía de acceso a la API de voz v2. El mismo modelo de plataforma también es compatible con:

  • Alertas de voz salientes con texto a voz
  • Detección de contestador automático
  • Llamadas por lotes con control de ritmo
  • Grabación de llamadas y transcripción
  • Enmascaramiento de números
  • Conexiones SIP
  • Streaming de audio sin procesar
  • Control de llamadas en directo mediante SVAML y webhooks

La El repositorio de tutoriales de la API de voz v2 de Sinch tiene ejemplos prácticos para estas opciones. Empieza con el tutorial 4.1-voice-relay para un agente basado en texto y luego pasa a 4.2-stream-audio cuando necesites acceso directo al stream de audio.

La API de voz v2 proporciona a un agente de texto existente una vía hacia una interacción de voz en directo sin convertir a ese agente en un sistema de procesamiento de audio. Clona 4.1-voice-relay, dirige system_prompt.md al prompt de tu propio agente y llama al número.

Recursos adicionales