Bot Framework の Activity を(改めて)整理

Bot Framework を使う上で重要なのが Activity。
・・・なのですが、知ってる人には当たり前すぎるためか、意外と情報が見つからない気がします。

先日、勉強会で Activity の観点で Bot Framework を紹介したのですが、思った以上に高評価でした(リップサービス込みだとしても)。
ちょっと所用で間が開いてしまいましたが、改めて Bot Framework の Activity を整理してみます。


■ Activity とは

Bot Framework で Activity とは、直感的には「吹き出し」の一つ一つのこと、相手に送信するメッセージの単位のことです。人間が発信するものもBot が発信するものも区別なく Activity です。

複数行

以下では、テキストだけ、画像を含むなど、見た目ごとに Activity を順に紹介します。
なおソースコードをもっとシンプルにできるのですが、今回は Activity ごとに記述するべき内容を見るために、全体としてはだいぶ冗長なコードになっています。

 


■ テキストのみを送信

Bot Framework の Activity は、「メール」と比べるとわかりやすいと思います。
メールは基本的にはテキストを送ります。HTML メールはリッチな見た目ですが、送信しているデータはテキストです。

以下では、

  • context ・・・Bot Framework の対話の文脈(コンテキストのほうが伝わりやすいですね)
  • activity ・・・以下のコードでは、Bot にとってユーザー(人間)から送信されてきた Activity のこと。この activity に返信することで、ユーザーと Bot とが対話を続けます。
  • text ・・・Bot から送信するテキストメッセージ

です。

Bot からテキストメッセージを送信するには、ユーザーからの activity に対して CreateReply することで、送信用の Activity を生成します。
string の引数で送信するテキストメッセージを指定することができます。次の例で手で来るように、Activity.Text プロパティに値を設定することでもテキストメッセージを指定できるので、その時のケースで使いやすいほうを使ってください。
あとは context の PostAsync メソッドに CreateReply した Activity を渡します。

private static async Task ReplyTextAsync(IDialogContext context, Activity activity, string text)
{
    var reply = activity.CreateReply(text);
    await context.PostAsync(reply);
}

テキスト

テキストメッセージは1行に限定されるものではありません。
“\n\n” で改行することができます。

private static async Task ReplyMultiLineText(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "Activity.Textプロパティにメッセージを入れても同じ" +
                 "\n\n複数行を返すこともできます\n\n" +
                 "URLを含むこともできます\n\nhttp://dev.botframework.com/";
    await context.PostAsync(reply);
}

複数行


■ 画像を送信

テキストだけではない内容をユーザーに送信したい場合、あとで紹介するようにカード(HeroCard, ThumbnailCard、今後は AdaptiveCard も)を使います。
ただしテキスト+画像1枚の場合は、Attachment クラスのインスタンスで直接画像の URL を指定することができます。

private static async Task ReplyImageAttachment(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "画像だけならば直接置くこともできます";
    reply.Attachments.Add(new Attachment
    {
        ContentUrl = "http://~/botneko_1.jpg",
        ContentType = "image/jpeg",
        Name = "正面"
    });
    await context.PostAsync(reply);
}

画像


■ カード

画像1枚と簡単な説明程度であれば、上のように Attachment クラスで直接画像の URL を指定する方法があります。
が、もっとリッチな情報を送信したいことも多いと思います。
そのような時はカードを使います。現時点では、HeroCard (大きいサイズの画像を貼る)と ThumbnailCard (小さいサイズの画像を貼る)の2種類があります。

どちらのクラスも、プロパティやカードの作り方は同じです。

HeroCard だとこんな風になります。
Activity のテキストの他に、HeroCard は Title および Subtitle の二つのテキストを持つことができます。画像との位置関係は下の図のようになります。

private static async Task ReplyImageInCard(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "画像はカードに置くこともできます";

    var heroCard = CreateHeroCard("正面", "カメラ目線",
        "http://~/botneko_1.jpg");
    var heroCardAttach = heroCard.ToAttachment();
    reply.Attachments.Add(heroCardAttach);

    await context.PostAsync(reply);
}

private static HeroCard CreateHeroCard(string title, string subtitle, string imagePath)
{
    var image = new CardImage
    {
        Url = imagePath,
        Alt = title
    };

    var card = new HeroCard
    {
        Title = title,
        Subtitle = string.IsNullOrEmpty(subtitle) ? null : subtitle,
        Images = new List<CardImage> { image }
    };

    return card;
}

HeroCard

ThumbnailCard だと、下のようになります。
こちらも、画像と Title, Subtitle との位置関係を見てみてください。(説明上はちょっとフライングですが、下の図の通り、カードにはボタンを置くこともできます)

private static async Task ReplyThumbnailCard(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "小さい画像のカードもあります";

    var thumbnailCard = CreateThumbnailCardWithButton("正面", "カメラ目線",
        "http://~/botneko_1.jpg");
    var thumbnailCardAttach = thumbnailCard.ToAttachment();

    reply.Attachments.Add(thumbnailCardAttach);

    await context.PostAsync(reply);
}

private static ThumbnailCard CreateThumbnailCardWithButton(string title, string subtitle, string imagePath)
{
    var image = new CardImage
    {
        Url = imagePath,
        Alt = title
    };
    var action = new CardAction
    {
        Type = "imBack",
        Title = title,
        Value = $"{title} ({subtitle})",
        Image = imagePath
    };

    var card = new ThumbnailCard
    {
        Title = title,
        Subtitle = string.IsNullOrEmpty(subtitle) ? null : subtitle,
        Images = new List<CardImage> { image },
        Buttons = new List<CardAction> { action },
        Tap = action
    };

    return card;
}

ThumbnailCard


■ ボタン

Activity にはボタンを配置することもできます。ボタンは1個だけではなく、複数個貼れます。

下の例では CardAction.Type として ImBack をしています。この場合は、ユーザーがボタンを慰した場合、Value の値を Bot に対して送信します。

private static async Task ReplyCardWithButtons(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "ボタンの利用";

    var buttonsCard = new HeroCard { Title = "カードにボタンを配置することもできます" };
    buttonsCard.Buttons.Add(new CardAction { Type = ActionTypes.ImBack, Title = "猫", Value = "猫" });
    buttonsCard.Buttons.Add(new CardAction { Type = ActionTypes.ImBack, Title = "犬", Value = "犬" });
    buttonsCard.Buttons.Add(new CardAction { Type = ActionTypes.ImBack, Title = "ウサギ", Value = "ウサギ" });
    var buttonsCardAttach = buttonsCard.ToAttachment();
    reply.Attachments.Add(buttonsCardAttach);

    await context.PostAsync(reply);
}

Button


■ 選択肢

Windows のメッセージボックスのように、ユーザーに Yes/No やいくつかの選択肢のいずれかを選択させたいこともあります。

二択の場合には PromptDialog.Confirm を呼び出します。それ以上の選択肢がある場合には PromptDialog.Choice を呼び出します。(Choice を二択で使うことも可能ですが)

PromptDialog.Confirm(context, ConfirmSelectAsync, "二択です");
private async Task ConfirmSelectAsync(IDialogContext context, IAwaitable<bool> result)
{
    var answer = await result;
    await context.PostAsync($"「{answer}」の選択ですね");

    context.Wait(MessageReceivedAsync);
}

二択

PromptDialog.Choice(context, ChoiceSelectAsync, new List<string> { "猫", "犬", "ウサギ" }, "三択です");
private async Task ChoiceSelectAsync(IDialogContext context, IAwaitable<object> result)
{
    var answer = await result;
    await context.PostAsync($"「{answer}」ですね");

    context.Wait(MessageReceivedAsync);
}

三択


■ リスト

Activity にはカードを複数添付することができます。
カード自体は HeroCard でも ThumbnailCard でも(それ以外、例えば AdaptiveCard でも)かまいません。

private static async Task ReplyHeroCardsInList(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "Attachmentを複数持つことができます";

    var heroCards = CreateHeroCardArray();
    foreach (var card in heroCards)
    {
        reply.Attachments.Add(card.ToAttachment());
    }

    await context.PostAsync(reply);
}

