チャットボットがユーザーとやり取りを行う動作、タスクは Dialog でブロック化し、(再)利用することができます。実行している Dialog のステート(状態) は MemoryStorage という領域に保存します。
- Dialog: タスクを実行するファンクション(関数)
- ComponentDialog: Dialog の実行順序設定、読み込み、実行&コントロールするクラスモジュール
-
MemoryStorage: Dialog のステートや、会話に必要な情報を保存する領域
- ConversationState: 会話の状態 (どのダイアログにいるのか) など、メッセージの受信&返信を会話として進める上で必要な情報
- UserState: ConversationState 以外のユーザー情報など
必要なコード
DialogBot
+- mainDialog.js // チャットボットのタスクを切り出した ComponentDialog (親)
+- subDialog.js // チャットボットのタスクを切り出した ComponentDialog (子)
+- dialogBot.js // Chatbot としての挙動
+- index.js // API としての基本動作
+- package.json // 必要なライブラリーや依存関係など
+- .env // 環境変数を設定
手順
MemoryStorage で Dialog のステートを管理するよう設定する
ひとまず MemoryStorage を定義して、ConversationState を生成します。
MainDialog という ComponentDialog (ファイル名は mainDialog.js) を作成し、こちらでタスクを実行します。
Bot インスタンスが起動するときに ComversationState および MainDialog を取得するようにします。
const restify = require('restify');
const path = require('path');
// BotFrameworkAdapter に追加して、MemoryStorage と ConversationState をインポート
// const { BotFrameworkAdapter } = require('botbuilder');
const { BotFrameworkAdapter, MemoryStorage, ConversationState } = require('botbuilder');
// Dialog を操作する js ファイル(dialogBot.js)を新たに作成、追加
// const { EchoBot } = require('./bot');
const { DialogBot } = require('./dialogBot');
:
(中略)
:
// 空の MemoryStorage を作成して、その配下に ConversationState を生成する
const memoryStorage = new MemoryStorage();
const conversationState = new ConversationState(memoryStorage);
:
(中略)
:
// Dialog と ConversationState を DialogBot で利用する
// const bot = new EchoBot();
const dialog = new MainDialog();
const bot = new DialogBot(conversationState, dialog);
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async (context) => {
// Route to main dialog.
await bot.run(context);
});
});
メッセージ応対を ComponentDialog で行うように設定する
dialogBot.js では、メッセージを受け取って返信する処理を ComponentDialog から行うように設定し、ConversationState を使って Dialog のコントロールを行います。
ConversationState に DialogState を保存し、DialogState に従って Dialog を実行するプロセスになります。
const { ActivityHandler } = require('botbuilder');
class DialogBot extends ActivityHandler {
constructor(conversationState, dialog) {
super();
// Dialog の読み込み
this.dialog = dialog;
// ConversationState から DialogState を取得
this.conversationState = conversationState;
this.dialogState = this.conversationState.createProperty('DialogState');
// メッセージを受信したとき
this.onMessage(async (turnContext, next) => {
// DialogState で指定された Dialog を実行、その次の Dialog をポイント
await this.dialog.run(turnContext, this.dialogState);
await next();
});
}
// ステートを保存するため、ActivityHandler.run を Override
async run(turnContext) {
await super.run(turnContext);
await this.conversationState.saveChanges(turnContext, false);
}
}
module.exports.DialogBot = DialogBot;
WaterfallDialog でタスクとその構成を記述する
ひとまず mainDialog.js 内に実行したいタスク(Step)を記述していきます。
WaterfallDialog を使って、Step を実行したい順に記述します。step.next() で次の Step に送り、最後の Step で step.endDialog() を呼び出して、この WaterfallDialog を終了します。
また、MainDialog が呼び出された時に この WaterfallDialog を実行する手順も記述します。
const { ComponentDialog, DialogSet, WaterfallDialog, DialogTurnStatus } = require('botbuilder-dialogs');
const MAIN_DIALOG = 'mainDialog';
class MainDialog extends ComponentDialog {
constructor() {
// 現在の Dialog の ID を設定
super(MAIN_DIALOG);
// この Dialog 内で実行するタスク(Step)を列記
this.addDialog(new WaterfallDialog('start', [
async (step) => {
await step.context.sendActivity('こんにちは!')
// 次の Step に送る
return step.next();
},
async (step) => {
await step.context.sendActivity('メインメニューです!')
// 最終 Step で この WaterfallDialog を終了する
return step.endDialog();
}
]));
this.initialDialogId = 'start';
}
async run(turnContext, dialogState) {
const dialogSet = new DialogSet(dialogState);
dialogSet.add(this);
// WaterfallDialog を順に実行
// 開始されていない場合は、initialDialogId に指定されている WaterfallDialog を実施
const dialogContext = await dialogSet.createContext(turnContext);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
}
module.exports.MainDialog = MainDialog;
Dialog Prompt でユーザーとのやり取りを簡略に記述
予め用意されている Dialog Prompt を利用すると、WaterfallDialog 内でユーザーへのメッセージ送信とユーザーのメッセージ取得を簡略に記述できます。
テキストを取得する TextPrompt, Yes|No を選ばせる ConfirmPrompt など、詳細はドキュメント↓を確認してください。
Microsoft Docs > Azure Bot Service > ダイアログライブラリ - プロンプト
作成した Dialog Prompt は step.prompt(オブジェクト名) で呼び出します。step.prompt() には step.next() の処理が含まれています。
// TextPrompt, NumberPrompt, ConfirmPrompt を追加
const { ComponentDialog, DialogSet, WaterfallDialog, DialogTurnStatus, TextPrompt, NumberPrompt, ConfirmPrompt } = require('botbuilder-dialogs');
// 作成する Dialog Prompt のオブジェクト名称を設定
const MAIN_DIALOG = 'mainDialog';
const NAME_PROMPT = 'namePrompt';
const YESNO_PROMPT ='yesnoPrompt';
const AGE_PROMPT = 'agePrompt';
class MainDialog extends ComponentDialog {
constructor() {
// 現在の Dialog の ID を設定
super(MAIN_DIALOG);
// 利用したい Dialog Prompt を新規作成して追加
this.addDialog(new TextPrompt(NAME_PROMPT));
this.addDialog(new ConfirmPrompt(YESNO_PROMPT));
this.addDialog(new NumberPrompt(AGE_PROMPT));
// この Dialog 内で実行するタスク(Step)を列記
this.addDialog(new WaterfallDialog('start', [
async (step) => {
await step.context.sendActivity('こんにちは!')
// 次の Step に送る
return step.next();
},
// Dialog Prompt で記述
async (step) => {
return await step.prompt(NAME_PROMPT, 'あなたの名前は?');
},
async (step) => {
// ユーザーからのメッセージ(入力値) step.result を取得、step.values の一時領域に保存
step.values.name = step.result;
return await step.prompt(YESNO_PROMPT, 'あなたの年齢を聞いてもよいですか?', ['はい', 'いいえ']);
},
async (step) => {
if (step.result)
{
return await step.prompt(AGE_PROMPT, 'では、あなたの年齢を入力してね');
}
else
{
// いいえ(false) の場合は、-1 を値として設定
return await step.next(-1);
}
},
async (step) => {
if (step.result >= 20)
{
await step.context.sendActivity(step.values.name + 'さん、あなたはお酒が飲めますね!');
}
await step.context.sendActivity('メインメニューです!')
// 最終 Step で この WaterfallDialog を終了する
return step.endDialog();
}
]));
this.initialDialogId = 'start';
}
:
(後略)
WaterfallDialog の実行 Step を ComponentDialog のメソッドとして独立させる
WaterfallDialog に記述している Step をこの ComponentDialog (MainDialog) 自体のメソッドとして独立させます。
(前略)
:
class MainDialog extends ComponentDialog {
constructor() {
// 現在の Dialog の ID を設定
super(MAIN_DIALOG);
// 利用したい Dialog Prompt を新規作成して追加
this.addDialog(new TextPrompt(NAME_PROMPT));
this.addDialog(new ConfirmPrompt(YESNO_PROMPT));
this.addDialog(new NumberPrompt(AGE_PROMPT));
// この Dialog 内で実行するタスク(Step)を列記
this.addDialog(new WaterfallDialog('start', [
this.initialStep.bind(this),
this.nameAskStep.bind(this),
this.ageComfirmStep.bind(this),
this.ageAskStep.bind(this),
this.finalStep.bind(this)
]));
this.initialDialogId = 'start';
}
// 実行するタスク(Step)
async initialStep(step) {
await step.context.sendActivity('こんにちは!')
// 次の Step に送る
return step.next();
}
async nameAskStep(step) {
return await step.prompt(NAME_PROMPT, 'あなたの名前は?');
}
async ageComfirmStep(step) {
step.values.name = step.result;
return await step.prompt(YESNO_PROMPT, 'あなたの年齢を聞いてもよいですか?');
}
async ageAskStep(step) {
if (step.result)
{
return await step.prompt(AGE_PROMPT, 'では、あなたの年齢を入力してね');
}
else
{
// いいえ(false) の場合は、-1 を値として設定
return await step.next(-1);
}
}
async finalStep(step) {
if (step.result >= 20)
{
await step.context.sendActivity(step.values.name + 'さん、あなたはお酒が飲めますね!');
}
await step.context.sendActivity('メインメニューです!')
// 最終 Step で この WaterfallDialog を終了する
return step.endDialog();
}
:
(後略)
WaterfallDialog の実行 Step を個別の ComponentDialog として独立させる
WaterfallDialog の実行 Step を別の ComponentDialog として独立させて利用します。
独立させる subDialog.js は mainDialog.js とほぼほぼ同じ、Dialog 名を SUB_DIALOG(subDialog) に設定する箇所だけ変更しています。(もちろん、各 Step を ComponentDialog のメソッドとして独立させていなくても良いです。)
const { ComponentDialog, DialogSet, WaterfallDialog, DialogTurnStatus ,TextPrompt, NumberPrompt, ConfirmPrompt } = require('botbuilder-dialogs');
const SUB_DIALOG = 'subDialog';
const NAME_PROMPT = 'namePrompt';
const YESNO_PROMPT ='yesnoPrompt';
const AGE_PROMPT = 'agePrompt';
class SubDialog extends ComponentDialog {
constructor() {
super(SUB_DIALOG);
this.addDialog(new TextPrompt(NAME_PROMPT));
this.addDialog(new ConfirmPrompt(YESNO_PROMPT));
this.addDialog(new NumberPrompt(AGE_PROMPT));
this.addDialog(new WaterfallDialog('start', [
this.initialStep.bind(this),
this.nameAskStep.bind(this),
this.ageComfirmStep.bind(this),
this.ageAskStep.bind(this),
this.finalStep.bind(this)
]));
this.initialDialogId = 'start';
}
// 実行するタスク(Step)
async initialStep(step) {
await step.context.sendActivity('こんにちは!')
// 次の Step に送る
return step.next();
}
async nameAskStep(step) {
return await step.prompt(NAME_PROMPT, 'あなたの名前は?');
}
async ageComfirmStep(step) {
step.values.name = step.result;
return await step.prompt(YESNO_PROMPT, 'あなたの年齢を聞いてもよいですか?');
}
async ageAskStep(step) {
if (step.result)
{
return await step.prompt(AGE_PROMPT, 'では、あなたの年齢を入力してね');
}
else
{
// いいえ(false) の場合は、-1 を値として設定
return await step.next(-1);
}
}
async finalStep(step) {
if (step.result >= 20)
{
await step.context.sendActivity(step.values.name + 'さん、あなたはお酒が飲めますね!');
}
await step.context.sendActivity('メインメニューです!')
// 最終 Step で この WaterfallDialog を終了する
return step.endDialog();
}
async run(turnContext, accessor) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(turnContext);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
}
module.exports.SubDialog = SubDialog;
mainDialog.js 側は、WaterfallDialog で利用する Dialog に SubDialog() を追加し、step.beginDialog() を使って呼び出します。もちろんメソッドに切り出して、呼び出してもOKです。
const { ComponentDialog, DialogSet, WaterfallDialog, DialogTurnStatus } = require('botbuilder-dialogs');
const { SubDialog } = require('./subDialog');
const MAIN_DIALOG = 'mainDialog';
const SUB_DIALOG = 'subDialog';
class MainDialog extends ComponentDialog {
constructor() {
// 現在の Dialog の ID を設定
super(MAIN_DIALOG);
// WaterfallDialog で実行する Dialog を定義
this.addDialog(new SubDialog());
// この Dialog 内で実行するタスク(Step)を列記
this.addDialog(new WaterfallDialog('start', [
async (step) => {
return await step.beginDialog(SUB_DIALOG)
}
]));
this.initialDialogId = 'start';
}
async run(turnContext, dialogState) {
const dialogSet = new DialogSet(dialogState);
dialogSet.add(this);
// WaterfallDialog を順に実行
// 開始されていない場合は、initialDialogId に指定されている WaterfallDialog から実施
const dialogContext = await dialogSet.createContext(turnContext);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
}
module.exports.MainDialog = MainDialog;