Bot Builder v4 でボット開発 : ダイアログから送れるメッセージの種類

Bot Builder ではユーザーに対して様々な種類のメッセージを送付することが出来ます。今回はダイアログからどのようにメッセージを送るかを見ていきます。

# 送れるメッセージの種類

これまではテキストや選択肢を送ってきましたが、他にも以下のようなメッセージを送ることが出来ます。

- 画像、音声、映像などのマルチメディアファイル
- ヒーローカード、サムネイルカード、アニメーションなどのリッチカード
- 複数のカードを含むカルーセル
- レシートカード、サインインカードなど特定用途のカード
- 提案 (クイックアクション) 
- より柔軟性の高いアダプティブカード

各種カードの仕様は [Bot Framework -- Cards](https://github.com/Microsoft/BotBuilder/blob/master/specs/botframework-activity/botframework-cards.md) を参照。

アダプティブカードは少し複雑なため、次回紹介します。

# MessageFactory 

MessageFactory を使うと様々なメッセージを簡単に作ることが出来ます。

[MessageFactory : GitHub のソース](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Builder/MessageFactory.cs)

# マルチメディアの送信

「ようこそ」メッセージをダイアログ化して、様々なメッセージを送ってみます。

1\. Dialogs フォルダに WelcomeDialog.cs を追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9e26a732-2d6e-701e-e8e2-d2f62f406783.png)

2\. コードを以下のものに差し替え。

```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

public class WelcomeDialog : ComponentDialog
{
    private MyStateAccessors accessors;

    public WelcomeDialog(MyStateAccessors accessors) : base(nameof(WelcomeDialog))
    {
        this.accessors = accessors;

        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            SendWelcomeImageAsync,
            EndDialogAsync
        };
        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("welcome", waterfallSteps));
        AddDialog(new ProfileDialog(accessors));
    }

    private async Task<DialogTurnResult> SendWelcomeImageAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 応答は必要ないのでプロンプトではなく SendActivityAsync で送信
        await stepContext.Context.SendActivityAsync(
            (Activity)MessageFactory.Attachment(
                attachment: new Attachment(
                    contentType: "image/jpg",
                    contentUrl: "https://picsum.photos/300/200/?image=433"),
                text: "ようこそ MyBot へ!"
            ));

        // 以下のコードでも同じものが作成可能。
        //MessageFactory.ContentUrl("https://picsum.photos/300/200/?image=433","image/jpg",text:"ようこそ MyBot へ!");
        
        return await stepContext.BeginDialogAsync(nameof(ProfileDialog), cancellationToken: cancellationToken);
    }
   
    private async Task<DialogTurnResult> EndDialogAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        return await stepContext.EndDialogAsync(true);
    }
}
```

3\. MyBot.cs の ActivityTypes.ConversationUpdate をハンドルする箇所で「ようこそ MyBot へ！」と送っているところを WelcomeDialog を呼ぶように変更。

```csharp
//await turnContext.SendActivityAsync("ようこそ MyBot へ！");
await dialogContext.BeginDialogAsync(nameof(WelcomeDialog), null, cancellationToken);
//await dialogContext.BeginDialogAsync(nameof(ProfileDialog), null, cancellationToken);
```

4\. コンストラクタで WelcomeDialog を追加して ProfileDialog をコメントアウト。

```csharp
// コンポーネントダイアログを追加
dialogs.Add(new WelcomeDialog(accessors));
dialogs.Add(new ProfileDialog(accessors));
```

5\. F5 を押下してデバッグ実行。動作を確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a9b0e2fe-f57c-5aec-4798-b58a73f8a9fc.png)

# リッチカードの送信

リッチカードにはヒーローカードやサムネイルカードなどいくつか種類がありますが、基本的には以下のような要素で構成されます。

- タイトルや説明などの文字列
- 画像がある場合その情報
- アクションボタン

上記のうち、ボタンはユーザーがクリックしてアクションを実行できます。アクションには以下のような種類があります。ただしチャネルによって使えるものと使えないものがあるようです。

- openUrl: リンクを開く
- imBack: ボットにメッセージを返しメッセージを画面に出す
- postBack: ボットにメッセージを返すが画面には何も出さない (一部のクライアントでは imBack と同じ挙動)
- call: 電話をかける
- playAudio/playVideo/showImage: 音声、動画、画像の再生や表示
- downloadFile: ファイルのダウンロード
- signin: サインイン
- messageBack: メッセージを返す (Teams 用)

詳細は[リッチ カード内のイベントを処理する](https://docs.microsoft.com/ja-jp/azure/bot-service/bot-builder-howto-add-media-attachments?view=azure-bot-service-4.0&tabs=csharp#process-events-within-rich-cards) を参照。

## ヒーローカードの送信
ここではヒーローカードを使ってプロファイル登録をするか聞きます。

1\. WelcomeDialog.cs を以下のように変更。

```csharp
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

public class WelcomeDialog : ComponentDialog
{
    private MyStateAccessors accessors;

    public WelcomeDialog(MyStateAccessors accessors) : base(nameof(WelcomeDialog))
    {
        this.accessors = accessors;

        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            SendWelcomeHeroCardAsync,
            CheckProfileAsync
        };
        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("welcome", waterfallSteps));
        AddDialog(new TextPrompt("checkStatus"));
        AddDialog(new ProfileDialog(accessors));
    }

    private async Task<DialogTurnResult> SendWelcomeHeroCardAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // カルーセルを作成。ここでは 1 つだけ Attachment を設定するため
        // カルーセル形式にはならない。ボタンには imBack を設定
        var activity = MessageFactory.Carousel(
            new Attachment[]
            {
                new HeroCard(
                    title: "ようこそ My Bot へ！プロファイル登録をしますか？",
                    images: new CardImage[] { new CardImage(url: "https://picsum.photos/300/200/?image=433") },
                    buttons: new CardAction[]
                    {
                        new CardAction(title: "はい", type: ActionTypes.ImBack, value: "はい"),
                        new CardAction(title: "スキップ", type: ActionTypes.ImBack, value: "スキップ")
                    })
                .ToAttachment(),
            });

        // TextPrompt を指定して文字列が返ること期待
        return await stepContext.PromptAsync("checkStatus", new PromptOptions
        {
            Prompt = (Activity)activity
        });
    }

    private async Task<DialogTurnResult> CheckProfileAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {       
        // 返事の内容によってプロファイル登録するか決定
        if (stepContext.Result.ToString() == "はい")
            return await stepContext.BeginDialogAsync(nameof(ProfileDialog), cancellationToken:cancellationToken);
        
        // 登録しない場合は匿名として設定
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
        userProfile.Name = "匿名";
        return await stepContext.EndDialogAsync(true);
    }
}
```

2\. F5 キーを押下してデバッグ実行。動作を確認。以下は「スキップ」をクリックした際の様子。「ようこそ ’匿名’ さん！」と出ていることを確認。 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/fc81e783-5bad-370b-0b06-0511a1c5447e.png)

3\. imBack 以外のアクションを試すために、OpenUrl を追加。

```csharp
buttons: new CardAction[]
{
    new CardAction(title:"はい", type: ActionTypes.PostBack, value: "はい"),
    new CardAction(title: "スキップ", type: ActionTypes.ImBack, value: "スキップ"),
    new CardAction(title: "詳細を見る", type: ActionTypes.OpenUrl, value: "https://dev.botframework.com"),
})
```

4\. 動作の確認。
※尚、「詳細を見る」をクリックすると既に起動しているブラウザがある場合、新しいタブで開くため、エミュレータの背面でアクションが実行されている場合があるので注意。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/2f05e77f-2828-1564-606c-6dbcda01b876.png)

## アニメーションカードの送信

ヒーローカード以外にもアニメーションカードを送ってみましょう。

1\. カルーセルを作る場所を以下のコードと差し替え。

```csharp
var activity = MessageFactory.Carousel(
    new Attachment[]
    {
        new HeroCard(
            title: "ようこそ My Bot へ！プロファイル登録をしますか？",
            images: new CardImage[] { new CardImage(url: "https://picsum.photos/300/200/?image=433") },
            buttons: new CardAction[]
            {
                new CardAction(title:"はい", type: ActionTypes.ImBack, value: "はい"),
                new CardAction(title: "スキップ", type: ActionTypes.ImBack, value: "スキップ"),
                new CardAction(ActionTypes.ShowImage, title: "Azure Bot Service", value: "https://picsum.photos/300/200/?image=433"),
            })
        .ToAttachment(),                
        new AnimationCard(
            title: "アニメーションサンプル",
            image: new ThumbnailUrl("https://docs.microsoft.com/en-us/bot-framework/media/how-it-works/architecture-resize.png"),
            media: new List<MediaUrl>() { new MediaUrl(url: "http://i.giphy.com/Ki55RUbOV5njy.gif") }
            ).ToAttachment(),                
    });
```
2\. 動作を確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/28f3fbb0-5edd-de46-4ae5-52e29f57631e.png)

### ToAttachment メソッド

ヒーローカードの追加もアニメーションカードの追加も、最後に ToAttachment と呼んでいますが、これは各カードを Attachment 化するために必要で、以下の拡張メソッドで定義されています。

[GitHub: CardEx.cs](https://github.com/Microsoft/botbuilder-dotnet/blob/master/libraries/Microsoft.Bot.Schema/CardEx.cs)

### MediaCard

インテリセンスを使うと見つけてしまうのが MediaCard というカードですが、これは AudioCard, VideoCard, AnimationCard などのカードを処理する際に使われるもので、開発者が直接呼び出すものではありません。よって ToAttachment メソッドもなければコンテンツタイプもありません。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/45365b69-a040-5aeb-0b7a-f38b6d64c30e.png)

### imBack と postBack の違い

説明にもありますが、imBack を使った場合はユーザーが見える形で返信が行われる一方、postBack の場合はユーザーにメッセージが見えません。システムコール的な処理で画面に文字を出したくない場合は postBack を使います。

# 提案 (クイックアクション) の送信

提案はシンプルに選択肢のボタンだけを表示して、ユーザーがクイックに操作を選択できるという機能です。ここではヒーローカードの代わりに使ってみます。

1\. WelcomeDialog.cs を以下のコードに差し替えます。

```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

public class WelcomeDialog : ComponentDialog
{
    private MyStateAccessors accessors;

    public WelcomeDialog(MyStateAccessors accessors) : base(nameof(WelcomeDialog))
    {
        this.accessors = accessors;

        // ウォーターフォールのステップを定義。処理順にメソッドを追加。
        var waterfallSteps = new WaterfallStep[]
        {
            SendSuggestionsAsync,
            CheckProfileAsync
        };
        // ウォーターフォールダイアログと各種プロンプトを追加
        AddDialog(new WaterfallDialog("welcome", waterfallSteps));
        AddDialog(new TextPrompt("checkStatus"));
        AddDialog(new ProfileDialog(accessors));
    }

    private async Task<DialogTurnResult> SendSuggestionsAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // カルーセルを作成。ここでは 1 つだけ Attachment を設定するため
        // カルーセル形式にはならない。ボタンには imBack を設定
        var activity = MessageFactory.SuggestedActions(
            new string[]
            {
                "はい","スキップ"
            },
            text: "ようこそ My Bot へ！プロファイル登録をしますか？");

        // TextPrompt を指定して文字列が返ること期待
        return await stepContext.PromptAsync("checkStatus", new PromptOptions
        {
            Prompt = (Activity)activity
        });
    }


    private async Task<DialogTurnResult> CheckProfileAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
    {
        // 返事の内容によってプロファイル登録するか決定
        if (stepContext.Result.ToString() == "はい")
            return await stepContext.BeginDialogAsync(nameof(ProfileDialog), cancellationToken: cancellationToken);

        // 登録しない場合は匿名として設定
        var userProfile = await accessors.UserProfile.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);
        userProfile.Name = "匿名";
        return await stepContext.EndDialogAsync(true);
    }
}
```

2\. 動作を確認します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9fa94a82-0b11-7197-3a14-3d9de6c96008.png)

# リトライと検証

これまでのプロンプト同様、リトライと検証はそのまま動作します。

# まとめ

全てを紹介した訳ではありませんが、テキストや選択肢以外にも多様な方法でユーザーと対話できることが分かりました。是非他のカードも使ってみてください。次回はアダプティブカードを見ていきます。

[次の記事へ](https://qiita.com/kenakamu/items/2725651445e55ec4545c)
[目次へ戻る](https://qiita.com/kenakamu/items/6dc043cfc1f199032883)

[この記事のサンプルコード](https://github.com/kenakamu/botbuilderv4completeguide/tree/article12)