Bot, 開発

複数の話題を扱えるBotを作る

SecretaryBotチームのマサです!今回は複数の話題を扱えるBotを作っていきましょう!

前回の記事では、Dialogを使って連続した会話を実装することができました。ただ、現時点では1つの話題しか扱えません。今回は複数の話題(飛行機予約、ホテル予約)を扱えるBotを作っていきましょう。

今回もサンプルを作成しているので、動作を見たい人は動かしてみてください。ちなみに[Book Flight]ボタンからクリックしてください。

複数の話題を扱う際のデザイン

モバイルアプリなどを設計・開発する際にはメインメニューがあると思いますが、Botが複数の話題を扱う際にもメインメニューを作ると便利です。そのメインメニューもDialogで実現することになりますが、多くのサンプルの中でRootDialogという名称で作成されています。RootDialogはメインメニューの表示やハンドリングに使われます。よくあるパターンは下記のような感じです。

  1. メインメニューを表示する。そのメニューをユーザーが選んで適切なDialogに処理を渡す
  2. ユーザーの入力をLUISなどで自然言語処理し、適切なDialogに処理を渡す。

いずれにせよ、RootDialogがルーター的な役割を果たして、適切なDialogに処理を渡しているのですが、今回は1の方式で実装しています。

private async Task MessageReceiveAsync (IDialogContext context, IAwaitable<IMessageActivity> result)
{
    var reply = await result;
    if(reply.Text.ToLower().Contains("help"))
    {
       await context.PostAsync("You can implement help menu here");
    }
    else
    {
        await ShowMainmenu(context);
    }
}

private async Task ShowMainmenu(IDialogContext context)
{
    //Show menues
    PromptDialog.Choice(context,this.CallDialog,this.mainMenuList,"What do you want to do?");
}

今回は所略していますがRootDialogStartAsyncメソッドはMessegeReceiveAsyncメソッドを呼び出しています。ヘルプ関連のメッセージも表示するようにしていますが、今回はそこはさらっと見ていただいて、ShowMainmenuメソッドを見ていきましょう。

ShowMainmenuメソッド内ではPromptDialog.ChoiceメソッドというBotFrameworkの提供するDialogのメソッドを使っています。こいつはなかなか便利なやつでして、第3引数にリストを渡してあげると、そのリストをボタン形式で出してくれます。また、第2引数に渡したメソッドをスタックにPushしてくれるので、そのメソッド内(CallDialog)でユーザーが選択したメニューに応じた処理を書けばよくなります。

private async Task CallDialog(IDialogContext context, IAwaitable<string> result)
{
    //This method is resume after user choise menu
    var selectedMenu = await result;
    switch (selectedMenu)
    {
        case FlightMenu:
            //Call child dialog without data
            context.Call(new FlightDialog(),ResumeAfterDialog);
            break;
        case HotelMenu:
            //Call child dialog with data
            context.Call(new HotelDialog(location), ResumeAfterDialog);
            break;
    }
}

CallDialog内ではユーザーの選択した情報に応じてselect文を書いています。そして、それぞれのメニューに応じたDialogをcontext.Callで呼び出しています。なお、context.Callの第2引数には呼び出されたDialogが終了した際に呼び出されるメソッドを渡すことができます。動作としては、ResumeAfterDialogをスタック上にPushしたうえで、Dialogを呼び出しています。

まずはFlightDialogに着目していきましょう。context.Callで呼び出された場合にも、FlightDialog.StartAsyncから処理が始まります。ここから先の動作は、前回の記事で説明した通りです。今回はAskOiginがスタックにPushされています。

[Serializable]
public class FlightDialog : IDialog<object>
{
    string destination;
    public async Task StartAsync(IDialogContext context)
    {
        await context.PostAsync("Welcome to FlightDialog");
        await context.PostAsync("Where do you want to go?");
        context.Wait(AskOrigin);
    }

    private async Task AskOrigin(IDialogContext context, IAwaitable<IMessageActivity> result)
    {
        var reply = await result;
        destination = reply.Text;
        await context.PostAsync($"OK you want to go {destination}.Where is the origin place?");
            context.Wait(BookFlight);
    }

    private async Task BookFlight(IDialogContext context,IAwaitable<IMessageActivity> result)
    {
        var reply = await result;
        await context.PostAsync($"OK, booked the flight from {reply.Text} to {destination}. Have a nice trip :)");
        context.Done<object>(destination);
    }
}

このサンプルで着目してほしいのは、destinationというインスタンス変数が定義されていることです。途中でこの変数に値を代入していますし、この変数を利用しています。Bot Frameworkはこのように変数を利用することができ「DialogがDoneされるまでこの値は保持されます」。つまり、文脈(メソッド)が変わったとしても話題(Dialog)が同じであれば状態を保持してくれます。

実はDialogはSerializeされた上でStateService内に保存/読み出しされるので、こういったことが可能になります。アプリ側では状態を保持しない(ステートレス)でも、きちんと話題・文脈を継続して意識できるのは、こういった仕組みがBot Frameworkから提供されているからなんですね。いいですね。

では、話題(Dialog)が変わったときに値を渡すにはどうすればいいのでしょうか?そんな時は上記のようにcontext.Doneするときに値を渡してあげてください。こうすることで先ほど作成し、現在スタックの一番下にあるRootDialogResumeAfterDialogが値を受け取ることができます。頭の整理のために今までの処理を図示すると下記のようになります。

こうやってスタックを意識しながらコードを書くことでよいBotが書けるので、是非そういった習慣をつけるようにしてみてください。それでは、引き続きコードの解説をしましょう。

private async Task ResumeAfterDialog(IDialogContext context,IAwaitable<object> result)
{
    //Resume this method after child Dialog is done.
    var test = await result;
    if(test != null)
    {
        location = test.ToString();
    }
    else
    {
        location = null;
    }
    //await this.ShowMainmenu(context); // If you want to show main menu when the dialog is done, please comment out this line.
    context.Wait(MessageReceiveAsync);
}

RootDialogResumeAfterDialogでは先ほど渡されたFlightDialogdestination変数の値を渡されます。それをRootDialoglocation変数に代入しています。ここで改めてRootDialoCallDiagloメソッドを読み直してみましょう。

今度はHotelDialogを呼び出しているほうに着目してみてください。HotelDialogを呼び出す際にlocation情報を渡しています。このようにして、話題(Dialog)が変わったとしても値を渡すことができます。

private async Task CallDialog(IDialogContext context, IAwaitable<string> result)
{
    //This method is resume after user choise menu
    var selectedMenu = await result;
    switch (selectedMenu)
    {
        case FlightMenu:
            //Call child dialog without data
            context.Call(new FlightDialog(),ResumeAfterDialog);
            break;
        case HotelMenu:
            //Call child dialog with data
            context.Call(new HotelDialog(location), ResumeAfterDialog);
            break;
    }
}

今回の記事でDialogについてディープに掘り下げられたと思います。みなさんもこの記事での学びを活かして役立つBotを開発してみてください。なお、Dialog間でデータを受け渡すための方法はいくつかあります。次回A`がStateServiceを使ったデータの保存/取得方法について解説します。お楽しみに!

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中