Referência de arquitetura · Telefonia + IA

ksir + voicemw

Como os dois serviços se conectam para transformar uma chamada SIP em uma conversa conduzida por bots de IA — multi-tenant, multi-trunk, com um orquestrador roteando cada chamada para o agente certo.

stack  JS tenant  vmtech trunks  2 agentes  cobranca · apresentacao provedor  Vono
01

Visão geral

Três camadas independentes, cada uma com uma responsabilidade clara. A chamada flui de baixo (rede SIP) para cima (lógica de negócio) e volta como áudio.

ksir — a borda SIP

Servidor que fala SIP/RTP com o provedor (Vono) e expõe uma API estilo Asterisk ARI (REST + WebSocket). É quem registra os troncos, recebe/origina chamadas e entrega o áudio. Multi-tenant via clients_dir: um JSON por cliente.

voicemw — o middleware

Serviço que conversa com o ksir via ARI. Faz STT (fala→texto), TTS (texto→fala), detecção de voz (VAD/barge-in) e, a cada turno, chama o webhook de negócio. Expõe a API POST /api/v1/calls para disparar chamadas.

orquestrador + agentes — a lógica de negócio (JS)

Webhooks que decidem o que falar. O orquestrador recebe o início da chamada e escolhe o agente. Os agentes (cobrancabot, apresentadorbot) conduzem a conversa turno a turno. Tudo no mesmo runtime JS, com contexto do lead carregado de arquivos por external_id.

Plataforma × regra de negócio. O ksir e o voicemw são a plataforma — infraestrutura genérica de telefonia e middleware, comum a todos os clientes. Já os scripts de orquestrador, cobrador e apresentador são regras de negócio: são desenvolvidos sob medida de acordo com cada empresa contratante — seu roteiro de conversa, seus critérios de roteamento, sua escada de desconto, seu produto. Trocar de cliente significa trocar (ou customizar) esses scripts, sem mexer na plataforma.

O painel administrativo também é sob medida. O site que administra os scripts de regra de negócio e exibe as informações e o status da telefonia (chamadas, campanhas, acordos, métricas) é customizado conforme a necessidade de cada cliente. Por exemplo: se o contratante é uma cobradora, o painel é construído segundo as diretrizes e parâmetros dela — telas de carteira de devedores, acompanhamento de acordos fechados, funil de negociação. Outro cliente, com outra operação, terá um painel modelado para a realidade dele. A plataforma (ksir + voicemw) permanece a mesma; o que muda é a camada de gestão e visualização.

02

Como tudo se conecta

O caminho completo de uma chamada outbound, do disparo via API até a voz do agente, e a volta do áudio do interlocutor.

Provedor / PSTN
ksir (SIP/ARI)
voicemw
orquestrador
agente cobrança
agente apresentação
Dialer / CRM POST /api/v1/calls voicemw tenant: vmtech · stasis_app: voicemw-vmtech STT · TTS · VAD/barge-in resolve tenant + agente · chama webhook ksir ARI 127.0.0.1:7077 · SIP 31.220.97.96:5060 trunk vmtech_vono → DID 554428880123 trunk ksir_vono → DID 554420181470 Vono (provedor SIP) → PSTN / celular orquestrador /orquestrador lê external_id / DID → decide agente agente · cobranca /cobrar · cobrancabot v4.9 contexto: devedor + dívida agente · apresentacao /apresentar · apresentadorbot contexto: lead + produto outbound_ contexts/ <external_id> .json nome, valor, produto… ① originate ② ARI ⑤ eventos/áudio ③ INVITE/RTP ④ started (webhook) agent_id ⑥ user_speech
  1. DisparoO dialer chama POST /api/v1/calls no voicemw com destino, external_id, outbound_trunk (opcional) e initial_text.
  2. Originate via ARIvoicemw manda o ksir originar a chamada: PJSIP/<destino>@<trunk>, no app voicemw-vmtech.
  3. INVITE ao provedorksir envia o INVITE pela Vono (com auth digest 407→retry) e estabelece o RTP em slin16 16 kHz.
  4. Evento started → orquestradorAo entrar em Stasis, voicemw chama o webhook principal do tenant (o orquestrador), que lê os parâmetros e decide o agente.
  5. Áudio + STTksir devolve o áudio do interlocutor; voicemw transcreve (STT) e detecta fala/silêncio (VAD).
  6. Turnos → agenteA cada fala do interlocutor (user_speech), voicemw chama o webhook do agente escolhido. O agente responde com comandos (play_audio, hangup, transfer).
03

Configuração do ksir

O ksir tem uma config global e um arquivo JSON por cliente (tenant) dentro de clients_dir. Cada cliente lista seus troncos SIP. Não há SIGHUP — para adicionar/editar clientes, reinicie o serviço.

config-ksir.json (global)

config-ksir.json global
{
  "ari_listen": "127.0.0.1:7077",     // API estilo ARI (REST + WebSocket)
  "sip_host":   "31.220.97.96",       // IP público que fala com a Vono
  "sip_port":   5060,
  "rtp_port_start": 30000,
  "rtp_port_end":   30999,
  "clients_dir": "./clients"          // 1 arquivo .json por tenant
}

clients/vmtech.json (o tenant e seus troncos)

Um tenant agrupa todos os troncos SIP de um mesmo cliente. Aqui o vmtech tem dois troncos da Vono — um por atividade. Cada tronco tem auth_user, dids e name únicos globalmente (o ksir valida no boot).

clients/vmtech.json tenant
{
  "tenant": "vmtech",
  "stasis_app": "voicemw-vmtech",   // app que recebe as chamadas no voicemw
  "trunks": [
    {
      "name":      "vmtech_vono",     // COBRANÇA
      "auth_user": "ajvy90872",
      "server":    "sip.vono2.me",
      "dids":      ["554428880123"]
    },
    {
      "name":      "ksir_vono",       // APRESENTAÇÃO
      "auth_user": "rgjb82929",
      "server":    "sip.vono2.me",
      "dids":      ["554420181470"]
    }
  ]
}

No boot o ksir registra os dois troncos na Vono (REGISTER ok) e injeta os channelvars TENANT, TRUNK_NAME e X_CID em cada chamada — é assim que o voicemw sabe a qual tenant/tronco ela pertence.

04

Configuração do voicemw

O voicemw é multi-tenant num único processo. Cada tenant tem seu stasis_app, trunk de saída, token de autenticação, webhook (o orquestrador) e a lista de agentes.

config-voicemw-ksir.json tenant + agentes
"tenants": [{
  "id": "vmtech",
  "stasis_app": "voicemw-vmtech",

  // tronco default + secundários (allowlist p/ outbound_trunk no POST)
  "outbound_trunk":  "vmtech_vono",      // usado quando o POST não pede trunk
  "outbound_trunks": ["ksir_vono"],      // SIPs secundários escolhíveis por chamada

  "auth_token": "<openssl rand -hex 32>",  // Bearer da API de disparo
  "caller_id":  "Empresa <554428880123>",

  // webhook principal = ORQUESTRADOR (recebe o 'started')
  "webhook": { "url": "https://teste.flexpage.io/orquestrador" },

  // cada agente = um webhook próprio; id casa com o retorno do orquestrador
  "agents": [
    { "id": "cobranca",     "url": "https://teste.flexpage.io/cobrar" },
    { "id": "apresentacao", "url": "https://teste.flexpage.io/apresentar" }
  ]
}]
CampoPapel
outbound_trunkTronco default de saída. Usado quando o POST não especifica trunk.
outbound_trunksAllowlist dos SIPs secundários escolhíveis por chamada. O default não se repete aqui (já entra automático).
auth_tokenBearer que autentica o POST /api/v1/calls e resolve o tenant.
webhook.urlO orquestrador. Recebe o evento started de toda chamada do tenant.
agents[].idIdentificador do agente. Deve bater exatamente com o agent_id devolvido pelo orquestrador.
agents[].urlWebhook do agente. Recebe os eventos da chamada a partir do 1º user_speech.
!

Os globais STT (Google v2 telephony pt-BR, projeto flexhook-82bac), TTS (Google Neural2-C), audio_capture (VAD agressividade 2, RTP 25000–25999, slin16) e originate_api (HTTPS 0.0.0.0:7090) ficam fora do bloco do tenant e valem para todos.

05

O orquestrador e os agentes

O webhook principal do tenant é o orquestrador. Ele só atua no started: lê os parâmetros de entrada e responde com o agent_id que deve conduzir a chamada. A partir daí, o voicemw roteia tudo direto para o webhook do agente.

Como o orquestrador decide

