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


> [Bot Framework v4 (node.js) をイチから学ぶ シリーズ Top](https://qiita.com/annie/items/86a87f127556c16fe138)

チャットボットがユーザーとやり取りを行う動作、タスクは **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 > ダイアログライブラリ - プロンプト](https://docs.microsoft.com/ja-jp/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0#prompts)

作成した 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.js* は *mainDialog.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;
```