Chatbotから話しかける(プロアクティブメッセージの送信)

2018年5月18日(金)
樋口 勝一
連載10回目となる今回は、Chatbotから先に話しかけるための手順を紹介します。

Microsoft Bot Frameworkで作るChatbot」も連載10回目となり、いよいよ最終回となります。これまで育ててきたChatbotは対話の流れの途中で分岐を可能にし、AIによるテキスト解析、画像によるコミュニケーションなど、実用に十分役立つ機能を数多く実装することができました。

最後に紹介するのは、Chatbotからユーザーに話しかけるための「プロアクティブメッセージ(Proactive Messages)」の送信方法です。これまでのChatbotは、ユーザー側から何か文字を送信しないかぎり画面上では何もアクションを取ることはありませんでした。ですが、企業の受付業務やカフェのオーダーを受けるような、サービスの最前線で活躍するChatbotには、積極的にユーザーに話しかけアプローチのチャンスをできるだけ多く持たせることが望ましいと考えます。さらに、Chatbotを仕事や生活の中に取り入れて利用する場合、スケージュールの通知や、システムのアラートなど、Chatbot側からメッセージを送信する機会は数多くあると考えられます。

最初にChatbotから話しかける

これまでは「こんにちは」などユーザー側から話しかけないとChatbotとの対話は始まりませんでした。

ユーザーからの話しかけで対話が始まる

ユーザーからの話しかけで対話が始まる

もちろんこれはこれで問題はないのですが、はじめてメッセージングの画面を開いたときにChatbotから話しかけることができれば、一層活躍の場が広がるでしょう。例えば受付業務でChatbotを利用する場合、最初に「いらっしゃいませ」とメッセージを送信できることは、とても重要な要素となります。ここで紹介するのは、ユーザーが初めてChatbotのメッセージング画面を開いた時にメッセージを送信する方法となります。Facebookなど継続的にメッセージング画面が開き続けるような場合は、最初の一度だけ当てはまります。WebChatやSkypeなどのように、メッセージング画面を閉じることで対話がリセットされる場合は、毎回Chatbotからメッセージが送信されることになります。

Bot Frameworkでは、対話の中で起こる様々なイベントを「ActivityTypes」として判別しています。対話の開始時にChatbotからメッセージを送信するタイミングは、ActivityTypesがConversationUpdateとなったときに該当します。このタイミングでChatbotからメッセージを送信するようにプログラムしていきましょう。

今回の記事で使用するファイル一式は、以下の2つのリンクからダウンロード可能です。

Chatbot本体
プロアクティブメッセージ送信用のアプリケーション

Chatbotが起動して最初に処理するのはMessagesController.csです。

リスト1:MessagesController.cs

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Autofac;
using System.Linq;

namespace Chatbot201707
{
    [BotAuthentication]
    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
            {
                await HandleSystemMessageAsync(activity);
            }

            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;
        }

        private async Task<Activity> HandleSystemMessageAsync(Activity message)
        {
            if (message.Type == ActivityTypes.DeleteUserData)
            {
                // Implement user deletion here
                // If we handle user deletion, return a real message
            }
            else if (message.Type == ActivityTypes.ConversationUpdate)
            {
                IConversationUpdateActivity update = message;
                using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
                {
                    var client = scope.Resolve<IConnectorClient>();
                    if (update.MembersAdded.Any())
                    {
                        var reply = message.CreateReply();
                        foreach (var newMember in update.MembersAdded)
                        {
                            if (newMember.Id == message.Recipient.Id)
                            {
                                reply.Text = $@"いらっしゃいませ!ご注文を伺います。

(メニューをご覧になる場合は「メニュー」と入力してください。)";
                                await client.Conversations.ReplyToActivityAsync(reply);

                            }

                        }
                    }
                }
            }
            else if (message.Type == ActivityTypes.ContactRelationUpdate)
            {
                // Handle add/remove from contact lists
                // Activity.From + Activity.Action represent what happened
            }
            else if (message.Type == ActivityTypes.Typing)
            {
                // Handle knowing tha the user is typing
            }
            else if (message.Type == ActivityTypes.Ping)
            {
            }

            return null;
        }
    }
}

