👋 Procurando pelo Sinch Engage? Você está agora no site principal da Sinch. Voltar para o Sinch Engage

Desenvolvedores

Conectar agentes de IA a chamadas telefônicas com a API de voz v2 da Sinch

Imagem para Conectar agentes de IA a chamadas telefônicas com a API de voz v2 da Sinch

Conectar um agente de IA a uma chamada telefônica ao vivo significa fazer a ponte entre a rede telefônica e o agente. Isso pode envolver integração SIP ou um caminho de mídia bruto por um WebSocket, além de conversão de fala em texto, conversão de texto para fala e o gerenciamento dos turnos de conversa entre quem liga e o agente. É muita infraestrutura de telefonia e mídia para construir e operar em torno de um agente de IA.

A API de voz v2 da Sinch poupa você desse trabalho. O destino Voice Relay dela conecta uma chamada ao vivo ao seu endpoint WebSocket, a Sinch executa a conversão de fala em texto e de texto para fala, e o seu aplicativo troca texto simples. A API de voz v2 também adiciona o Voice Streams, um caminho de áudio bidirecional bruto para os casos em que você mesmo quer processar a mídia.

Está em pré-visualização pública, então os recursos, limites, documentação e comportamento ainda podem mudar e nenhum SLA se aplica, mas está aberto para testes, avaliação e tráfego inicial de produção. Executei todo o caminho de ponta a ponta com o exemplo do tutorial: um agente LangChain atendendo a uma chamada telefônica real, com interrupções incluídas.

Por que o Voice Relay é importante

Um agente de IA geralmente começa como um aplicativo de texto. Ele aceita uma mensagem, chama um modelo e retorna uma resposta de texto. O Voice Relay move a fronteira de áudio para a plataforma Sinch. Assim, você mantém o agente, as ferramentas, os prompts e o estado dele, e trabalha com texto em vez de áudio bruto. Ele já inclui interrupções, então quem ligou pode intervir enquanto uma resposta está sendo reproduzida.

Isso torna o Voice Relay um ponto de entrada prático para recepcionistas de IA, bots de demonstração de produtos e helpdesks internos.

O modelo da API de voz v2

A API de voz v2 organiza uma interação em torno de três recursos:

  • Uma sessão agrupa chamadas e conexões relacionadas e permanece ativa até que suas chamadas associadas terminem.
  • Uma chamada representa a conexão de um participante, como um segmento de telefone, SIP ou streaming.
  • Uma ponte conecta chamadas dentro de uma sessão quando vários participantes precisam se comunicar.

A linguagem SVAML (Sinch Voice API Markup Language) descreve o que acontece durante a interação. Um comando dial pode criar um segmento de chamada. Eventos aninhados podem definir o que acontece quando a chamada é atendida, dá ocupado, excede o tempo limite ou falha. Um webhook pode assumir o controle quando o aplicativo precisa tomar uma decisão dinâmica.

Esse modelo é importante para agentes de IA porque a conversa e a chamada estão relacionadas, mas não são a mesma coisa. O agente de IA gerencia a conversa. A SVAML e a API de voz gerenciam o fluxo de chamada. Essa separação permite que você adicione uma transferência para humanos, outro segmento de chamada ou um caminho de mídia diferente sem colocar toda essa lógica dentro do prompt do modelo.

Você precisará de

  • Uma conta da Sinch com acesso à API de voz v2, no painel do Sinch Build
  • Um ID de projeto da Sinch, ID de chave de acesso e segredo de chave de acesso
  • Um número virtual da Sinch ativado
  • Uma ID do serviço da API de voz v2, em Voice > Programmable Voice > Services no painel
  • Python 3.10 ou mais recente
  • Uma chave de API para a OpenAI, Anthropic Claude ou Google Gemini
  • O ngrok com um authtoken registrado ou outra maneira de expor um servidor local por wss://

O passo a passo usa o repositório sinch-voice-tutorials . O tutorial 4.1-voice-relay dele é um servidor WebSocket em Python apoiado pelo LangChain, que lê sua configuração de variáveis de ambiente e tem suporte para a OpenAI, Anthropic Claude e Google Gemini.

