
XStateで複雑なIVRシステムを簡単に作成
所要時間:1 分
IVRシステムがそう呼ばれていることを知らなかったとしても、おそらくあなたはいつもIVRシステムを使用していることでしょう。IVRシステム IVRシステムは、電話番号に電話をかけ、音声ガイダンスを聞き、必要な情報を得るために通話をナビゲートします。Vonageでは、本格的なIVRシステムをWebサーバを立ち上げるのと同じくらい簡単に作成することができます。この記事では、非常に複雑で精巧なIVRシステムを、コードをシンプルでメンテナンスしやすい状態に保ちながら作成する方法を紹介します。そのために、ここでは XStateを使用します。
35行未満のコードでIVRシステムを構築
VonageでIVRシステムを導入する鍵は、Vonageにコールの各ステップの処理方法を指示するウェブサーバーを作成することです。一般的には、ユーザーがあなたの仮想着信番号に電話をかけるとすぐに、VonageはあなたのエンドポイントにHTTPリクエストを送信します。 /answerで構成されるJSONペイロードで応答することを期待します。 NCCOオブジェクトで構成されるJSONペイロードで応答することを期待します。同様に、ユーザーがキーパッドを使って次に何を聞きたいかを選択すると、Vonageは別のエンドポイントにリクエストを送る。 /dtmf..エンドポイント /dtmfエンドポイントは、ユーザーが選択した番号を含むリクエストペイロードで呼び出されます。
を使った場合のコードを見てみよう。 エクスプレスを使った場合のコードを見てみよう。
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}!`)); 自分で試してみる
すぐにアプリのコードを書き始めることができます。しかし、呼び出してすべてが機能していることを自分でテストできるようにするためには、以下を完了させる必要がある:
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.
WebサーバーがWeb上でアクセス可能であることを確認してください。これを行うには、ローカルの開発マシン Ngrokを使って開発するか Glitch.
Voiceアプリケーションを作成します。これは Vonageウェブサイトまたは Vonage CLI.のパブリックURLを入力する必要があります。
/answerを入力する必要があります。
これがすべて整えば、あなたは自分の電話番号に電話をかけることができ、ウェブサーバーから返されたデータに基づいた音声応答を聞くことができる。
IVRシステムの "Hello World "を超える
上記の例は期待通りに機能しますが、現実のIVRシステムは何度もユーザーに入力を求め、通話中のユーザーの状態に基づいてユーザーの数値入力を解釈します。このことを説明するために、この例では、ユーザーは興味のあるレストランの場所を選択し、次に営業時間を聞くか予約を入れるかを選択するよう求められると仮定します。この両方の場合、ユーザーはキーパッドで1を押すかもしれないが、これをど のように解釈するかは、前のオーディオキューと、通話中のユーザーの状態に依存す る。
このユースケースをサポートするには、先ほど書いたコードを変更する必要があります。理想的には、機能を追加してIVRシステムを複雑化しても、コードがシンプルなまま維持され、構造を考え直す必要がないように変更することです。これを実現するために、コール構造を 有限状態マシンを使用します。 XStateJavascript用のステートマシン・ライブラリです。
##ステートマシン入門
ステートマシンは、単に「マシン」のモデルであり、いつでも1つの状態にしか存在できず、特定の入力が与えられた場合にのみ1つの状態から別の状態に遷移することができる。XStateをはじめとするステートマシン・ライブラリを使えば、ステートマシンの「ルール」が確実に実行されるように、コードでマシンをモデル化し、インスタンス化することができる。
通話構造をステートマシンとしてモデル化する
コール構造をステート・マシンとしてモデル化するには、XStateが公開している Machine関数を使用する:
// machine.js
const { Machine } = require('xstate');
module.exports = Machine({
id: 'call',
initial: 'intro',
states: {
intro: {
on: {
'DTMF-1': 'mainStLocation',
'DTMF-2': 'broadwayLocation'
}
},
mainStLocation: {
},
broadwayLocation: {
}
}
});上のコードでわかるように、このコールは3つの状態のうちの1つしか取ることができない:
を選択する。
introユーザーが紹介を聞きながら、興味のある場所を選ぶように指示されている状態。その
mainStLocation仮定のレストラン・チャイのメイン・ストリートの場所についての情報を聞いている状態。その
broadwayLocationブロードウェイの場所の情報を聞いているときの状態。
それもわかるだろう:
この状態に移行する唯一の方法は
mainStLocation状態に移行する唯一の方法はintro状態にありDTMF-1イベント.に移行する唯一の方法は
broadwayLocation状態に遷移する唯一の方法は、イントロ状態でDTMF-2イベントを送ることである。
各ステートに関連するNCCOオブジェクトをイベント定義内に配置するには、XStateの メタプロパティ
// 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." }
]
}
}
}
}); マシンの活用
関数が返すオブジェクトは Machine関数が返すオブジェクトは、マシンの構造を定義する、不変のステートレス オブジェクトとして扱われるべきである。呼び出しのステートのソース・オブ・トゥルースとして使用できるマシンのインスタンスを実際に作成するには、XState interpret関数を使用する。この interpret関数は、"サービス".サービスの stateプロパティを使用して、各マシンインスタンスの現在の状態にアクセスできます。また、サービスの send()メソッドを使用して、マシン・インスタンスの状態を変更するイベントを送信できる。ここでは callManagerモジュールを作成し、着信ごとにマシン・インスタンスを作成し、コールの進行に応じて適切なイベントを送信し、コールが終了したら各マシン・インスタンスを削除する。
// 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();ご覧のように、各コールは、Vonageが各コールに割り当てる uuid で識別され、Vonageが各コールに割り当てる。
すべてをまとめる
これで、Vonageバックエンドがエンドポイントを呼び出すたびに callManagerに延期するようにウェブサーバのコードを修正することができます。
/// 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}!`));ご覧のように、通話がいつ終了したかを知るために、/eventエンドポイントを追加しました。これを "Event URL "ウェブフックとしてVonageアプリケーションに関連付けると、通話全体の状態が変化したとき(ユーザーが電話を切ったときなど)、Vonageは非同期でこのエンドポイントにリクエストを行います。イベント /answerまたは /dtmfエンドポイントとは異なり、このリクエストにNCCOオブジェクトで応答して、ユーザーが聞く内容に影響を与えることはできません。
通話構造を簡単に変更
アプリコードのリファクタリングが完了したばかりだが、動作は以前とまったく同じだ。しかし、以前とは対照的に、呼び出し構造を変更することは、関数に渡すJSONオブジェクトを変更するのと同じくらい簡単になりました。 Machine関数に渡すJSONオブジェクトを変更するだけです。
したがって、前述のように、ユーザーに、その場所の営業時間を聞きたいか、予約を入れたいかを決めさせたい場合、Machineの定義に、さらにいくつかのステート、遷移、NCCOアレイを追加するだけでよい。
// 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: { /* ... */ }
}
}); さらなるXStateの良さ
XStateにはさらに便利な機能があり、コールモデルが複雑になればなるほど、私たちを助けてくれる。
XStateビジュアライザー
XState Visualizerは、既存のXState Machine定義に基づいてStatechartダイアグラムを生成するオンライン・ツールです。Statechartを生成するために必要なことは、XStateMachineの関数への呼び出しを貼り付けることだけです。 Machine関数への呼び出しを貼り付けるだけです。これは、開発者以外の関係者と共有し、呼び出し構造について議論するのに特に便利です。