以下、着目すべき点を説明していきます。

else if (message.Type == ActivityTypes.ConversationUpdate)

ActivityTypesがConversationUpdateのときにメッセージを送信します。

IConversationUpdateActivity update = message~

最初にChatbotからメッセージを送信する場合はほぼ同じパターンとなるので、ここでは詳細の解説は省きますが、おおまかにはCreateReplyでメッセージオブジェクトを作成してテキストを指定し、ReplyToActivityAsyncメソッドで送信しています。送信するメッセージは、これまでと同様に「いらっしゃいませ!ご注文を伺います。(メニューをご覧になる場合は「メニュー」と入力してください。)」としています。

次に、Chatbotからメッセージを送信した後の処理を追加します。

リスト2:メッセージ送信後の処理を追加(RootDialog.cs)

public Task StartAsync(IDialogContext context)
{
    //context.Wait(HelloMessage);
    context.Wait(MessageReceivedAsync);
    return Task.CompletedTask;
}

Chatbotからメッセージを送信すると、これまでと同様にRootDialog.csが読み込まれます。最初に実行されるのはStartAsyncメソッドです。context.Waitメソッドでユーザーからの入力待ちとなります。ユーザーからのメッセージを受信するとメニューを表示するか、AIによるテキスト解析を行うための「MessageReceivedAsync」メソッドを呼び出します。

以上で最初にChatbotから話しかけるように修正されました。早速エミュレーターで確認してみましょう。

Chatbot側から対話が始まる

Chatbot側から対話が始まる

エミュレーターを起動して接続しただけで、Chatbotからメッセージが送信されてきました。

通常どおりの対話

通常どおりの対話

続けて「メニュー」と入力すれば、これまでと同様にChatbotとの対話が始まります。

プッシュでメッセージを送信する

対話情報の取得

次に紹介するのは、Chatbotとの対話中に外部からプッシュでメッセージを送信する方法です。スケージュールの通知や、システムのアラート、対話の内容にマッチした広告の表示など様々な利用方法が考えられます。

Chatbotとユーザーの対話中にメッセージを送信するためには、該当する対話を特定するためのいくつかの情報が必要となります。これらはユーザーやChatbotが接続しているチャンネルごとに異なりますので、その都度情報を取得する必要があります。

Chatbotが接続しているチャンネル

Chatbotが接続しているチャンネル

必要な情報を、以下の表に示します。

対話を特定するために必要な情報

情報内容
message.From.Idメッセージ送信者ID
message.Recipient.Idメッセージ受信者ID
message.ServiceUrlChatbotが接続しているチャンネルのURL
message.Conversation.Id対話ID

これらの情報は、実際にChatbotとユーザーが対話しはじめると取得することができます。RootDialog.cs内でChatbotの「メニュー」を表示するためのMessageReceivedAsyncメソッドに、隠しコマンドとして情報取得のためのコマンドを追加しておきましょう(隠しコマンドは、ユーザーが使用することは想定していません)。

リスト3:RootDialog.csのMessageReceivedAsyncメソッド