public static HeroCard[] CreateHeroCardArray()
{
    var cards = new HeroCard[3];
    cards[0] = CreateHeroCardWithButton("正面", "カメラ目線",
        "http://~/botneko_1.jpg");
    cards[1] = CreateHeroCardWithButton("袋", "狭い場所が好き",
        "http://~/botneko_2.jpg");
    cards[2] = CreateHeroCardWithButton("後ろ", "かすかにウサギの模様",
        "http://~/botneko_3.jpg");

    return cards;
}

リスト

コードは省略しますが、ThumbnailCard のリストはこんな実行結果になります。

ThumbnailCard リスト

添付可能な数は Activity.Attachments が IList<Attachment> であることから制限はありません。ただし縦方向のスクロールが発生してしまうので、Chat アプリケーションとしてはユーザビリティの注意が必要です。カードの個数や使いどころは十分に注意してください。


■ 横方向のリスト

複数の Attachment は横方向に並べることもできます。縦方向が “List” であるのに対して、縦方向は “Carousel” です。

縦方向のリストとの違いは、Activitiy.AttachmentLayout  プロパティに AttachmentLayoutTypes.Carousel を設定する点だけです。

private static async Task ReplyHeroCardsInCarousel(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "リストは横に並べることができます";

    var heroCarouselCards = CreateHeroCardArray();
    foreach (var card in heroCarouselCards)
    {
        reply.Attachments.Add(card.ToAttachment());
    }
    reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;

    await context.PostAsync(reply);
}

public static HeroCard[] CreateHeroCardArray()
{
    var cards = new HeroCard[3];
    cards[0] = CreateHeroCardWithButton("正面", "カメラ目線",
        "http://~/botneko_1.jpg");
    cards[1] = CreateHeroCardWithButton("袋", "狭い場所が好き",
        "http://~/botneko_2.jpg");
    cards[2] = CreateHeroCardWithButton("後ろ", "かすかにウサギの模様",
        "http://~/botneko_3.jpg");

    return cards;
}

横方向リスト

ThumbnailCard 横リスト


 

長い記事になりましたが、以上が Activity の使い方のバリエーションです。

Build 2017 で AdaptiveCard が紹介され、これを使えばさらに表現力のあるカードを作れます。その場合でも、Activity の使い方は変わりません。

まずは本稿で Bot のユーザビリティを理解してください。