自己言及的トランジション
状態は、それ自体に遷移することができる。 それ自身.これは、ユーザーに最新の情報を再生させたい場合に便利です。
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 }
]
}
} 永続性
マシンがある状態から別の状態に遷移するたびに呼び出される関数を、サービスの onTransitionメソッドを使用する。これは、将来の参照/分析のために、ユーザーが取っているステップを記録し、リモートデータベースに送信するのに便利です。
一般的に、XStateは シリアライズをサポートしている。
ストリクト・モード
通話中の任意の時点でユーザーにキーパッド入力を促す場合、ユーザーが予期しない入力値を入力する可能性があります。たとえば、ユーザーは通話中、予約する場合は1を選択し、営業時間を聞く場合は2を押すことを期待している状態かもしれません。しかし、ユーザーが9を押した場合、送信されるイベントは DTMF-9となり、現在の状態からは遷移できません。理想的には、ユーザーが無効な入力をしたことを検出し、再度選択するように指示する一般的な方法を見つけたいと思います。
マシンを strict: trueを定義することで send()メソッドに例外を投げさせることができる。さらにそのエラーをキャッチして、適切なNCCOレスポンスで返答し、ユーザーに選択をやり直すように指示することができる。
まとめ
この投稿では、XStateライブラリを紹介し、Vonageが提供するIVRシステムで、実際のユースケースに適した方法で通話の進行を制御する方法を紹介しました。この投稿で取り上げたコード一式は こちら.さらに詳しい情報をお知りになりたい場合は Vonageと XStateには素晴らしいドキュメントがあります。
