LoginSignup
2
2

More than 3 years have passed since last update.

Bot Framework v4 (node.js) をイチから学ぶ (2) Dialog で処理をブロック化して呼び出し実行

Last updated at Posted at 2020-08-05

Bot Framework v4 (node.js) をイチから学ぶ シリーズ Top

チャットボットがユーザーとやり取りを行う動作、タスクは 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 を取得するようにします。

index.js
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 を実行するプロセスになります。

dialogBot.js
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 を実行する手順も記述します。

mainDialog.js
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() の処理が含まれています。

mainDialog.js
// 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) 自体のメソッドとして独立させます。

mainDialog.js
(前略)
:
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.jsmainDialog.js とほぼほぼ同じ、Dialog 名を SUB_DIALOG(subDialog) に設定する箇所だけ変更しています。(もちろん、各 Step を ComponentDialog のメソッドとして独立させていなくても良いです。)

subDialog.js
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です。

mainDialog.js
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;
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2