Xamarin 日本語情報

Xamarin(ザマリン) の代理店だったエクセルソフト田淵のブログです。主に Xamarin に関するエントリーをアップしていきます。(なるべく正しい有益な情報を掲載していきたいと考えていますが、このブログのエントリーは所属組織の公式見解ではありませんのでご注意ください)

Bot Framework Composer でチャットボットを作成し Xamarin アプリから接続する~その3

こんにちは。エクセルソフトの田淵です。

「Bot Framework Composer でチャットボットを作成し Xamarin アプリから接続する」の最終回です。

その1ではローカルで動作する Bot を Bot Framework Composer を使って作成する方法を記載しました。

blog.ytabuchi.dev

その2では作成した Bot を Azure Bot Service で動かす方法を記載しました。

blog.ytabuchi.dev

今回はやっと、Xamarin アプリから接続する方法です!

本エントリーは

の 21日目の投稿です。

完成品

こんな感じです。

ソースコード

ソースは以下にアップしてあります。

github.com

Direct Line API とは

docs.microsoft.com

公式ドキュメントにあるように、

認証>会話の開始>メッセージの送信>メッセージの受信

を行います。

開発用にということで、以下の記述があります。

Bot Framework では、C# と Node.js からの Direct Line API 3.0 へのアクセスを容易にするクライアント ライブラリが提供されています。

  • Visual Studio プロジェクト内で .NET クライアント ライブラリを使用するには、Microsoft.Bot.Connector.DirectLine NuGet パッケージをインストールしてください。

が、この Microsoft.Bot.Connector.DirectLine が古いです。

Version Downloads Last updated
3.0.2 190,316 2017/05/10

もう少し公式ドキュメントを探すと、以下のドキュメントが見つかります。

docs.microsoft.com

ここには、

Direct Line App Service 拡張機能との対話は、従来の Direct Line とは異なる方法で行われます。これは、ほとんどの通信が WebSocket で行われるためです。 更新された Direct Line クライアントには、WebSocket の開始/終了、WebSocket 経由でのコマンドの送信、およびボットから返される Activity の受信のためのヘルパー クラスが含まれています。

とあるので、Microsoft.Bot.Connector.DirectLine v3.0.2 で Rest Client としてのライブラリは開発が止まっていて、WebSocket を使うライブラリとして新しくリリースされることになるのではないでしょうか。

まずは、今までの Rest Client のライブラリを使ったクライアントの作成をしてみました。

Xamarin 用コードの解説

基本的には @okazuki さんのソースの焼き直しですw

blog.okazuki.jp

blog.okazuki.jp

基本の流れ

私のソースはかずきさんのを参考にしながら Prism で作成してありますが、基本的な流れは最初にお伝えした

認証>会話の開始>メッセージの送信>メッセージの受信

です。具体的には以下のようなコードで実現できます。ManualConversationPage はコードビハインドで素直に以下のコードを叩いているので流れは分かりやすいかと思います。

using Microsoft.Bot.Connector.DirectLine;

string watermark = "";

// クライアントを作成して会話を開始し、ConversationID を取得します。
var client = new DirectLineClient("<YOUR_SECRET>");
var conversation = await client.Conversations.StartConversationAsync();
var conversationId = conversation.ConversationId;

// ConversationID を指定して会話を開始します。
// ポストした際に得られる responseId を控えておきます。
var response = await client.Conversations.PostActivityAsync(
    ConversationIDLabel.Text,
    new Activity
    {
        From = new ChannelAccount("<ANY_ACCOUNT>"),
        Text = "<ANY_TEXT>",
        Type = ActivityTypes.Message,
    });
var responseId = response.Id;

// ConversationID を指定して、Activity(複数)を取得します。
// 毎回 ActivitySet の Watermark が更新されるのでそれを指定することで最新のやり取りのみを取得できます。
var activities = await client.Conversations.GetActivitiesAsync(conversationId, watermark);
watermark = activities.Watermark;

