https://d226lax1qjow5r.cloudfront.net/blog/blogposts/creating-a-complex-ivr-system-with-ease-with-xstate-dr/unnamed.jpg

Créer un système IVR complexe en toute simplicité avec XState

Publié le May 13, 2021

Temps de lecture : 7 minutes

Même si vous ne savez pas que c'est ainsi qu'on les appelle, vous utilisez probablement des systèmes IVR en permanence. Un système SVI vous permet d'appeler un numéro de téléphone, d'écouter les signaux audio et de naviguer dans l'appel pour obtenir les informations dont vous avez besoin. Vonage rend la création d'un système SVI complet aussi simple que l'installation d'un serveur Web. Dans ce billet, nous verrons comment créer des systèmes IVR très complexes et élaborés tout en gardant un code simple et facile à maintenir. Pour ce faire, nous utiliserons les éléments suivants XState qui est une bibliothèque populaire de machines à états pour Javascript.

Un système IVR avec moins de 35 lignes de code

La clé de la mise en œuvre d'un système IVR avec Vonage est de créer un serveur Web qui indiquera à Vonage comment traiter chaque étape de l'appel. Typiquement, cela signifie que dès qu'un utilisateur appelle votre numéro entrant virtuel, Vonage envoie une requête HTTP à votre point de terminaison /answer et s'attend à ce que vous répondiez avec une charge utile JSON composée des éléments suivants NCCO qui spécifient ce que l'utilisateur doit entendre. De même, lorsque l'utilisateur utilise son clavier pour choisir ce qu'il veut écouter ensuite, Vonage envoie une demande à un autre point de terminaison, généralement appelé /dtmf. Le point de terminaison /dtmf est appelé avec une requête contenant le numéro choisi par l'utilisateur, que votre serveur doit utiliser pour déterminer l'ensemble d'objets NCCO à utiliser.

Voyons ce que cela donne dans le code lorsque l'on utilise express pour alimenter notre serveur 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}!`));

Essayez-le vous-même

Vous pouvez commencer à écrire le code de votre application immédiatement. Mais pour être en mesure d'appeler et de tester par vous-même que tout fonctionne, vous devez effectuer les opérations suivantes :

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

  • Assurez-vous que votre serveur Web est accessible sur le Web. Vous pouvez le faire en exposant votre machine de développement locale en utilisant Ngrok ou en développant avec Glitch.

  • Créez une application Voice. Vous pouvez le faire via le site web de Vonageou en utilisant la CLI de Vonage. Vous devrez entrer l'url publique de votre point d'accès lorsque vous configurerez votre application. /answer lorsque vous configurez votre application.

  • Obtenez un numéro entrant virtuel et connectez-le à votre application à l'aide de la fonction site web ou de l'interface CLI.

Lorsque tout cela sera en place, vous pourrez appeler votre numéro et entendre la réponse audio basée sur les données renvoyées par votre serveur Web.

Aller au-delà du "Hello World" des systèmes IVR

L'exemple ci-dessus fonctionne comme prévu, mais dans le monde réel, un système SVI demandera à l'utilisateur de saisir des données à plusieurs reprises et interprétera les données numériques saisies par l'utilisateur en fonction de l'état de l'appel. Pour illustrer cela, supposons que, dans notre exemple, on demande à l'utilisateur de choisir l'emplacement du restaurant qui l'intéresse, puis de choisir s'il veut écouter les heures d'ouverture ou faire une réservation. Dans ces deux cas, l'utilisateur peut appuyer sur la touche 1 de son clavier, mais la façon dont nous l'interprétons dépend du signal audio précédent et de l'état de l'utilisateur dans l'appel.

Pour prendre en charge ce cas d'utilisation, nous devrons modifier le code que nous venons d'écrire. Idéalement, nous le modifierons de telle sorte qu'au fur et à mesure que nous ajouterons des fonctionnalités et que nous rendrons notre système IVR plus complexe, notre code restera simple et nous n'aurons pas à repenser la façon de le structurer. Pour ce faire, nous modéliserons notre structure d'appel comme une machine à état fini en utilisant XStateune bibliothèque de machines à états pour Javascript.

##Une introduction aux machines à états

Une machine à états est simplement un modèle de "machine" qui ne peut se trouver que dans un seul état à un moment donné et qui ne peut passer d'un état à un autre qu'en fonction d'entrées spécifiques. XState et d'autres bibliothèques de machines à états vous permettent de modéliser et d'instancier une machine dans le code, de manière à garantir l'application des "règles" de la machine à états.

Modéliser notre structure d'appel comme une machine à états

Pour modéliser notre structure d'appel comme une machine à états, nous utiliserons la fonction Machine que XState expose :

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

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

Comme vous pouvez le voir dans le code ci-dessus, notre appel ne peut se trouver que dans l'un des trois états suivants :

  • L'état où l'utilisateur écoute l'introduction et est invité à choisir le lieu qui l'intéresse. intro où l'utilisateur écoute l'introduction et est invité à choisir le lieu qui l'intéresse.

  • L'État mainStLocation L'état dans lequel ils écoutent des informations sur l'emplacement de notre hypothétique restaurant chai sur la rue principale.

  • Le broadwayLocation lorsqu'ils écoutent des informations sur l'emplacement de Broadway.

Vous pouvez également le constater :

  • Le seul moyen de passer à l'état mainStLocation est d'être dans l'état intro et d'envoyer l'événement DTMF-1 événement.

  • La seule façon de passer à l'état broadwayLocation est d'être dans l'état intro et d'envoyer l'événement DTMF-2 événement.

