
Teilen Sie:
Yonatan Mevorach is a Web Developer, blogger, and open-source contributor.
Einfaches Erstellen eines komplexen IVR-Systems mit XState
Lesedauer: 7 Minuten
Selbst wenn Sie nicht wussten, dass sie so genannt werden, benutzen Sie wahrscheinlich ständig IVR-Systeme. Ein IVR-System ermöglicht es Ihnen, eine Telefonnummer anzurufen, sich die Audiohinweise anzuhören und durch den Anruf zu navigieren, um die gewünschten Informationen zu erhalten. Vonage macht die Erstellung eines vollwertigen IVR-Systems so einfach wie das Einrichten eines Webservers. In diesem Beitrag werden wir sehen, wie man sehr komplexe und aufwendige IVR-Systeme erstellt und dabei den Code einfach und leicht zu pflegen hält. Um dies zu erreichen, werden wir die XState eine beliebte State Machine Bibliothek für Javascript.
Ein IVR-System mit weniger als 35 Codezeilen
Der Schlüssel zur Implementierung eines IVR-Systems mit Vonage besteht darin, einen Webserver zu erstellen, der Vonage anweist, wie jeder Schritt des Anrufs zu behandeln ist. In der Regel bedeutet dies, dass Vonage, sobald ein Benutzer Ihre virtuelle Eingangsnummer anruft, eine HTTP-Anfrage an Ihren /answer Endpunkt und erwartet, dass Sie mit einer JSON-Nutzlast antworten, die aus NCCO Objekten, die angeben, was der Benutzer hören soll. Ähnlich verhält es sich, wenn der Benutzer über die Tastatur auswählt, was er als Nächstes hören möchte: Vonage sendet dann eine Anfrage an einen anderen Endpunkt, der normalerweise /dtmf. Der /dtmf Endpunkt wird mit einem Request-Payload aufgerufen, der die vom Benutzer gewählte Nummer enthält, die Ihr Server verwenden sollte, um herauszufinden, mit welchem Satz von NCCO-Objekten er antworten soll.
Schauen wir uns an, wie das im Code aussieht, wenn wir express zum Betreiben unseres Webservers verwenden.
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}!`)); Probieren Sie es selbst aus
Sie können sofort mit dem Schreiben Ihres Anwendungscodes beginnen. Damit Sie aber selbst testen können, ob alles funktioniert, müssen Sie Folgendes tun:
Vonage API-Konto
Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.
In diesem Lernprogramm wird auch eine virtuelle Telefonnummer verwendet. Um eine zu erwerben, gehen Sie zu Rufnummern > Rufnummern kaufen und suchen Sie nach einer Nummer, die Ihren Anforderungen entspricht.
Stellen Sie sicher, dass Ihr Webserver über das Internet zugänglich ist. Sie können dies tun, indem Sie Ihren lokalen Entwicklungsrechner freigeben mit Ngrok oder durch die Entwicklung mit Glitch.
Erstellen Sie eine Voice-Anwendung. Dies können Sie über die Vonage-Websiteoder über die Vonage CLI. Sie müssen die öffentliche URL Ihres
/answerEndpunktes eingeben, wenn Sie Ihre Anwendung einrichten.Besorgen Sie sich eine virtuelle eingehende Nummer und verbinden Sie diese mit Ihrer App über die Website oder CLI.
Wenn das alles eingerichtet ist, können Sie Ihre Nummer anrufen und hören die Audioantwort, die auf den Daten basiert, die Sie von Ihrem Webserver zurückgeben.
Mehr als die "Hallo-Welt" der IVR-Systeme
Das oben gezeigte Beispiel funktioniert wie erwartet, aber ein reales IVR-System wird den Benutzer mehrmals zur Eingabe auffordern und die numerischen Eingaben des Benutzers je nach Zustand des Benutzers während des Anrufs interpretieren. Um dies zu veranschaulichen, nehmen wir an, dass der Benutzer in unserem Beispiel aufgefordert wird, den Ort des Restaurants auszuwählen, für das er sich interessiert, und dann zu entscheiden, ob er die Öffnungszeiten hören oder eine Reservierung vornehmen möchte. In beiden Fällen kann der Benutzer die Taste 1 auf seiner Tastatur drücken, aber wie wir dies interpretieren, hängt von dem vorhergehenden Audio-Hinweis und dem Status des Benutzers im Anruf ab.
Um diesen Anwendungsfall zu unterstützen, müssen wir den Code, den wir gerade geschrieben haben, ändern. Im Idealfall werden wir ihn so ändern, dass unser Code einfach bleibt, wenn wir im Laufe der Zeit weitere Funktionen hinzufügen und unser IVR-System komplexer machen, ohne dass wir seine Struktur neu überdenken müssen. Um dies zu erreichen, modellieren wir unsere Anrufstruktur als eine Endliche Zustandsmaschine unter Verwendung von XStateeiner Zustandsautomaten-Bibliothek für Javascript.
##Eine Fibel über Zustandsautomaten
Ein Zustandsautomat ist einfach ein Modell für eine "Maschine", die sich zu einem bestimmten Zeitpunkt nur in einem Zustand befinden kann und nur bei bestimmten Eingaben von einem Zustand in einen anderen übergehen kann. Mit XState und anderen State-Machine-Bibliotheken können Sie einen Automaten im Code modellieren und instanziieren, so dass die "Regeln" des Automaten garantiert durchgesetzt werden können.
Modellierung unserer Aufrufstruktur als Zustandsmaschine
Um unsere Aufrufstruktur als State Machine zu modellieren, verwenden wir die Machine Funktion, die XState zur Verfügung stellt:
// machine.js
const { Machine } = require('xstate');
module.exports = Machine({
id: 'call',
initial: 'intro',
states: {
intro: {
on: {
'DTMF-1': 'mainStLocation',
'DTMF-2': 'broadwayLocation'
}
},
mainStLocation: {
},
broadwayLocation: {
}
}
});Wie Sie im obigen Code sehen können, kann unser Aufruf nur einen von drei Zuständen einnehmen:
Der
introZustand, in dem der Nutzer die Einführung anhört und aufgefordert wird, den Ort auszuwählen, der ihn interessiert.Die
mainStLocationZustand, in dem sie Informationen über den Standort unseres hypothetischen Restaurants Chai in der Main St. anhörenDie
broadwayLocationZustand, wenn sie Informationen über den Standort Broadway hören.
Das können Sie auch sehen:
Der einzige Weg zum Übergang in den
mainStLocationZustand zu gelangen, ist, sich imintroZustand und das Senden desDTMF-1Ereignis.Der einzige Weg zum Übergang in den
broadwayLocationZustand zu gelangen, ist, sich im Intro-Zustand zu befinden und dasDTMF-2Ereignis.
Wir können die NCCO-Objekte, die sich auf jeden Zustand beziehen, innerhalb der Ereigniskonfiguration unter Verwendung der XState Metaeigenschaft
// 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." }
]
}
}
}
}); Nutzung unserer Maschine
Das Objekt, das die Machine Funktion zurückgibt, sollte als unveränderliches zustandsloses Objekt behandelt werden, das die Struktur unserer Maschine definiert. Um tatsächlich eine Instanz unserer Maschine zu erstellen, die wir als Quelle der Wahrheit für den Zustand eines Aufrufs verwenden können, verwenden wir die XState interpret Funktion. Die Funktion interpret Funktion gibt ein Objekt zurück, das als "Dienst". Sie können auf den aktuellen Zustand jeder Maschineninstanz zugreifen, indem Sie die state Eigenschaft des Dienstes zugreifen. Und Sie können ein Ereignis senden, um den Zustand der Maschineninstanz zu ändern, indem Sie die Methode des Dienstes send() Methode. Wir erstellen ein callManager Modul, das für die Erstellung von Maschineninstanzen für jeden eingehenden Anruf, das Senden der entsprechenden Ereignisse im Verlauf des Anrufs und das Entfernen jeder Maschineninstanz nach Beendigung des Anrufs zuständig ist.
// 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();Wie Sie sehen können, wird jeder Anruf durch seinen Namen identifiziert. uuid identifiziert, die Vonage den einzelnen Anrufen zuordnet.
Alles zusammenfügen
Jetzt können wir unseren Webserver-Code so ändern, dass er auf den callManager zu verschieben, wenn das Vonage-Backend unsere Endpunkte aufruft.
/// 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}!`));Wie Sie sehen können, haben wir einen /event Endpunkt hinzugefügt, um zu wissen, wann der Anruf beendet ist. Wenn Sie diesen Endpunkt mit Ihrer Vonage-Anwendung als Webhook "Event URL" verknüpfen, wird Vonage asynchron eine Anfrage an diesen Endpunkt stellen, wenn sich der Gesamtstatus des Anrufs ändert (z. B. wenn der Benutzer auflegt). Anders als die /answer oder /dtmf Endpunkt können Sie nicht mit NCCO-Objekten auf diese Anfrage reagieren und beeinflussen, was der Benutzer hört.
Einfaches Ändern der Anrufstruktur
Wir haben gerade eine Umstrukturierung unseres Anwendungscodes abgeschlossen, aber er verhält sich genau so wie vorher. Aber im Gegensatz zu vorher wird die Änderung der Aufrufstruktur jetzt so einfach wie die Änderung des JSON-Objekts, das wir an die Machine Funktion übergeben.
Wenn wir also, wie bereits erwähnt, den Benutzer entscheiden lassen wollen, ob er sich die Öffnungszeiten des Lokals anhören oder eine Reservierung vornehmen möchte, müssen wir nur ein paar weitere Zustände, Übergänge und NCCO-Arrays in die Definition unserer Maschine aufnehmen.
// 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: { /* ... */ }
}
}); Mehr XState Goodness
XState verfügt über weitere nützliche Funktionen, die uns helfen können, wenn unser Aufrufmodell komplizierter wird.
XState-Visualisierer
Der XState Visualizer ist ein Online-Tool zur Erstellung von Statechart-Diagrammen auf der Grundlage Ihrer bestehenden XState Machine-Definitionen. Alles, was wir tun müssen, um ein Statechart zu erzeugen, ist, den Aufruf der Machine Funktion einfügen. Dies ist besonders praktisch, um es mit Nicht-Entwicklern zu teilen und Diskussionen über die Aufrufstruktur zu führen.