// 取得した Activities コレクションの中から主要なプロパティを表示します。
foreach (var act in activities.Activities)
{
    Console.WriteLine($"Channel ID: {act.ChannelId} ID: {act.Id}, ReplyToID: {act.ReplyToId}, Text: {act.Text}\n");
}

// responseId が `ReplyToId` と同じであれば Bot からの返信ということが分かります。
var result = activities.Activities.LastOrDefault(x => x.ReplyToId == responseId);

コメントにあるとおりポストしてポストの ID を取得して、 ReplyToId が同じものがそのポストに対する返信。という流れなのでそれに沿ってアプリのビューを組み立てていけば OK です。

Rest Client では同じ会話の ID はすべて activities (Microsoft.Bot.Connector.DirectLine.ActivitySet)Activities (IList<Activity>) に入りますが、ActivitySet.Watermark が更新されるので、その値を GetActivitiesAsync の第二引数に渡してあげることで最新の情報のみが取得できます。

Watermark を指定しないと以下のように全部取れます。それぞれの会話の ID は、最初に発行される ConversationId|0000001 のように数字が付与されたものになっています。

f:id:ytabuchi:20191217095209p:plain:w450

Thumbnail card を取得する方法

応用編として、Bot 作成の段階で Thumbnail Card を使っています。

Bot Framework Composer でチャットボットを作成し Xamarin アプリから接続する~その1 - Xamarin 日本語情報

[ThumbnailCard
    title = @{dialog.weather.name} の天気
    text = 天気は {dialog.weather.weather[0].description}、@{dialog.weather.main.temp}℃です。
    image = https://openweathermap.org/img/wn/{dialog.weather.weather[0].icon}@2x.png
]

の部分です。

Thumbnail Card を作ると、Text プロパティではなく、上記の配列ほぼそのままの JSON が Activities.Attachments.Content として取得できます。また、その際に通常は nullAttachmentLayoutlist になっています。(上記の JSON の通りであれば、images[] となぜ image だけ配列なのかさっぱり分からんのですが、weather[0].icon を使っているからですかね…w)

f:id:ytabuchi:20191217180715p:plain:w750

そのため以下のようにして、Thumbnail Card だった場合は JSON をデシリアライズして Activity をプロパティに持つ CardActivity に移し替えて、それ以外は何もせずに CardActivity に移し替えています。

foreach (var message in result.messages)
{
    if (message.AttachmentLayout == "list")
    {
        var cardActivity = new CardActivity(message);

        var json = message.Attachments.FirstOrDefault()?.Content.ToString();
        var cardInfo = JsonConvert.DeserializeObject<CardInfo>(json);

        cardActivity.CardTitle = cardInfo.CardTitle;
        cardActivity.CardText = cardInfo.CardText;
        cardActivity.CardImage = cardInfo.CardImages.FirstOrDefault()?.Url;

        Messages.Add(cardActivity);
    }
    else
    {
        Messages.Add(new CardActivity(message));
    }
}

CardActivity クラスはこんな感じ。

public class CardActivity
{
    public string CardTitle { get; set; }
    public string CardText { get; set; }
    public string CardImage { get; set; }
    public Activity Activity { get; }

    public CardActivity(Activity activity)
    {
        Activity = activity;
    }
}

ListView の Template 切り替え

結果として、ListView の Template は 3つ作り(リンクは GitHub)、以下の Template Selector でテンプレートを切り分ける。という処理を行っています。

if (((CardActivity)item).Activity.AttachmentLayout == "list")
    return CardTemplate;
return ((CardActivity)item).Activity.From.Id == "ytabuchichatbot" ? OutputTemplate : InputTemplate;

CardActivity か、それ以外。それ以外は Azure Bot Service の表示名が取得できる From.Id で決め打ちしちゃっています。 Azure Bot Service の表示名は「Web アプリボット>設定」から確認できます。