Nous pouvons choisir de placer les objets NCCO liés à chaque état à l'intérieur de la définition de l'événement à l'aide de la métapropriété métapropriété

// 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." }
        ]
      }
    }
  }
});

Utilisation de notre machine

L'objet que la fonction Machine doit être traité comme un objet sans état immuable qui définit la structure de notre machine. Pour créer une instance de notre machine que nous pouvons utiliser comme source de vérité pour l'état d'un appel, nous utiliserons la fonction XState interpret pour créer une instance de notre machine que nous pouvons utiliser comme source de vérité pour l'état d'un appel. La fonction interpret renvoie un objet appelé " service ".service". Vous pouvez accéder à l'état actuel de chaque instance de machine en utilisant la propriété state du service. Et vous pouvez envoyer un événement pour changer l'état de l'instance de machine en utilisant la méthode send() du service. Nous allons créer un callManager qui sera chargé de créer des instances de machine pour chaque appel entrant, d'envoyer les événements appropriés au fur et à mesure que l'appel progresse et de supprimer chaque instance de machine à la fin de l'appel.

// 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();

Comme vous pouvez le voir, chaque appel est identifié par son numéro de téléphone. uuid que Vonage se charge d'attribuer à chaque appel.

La mise en place de l'ensemble

Nous pouvons à présent modifier le code de notre serveur Web pour qu'il renvoie à la fonction callManager chaque fois que le backend de Vonage appelle nos points d'extrémité.

/// 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}!`));

Comme vous pouvez le voir, pour savoir quand l'appel est terminé, nous avons ajouté un point de terminaison /event. Si vous l'associez à votre Application Vonage en tant que webhook "Event URL", Vonage fera une demande asynchrone lorsque l'état général de l'appel change (par exemple, l'utilisateur raccroche). Contrairement à l'option /answer ou /dtmf vous ne pouvez pas répondre à cette demande avec des objets NCCO et influencer ce que l'utilisateur entend.

Modifier la structure des appels en toute simplicité

Nous venons de procéder à une refonte du code de notre application, mais celle-ci se comporte exactement de la même manière qu'auparavant. Mais contrairement à ce qui se passait auparavant, la modification de la structure d'appel devient aussi simple que de changer l'objet JSON que nous passons à la fonction Machine à la fonction

Ainsi, si, comme nous l'avons mentionné précédemment, nous voulons permettre à l'utilisateur de décider s'il souhaite écouter les heures d'ouverture du lieu ou faire une réservation, il nous suffit d'ajouter quelques états, transitions et tableaux NCCO à la définition de notre machine.

// 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: { /* ... */ }
  }
});

Plus d'informations sur XState

XState dispose de fonctionnalités plus utiles qui peuvent nous aider à mesure que notre modèle d'appel devient plus complexe.

Visualisateur XState

Le XState Visualizer est un outil en ligne qui permet de générer des diagrammes d'états à partir de vos définitions de machines XState existantes. Tout ce que nous avons à faire pour générer un diagramme d'état est de coller votre appel à la fonction Machine à l'aide d'une fonction. Ceci est particulièrement pratique pour partager avec des parties prenantes non-développeurs afin d'avoir des discussions sur la structure de l'appel.

chart

Des transitions qui s'autoréférencent

Un état peut se transformer en lui-même. Cela peut s'avérer utile lorsque l'on souhaite permettre à l'utilisateur de lire la dernière information donnée.

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

Persistance

Vous pouvez enregistrer une fonction qui sera appelée chaque fois que la machine passe d'un état à un autre en utilisant la méthode onTransition du service. Cela peut être utile pour enregistrer les étapes suivies par l'utilisateur et les envoyer à une base de données distante pour référence/analyse ultérieure.

En général, XState prend en charge sérialisation les données d'une instance de machine afin que vous puissiez les conserver.

Mode strict

Lorsque vous demandez à l'utilisateur d'entrer des données sur le clavier à n'importe quel moment de l'appel, il est possible que l'utilisateur entre une valeur que vous n'attendez pas. Par exemple, l'utilisateur peut se trouver dans une situation où vous vous attendez à ce qu'il choisisse 1 s'il souhaite faire une réservation ou qu'il appuie sur 2 pour écouter les heures d'ouverture. Mais si l'utilisateur appuie sur 9, l'événement envoyé sera DTMF-9 et ce n'est pas une transition possible compte tenu de l'état actuel. Idéalement, nous aimerions trouver un moyen générique de détecter si l'utilisateur a saisi une entrée non valide et lui demander de refaire sa sélection.

En définissant notre machine avec strict: true nous pouvons faire en sorte que la méthode send() de lever une exception si on lui transmet un événement qui n'est pas possible compte tenu de l'état actuel. Nous pouvons ensuite rattraper cette erreur et répondre avec une réponse NCCO appropriée qui dira à l'utilisateur de refaire sa sélection.

Conclusion

Dans ce billet, nous avons présenté la bibliothèque XState et la façon dont elle peut être utilisée pour contrôler la progression d'un appel dans un système IVR alimenté par Vonage, d'une manière qui s'adapte bien à un cas d'utilisation réel. Le code complet couvert dans cet article peut être trouvé ici. Si vous cherchez plus d'informations, les deux sites de Vonage et XState disposent d'une excellente documentation.

Partager:

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

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