Conectar um agente com o Voice Relay

Clone o repositório de tutoriais e acesse o exemplo do Voice Relay:

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

Crie um ambiente e instale as dependências do exemplo:

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

Os comandos aqui são para macOS e Linux. No Windows, use python no lugar de python3 e ative com .venv\Scripts\activate.

Copie a configuração de exemplo:

cp .env.example .env

Defina um provedor e sua chave de API no arquivo .env:

PROVIDER=openai
API_KEY=YOUR_LLM_API_KEY

O arquivo .env.example do repositório usa openai como provedor padrão. O exemplo também é compatível com claude para Anthropic Claude e gemini para Google Gemini, e o requirements.txt instala os pacotes para os três. Portanto, para mudar de provedor, basta alterar as variáveis PROVIDER e API_KEY. Você também pode definir MODEL, TEMPERATURE, MAX_TOKENS e PORT, cujos padrões aparecem no log de inicialização abaixo, além de GREETING para alterar o Hello! com que o agente inicia a conversa.

Inicie o servidor local:

python server.py

O servidor informa o prompt do sistema que carregou, depois suas configurações de provedor e modelo, e, em seguida, o endereço local em que está escutando:

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

Verifique essa saída antes de fazer uma chamada. Uma chave de provedor ausente ou uma dependência quebrada aparece aqui, e não no meio de uma conversa.

O comportamento do agente vem do arquivo system_prompt.md. Edite esse arquivo para alterar a persona, o domínio ou as instruções e, em seguida, reinicie o server.py, pois o prompt é carregado na inicialização.

A Sinch se conecta internamente ao seu servidor de relay, então é preciso ter um endereço público. Deixe o server.py rodando no primeiro terminal e abra um segundo. Se você ainda não tiver o ngrok, instale-o e registre um authtoken na página Your Authtoken no painel do ngrok. O nível gratuito abrange este passo a passo, e o ngrok se recusa a iniciar um túnel até que um token seja configurado:

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

O ngrok assume o controle do terminal e imprime uma tabela de status. A linha que importa é o endereço de encaminhamento:

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

Pegue esse endereço, altere apenas o esquema para wss:// e use-o como o endpoint do Voice Relay:

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

Esse valor wss:// vai na configuração do serviço da Sinch, não no servidor de relay, que permanece na porta local 8765. Daqui em diante, deixe o server.py e o ngrok em execução.

O destino mínimo do Voice Relay fica assim:

                                

                                    {
  "type": "VOICE_RELAY",
  "voiceRelay": {
    "endpoint": "wss://YOUR_NGROK_ID.ngrok-free.dev",
    "ttsVoice": "Tiffany",
    "sttLanguage": "en-US"
  }
}
                                
                            
Campo Obrigatório Notas 
endpoint Sim O endereço wss:// ao qual a Sinch se conecta 
ttsVoice Sim A voz Tiffany corresponde a este tutorial. Veja o referência de vozes compatíveis antes de alterá-la 
sttLanguage Sim Uma tag de idioma BCP-47 
enableInterruptions Não O padrão é true 
callHeaders Não Até 16 pares de chave/valor, sendo que cada chave e valor deve ter 255 caracteres ou menos

Seu serviço precisa do número antes que qualquer uma dessas coisas importe. No painel, abra o serviço, acesse “Voice channels” e escolha “Configure” na linha “Phone”. Em seguida, clique em “Add numbers” e escolha seu número virtual. Um número pertence a apenas um serviço de cada vez. Portanto, se ele já estiver em outro serviço, o painel pedirá que você confirme a reatribuição, e as chamadas recebidas para esse número seguirão para o novo serviço a partir de então.

