チャットボットがユーザーとやり取りを行う動作、タスクは 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 を作成、その配下にステートを含む会話に必要な情報を保存します。
ConversationState で Dialog のステートを管理する
ひとまず MemoryStorage を定義して、ConversationState を生成します。
MainDialog という ComponentDialog (ファイル名は mainDialog.js) を作成し、こちらでタスクを実行します。
Bot インスタンスが起動するときに ComversationState および MainDialog を取得するようにします。
constrestify=require('restify');constpath=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 を生成するconstmemoryStorage=newMemoryStorage();constconversationState=newConversationState(memoryStorage);:(中略):// Dialog と ConversationState を DialogBot で利用する// const bot = new EchoBot();constdialog=newMainDialog();constbot=newDialogBot(conversationState,dialog);server.post('/api/messages',(req,res)=>{adapter.processActivity(req,res,async(context)=>{// Route to main dialog.awaitbot.run(context);});});
メッセージ応対を ComponentDialog で行うように設定する
dialogBot.jsでは、メッセージを受け取って返信する処理を ComponentDialog から行うように設定し、ConversationStateを使って Dialog のコントロールを行います。
ConversationState に DialogStateを保存し、DialogState に従って Dialog を実行するプロセスになります。
const{ActivityHandler}=require('botbuilder');classDialogBotextendsActivityHandler{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 をポイントawaitthis.dialog.run(turnContext,this.dialogState);awaitnext();});}// ステートを保存するため、ActivityHandler.run を Overrideasyncrun(turnContext){awaitsuper.run(turnContext);awaitthis.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');constMAIN_DIALOG='mainDialog';classMainDialogextendsComponentDialog{constructor(){// 現在の Dialog の ID を設定super(MAIN_DIALOG);// この Dialog 内で実行するタスク(Step)を列記this.addDialog(newWaterfallDialog('start',[async(step)=>{awaitstep.context.sendActivity('こんにちは!')// 次の Step に送るreturnstep.next();},async(step)=>{awaitstep.context.sendActivity('メインメニューです!')// 最終 Step で この WaterfallDialog を終了するreturnstep.endDialog();}]));this.initialDialogId='start';}asyncrun(turnContext,dialogState){constdialogSet=newDialogSet(dialogState);dialogSet.add(this);// WaterfallDialog を順に実行// 開始されていない場合は、initialDialogId に指定されている WaterfallDialog を実施constdialogContext=awaitdialogSet.createContext(turnContext);constresults=awaitdialogContext.continueDialog();if(results.status===DialogTurnStatus.empty){awaitdialogContext.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 のオブジェクト名称を設定constMAIN_DIALOG='mainDialog';constNAME_PROMPT='namePrompt';constYESNO_PROMPT='yesnoPrompt';constAGE_PROMPT='agePrompt';classMainDialogextendsComponentDialog{constructor(){// 現在の Dialog の ID を設定super(MAIN_DIALOG);// 利用したい Dialog Prompt を新規作成して追加this.addDialog(newTextPrompt(NAME_PROMPT));this.addDialog(newConfirmPrompt(YESNO_PROMPT));this.addDialog(newNumberPrompt(AGE_PROMPT));// この Dialog 内で実行するタスク(Step)を列記this.addDialog(newWaterfallDialog('start',[async(step)=>{awaitstep.context.sendActivity('こんにちは!')// 次の Step に送るreturnstep.next();},// Dialog Prompt で記述async(step)=>{returnawaitstep.prompt(NAME_PROMPT,'あなたの名前は?');},async(step)=>{// ユーザーからのメッセージ(入力値) step.result を取得、step.values の一時領域に保存step.values.name=step.result;returnawaitstep.prompt(YESNO_PROMPT,'あなたの年齢を聞いてもよいですか?',['はい','いいえ']);},async(step)=>{if(step.result){returnawaitstep.prompt(AGE_PROMPT,'では、あなたの年齢を入力してね');}else{// いいえ(false) の場合は、-1 を値として設定returnawaitstep.next(-1);}},async(step)=>{if(step.result>=20){awaitstep.context.sendActivity(step.values.name+'さん、あなたはお酒が飲めますね!');}awaitstep.context.sendActivity('メインメニューです!')// 最終 Step で この WaterfallDialog を終了するreturnstep.endDialog();}]));this.initialDialogId='start';}:(後略)
WaterfallDialog の実行 Step を ComponentDialog のメソッドとして独立させる
WaterfallDialog に記述している Step をこの ComponentDialog (MainDialog) 自体のメソッドとして独立させます。
(前略):classMainDialogextendsComponentDialog{constructor(){// 現在の Dialog の ID を設定super(MAIN_DIALOG);// 利用したい Dialog Prompt を新規作成して追加this.addDialog(newTextPrompt(NAME_PROMPT));this.addDialog(newConfirmPrompt(YESNO_PROMPT));this.addDialog(newNumberPrompt(AGE_PROMPT));// この Dialog 内で実行するタスク(Step)を列記this.addDialog(newWaterfallDialog('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)asyncinitialStep(step){awaitstep.context.sendActivity('こんにちは!')// 次の Step に送るreturnstep.next();}asyncnameAskStep(step){returnawaitstep.prompt(NAME_PROMPT,'あなたの名前は?');}asyncageComfirmStep(step){step.values.name=step.result;returnawaitstep.prompt(YESNO_PROMPT,'あなたの年齢を聞いてもよいですか?');}asyncageAskStep(step){if(step.result){returnawaitstep.prompt(AGE_PROMPT,'では、あなたの年齢を入力してね');}else{// いいえ(false) の場合は、-1 を値として設定returnawaitstep.next(-1);}}asyncfinalStep(step){if(step.result>=20){awaitstep.context.sendActivity(step.values.name+'さん、あなたはお酒が飲めますね!');}awaitstep.context.sendActivity('メインメニューです!')// 最終 Step で この WaterfallDialog を終了するreturnstep.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');constSUB_DIALOG='subDialog';constNAME_PROMPT='namePrompt';constYESNO_PROMPT='yesnoPrompt';constAGE_PROMPT='agePrompt';classSubDialogextendsComponentDialog{constructor(){super(SUB_DIALOG);this.addDialog(newTextPrompt(NAME_PROMPT));this.addDialog(newConfirmPrompt(YESNO_PROMPT));this.addDialog(newNumberPrompt(AGE_PROMPT));this.addDialog(newWaterfallDialog('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)asyncinitialStep(step){awaitstep.context.sendActivity('こんにちは!')// 次の Step に送るreturnstep.next();}asyncnameAskStep(step){returnawaitstep.prompt(NAME_PROMPT,'あなたの名前は?');}asyncageComfirmStep(step){step.values.name=step.result;returnawaitstep.prompt(YESNO_PROMPT,'あなたの年齢を聞いてもよいですか?');}asyncageAskStep(step){if(step.result){returnawaitstep.prompt(AGE_PROMPT,'では、あなたの年齢を入力してね');}else{// いいえ(false) の場合は、-1 を値として設定returnawaitstep.next(-1);}}asyncfinalStep(step){if(step.result>=20){awaitstep.context.sendActivity(step.values.name+'さん、あなたはお酒が飲めますね!');}awaitstep.context.sendActivity('メインメニューです!')// 最終 Step で この WaterfallDialog を終了するreturnstep.endDialog();}asyncrun(turnContext,accessor){constdialogSet=newDialogSet(accessor);dialogSet.add(this);constdialogContext=awaitdialogSet.createContext(turnContext);constresults=awaitdialogContext.continueDialog();if(results.status===DialogTurnStatus.empty){awaitdialogContext.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');constMAIN_DIALOG='mainDialog';constSUB_DIALOG='subDialog';classMainDialogextendsComponentDialog{constructor(){// 現在の Dialog の ID を設定super(MAIN_DIALOG);// WaterfallDialog で実行する Dialog を定義this.addDialog(newSubDialog());// この Dialog 内で実行するタスク(Step)を列記this.addDialog(newWaterfallDialog('start',[async(step)=>{returnawaitstep.beginDialog(SUB_DIALOG)}]));this.initialDialogId='start';}asyncrun(turnContext,dialogState){constdialogSet=newDialogSet(dialogState);dialogSet.add(this);// WaterfallDialog を順に実行// 開始されていない場合は、initialDialogId に指定されている WaterfallDialog から実施constdialogContext=awaitdialogSet.createContext(turnContext);constresults=awaitdialogContext.continueDialog();if(results.status===DialogTurnStatus.empty){awaitdialogContext.beginDialog(this.id);}}}module.exports.MainDialog=MainDialog;