f:id:ytabuchi:20191218145536p:plain:w450

最終的に View を表示している部分は以下です。

<StackLayout>
    <ListView VerticalOptions="FillAndExpand"
              HasUnevenRows="True"
              ItemTemplate="{StaticResource messageTemplateSelector}"
              ItemsSource="{Binding Messages}"
              SelectionMode="None"
              SeparatorVisibility="None" />
    <StackLayout Margin="15,5" Orientation="Horizontal">
        <Entry HorizontalOptions="FillAndExpand" Text="{Binding InputMessage, Mode=TwoWay}" />
        <Button Padding="10"
                Command="{Binding SendCommand}"
                Text="Send" />
    </StackLayout>
</StackLayout>

まとめ

Bot Framework Composer で作成した Azure Bot Service に Direct Line API SDK を使ってアクセスする方法を紹介しました。

これで、WPF/UWP でも、ASP.NET でも専用の UI で Bot とやり取りすることができるようになりました!

(Rest API をループしてコールしているので、無駄が多いですね。今後は後述する WebSocket ベースでやるのが良いと思います。)

まとめのおまけ

Xamarin でできないかなーと探している時に正に!という公式ドキュメントを見つけました。

ボットが実行されるクロスプラットフォーム モバイル アプリの作成

この例では、クロスプラットフォーム モバイル アプリケーションを構築するための一般的ツールである Xamarin を使用して、ボットが実行されるモバイル アプリを作成します。

おお!

最初に、シンプルな Web ビュー コンポーネントを作成し、それを使って Web チャット コントロールをホストします。 次に、Azure portal を使用して、Web チャット チャネルを追加します。 次に、登録されている Web チャット URL を、Xamarin アプリ内の Web ビュー コントロールのソースとして指定します。

うん?

public class WebPage : ContentPage
{
    public WebPage()
    {
        var browser = new WebView();
        browser.Source = "https://webchat.botframework.com/embed/<YOUR SECRET KEY HERE>";
        this.Content = browser;
    }
}

Web チャット!笑

まぁ、Web チャットもかなりカスタマイズできるようなのでまずはこっちでやってみるというのも手ではないでしょうかw

docs.microsoft.com

Direct Line の新しい WebSocket ベースのライブラリ

最初の方にも記載しましたが、以下の公式ドキュメントで新しい WebSocket ベースのライブラリが紹介されています。

docs.microsoft.com

実際に .NET Core 2.2 のプロジェクトを作ってドキュメントの通りやってみました。

string endpoint = "https://<YOUR_BOT_HOST>.azurewebsites.net/.bot/";
string secret = "<YOUR_BOT_SECRET>";

var tokenClient = new DirectLineClient(
    new Uri(endpoint),
    new DirectLineClientCredentials(secret));
var conversation = await tokenClient.Tokens.GenerateTokenForNewConversationAsync(); // ←ここでエラー

2019年12月中旬現在では、トークンを取得するためにエンドポイントから Conversation を取得する上記の部分で、

$exception
{"Response status code indicates server error: 404 (NotFound)."}
Microsoft.Rest.TransientFaultHandling.HttpRequestWithStatusException

のエラーが出ていて、conversation が null のままになってしまいます。

エンドポイントを https://yt-bot-sample.azurewebsites.net/api/messages/.bot/ にしてみたり、ストリーミングエンドポイントにチェックが入っていなかったので付けてみたり、

f:id:ytabuchi:20191213094255p:plain:w600

F0(無料枠)から S1(一般枠 99.9% の SLA が付く代わりに 1,000メッセージあたり 56円が掛かる)にしてみたり、

f:id:ytabuchi:20191213095711p:plain:w600

してみましたが、404 が解消できませんでした。どなたか動いた方がいらっしゃったら本エントリーのコメントか、@ytabuchi までメンション頂けると嬉しいです。

以上です。

エクセルソフト | ダウンロード | 学習用リソース | JXUG リンクページ | ブログ購読