private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result)
{
    var activity = await result;
    switch (activity.Type)
    {
        case ActivityTypes.Message:
            var message = activity as IMessageActivity;
            switch (activity.Text)
            {
                case "メニュー":
                    MenuMessage(context);
                    break;

                case "ID":
                    await context.PostAsync($@"
ChannelId : {message.ChannelId}

FromId : {message.From.Id}

RecipientId : {message.Recipient.Id}

ServiceUrl : {message.ServiceUrl}

ConversationId : {message.Conversation.Id}
");
                    context.Wait(MessageReceivedAsync);
                    break;

                default:
                    await context.Forward(new ChatDialog(), ChatResumeAfterDialog, message, CancellationToken.None);
                    break;
            }
            break;
    }
}

着目すべき点をおさえておきましょう。

case "ID"

「ID」と入力された場合にメッセージ送信に必要な情報を表示します。

ChannelId~ConversationId

それぞれの情報を表示します。加えて、どのチャンネルの情報かを識別するためにmessage.ChannelIdも表示しています。Webchatの場合、改行は二度必要となります。

エミュレーターで確認してみましょう。

メッセージ送信に必要な情報を表示

メッセージ送信に必要な情報を表示

実用段階で考えると、これらの情報は対話開始時に自動的にバックグランドでjsonファイルなどの形式で取得しておくことになるかと思います。

メッセージ送信アプリケーションの作成

今回はプッシュでメッセージを送信するために、Chatbotのアプリケーションとは別に外部から割り込み用のメッセージを送信するためのアプリケーションを作成しましょう。このようにMicrosoft Bot Frameworkは、Chatbotを開発するためだけではなく、Chatbotをより高機能で使いやすくするために機能拡張を行うためのFrameworkとしても利用することができます。

まずはVisual Studioでコンソールアプリケーションを一つ作成します。

コンソールアプリケーションの作成

コンソールアプリケーションの作成

「NuGetパッケージマネージャー」から「Microsoft.Bot.Builder」をインストールします。原稿執筆時点では、最新バージョンが3.15となっています。Chatbotアプリケーションで利用している「Microsoft.Bot.Builder」のバージョンがこれよりも古い場合は、併せて最新版にアップデートをしておきましょう。

Microsoft.Bot.Builderをインストール

Microsoft.Bot.Builderをインストール

リスト4:Program.cs

using Microsoft.Bot.Connector;
using System;
using System.Collections.Generic;

namespace Chatbot201707_ProactiveMessager
{
    class Program
    {
        static void Main(string[] args)
        {
            var appId = "<Microsoft App ID>";
            var appPassword = "<Password>";

            //Emulator
            var fromId = "default - user";
            var recipientId = "95i2nkda1dd4";
            var serviceUrl = "http://localhost:64607";
            var conversationId = "g011b863i3j";

            //Facebook
            //var fromId = "1628934843893271";
            //var recipientId = "145452079480720";
            //var serviceUrl = "https://facebook.botframework.com";
            //var conversationId = "1628934843893271-145452079480720";

            //Skype
            //var fromId = "18Afm0cxtapI0G7r6uyqCtK3MDtt38YDR5O6_EHo6NVk";
            //var recipientId = "28:2595fdc3-27b1-4810-a040-efc3da267409";
            //var serviceUrl = "https://smba.trafficmanager.net/apis/";
            //var conversationId = "29:18Afm0cxtapI0G7r6uyqCtK3MDtt38YDR5O6_EHo6NVk";

            //WebChat
            //var fromId = "KEBORI0rmk6";
            //var recipientId = "Chatbot201707@dTuM__gWAS0";
            //var serviceUrl = "https://webchat.botframework.com/";
            //var conversationId = "fd8a04848c4744ddbbcce046f6086967";

            MicrosoftAppCredentials.TrustServiceUrl(serviceUrl);
            var connector = new ConnectorClient(new Uri(serviceUrl), appId, appPassword);
            var botAccount = new ChannelAccount(id: recipientId);
            var userAccount = new ChannelAccount(id: fromId);

            IMessageActivity message = Activity.CreateMessageActivity();
            message.Conversation = new ConversationAccount(id: conversationId);
            message.From = botAccount;
            message.Recipient = userAccount;
            message.Locale = "ja-jp";
            message.Text = $@"Chatbotからのお知らせです。

本日のおすすめは、季節のフルーツとれたてのあまーいいちごです。";
            var cardImage = new CardImage { Url = "https://chatbot201707.azurewebsites.net/img/strawberry.jpg" };
            var heroCard = new HeroCard { Title = "季節のフルーツ", Images = new List<CardImage> { cardImage }};
            message.Attachments.Add(heroCard.ToAttachment());
            connector.Conversations.SendToConversation((Activity)message);
        }
    }
}

着目すべき点をおさえておきましょう。

var appId~var appPassword

Microsoft Azureに登録したChatbotの「Microsoft App ID」と「Password」を指定します。Azureポータルの「ボットチャンネル登録」から該当Chatbotの「設定」メニュー「Microsoft App ID(管理)」から確認することができます。

Microsoft App IDとパスワードの確認

Microsoft App IDとパスワードの確認

//Emulator~//WebChat

現在登録しているチャンネルごとに必要な情報を取得して割り当てています。チャンネルごとに値の様式が異なることが確認できます。conversationIdは対話ごとに毎回異なるので、その都度取得する必要があります。

MicrosoftAppCredentials.TrustServiceUrl……

メッセージ送信に必要な情報をそれぞれ割り当てます。

IMessageActivity message = Activity.CreateMessageActivity()

送信するメッセージを作成します。

message.Conversation = new ConversationAccount……

conversationIdで、どの対話にメッセージを割り込ませるかを指定します。

message.Conversation~connector.Conversations.SendToConversation……

メッセージの宛先、送信者、言語、テキストを指定します。メッセージには前回紹介したように画像も差し込むことができますので、HeroCardを使って画像を添付して送信します。

作成したコンソールアプリケーションを実行して、エミュレーターで確認してみましょう。

メッセージの送信

メッセージの送信

エミュレーター以外の他のチャンネルも実際に確認してみましょう。

FacebookのMessenger

FacebookのMessenger

Skype

Skype

WebChat

WebChat

以上が、Chatbotへのプロアクティブメッセージの送信方法です。

それぞれレイアウトは異なりますが、同じコードを利用してプッシュでメッセージを配信することができました。メッセージングアプリケーションが異なっても、一つのコードで同様のメッセージ送信を実現できるのが、Chatbotのよいところです。今回は手作業でメッセージを送信していますが、タスクスケジューラーに登録して定期的にメッセージを自動送信したり、AIの言語解析を利用して特定のキーワードが入力されたらお勧めを紹介したりするなど、用途は限りなく広がります。

10回にわたり紹介してきました「Microsoft Bot Frameworkで作るChatbot」いかがでしたでしょうか。

まだまだ紹介しきれていない機能やテクニックもたくさんあります。実際にChatbotを作成して少しずつ育てていくと理解が深まり、より便利で使いやすいChatbotに成長していくはずです。この記事がMicrosoft Bot FrameworkでChatbotを育てる方のお役に立つことができればうれしいです。

GMOインターネット株式会社 Windowsソリューション チーフエグゼクティブ

GMOインターネットでWindowsのサービス開発運用に関わって16年、数年単位で進化し続けるMicrosoftのWindowsは新しもの好きにはたまらない製品です。自動販売機に見たことのないジュースがあれば、迷わすボタンを押します。そんなチャレンジが僕の人生を明るく、楽しくしてくれています。

お名前.com デスクトップクラウド
http://www.onamae-desktop.com/

お名前.com VPS Hyper-V
http://www.onamae-server.com/vps/hyperv/

連載バックナンバー

Web開発技術解説
第10回

Chatbotから話しかける(プロアクティブメッセージの送信)

2018/5/18
連載10回目となる今回は、Chatbotから先に話しかけるための手順を紹介します。
Web開発技術解説
第9回

Chatbotで画像コミュニケーションを実現する

2018/5/14
連載9回目となる今回は、Chatbotとの文字でのやり取りに画像を追加する方法を解説します。
Web開発技術解説
第8回

FacebookでChatbotを公開する

2018/4/17
連載8回目となる今回は、FacebookのMessengerを介してChatbotとやり取りする方法を解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています