https://a.storyblok.com/f/270183/89879/e33247e622/unnamed.jpg

Como criar um sistema IVR complexo com facilidade usando o XState

Publicado em May 13, 2021

Tempo de leitura: 7 minutos

Mesmo que você não soubesse que é assim que eles são chamados, provavelmente usa sistemas IVR o tempo todo. Um sistema IVR permite que você ligue para um número de telefone, ouça as instruções de áudio e navegue pela chamada para obter as informações de que precisa. A Vonage torna a criação de um sistema IVR completo tão simples quanto iniciar um servidor web. Nesta postagem, veremos como criar sistemas IVR muito complexos e elaborados, mantendo o código simples e fácil de manter. Para isso, usaremos XState , que é uma biblioteca popular de máquinas de estados para JavaScript.

Um sistema IVR com menos de 35 linhas de código

O segredo para implementar um sistema IVR com a Vonage é criar um servidor web que instrua a Vonage sobre como lidar com cada etapa da chamada. Normalmente, isso significa que, assim que um usuário ligar para o seu número virtual de entrada, a Vonage enviará uma solicitação HTTP para o seu /answer ponto de extremidade e esperará que você responda com uma carga JSON composta por objetos NCCO que especificam o que o usuário deve ouvir. Da mesma forma, quando o usuário usa o teclado para escolher o que deseja ouvir em seguida, a Vonage faz uma solicitação a um endpoint diferente, normalmente chamado de /dtmf. O /dtmf ponto de extremidade será chamado com uma carga de solicitação que inclui o número que o usuário escolheu, o qual seu servidor deve usar para determinar com qual conjunto de objetos NCCO deve responder.

Vamos ver como isso fica no código ao usar Express para alimentar nosso servidor web.

const express = require('express');
const bodyParser = require('body-parser');

const port = process.env.PORT || 3000;
const app = express();
app.use(bodyParser.json());

app.post('/answer', (req, res) => {
  const ncco = [
    { action: 'talk', text: "Hi. You've reached Joe's Restaurant! Springfield's top restaurant chain!" },
    { action: 'talk', text: 'Please select one of our locations.' },
    { action: 'talk', text: 'Press 1 for our Main Street location.' },
    { action: 'talk', text: 'Press 2 for our Broadway location.' },
    { action: 'input', eventUrl: [ 'https://example.com/dtmf' ], maxDigits: 1 },
  ];
  res.json(ncco);
});

app.post('/dtmf', (req, res) => {
  const { dtmf } = req.body;
  let ncco;
  switch (dtmf) {
    case '1':
      ncco = [ { action: 'talk', text: "Joe's Main Street is located at Main Street number 11, Springfield." } ];
      break;
    case '2':
      ncco = [ { action: 'talk', text: "Joe's Broadway is located at Broadway number 46, Springfield." } ];
      break;
  }
  res.json(ncco);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Experimente você mesmo

Você pode começar a escrever o código do seu aplicativo imediatamente. Mas, para poder acessá-lo e testar por conta própria se tudo está funcionando, você precisará realizar o seguinte:

  • Certifique-se de que seu servidor Web esteja acessível na Internet. Você pode fazer isso expondo sua máquina de desenvolvimento local usando o Ngrok ou desenvolvendo usando Glitch.

  • Crie um aplicativo de Voice. Você pode fazer isso pelo site da Vonageou usando a CLI da Vonage. Você precisará inserir a URL pública do seu /answer ponto de extremidade ao configurar seu aplicativo.

  • Obtenha um número virtual de recebimento e conecte-o ao seu aplicativo usando o site ou CLI.

Quando tudo isso estiver configurado, você poderá ligar para o seu número e ouvirá a resposta de áudio baseada nos dados que você enviar do seu servidor web.

Indo além do “Hello World” dos sistemas IVR

O exemplo mostrado acima funciona conforme o esperado, mas um sistema IVR real solicitará várias vezes que o usuário forneça informações e interpretará a entrada numérica do usuário com base na situação do usuário durante a chamada. Para ilustrar isso, vamos supor que, em nosso exemplo, o usuário seja solicitado a escolher a localização do restaurante na qual está interessado e, em seguida, a escolher se deseja ouvir o horário de funcionamento ou fazer uma reserva. Em ambos os casos, o usuário pode pressionar o 1 no teclado, mas a forma como interpretamos isso depende da mensagem de áudio anterior e do estado do usuário na chamada.

Para dar suporte a esse caso de uso, precisaremos alterar o código que acabamos de escrever. O ideal é alterá-lo de forma que, à medida que adicionarmos funcionalidades e tornarmos nosso sistema IVR mais complexo com o tempo, nosso código permaneça simples e não tenhamos que repensar como estruturá-lo. Para isso, vamos modelar nossa estrutura de chamadas como uma Máquina de Estados Finitos usando XState, uma biblioteca de máquinas de estados para JavaScript.

##Introdução às máquinas de estados

Uma máquina de estados é simplesmente um modelo de uma “máquina” que pode estar em apenas um estado a qualquer momento e só pode transitar de um estado para outro mediante entradas específicas. O XState e outras bibliotecas de máquinas de estados permitem que você modele e instancie uma máquina no código, de forma que as “regras” da máquina de estados sejam garantidamente aplicadas.

Modelando nossa estrutura de chamadas como uma máquina de estados

Para modelar nossa estrutura de chamadas como uma máquina de estados, usaremos a Machine função que o XState disponibiliza:

// machine.js
const { Machine } = require('xstate');

module.exports = Machine({
  id: 'call',
  initial: 'intro',
  states: {
    intro: {
      on: {
        'DTMF-1': 'mainStLocation',
        'DTMF-2': 'broadwayLocation'
      }
    },
    mainStLocation: {
    },
    broadwayLocation: {
    }
  }
});

Como você pode ver no código acima, nossa chamada só pode estar em um dos três estados:

  • O intro estado em que o usuário está ouvindo a introdução e é orientado a escolher o local de seu interesse.

  • O mainStLocation situação em que estão ouvindo informações sobre a localização da Main St. do nosso hipotético restaurante chai

  • O broadwayLocation estado em que se encontram quando estão ouvindo informações sobre o local na Broadway.

Você também pode observar que:

  • A única maneira de fazer a transição para o mainStLocation estado é estar no intro estado e enviar o DTMF-1 evento.

  • A única maneira de fazer a transição para o broadwayLocation estado é estar no estado de introdução e enviar o DTMF-2 evento.

Podemos optar por agrupar os objetos NCCO relacionados a cada estado dentro da definição do evento, utilizando a metapropriedade

// machine.js
const { Machine } = require('xstate');

module.exports = Machine({
  id: 'call',
  initial: 'intro',
  states: {
    intro: {
      on: {
        'DTMF-1': 'mainStLocation',
        'DTMF-2': 'broadwayLocation'
      },
      meta: {
        ncco: [
          { action: 'talk', text: "Hi. You've reached Joe's Restaurant! Springfield's top restaurant chain!" },
          { action: 'talk', text: 'Please select one of our locations.' },
          { action: 'talk', text: 'Press 1 for our Main Street location.' },
          { action: 'talk', text: 'Press 2 for our Broadway location.' },
          { action: 'input', eventUrl: [ 'https://example.com/dtmf' ], maxDigits: 1 }
        ]
      }
    },
    mainStLocation: {
      meta: {
        ncco: [
          { action: 'talk', text: "Joe's Main Street is located at Main Street number 11, Springfield." }
        ]
      }
    },
    broadwayLocation: {
      meta: {
        ncco: [
          { action: 'talk', text: "Joe's Broadway is located at Broadway number 46, Springfield." }
        ]
      }
    }
  }
});

Como utilizar nossa máquina

O objeto que a Machine função retorna deve ser tratado como um objeto imutável e sem estado que define a estrutura da nossa máquina. Para criar de fato uma instância da nossa máquina que possamos usar como fonte de verdade para o estado de uma chamada, usaremos a função XState interpret . A interpret função retorna um objeto conhecido como “Serviço”. Você pode acessar o estado atual de cada instância da máquina usando a state propriedade do serviço. E você pode enviar um evento para alterar o estado da instância da máquina usando o método send() . Vamos criar um callManager módulo responsável por criar instâncias de máquina para cada chamada recebida, enviar os eventos apropriados à medida que a chamada avança e remover cada instância de máquina quando a chamada terminar.

// callManager.js
const { interpret } = require('xstate');
const machine = require('./machine');

class CallManager {
  constructor() {
    this.calls = {};
  }

  createCall(uuid) {
    const service = interpret(machine).start();
    this.calls\[uuid] = service;
  }

  updateCall(uuid, event) {
    const call = this.calls\[uuid];
    if(call) {
      call.send(event);
    }
  }

  getNcco(uuid) {
    const call = this.calls\[uuid];
    if(!call) {
      return \[];
    }
    return call.state.meta[`${call.id}.${call.state.value}`].ncco;
  }

  endCall(uuid) {
    delete this.calls\[uuid];
  }
}

exports.callManager = new CallManager();

Como você pode ver, cada chamada é identificada por seu uuid  que a Vonage se encarrega de atribuir a cada chamada.

Juntando tudo isso

Agora podemos modificar o código do nosso servidor Web para que ele recorra ao callManager sempre que o backend da Vonage chamar nossos endpoints.

/// server.js
const express = require('express');
const bodyParser = require('body-parser');
const { callManager} = require('./callManager');

const port = process.env.PORT || 3000;
const app = express();
app.use(bodyParser.json());

app.post('/answer', (req, res) => {
  callManager.createCall(req.body.uuid);
  const ncco = callManager.getNcco(req.body.uuid);
  res.json(ncco);
});

app.post('/dtmf', (req, res) => {
  callManager.updateCall(req.body.uuid, `DTMF-${req.body.dtmf}`);
  const ncco = callManager.getNcco(req.body.uuid);
  res.json(ncco);
});

app.post('/event', (req, res) => {
  if(req.body.status == 'completed') {
    callManager.endCall(req.body.uuid);
  }
  res.json({ status: 'OK' });
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Como você pode ver, para saber quando a chamada terminou, adicionamos um endpoint /event. Se você associá-lo ao seu aplicativo da Vonage como o webhook “URL do evento”, a Vonage enviará uma solicitação a ele de forma assíncrona quando o estado geral da chamada mudar (por exemplo, quando o usuário desligar). Ao contrário do /answer ou /dtmf , você não pode responder com objetos NCCO a essa solicitação e influenciar o que o usuário ouve.

Alterando a estrutura de chamadas com facilidade

Acabamos de concluir uma refatoração do código do nosso aplicativo, mas ele continua funcionando exatamente da mesma forma que antes. No entanto, ao contrário do que acontecia antes, agora modificar a estrutura da chamada é tão simples quanto alterar o objeto JSON que passamos para a Machine função.

Portanto, se, como mencionado anteriormente, quisermos permitir que o usuário decida se deseja ouvir o horário de funcionamento do local ou fazer uma reserva, basta adicionarmos mais alguns estados, transições e matrizes NCCO à definição da nossa Máquina.

// machine.js
const { Machine } = require('xstate');

module.exports = Machine({
  id: 'call',
  initial: 'intro',
  states: {
    intro: {
      on: {
        'DTMF-1': 'mainStLocation',
        'DTMF-2': 'broadwayLocation'
      },
      meta: {
        ncco: [
          { action: 'talk', text: "Hi. You've reached Joe's Restaurant! Springfield's top restaurant chain!" },
          { action: 'talk', text: 'Please select one of our locations.' },
          { action: 'talk', text: 'Press 1 for our Main Street location.' },
          { action: 'talk', text: 'Press 2 for our Broadway location.' },
          { action: 'input', eventUrl: [ 'https://example.com/dtmf' ], maxDigits: 1 }
        ]
      }
    },
    mainStLocation: {
      on: {
        'DTMF-1': 'mainStReservation',
        'DTMF-2': 'mainStHours',
      },
      meta: {
        ncco: [
          { action: 'talk', text: "Joe's Main Street is located at Main Street number 11, Springfield." },
          { action: 'talk', text: 'Press 1 to make a reservation.' },
          { action: 'talk', text: 'Press 2 to hear our operating hours.' },
          { action: 'input', eventUrl: [ 'https://example.com/dtmf' ], maxDigits: 1 },
        ]
      }
    },
    broadwayLocation: {
      on: {
        'DTMF-1': 'broadwayReservation',
        'DTMF-2': 'broadwayHours',
      },
      meta: {
        ncco: [
          { action: 'talk', text: "Joe's Broadway is located at Broadway number 46, Springfield." },
          { action: 'talk', text: 'Press 1 to make a reservation.' },
          { action: 'talk', text: 'Press 2 to hear our operating hours.' },
          { action: 'input', eventUrl: [ 'https://example.com/dtmf' ], maxDigits: 1 },
        ]
      }
    },
    mainStReservation: { /* ... */ },
    mainStHours: { /* ... */ },
    broadwayReservation: { /* ... */ },
    broadwayHours: { /* ... */ }
  }
});

Mais vantagens do XState

O XState oferece mais recursos úteis que podem nos ajudar à medida que nosso modelo de chamadas se torna mais complexo.

Visualizador XState

O XState Visualizer é uma ferramenta online para gerar diagramas de Statechart com base nas suas definições existentes de máquinas XState. Para gerar um Statechart, basta colar sua chamada à função Machine função. Isso é particularmente útil para compartilhar com partes interessadas que não são desenvolvedores, a fim de discutir a estrutura das chamadas.

chart

Transições autorreferenciadas

Um estado pode fazer a transição para si mesmo. Isso pode ser útil em casos em que se deseja permitir que o usuário reproduza a informação mais recente fornecida.

mainStHours: {
  on: {
    'DTMF-1': 'mainStHours',
    'DTMF-2': 'intro'  },
  meta: {
    ncco: [
      { action: 'talk', text: "Joe's Main Street is open Monday through Friday, 8am to 8pm." },
      { action: 'talk', text: 'Saturday and Sunday 9am to 7pm.' },
      { action: 'talk', text: 'Press 1 to hear this information again.' },
      { action: 'talk', text: 'Press 2 to go back to the opening menu.' },
      { action: 'input', eventUrl: [ 'https://example.com/dtmf' ], maxDigits: 1 }
    ]
  }
}

Persistência

É possível registrar uma função para ser chamada sempre que a máquina passar de um estado para outro, utilizando o método onTransition . Isso pode ser útil para registrar as etapas que o usuário está realizando e enviá-las a um banco de dados remoto para referência ou análise futura.

Em geral, o XState oferece suporte à a serialização os dados de uma instância da máquina para que você possa armazená-los.

Modo estrito

Ao solicitar que o usuário digite algo no teclado em qualquer momento da chamada, é possível que ele insira um valor que você não esperava. Por exemplo, o usuário pode estar em uma etapa da chamada em que você espera que ele escolha o 1 caso queira fazer uma reserva ou pressione o 2 para ouvir o horário de funcionamento. Mas se o usuário pressionar o 9, o evento enviado será DTMF-9 e essa não é uma transição possível, dado o estado atual. O ideal seria encontrar uma maneira genérica de detectar quando o usuário inseriu uma entrada inválida e orientá-lo a fazer a seleção novamente.

Ao definir nossa máquina com strict: true , podemos fazer com que o send() método lance uma exceção caso receba um evento que não seja possível no estado atual. Podemos então interceptar esse erro mais adiante e responder com uma resposta NCCO apropriada, que solicitará ao usuário que faça a seleção novamente.

Conclusão

Nesta postagem, apresentamos a biblioteca XState e como ela pode ser usada para controlar o andamento de uma chamada em um sistema IVR com tecnologia da Vonage, de forma que se adapte bem a um caso de uso real. O código completo abordado nesta postagem pode ser encontrado aqui. Se você estiver procurando mais informações, tanto a Vonage quanto XState possuem uma excelente documentação.

Compartilhar:

https://a.storyblok.com/f/270183/150x150/a3d03a85fd/placeholder.svg
Yonatan Mevorach

Yonatan Mevorach is a Web Developer, blogger, and open-source contributor.