さらに長くなりますが、最後に RootDialog.cs の全ソースを載せます。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web.ModelBinding;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace BotActivityDemo.Dialogs
{
    [Serializable]
    public class RootDialog : IDialog&lt;object&gt;
    {
        public Task StartAsync(IDialogContext context)
        {
            context.Wait(MessageReceivedAsync);

            return Task.CompletedTask;
        }

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            var activity = await result as Activity;
            if (activity != null)
            {
                var text = activity.Text;

                switch (text)
                {
                    case "テキスト":
                        await ReplyTextAsync(context, activity, "テキストだけならば、Activityだけで対話できます");
                        return;
                    case "複数行":
                        await ReplyMultiLineText(context, activity);
                        break;
                    case "画像":
                        await ReplyImageAttachment(context, activity);
                        break;
                    case "カード":
                        await ReplyImageInCard(context, activity);
                        break;
                    case "ボタン付きカード":
                        await ReplyCardWithSingleButton(context, activity);
                        break;
                    case "ボタン":
                        await ReplyCardWithButtons(context, activity);
                        break;
                    case "サムネイル":
                        await ReplyThumbnailCard(context, activity);
                        break;
                    case "二択":
                        PromptDialog.Confirm(context, ConfirmSelectAsync, "二択です");
                        return;
                    case "三択":
                        PromptDialog.Choice(context, ChoiceSelectAsync, new List&lt;string&gt; { "猫", "犬", "ウサギ" }, "三択です");
                        return;
                    case "リスト":
                        await ReplyHeroCardsInList(context, activity);
                        break;
                    case "サムネイルリスト":
                        await ReplyThumbnailCardsInList(context, activity);
                        break;
                    case "横リスト":
                        await ReplyHeroCardsInCarousel(context, activity);
                        break;
                    case "サムネイル横リスト":
                        await ReplyThumbnailCardsInCarousel(context, activity);
                        break;
                    default:
                        await ReplyTextAsync(context, activity, $"「{text}」と言いましたね");
                        break;
                }
            }

            context.Wait(MessageReceivedAsync);
        }

        private static async Task ReplyTextAsync(IDialogContext context, Activity activity, string text)
        {
            var reply = activity.CreateReply(text);
            await context.PostAsync(reply);
        }

        private static async Task ReplyMultiLineText(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "Activity.Textプロパティにメッセージを入れても同じ" +
                         "\n\n複数行を返すこともできます\n\n" +
                         "URLを含むこともできます\n\nhttp://dev.botframework.com/";
            await context.PostAsync(reply);
        }

        private static async Task ReplyImageAttachment(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "画像だけならば直接置くこともできます";
            reply.Attachments.Add(new Attachment
            {
                ContentUrl = "botneko_1.jpg",
                ContentType = "image/jpeg",
                Name = "正面"
            });
            await context.PostAsync(reply);
        }

        private static async Task ReplyImageInCard(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "画像はカードに置くこともできます";

            var heroCard = CreateHeroCard("正面", "カメラ目線",
                "botneko_1.jpg");
            var heroCardAttach = heroCard.ToAttachment();
            reply.Attachments.Add(heroCardAttach);

            await context.PostAsync(reply);
        }

        private static async Task ReplyCardWithSingleButton(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "カードにボタンを入れることもできます";

            var heroButtonCard = CreateHeroCardWithButton("正面", "カメラ目線",
                "botneko_1.jpg");
            var heroButtonCardAttach = heroButtonCard.ToAttachment();

            reply.Attachments.Add(heroButtonCardAttach);

            await context.PostAsync(reply);
        }

        private static async Task ReplyCardWithButtons(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "ボタンの利用";

            var buttonsCard = new HeroCard { Title = "カードにボタンを配置することもできます" };
            buttonsCard.Buttons.Add(new CardAction { Type = ActionTypes.ImBack, Title = "猫", Value = "猫" });
            buttonsCard.Buttons.Add(new CardAction { Type = ActionTypes.ImBack, Title = "犬", Value = "犬" });
            buttonsCard.Buttons.Add(new CardAction { Type = ActionTypes.ImBack, Title = "ウサギ", Value = "ウサギ" });

            var buttonsCardAttach = buttonsCard.ToAttachment();
            reply.Attachments.Add(buttonsCardAttach);

            await context.PostAsync(reply);
        }

        private static async Task ReplyThumbnailCard(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "小さい画像のカードもあります";

            var thumbnailCard = CreateThumbnailCardWithButton("正面", "カメラ目線",
                "botneko_1.jpg");
            var thumbnailCardAttach = thumbnailCard.ToAttachment();

            reply.Attachments.Add(thumbnailCardAttach);

            await context.PostAsync(reply);
        }

        private async Task ConfirmSelectAsync(IDialogContext context, IAwaitable<bool> result)
        {
            var answer = await result;
            await context.PostAsync($"「{answer}」の選択ですね");

            context.Wait(MessageReceivedAsync);
        }

        private async Task ChoiceSelectAsync(IDialogContext context, IAwaitable<object> result)
        {
            var answer = await result;
            await context.PostAsync($"「{answer}」ですね");

            context.Wait(MessageReceivedAsync);
        }

        private static async Task ReplyHeroCardsInList(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "Attachmentを複数持つことができます";

            var heroCards = CreateHeroCardArray();
            foreach (var card in heroCards)
            {
                reply.Attachments.Add(card.ToAttachment());
            }

            await context.PostAsync(reply);
        }

        private static async Task ReplyThumbnailCardsInList(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "ThumbnailCardももちろんリストにできます";

            var thumbnailCards = CreateThumbnailCardArray();
            foreach (var card in thumbnailCards)
            {
                reply.Attachments.Add(card.ToAttachment());
            }

            await context.PostAsync(reply);
        }

        private static async Task ReplyHeroCardsInCarousel(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "リストは横に並べることができます";

            var heroCarouselCards = CreateHeroCardArray();
            foreach (var card in heroCarouselCards)
            {
                reply.Attachments.Add(card.ToAttachment());
            }
            reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;

            await context.PostAsync(reply);
        }

        private static async Task ReplyThumbnailCardsInCarousel(IDialogContext context, Activity activity)
        {
            var reply = activity.CreateReply();
            reply.Text = "ThumbnailCardも横に並べられます";

            var thumbnailCarouselCards = CreateThumbnailCardArray();
            foreach (var card in thumbnailCarouselCards)
            {
                reply.Attachments.Add(card.ToAttachment());
            }
            reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;

            await context.PostAsync(reply);
        }

        private static HeroCard CreateHeroCard(string title, string subtitle, string imagePath)
        {
            var image = new CardImage
            {
                Url = imagePath,
                Alt = title
            };

            var card = new HeroCard
            {
                Title = title,
                Subtitle = string.IsNullOrEmpty(subtitle) ? null : subtitle,
                Images = new List<CardImage> { image }
            };

            return card;
        }

        private static HeroCard CreateHeroCardWithButton(string title, string subtitle, string imagePath)
        {
            var image = new CardImage
            {
                Url = imagePath,
                Alt = title
            };
            var action = new CardAction
            {
                Type = "imBack",
                Title = title,
                Value = $"{title} ({subtitle})",
                Image = imagePath
            };

            var card = new HeroCard
            {
                Title = title,
                Subtitle = string.IsNullOrEmpty(subtitle) ? null : subtitle,
                Images = new List<CardImage> { image },
                Buttons = new List<CardAction> { action },
                Tap = action
            };

            return card;
        }

        private static ThumbnailCard CreateThumbnailCardWithButton(string title, string subtitle, string imagePath)
        {
            var image = new CardImage
            {
                Url = imagePath,
                Alt = title
            };
            var action = new CardAction
            {
                Type = "imBack",
                Title = title,
                Value = $"{title} ({subtitle})",
                Image = imagePath
            };

            var card = new ThumbnailCard
            {
                Title = title,
                Subtitle = string.IsNullOrEmpty(subtitle) ? null : subtitle,
                Images = new List<CardImage> { image },
                Buttons = new List<CardAction> { action },
                Tap = action
            };

            return card;
        }

        public static HeroCard[] CreateHeroCardArray()
        {
            var cards = new HeroCard[3];
            cards[0] = CreateHeroCardWithButton("正面", "カメラ目線",
                "botneko_1.jpg");
            cards[1] = CreateHeroCardWithButton("袋", "狭い場所が好き",
                "botneko_2.jpg");
            cards[2] = CreateHeroCardWithButton("後ろ", "かすかにウサギの模様",
                "botneko_3.jpg");

            return cards;
        }

        public static ThumbnailCard[] CreateThumbnailCardArray()
        {
            var cards = new ThumbnailCard[3];
            cards[0] = CreateThumbnailCardWithButton("正面", "カメラ目線",
                "botneko_1.jpg");
            cards[1] = CreateThumbnailCardWithButton("袋", "狭い場所が好き",
                "botneko_2.jpg");
            cards[2] = CreateThumbnailCardWithButton("後ろ", "かすかにウサギの模様",
                "botneko_3.jpg");

            return cards;
        }
    }
}

 

広告
カテゴリー: Bot Framework, Bot Service, Cogbot, 未分類 タグ: , , パーマリンク

Bot Framework の Activity を(改めて)整理 への4件のフィードバック

  1. ピンバック: Bot Framework で AdaptiveCard を使う (C# の場合) | 技術との戯れ

  2. ピンバック: Bot Framework の AdaptiveCard で要素を横方向に並べる (C# の場合) | 技術との戯れ

  3. ピンバック: Bot Framework の AdaptiveCard を Visualizer でデザインする | 技術との戯れ

  4. ピンバック: 「Adaptive Card のデザインツール」 LTしました (第7回 Cogbot 勉強会 / 2017年7月28日) | 技術との戯れ

コメントを残す

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

WordPress.com ロゴ

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

Twitter 画像

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

Facebook の写真

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

Google+ フォト

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

%s と連携中