Selbstreferenzierende Übergänge
Ein Zustand kann in sich selbst übergehen sich selbst. Dies kann nützlich sein, wenn Sie dem Benutzer die Möglichkeit geben wollen, die zuletzt gegebene Information wiederzugeben.
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 }
]
}
} Persistenz
Sie können eine Funktion registrieren, die immer dann aufgerufen wird, wenn die Maschine von einem Zustand in einen anderen übergeht, indem Sie die onTransition Methode. Dies kann nützlich sein, um die Schritte des Benutzers zu protokollieren und sie zur späteren Bezugnahme/Analyse an eine entfernte Datenbank zu senden.
Im Allgemeinen unterstützt XState Serialisierung der Daten einer Maschineninstanz, so dass Sie diese persistieren können.
Strenger Modus
Wenn Sie den Benutzer an einem beliebigen Punkt des Anrufs zur Eingabe über die Tastatur auffordern, ist es möglich, dass der Benutzer einen Eingabewert eingibt, den Sie nicht erwarten. Der Benutzer kann sich beispielsweise in einem Zustand des Anrufs befinden, in dem Sie erwarten, dass er 1 wählt, wenn er eine Reservierung vornehmen möchte, oder 2 drückt, um die Öffnungszeiten zu erfahren. Wenn der Benutzer jedoch die 9 drückt, wird das folgende Ereignis gesendet DTMF-9 und das ist angesichts des aktuellen Zustands kein möglicher Übergang. Idealerweise würden wir gerne einen generischen Weg finden, um zu erkennen, wann der Benutzer eine ungültige Eingabe getätigt hat, und ihn anweisen, die Auswahl erneut zu treffen.
Durch die Definition unserer Maschine mit strict: true definieren, können wir bewirken, dass die send() Methode eine Ausnahme auslösen, wenn ihr ein Ereignis übergeben wird, das im aktuellen Zustand nicht möglich ist. Wir können dann diesen Fehler weiter oben abfangen und mit einer geeigneten NCCO-Antwort antworten, die dem Benutzer sagt, dass er die Auswahl erneut treffen soll.
Einpacken
In diesem Beitrag haben wir die XState-Bibliothek vorgestellt und gezeigt, wie sie verwendet werden kann, um den Fortschritt eines Anrufs in einem von Vonage betriebenen IVR-System zu kontrollieren, und zwar auf eine Weise, die für einen realen Anwendungsfall gut skaliert. Den kompletten Code, der in diesem Beitrag behandelt wurde, finden Sie hier. Wenn Sie nach weiteren Informationen suchen, finden Sie sowohl Vonage und XState eine ausgezeichnete Dokumentation.