Três camadas de prioridade. A primeira que casar vence:

PrioridadeCritérioExemplo → agente
1Prefixo do external_idFAT-/COB-/DIV- → cobranca · LEAD-/APR- → apresentacao
2DID próprio da chamada554428880123 → cobranca · 554420181470 → apresentacao
3Fallback→ cobranca (default)
resposta do orquestrador no 'started' delegação
{
  "call_id":  "a96c9de3-…",
  "agent_id": "apresentacao",   // ← voicemw passa a rotear p/ este agente
  "commands": []                // vazio → voicemw toca o initial_text do POST
}
!

O agente não recebe o evento started — o orquestrador o consome. Por isso cada agente recarrega o contexto do lead de forma lazy no primeiro user_speech (if (!sess.devedor) recarrega…). Os dois bots já fazem isso. Consequência prática: a saudação inicial vem do initial_text do POST — que deve ser LGPD-safe (só nome + confirmação de identidade, sem valor da dívida).

06

Anatomia de uma chamada

Sequência real de uma chamada de apresentação, validada em log (call a96c9de3).

  1. POST de disparoexternal_id: LEAD-…, outbound_trunk: ksir_vono, initial_text com a saudação.
  2. Originatevoicemw → ksir: PJSIP/5544991474799@ksir_vono, trunk_override aplicado, caller 554428880123.
  3. INVITE + RTPksir → Vono: digest auth (407→retry), mídia slin16 estabelecida.
  4. started → orquestradorDECISÃO: agente="apresentacao" (external_id_prefix, LEAD-) → responde agent_id.
  5. Delegaçãovoicemw: agente: delegado pelo orquestrador → apresentacao. Toca o initial_text.
  6. user_speech → agenteApresentadorbot recebe o turno, faz lazy-reload do contexto do lead, conduz a conversa.
  7. Comandos de voltaAgente responde play_audio (vira TTS), e ao fim hangup ou transfer.
07

Disparando chamadas

Os dois fluxos usam o mesmo endpoint e o mesmo Bearer. Muda o outbound_trunk, o prefixo do external_id e o initial_text.

Cobrança

curl — cobrança → cobranca
curl -sk -X POST https://pbx-01.flexpage.io:7090/api/v1/calls \
  -H "Authorization: Bearer $TOKEN_VMTECH" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": "5544991474799",
    "external_id": "FAT-2025-0042",
    "initial_text": "Olá, aqui é uma ligação para Fulano. É com você que estou falando?"
  }'   # sem outbound_trunk → usa o default vmtech_vono

Apresentação

curl — apresentação → apresentacao
curl -sk -X POST https://pbx-01.flexpage.io:7090/api/v1/calls \
  -H "Authorization: Bearer $TOKEN_VMTECH" \
  -H "Content-Type: application/json" \
  -d '{
    "destination": "5544991474799",
    "external_id": "LEAD-2026-ALERTME-00001",
    "outbound_trunk": "ksir_vono",
    "initial_text": "Oi, tudo bem? Aqui é sobre o Alertme. Posso te falar rapidinho?"
  }'
Campo do POSTEfeito
destinationNúmero de destino (o telefone do lead/devedor).
external_idIdentifica o lead e roteia o agente pelo prefixo. Precisa ter JSON de contexto correspondente.
outbound_trunkEscolhe o SIP de saída. Omitido → tronco default. Deve estar na allowlist do tenant.
initial_textPrimeira fala (TTS). Como o agente não vê o started, é aqui que mora a saudação — mantenha LGPD-safe.
08

Checklist de produção

  1. Gerar os tokens reaisopenssl rand -hex 32 para cada tenant. Trocar o placeholder antes de expor a API.
  2. Conferir os arquivos de contextoCada external_id disparado precisa de outbound_contexts/<external_id>.json — senão cai em fallback/encerramento.
  3. Casar os IDs dos agentesOs agent_id do orquestrador devem existir em agents[]. Divergência → agent_id desconhecido.
  4. Validar os troncos no ksirOs nomes em outbound_trunks devem existir como trunks[].name no JSON do tenant.
  5. initial_text LGPD-safeSem valor de dívida na abertura — só nome e confirmação de identidade. A divulgação vem depois, confirmada a identidade.
  6. Reiniciar o ksir ao mudar clientesNão há SIGHUP no ksir. Adições/edições de tenant ou trunk exigem restart.