なんとなく動いた・・・で終わらないためのMicrosoft Bot Framework ～ そのに Dialog～


Microsoft.Bot.Builder.DialogsはBotとのコミュニケーションの核となる機能を提供する。

## Conversation
トップレベルの機能としてMicrosoft.Bot.Builder.Dialogs.Conversationがあり、このクラスから受信したメッセージを処理（SendAsync）したり、処理を再開する（ResumeAsync）ことが出来る。


```csharp
    public class MessagesController : ApiController
    {
        /// <summary>   
        /// POST: api/Messages
        /// Receive a message from a user and reply to it
        /// </summary>
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
            {
                await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
            }
            else
            {
                HandleSystemMessage(activity);
            }
            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }
```

では、SendAsyncはどのような処理をするのだろうか？答えは [マニュアル](https://docs.microsoft.com/en-us/dotnet/api/microsoft.bot.builder.dialogs.conversation.sendasync?view=botbuilder-3.8)を見えれば明らかだ。
1.  必要なコンポーネントの初期化
2.  ボットに送信されたメッセージ（iMessageActivity；会話の中の1つのメッセージを表現するインタフェース）から 会話のステートをデシリアライズ
3.  サスペンド状態の会話プロセスを再開
4.  ユーザーに送信されるメッセージ(IMessageActivity)のキュー
5.  更新された会話ステートのシリアライズ


では、SendAsyncにも会話を継続するためのResume処理が含まれているのになぜResumeAsyncメソッドが提要されているのだろうか？これはResumeAsynの引数を見ると明らかになる。ResumeAsyncでは下記のようにConversationReferenceやConversationReferenceを渡すことができる。
```csharp
ResumeAsync(ResumptionCookie, IActivity, CancellationToken)  
ResumeAsync(ConversationReference, IActivity, CancellationToken) 
```

Conversationを再開させるためにSendAsycnでは指定できなかったResumptionCookie、ConversationReferenceを指定できることがResumAsyncの特徴である。なお、ResumptionCookieはユーザーとの会話を再開するために使われる所謂Http coookieのような機能を持つが[Release notes](https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-release-notes) にある通りディスコン予定なので利用しないで頂きたい。よって[ConversationReference](https://docs.microsoft.com/en-us/dotnet/api/microsoft.bot.connector.conversationreference?view=botbuilder-3.8)一択で次のようなプロパティを持ち、明示的にActivityId等を指定して会話を再開させることが可能だ。
![image](https://qiita-image-store.s3.amazonaws.com/0/33998/f639e998-7b73-8373-19e5-1d7150a7656f.png)





## Dialog
会話はインタフェースMicrosoft.Bot.Builder.Dialogs.IDialogを継承したStartAsyncメソッドを実装する。

```csharp
    public class RootDialog : IDialog<object>
    {
        private string description;

        public Task StartAsync(IDialogContext context)
     
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

```

また、独自実装だけでなくすでに実装済みの様々なDialogクラスがあるので用途によって使い分けると便利だ。代表的にものを３つだけご紹介する。
* LuisDialog - Luis の Intentにより処理を分ける
* PromptDialog  -　テキストや指定した選択肢をもとに処理を分ける。いくつかのStyleを選択可能なので[こちら](http://qiita.com/hiiyan0402/items/f7f5f3b97834d9f4c7e6)の記事を参考にしていただきたい。
* CommandDialog - 正規表現パターンに一致した場合に処理する


例えばLuisDialogでは以下のような実装になる。
```csharp
using Microsoft.Bot.Builder.Luis;
using Microsoft.Bot.Builder.Luis.Models;

[Serializable]
[LuisModel("xxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxx")]
public class RootDialog : LuisDialog<object>
{
     [LuisIntent("")]
    [LuisIntent("None")]
    public async Task None(IDialogContext context, LuisResult result)
    {
        await context.PostAsync($"I'm sorry, I did not understand {result.Query}.\nType 'help' to know more about me :)");
        context.Done<object>(null);
    }
    [LuisIntent("Help")]
    public async Task Help(IDialogContext context, LuisResult result)
    {
        await context.PostAsync("I'm the help desk bot and I can help you create a ticket or explore the knowledge base.\n" +
                                "You can tell me things like _I need to reset my password_ or _explore hardware articles_.");
        context.Done<object>(null);
    }
    [LuisIntent("SubmitTicket")]
    public async Task SubmitTicket(IDialogContext context, IAwaitable<IMessageActivity> messageActivity, LuisResult result)
    {
        EntityRecommendation categoryEntityRecommendation, severityEntityRecommendation;

        result.TryFindEntity("category", out categoryEntityRecommendation);
        result.TryFindEntity("severity", out severityEntityRecommendation);

        this.category = ((Newtonsoft.Json.Linq.JArray)categoryEntityRecommendation?.Resolution["values"])?[0]?.ToString();
        this.severity = ((Newtonsoft.Json.Linq.JArray)severityEntityRecommendation?.Resolution["values"])?[0]?.ToString();
        this.description = result.Query;

        await this.EnsureTicket(context);

        var activity = await messageActivity;
        await this.SendSearchToBackchannel(context, activity, this.description);
    }
```

LuisDialogは内部的に以下の処理を実行している。
1. LuisDialog内部でStartAsyncが呼び出されLuisModeで指定したLUIS APIをコール
2. Responseに含まれるLUIS Intentを自動的に解釈してLuisIntent Attributeで指定した値とマッチしたメソッドを実行。ちなみにNoneはマッチしたIntentが存在しない場合に実行される。



PromptDialogは以下のような実装になる。この例では３つの選択肢（high, normal, low）からユーザーに１つ選び、その後の処理をSeverityMessageReceivedAsyn担うことになる。
```csharp
        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            var severities = new string[] { "high", "normal", "low" };
            PromptDialog.Choice(context, this.SeverityMessageReceivedAsync, severities, "Which is the severity of this problem?");
        }

        private async Task SeverityMessageReceivedAsync(IDialogContext context, IAwaitable<string> argument)
        {
            this.severity = await argument;

            var text = $"Great! I'm going to create a **{this.severity}** severity ticket ";
            await context.PostAsync(text);
        }

```

## Dialog Context

会話の文脈やデータなどを管理するコンテキストクラスである。
IDialogContextの定義は以下のようになっている。
```csharp
using Microsoft.Bot.Builder.Dialogs.Internals;

namespace Microsoft.Bot.Builder.Dialogs
{
    //
    // 概要:
    //     The context for the execution of a dialog's conversational process.
    public interface IDialogContext : IDialogStack, IBotContext, IBotData, IBotToUser
    {
    }
}
```

IBotToUserはその名の通りユーザーに対してメッセージを送ることができる。拡張メソッドでSayAsyncが提供されているため音声出力の実装も可能だ。詳しくは[こちら](http://qiita.com/hiromasa-masuda/items/dfeba51baa3fd4e7a150) を参考にしていただきたい。
```csharp
    public interface IBotToUser
    {
        // 概要:
        //     Make a message.
        IMessageActivity MakeMessage();
        //     Post a message to be sent to the user.
        Task PostAsync(IMessageActivity message, CancellationToken cancellationToken = default(CancellationToken));
    }
```


データ管理用のインタフェースで用途に応じてスコープを選択できるようになっている。
```csharp
    //
    // 概要:
    //     Private bot data.
    public interface IBotData
    {
        //     全てのチャネルや会話にまたがった1ユーザーのデータ
        IBotDataBag UserData { get; }
        //    1つの 会話に関連したプライベートなデータ.
        IBotDataBag ConversationData { get; }
        //     1つの会話の1ユーザーに関連したプレイべーとなデータ
        IBotDataBag PrivateConversationData { get; }
        //     Flushes the bot data to Microsoft.Bot.Builder.Dialogs.Internals.IBotDataStore`1
        Task FlushAsync(CancellationToken cancellationToken);
        //     Loads the bot data from Microsoft.Bot.Builder.Dialogs.Internals.IBotDataStore`1
        Task LoadAsync(CancellationToken cancellationToken);
    }
```

実際のデータは以下のようにKey-Valueで保存されていることがわかる。
```csharp
    public interface IBotDataBag
    {
        int Count { get; }
        void Clear();
        bool ContainsKey(string key);
        bool RemoveValue(string key);
        void SetValue<T>(string key, T value);
        bool TryGetValue<T>(string key, out T value);
    }
 ```

ここにストアしたデータはAPI経由でもアクセスできるようになる。Azure Storageでもお馴染みのEtagを使った楽観的同時実行制御をサポートしているが現時点ではテストやPoC用途でProduction環境での利用は想定していないようだから気を付けていただきたい。
https://docs.microsoft.com/ja-jp/bot-framework/rest-api/bot-framework-rest-state

今回はBotアプリを開発する際に軸となるConversationについて紹介した。次回はRich Cardについて解説する予定だ。Botではテキストで会話を行うのが一般的だが、Rich Cardやメディア（画像、ビデオ、オーディオファイル等）を送ることでより魅力的なBotの実装が可能になる。