Bot Framework の AdaptiveCard を Visualizer でデザインする

Bot Framework の AdaptiveCard 関連の第3回です。
(1回目は こちら、2回目は こちら。Bot Framework の Activity のまとめは こちら

今回は、AdaptiveCard のポータルサイトにある Visualizer で開発してみます。
実際の見た目を確認しながら開発できるので C# よりも簡単にデザインできます。(だったら、最初からこれを紹介すればいいのにという声は今回はスルー)

Visualizer


AdaptiveCard に配置できる要素は、Adaptive Card ポータルSchema Explorer で確認できます。
その上で Visualizer で開発しますが、イメージを膨らませるにはまずサンプルを見てみるのがいいと思います。

今回は Visualizer の操作やデザイン自体には踏み込まず、サンプルの Weather Large をそのまま C# の Bot アプリケーションに組み込むことにします。

  1. Visualizer でカードをデザインし(今回はサンプルそのままですが)、JSON のコードを丸ごとクリップボードにコピーします。
    Visualizer Copy
  2. Bot アプリケーションのプロジェクトで、App_Data フォルダーを作り、ここに空の JSON ファイルを一つ作ります。
    (※App_Data フォルダーでなくてもいいですが)
    Create JSON File
  3. 今作った JSON ファイルに、Visualizer でコピーしたソースを貼り付けます。
    adaptivecard_json_source
  4. RootDialog.cs に以下のようなメソッドを追加します。(もちろん、MessageReceivedAsync メソッドに、以下のメソッドを呼び出すコードの追加も忘れずに)
    private static async Task ReplyJsonAdaptiveCard(IDialogContext context, Activity activity)
    {
        var directory = HttpContext.Current.Server.MapPath("~/app_data/");
        var jsonpath = Path.Combine(directory, "weather.json");
    
        string json;
        using (var sr = new StreamReader(jsonpath))
        {
            json = await sr.ReadToEndAsync();
        }
    
        var card = JsonConvert.DeserializeObject<AdaptiveCard>(json);
    
        var reply = activity.CreateReply();
        reply.Text = "CardのレイアウトはJsonで定義できます";
    
        var attachment = new Attachment
        {
            Content = card,
            ContentType = "application/vnd.microsoft.card.adaptive",
            Name = "Jsonデモ"
        };
    
        reply.Attachments.Add(attachment);
    
        await context.PostAsync(reply);
    }
    

手順は以上です。

実行するとこんな見た目になります。

AdaptiveCard Json

現時点ではまだ Preview です。またChannel ごとの制約(Coming soon だったり、レンダリングに特徴があったり)もありますが、工夫次第でユーザーに分かりやすい応答を行う Bot が作れそうです。

カテゴリー: Bot Framework, Bot Service, Cogbot, 未分類 | タグ: , , | コメントをどうぞ

Bot Framework の AdaptiveCard で要素を横方向に並べる (C# の場合)

Bot Framework でリッチなレイアウトを実現できるのが AdaptiveCard です。

要素を縦に並べるのであれば、AdaptiveCard.Body に順に Add していきます。

では、要素を横に並べたい場合はどうするかというと、

  1. Body に ColumnSet を Add
  2. その ColumnSet に Column を Add
  3. さらにその Column の中に要素を Add

していきます。文章だと分かりづらいですね・・・。


具体的にコードを見ていきます。

private static async Task ReplyColumnSetAdaptiveCard(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "要素を横に並べるにはColumnSetを使います";

    var card = CreateColumnSetAdaptiveCard("正面", "カメラ目線",
        "http://~/botneko_1.jpg");

    var attachment = new Attachment
    {
        Content = card,
        ContentType = "application/vnd.microsoft.card.adaptive",
        Name = "ColumnSet デモ"
    };

    reply.Attachments.Add(attachment);

    await context.PostAsync(reply);
}

private static AdaptiveCard CreateColumnSetAdaptiveCard(string title, string subtitle, string imagePath)
{
    var card = new AdaptiveCard();

    var columnSet = new ColumnSet();
    card.Body.Add(columnSet);

    var leftColumn = new Column();
    leftColumn.Items.Add(new Image
    {
        Url = imagePath,
        AltText = title,
        Size = ImageSize.Medium,
    });
    leftColumn.Size = "auto";
    columnSet.Columns.Add(leftColumn);

    var rightColumn = new Column();
    rightColumn.Items.Add(new TextBlock
    {
        Text = title,
        Size = TextSize.ExtraLarge,
        Color = TextColor.Accent
    });
    rightColumn.Items.Add(new TextBlock
    {
        Text = subtitle
    });
    columnSet.Columns.Add(rightColumn);

    card.Body.Add(new TextBlock
    {
        Text = subtitle,
        Color = TextColor.Warning
    });

    card.Actions.Add(new SubmitAction
    {
        Title = title,
        Data = title
    });

    return card;
}

Activity の作り方、カードの Attach の仕方などは、他の種類のカードColumnSet を使用しない AdaptiveCard と全く同じです。

ColumnSet を AdaptiveCard.Body に Add するのも、他の要素と同じです。
ColumnSet に Column を複数個置けますが、置きすぎには注意です。Bot はフルスクリーンで動かすようなアプリではないので、カードのサイズ(幅も高さも)には気を付ける必要があります。

AdaptiveCard_ColumnSet_act

Column の使い方で一つ覚えておくといいのは、Size プロパティですね。”auto” を指定すると自動的にサイズを調整してくれます。
上のコードでいうと、画像のサイズに合わせて Column も最小のサイズにしてくれます。この指定がないと、例えば 2個の Column ならば、それぞれ Card の半分の幅になるので、他の要素の配置にもよりますが「間延び」して見えることもありそうです。


AdaptiveCard の Visualizer を使うと、リアルタイムで視覚的にレイアウトを確認しながら AdaptiveCard を開発できます。それについてはまた改めて。

今回のカードも簡単な構造ですが、サンプルを参考に凝った(=ユーザーに伝わりやすい)、でも、やりすぎないレイアウトを考えてみてください。

カテゴリー: Bot Framework, Bot Service, Cogbot, 未分類 | タグ: , , | 1件のコメント

Bot Framework で AdaptiveCard を使う (C# の場合)

Bot Framework で大事なのが Activity の理解であることだということを、前回の記事 で紹介しました。

Bot からのメッセージとして、画像やボタンを含むメッセージを送信したいこともあり、
その場合は、大きめのサイズの画像を含む HeroCard、または小さめのサイズの画像を含む ThumbnailCard を使います。

HeroCard と ThumbnailCard は画像のサイズに違いはありますが、

  • タイトル
  • サブタイトル
  • 画像
  • ボタン

を置くことができ、これらの要素はそれぞれのカードで決まった位置に配置されます。
これは別の見方をすると、内容にもレイアウトにも制約があるということです。含める画像は一つです。タイトル・サブタイトル以外の追加の情報を含めることもできません。(Activity 自体の Text プロパティを組み合わせれば、もう一つのテキストを使えることになりますが)


この制約を超えられるのが AdaptiveCard です。Build 2017 で発表されました。

名前からわかる通り、AdaptiveCard もカードの一種です。Acitivity への添付の仕方は他のカードと同じです。

カードに含む要素をどうやって指定するかが、AdaptiveCard のポイントです。
AdaptiveCard に含められる要素(Schema)は、Schema Explorer で確認できます。これらを組み合わせて、よりリッチなカードを作ることができます。

以下では、C# で AdaptiveCard を作り、Bot からのメッセージとして送信してみます。
簡単な例ですが、AdaptiveCard の使い方がわかると思います。


C# のプロジェクトで AdaptiveCard を使うには、AdaptiveCard の NuGet パッケージをインストールします。”Microsoft.AdaptiveCards” がそれです。

nuget_adaptivecard

AdaptiveCard は、まずインスタンスを作り、続いて Body プロパティに必要な要素をd Add していきます。
すべての要素は縦方向に順に並びます。(横に並べる方法は次回のお楽しみ、ColumnSet を使います)

private static AdaptiveCard CreateAdaptiveCard(string title, string subtitle, string imagePath)
{
    var card = new AdaptiveCard();
    card.Body.Add(new TextBlock
    {
        Text = title,
        Size = TextSize.ExtraLarge,
        Color = TextColor.Accent
    });
    card.Body.Add(new TextBlock
    {
        Text = subtitle
    });
    card.Body.Add(new Image
    {
        Url = imagePath,
        AltText = title,
        Size = ImageSize.Large,
    });
    card.Body.Add(new TextBlock
    {
        Text = subtitle,
        Color = TextColor.Warning
    });

    card.Actions.Add(new SubmitAction
    {
        Title = title,
        Data = title
    });

    return card;
}

 

AdaptiveCard ができたら、他の種類のカードとまったく同じように Activity.Attachments に Add します。

private static async Task ReplyAdaptiveCard(IDialogContext context, Activity activity)
{
    var reply = activity.CreateReply();
    reply.Text = "AdaptiveCard は Build 2017 で発表されました";

    var card = CreateAdaptiveCard("正面", "カメラ目線",
        "http://~/botneko_1.jpg");

    var attachment = new Attachment
    {
        Content = card,
        ContentType = "application/vnd.microsoft.card.adaptive",
        Name = "Adaptive Card デモ"
    };

    reply.Attachments.Add(attachment);

    await context.PostAsync(reply);
}

 

これで Bot からリッチな AdaptiveCard を送信できます。(今回はかなりシンプルですが)

AdaptiveCard

現時点での注意としては、AdaptiveCard はまたまだ開発途中の技術であること。残念な点としては、Skype がまだ AdaptiveCard に対応していないことです。

それでも、そう遠くない時期に Skype も AdaptiveCard に対応するはずですし、Bot からリッチで綺麗な情報をユーザーに返せるようになるわけなので、まずは触ってみることをお勧めします。
Bot の活用がもっと進むといいですね。

カテゴリー: Bot Framework, Bot Service, Cogbot, 未分類 | タグ: , , | 2件のコメント

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, 未分類 | タグ: , , | 3件のコメント

QnA Maker + Bot Builder SDK for C# で複数回答を返す Q&A Bot を作る

2回シリーズで、QnA Maker + Bot Builder SDK for C# で Q&A Bot を作成する手順を紹介しました。

今回はその続編として、複数回答を返す Bot を作成してみます。

Q&A に限らず、Bot からの応答は絶対一つに限られるわけではなく、スコア付きで複数の回答の候補が見つかることが多いはずです。

前回説明したシンプルな Q&A Bot では、一番スコアの高いものを自動的に返答していました。これを実現しているのが QnAMakerDialog のデフォルトの実装です。

それに対して今回は、

  • 該当しそうな回答が見つからない場合の「ごめんなさい」メッセージ
  • 2番目以降のスコアの回答
  • スコアは低いけど、こんな回答が関連しているかもというもの

を返すようにしたいと思います。


■ QnA Maker と QnAMakerDialog の復習

QnA Maker でナレッジベースを作る方法、Bot Builder SDK の QnAMakerDialog の基本的な使い方は、前の投稿を参考にしてください。

参考までに、今回のナレッジベースはこんな内容です。

2017-06-28 16-54-37


■ QnAMakerDialog の QnAMaker 属性

QnAMakerDialog を継承するクラスでは、QnAMaker 属性を付与します。

この属性は 5個の引数を指定できます。

QnAMaker 属性の最も大事な点は、QnA Service の KnowlegdebaseId (属性の 1個目の引数) と SubscriptionKey (属性の 2個目の引数) を渡すことですが、他の3個の引数は以下のような意味を持ちます。

  • 第3引数・・・回答が見つからない場合のメッセージ
  • 第4引数・・・回答の候補とするスコアの最低値、デフォルトは 0.3
  • 第5引数・・・回答の候補として何個取得するか、デフォルトは 1

例えば、以下の場合には、スコアが 0.3未満のものは回答の候補に含めません。また回答の個数は 3個までという意味です。

2017-06-28 17-26-54


■ 回答が見つからない場合

回答が見つからない場合は、QnAMaker 属性の第3引数の文字列を返します。

例えば、今回のナレッジベースではライオンについての Q&A は持っていないため、以下の回答を返します。

2017-06-28 17-14-28

■ 複数の回答を返したい場合

複数の回答を返したい場合は、QnAMakerDialog の RespondFromQnAMakerResultAsync メッセージをオーバーライドします。

2017-06-28 17-39-08

QnAMaker 属性の第4引数のスコア以上のものが、第5引数の個数までナレッジベースから取得されます。

取得した回答は resultAnswers プロパティに入っているので、各アイテムの Answer プロパティで回答文が、Score プロパティでその回答のスコアが得られます。

2017-06-28 17-13-01

■ スコアが低い回答しか得られない場合

ナレッジベースから一応回答は得られたもののスコアが低い場合には、QnAFeedbackStepAsync メソッドが呼ばれます。このメソッドをオーバーライドすることで明示的に回答を返すことができます。

念のため、IsConfidentAnswer メソッドでスコアが低いことを確認した上で(文字通り「自信のある回答かどうか」)、Answers プロパティの値を整形します。Answers の要素は QnAMakerResult であり、スコアが低いことを除いては RespondFromQnAMakerResultAsync メソッドの時と同じように処理できます。

2017-06-28 17-48-42

2017-06-28 17-13-532017-06-28 17-14-59


複数個の回答候補やスコアの低いものをどのようにユーザーに見せるかはアプリ次第ですが、今回のコードでよりユーザーにやさしい回答を返すことができます。

非常に簡単なコードですが、Q&A Bot としては一通りの応答をしてくれるようになりました。

あとは運用しながら、ナレッジベースをどうやって育てていくかがポイントになりそうです。

念のため、以下に QnAMakerDialog を継承したクラス(今回は AnimalQnADialog)のコードを載せておきます。

using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.CognitiveServices.QnAMaker;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;

namespace AnimalQnaBot.Dialogs
{
    [Serializable]
    [QnAMaker("63174189522d4406910a50b3fd501515", "4d723a69-e337-420f-8e30-4d3f97ac35b0",
        "別の言い方で試してください", 0.3, 3)]
    public class AnimalQnaDialog : QnAMakerDialog
    {
        protected override async Task RespondFromQnAMakerResultAsync(IDialogContext context, IMessageActivity message, QnAMakerResults result)
        {
            var bestAnswer = result.Answers.First();
            await context.PostAsync($"{bestAnswer.Answer} ({bestAnswer.Score:0.00})");

            if (result.Answers.Count > 1)
            {
                var sb = new StringBuilder();
                sb.Append("以下が答えかもしれません");

                foreach (var answer in result.Answers.Skip(1))
                    sb.Append($"\n\n{answer.Answer} ({answer.Score:0.00})");

                await context.PostAsync(sb.ToString());
            }
        }

        protected override async Task QnAFeedbackStepAsync(IDialogContext context, QnAMakerResults result)
        {
            if (!IsConfidentAnswer(result))
            {
                var sb = new StringBuilder();
                sb.Append("自信がありませんが、以下が関連しているかもしれません");

                foreach (var answer in result.Answers)
                    sb.Append($"\n\n{answer.Answer} ({answer.Score:0.00})");

                await context.PostAsync(sb.ToString());
            }
        }
    }
}
カテゴリー: Bot Framework, Cogbot, Cognitive Services | タグ: , , | コメントをどうぞ

QnA Maker + Bot Builder SDK for C# で簡単に Q&A Bot を作る (2) ~ C# で Bot Aplication 編

QnA Maker と Bot Builder SDK for C# でQ&A Bot アプリケーションを作成する手順を紹介します。

2 回に分けた 2回目の今回は Bot Builder SDK for C# で Bot アプリケーション開発 の手順を紹介します。

※1回目の QnA Maker 操作手順は こちら

QnA Maker と Bot Builder SDK、それに Cognitive Services の NuGet パッケージを使うと、非常に簡単に、コードらしいコードを書かずに Q&A Bot が作れます。

より柔軟な応答をするような Q&A Bot を作るにはコードも必要ですが、まずはアプリとしてきちんと動作するものを簡単に作ってみます。


■ Bot アプリケーション開発の基本(復習)

Bot アプリケーション開発の基本、プロジェクトテンプレートから新規アプリを作り、Emulator で実行するまでについては、こちら を参照。

念のため、本当に概要のみ紹介すると、

  1. Bot Application のプロジェクトテンプレートをダウンロード、VS のテンプレートフォルダーにコピー
  2. 新規プロジェクトを作成
  3. F5 で実行
  4. Bot Framework Emulator で動作確認

今回の Q&A Bot はここをスタートとします。

■ Q&A Bot アプリケーションの開発手順

上記で準備した Bot アプリを Q&A Bot アプリにするには以下の手順を踏みます。

  1. Cognitive Services の NuGet パッケージをインストールします。2017-06-21 22-33-59
  2. QnAMakerDialog を継承するクラスを追加します。今回は特にメソッドの追加は必要なく、SubscriptionKey と KnowledgebaseId を指定するためにクラスを作成します。(ここでは AnimalQnaDialog.cs とします)作成したクラス QnAMaker に属性として “QnAMaker” を追加して SubscriptionKey と KnowledgebaseId を指定します。もう一つのオプションは、回答を取得できなかった場合の応答です。
    QnAMaker 属性はあと二つパラメーターの指定が可能ですが、今回はここまでにしておきます。
    2017-06-22-0-00-56

  3. MessageControler.cs で、ユーザーの入力を処理する IDialog として、先ほど作成した AnimalQnaDialog を指定します。
    2017-06-26 13-52-12

以上です。簡単です。

Bot Framework Emulator を実行すると、質問に対して応答してくれます。

2017-06-22 0-08-58

少し触ってみて適切な応答を返してくれない場合は、QnA Maker に戻ってナレッジの追加・編集と再学習をしてください。


今回は Bot アプリケーションらしい開発をしていませんが、基本的な手順と Bot がどのように動作するかの感触は掴んでもらえると思います。

また、Q&A Bot のイメージもつかめるかと思います。

要求としてこれで足りる時は(ナレッジの登録や再学習は適宜必要ですが)、QnA Maker を利用、これだと足りないなという時には LUIS を使ってもっと高度な Bot アプリを作るのがよいと思います。

その切り分けをするためにも、まずは QnA Maker を試してみるといいですね。

カテゴリー: Bot Framework, Cogbot, Cognitive Services | タグ: , , | 2件のコメント

QnA Maker + Bot Builder SDK for C# で簡単に Q&A Bot を作る (1) ~ QnA Maker 編

QnA Maker と Bot Builder SDK for C# でQ&A Bot アプリケーションを作成する手順を紹介します。

2 回に分けた 1 回目の今回は QnA Maker の操作手順を紹介します。
※2回目の Bot Builder SDK for C# で Bot 開発は こちら

QnA Maker 自体は他のサイトでも紹介されていますが、既存の Q&A ページからインポートする手順の紹介が多いようなので、今回は TSV ファイルからインポートしてみます。


■ QnA Maker とは

QnA Maker は、文字通り Q&A のナレッジベースを開発・公開するサービスです。

ユーザーの質問に対して適切な回答(適切だと思われる回答)を返してくれます。

Cognitive Service のサービスの一つである LUIS は、あらかじめ Intent  や Entity を定義して、ユーザーの意図をどのように解析するべきか学習させます。(その分、手間がかかることもある)
それに対して QnA Maker はもっとシンプルで、質問と回答とのセットを登録して、QnA Maker に対して学習を指示するだけです。精度が出づらいこともありますが、ナレッジを早く開発できます。

■ QnA Maker の操作手順

QnA Maker (https://qnamaker.ai/) で Q&A のナレッジベースを作る手順は以下の通り。

  1. [Create new Service] で新しいサービスを作成します。2017-06-21 0-02-32
  2. (多くの  QnA Maker 紹介ページでは、ここで既存のFAQ ページを指定するが)[FAQ File] を指定する。ナレッジを管理するには TSV (タブ区切り CSV、なお文字コードは UTF-8 にしないと内容を読み取ってくれないので注意)
    2017-06-21 0-03-14

    参考までに、今回は動物について答えてくれるこんな TSV ファイルを作ってみました。(今回のサンプルではこの程度で)
    2017-06-22 23-57-20
    TSV ファイルの Q&A を取り込むことができたら、ページ下部の [Create] をクリックします。今回のサンプルであれば 10秒もあればナレッジが作成されます。
  3. Test 画面でナレッジをテストします。この画面で質問を追加したり、質問に紐づける回答を変更したりすることができます。
    2017-06-21 0-12-35Q&A を追加・変更した場合は、必ず [Save and Retrain] をクリックしてください。(Save せずに別の画面に遷移すると、今回の追加・変更は失われます)[Knowledge Base] 画面で Q&A の追加・変更も可能です。
    2017-06-25 17-16-15
    学習できたら [Publish] でサービスを展開します。
    2017-06-21 0-16-502017-06-21 0-17-47

以上で Q&A のナレッジは完成。

KnowledgebaseId, SubscriptionKey は、Bot アプリで使います。これらの値はパスワードではないので、いつでもこの画面で確認できます。メモ帳にコピペしておくなどは不要です。


Bot Framework を理解するには Activity が必要ですが、意外とActivity 周りの記事がなかなか見つかりません。
Bot Framework を知っている人には当たり前すぎるんですかね?

今回は、その Activity に触れる前に、まず Bot アプリケーションを簡単に開発できる Q&A Bot(の前段として QnA Maker)を紹介しました。
今回と次回の記事とで Bot Framework の感触をつかんでから、もっと深いところに潜り込んでみてください。

カテゴリー: Bot Framework, Cogbot, Cognitive Services | タグ: , , | 2件のコメント