Com o número pronto, roteie a chamada atribuindo ao serviço um comportamento de chamada estático ou um webhook que retorne a SVAML. A configuração estática faz cinco coisas:

  1. Atende a chamada de entrada.
  2. Adiciona o segmento de entrada à main-bridge.
  3. Cria um segmento de chamada com um destino VOICE_RELAY.
  4. Adiciona o segmento de relay à mesma main-bridge quando ele atender.
  5. Encerra o segmento de relay quando o segmento de entrada termina.

O server.py e o ngrok estão ocupando seus terminais agora, então abra um terceiro para esta etapa. Preencha os cinco valores no topo da solicitação e execute-a. Se preferir usar o painel, cole o corpo SVAML interno do static.txt no editor de comportamento de chamada predefinido dele.

                                

                                    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

                                
                            

Um PATCH bem-sucedido retorna o serviço atualizado, para que você possa ler seu endpoint de volta na resposta e confirmar que funcionou:

                                

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

O corpo SVAML interno também está disponível no arquivo static.txt.

Como saber se deu certo

Ligue para o número da Sinch associado ao serviço. Você deverá ouvir Hello!, ou a saudação que você definiu com GREETING. Em seguida, faça uma pergunta em voz alta e ouça a resposta do agente.

O terminal do servidor registra ambas as direções do WebSocket, e esse log é a coisa mais útil na tela durante uma primeira chamada. O meu ficou assim, reduzido a uma interação:

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

Vale a pena saber duas coisas sobre esse log antes da sua primeira chamada. A mensagem connect carrega interruptionsEnabled: true, que é onde você pode confirmar o padrão de interrupção em vez de tirá-lo da referência de configuração. E a conexão é fechada com o código 1006 quando a pessoa que ligou desliga a chamada, o que parece um fechamento anormal, mas é como um desligamento normal aparece aqui.

Uma interrupção (interrupt) com reason=speech-detected aparece em todos os turnos, porque a Sinch envia uma sempre que ouve quem está ligando. É a posição dela que diz algo a você: depois de textPlaybackStop, ela apenas marca o início do turno de quem ligou, enquanto entre textPlaybackStart e textPlaybackStop, ela é uma intervenção (barge-in). Quando eu falei por cima de uma resposta, a reprodução parou e a transcrição seguinte foi o que eu havia dito por cima:

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

Isso é uma interface de voz na frente de um agente de IA baseado em texto, sem que você precise lidar com um único quadro de áudio.

Onde o seu aplicativo fica

A Sinch e o servidor WebSocket trocam um pequeno protocolo JSON, com um comando por mensagem, e o server.py lida com isso para você. As correções de transcrição são a parte que vale a pena entender antes de você avançar em cima do exemplo.

A Sinch envia uma transcrição antecipada e, depois, pode enviar uma corrigida para a mesma fala com isCorrection: true. O texto corrigido inclui o texto anterior em vez de substituir apenas a parte alterada. Isso aconteceu uma vez em três chamadas: Thank you. foi seguido por Thank you.\nOh, that's nice., e ambos foram para o modelo, de modo que o agente respondeu à mesma fala duas vezes. Decida se vai aguardar uma correção, cancelar a solicitação em andamento ou ignorar as correções totalmente.

A fronteira do aplicativo é mais ou menos assim:

Fala de quem liga
|
Fala para texto da Sinch
|
Mensagem de texto do Voice Relay
|
Agente de IA
|
Resposta de texto do Voice Relay
|
Texto para fala da Sinch
|
Quem liga ouve a resposta

Voice Relay ou streaming de áudio bruto?

Escolha Quando 
Voice Relay Seu agente aceita e retorna texto, e a Sinch lida com o STT e o TTS. 
Voice Streams Você precisa do áudio bruto e possui o próprio pipeline de STT/TTS. 

O Voice Streams é adequado para um pipeline de fala personalizado, um processador de áudio especializado ou um provedor cujo protocolo exija acesso direto ao áudio. Isso também significa que você tem mais responsabilidade sobre o comportamento de latência e falhas.

Considerações sobre a produção

As interações por voz deixam a latência evidente. Uma resposta lenta do modelo gera silêncio para quem ligou. Por isso, meça o tempo desde o evento de fala de entrada até a primeira resposta e até a resposta concluída. Use um modelo e um prompt que se adequem a uma conversa telefônica, em vez de otimizar apenas para obter a qualidade máxima da resposta.

O exemplo do repositório espera pela resposta completa do modelo antes de enviar qualquer coisa de volta, então a pessoa não ouve nada até que o modelo termine de gerar a resposta. Esse é o primeiro lugar a se observar se as pausas parecerem longas.

O exemplo também mantém o histórico de conversas na memória para cada conexão do WebSocket. Isso não é problema para um primeiro teste, mas não é um armazenamento de conversas durável. Decida qual estado pertence a uma chamada, qual pertence a um cliente e qual precisa sobreviver a uma reconexão.

Interrupções exigem tratamento explícito. Se a pessoa falar enquanto uma resposta está sendo reproduzida, você poderá receber um evento de interrupção enquanto uma solicitação ao LLM ainda estiver em execução. Cancele a solicitação onde puder, ou marque cada resposta com um ID de turno e descarte saídas obsoletas antes de enviá-las de volta à Sinch. O mesmo ID de turno lida com transcrições corrigidas, que chegam como uma segunda mensagem de text para uma fala que você já enviou ao modelo.

O exemplo local também precisa de um tratamento de falhas mais robusto antes do uso em produção:

  • Envie uma resposta curta de fallback quando o provedor do modelo falhar.
  • Persista o estado da conversa quando a chamada precisar sobreviver à reinicialização de um processo.
  • Mantenha o endpoint do WebSocket disponível por toda a duração da chamada.
  • Registre os identificadores de chamada, sessão, conexão e tempo do modelo sem registrar o conteúdo sensível da conversa desnecessariamente.
  • Use credenciais de cliente OAuth 2.0 para acesso à API em produção. A autenticação básica é útil para testes iniciais.

Solução de problemas

Verifique a configuração do provedor antes de fazer uma chamada. Uma chave de LLM inválida ou ausente se manifesta como uma chamada silenciosa em vez de um erro, pois o modelo não é chamado até o primeiro turno. Portanto, confirme se PROVIDER e API_KEY correspondem entre si no arquivo .env.

A URL gratuita do ngrok muda quando o túnel é reiniciado. Atualize o endpoint na configuração do serviço da API de voz v2 sempre que isso acontecer, ou a Sinch não conseguirá acessar o servidor atual. Não é preciso reiniciar o server.py para isso.

Status de pré-visualização

Os termos de pré-visualização permitem testes, avaliação, uso comercial antecipado e tráfego ao vivo, e afirmam que a plataforma é projetada para ter suporte para volumes de nível de produção. O serviço é fornecido “no estado em que se encontra” e “conforme a disponibilidade”. Limites de uso podem ser aplicados, e nenhum SLA se aplica. A Sinch pode adicionar funcionalidades, e alterações incompatíveis não podem ser descartadas antes da disponibilidade geral.

O que vem a seguir

O Voice Relay é um dos caminhos para a API de voz v2. O mesmo modelo de plataforma também é compatível com:

  • Alertas de voz de saída com conversão de texto para fala
  • Detecção de secretária eletrônica
  • Chamadas em lote com controle de ritmo (call pacing)
  • Gravação de chamadas e transcrição
  • Mascaramento de números
  • Conexões SIP
  • Streaming de áudio bruto
  • Controle de chamadas ao vivo por meio de SVAML e webhooks

A O repositório de tutoriais da API de voz v2 da Sinch tem exemplos práticos para esses caminhos. Comece pelo tutorial 4.1-voice-relay para um agente baseado em texto e, em seguida, passe para o 4.2-stream-audio quando precisar de acesso direto ao stream de áudio.

A API de voz v2 oferece a um agente de texto existente um roteamento para uma interação de voz ao vivo, sem transformá-lo em um sistema de processamento de áudio. Clone o 4.1-voice-relay, aponte o system_prompt.md para o prompt do seu próprio agente e ligue para o número.

Recursos adicionais