LoginSignup
3
5

More than 5 years have passed since last update.

Microsoft Bot Framework v3.0 からはじめる BOT 開発: 天気予報 BOT~自然言語判定を組み込む - Cognitive Services LUIS(2)

Last updated at Posted at 2017-08-16

「明日の横浜の天気は?」そんなナチュラルな入力を解釈する Chat Bot は簡単に作れます。Microsoft Bot Framework と Cognitive Services LUIS (Language Understanding Intelligent Service) を利用して、自然言語で入力された文章を分類し、キーワード(今回は日程と場所)を取得、Livedoor 天気予報 Weather Hacks の天気予報 API を利用してデータを取得、日付と気温と共に天気アイコンを表示するカードを作成、表示する BOT を作成します。

下記手順 3 まで行って作成できる 天気予報BOT↓

手順

  1. 天気予報 BOT - Adaptive Card : 天気情報を取得して表示する
  2. 天気予報 BOT - Cognitive Services LUIS(1): 自然言語(文章)を解釈して、場所や日時を取得する
  3. 天気予報 BOT - Cognitive Services LUIS(2): 自然言語判定結果をBOTに組み込む (※このページ)

ご参考まで
1. 天気予報 BOT - Adaptive Card 編で作成したアプリはこちらからダウンロードできます
Bot Framework × Adaptive Cards: WeatherBot アプリサンプル
2. 天気予報 BOT - LUIS(1) で作成した App (JSON) はこちらからダウンロードできます。LUIS 管理画面から Import し、Train & Publish を行ってお使いください。
WeatherBot LUIS App サンプル(JSON)

開発環境

Windows 10 + Visual Studio 2017 Enterprise, Bot Framework v3.8 (C#) で作成を行っています。

無償の Visual Studio 2017 Community or 2015 Community でOKなので、既存の環境がない場合は、ダウンロードしてインストールします。
Visual Studio 2017 Community ダウンロードサイト
Visual Studio 用の Bot Framework C# テンプレート
Bot Framework Channel Emulator (Windows版) ※Mac/Linux は Console版 をご利用ください

Bot Framework 開発環境の作り方は、Microsoft Bot Framework v3.0 からはじめる BOT 開発: Bot Framework を使うための開発環境 をご覧ください。

天気予報 BOT - Cognitive Services LUIS(2) : 自然言語判定結果をBOTに組み込む

LUIS で判別した Intent によって Bot の動作を分ける (LuisDialog)

Dialogs フォルダ を開き、RootDialog.cs を編集します。
RootDialog.cs の冒頭に Microsoft.Bot.Builder.LUIS への参照を追加します。

RootDialog.cs
using Microsoft.Bot.Builder.LUIS;
using Microsoft.Bot.Builder.Luis.Models;

RootDialig クラスを IDialog から LuisDialig に変更します。
また、[Serializable] の前に LuisModel を追加します。AppIDEndpointKey は前回作成した LUIS App の API アクセス用 URL から抽出してコピー&ペーストします。

RootDialog.cs
[LuisModel("[AppID]", "[EndpointKey]")]
[Serializable]
//public class RootDialog : IDialog<object>
public class RootDialog : LuisDialog<object>
{
    :(後略)

StartAsync は使用しないため、コメントアウトまたは削除します。
LUIS により自然言語解析を行い、Intent が "None" または判定できなかった時の動作を Task None として以下のように追加します。ここでは初期メッセージを表示します。

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

[LuisIntent("")]
[LuisIntent("None")]
private async Task None (IDialogContext context, LuisResult result)
{
    await context.PostAsync($"日本の都市の天気予報を調べる、天気予報Botです。");
    context.Done<object>(null);
}

Intent が "GetWeather" と判定された時の動作は、Task GetWeatherAsync として下記のように追加します。

RootDialog.cs
[LuisIntent("GetWeather")]
private async Task GetWeatherAsync(IDialogContext context, LuisResult result)
{
    var selectedDay = "";
    var cityName = "";
    var cityId = "";

    // LUIS の判定結果から Entity を取得
    // ※次以降の項目で作成します

    // 都市名から都市IDを取得
    cityId = await GetLocationAsync(cityName);

    if (cityId == "")
    {
        await context.PostAsync($"ゴメンナサイ、分からなかったです。日本の都市名を入れてね。");
        context.Done<object>(null);
    }
    else
    {

        // 天気を取得
        WeatherModel weather = await GetWeatherAsync(cityId);

        // 取得した天気情報をカードにセット
        var weatherCard = GetCard(weather, selectedDay);
        var attachment = new Attachment()
        {
            Content = weatherCard,
            ContentType = "application/vnd.microsoft.card.adaptive",
            Name = "Weather Forecast"
        };

        var message = context.MakeMessage();
        message.Attachments.Add(attachment);

        // 返答メッセージをPost
        await context.PostAsync(message);
        context.Done<object>(null);

    }
}

天気データの取得、Card へのセットは、Adaptive Cards 編 で作成した MessageReceivedAsync とほぼ同じコードになっています。
[追加]
GetLocationAsync: 都市名から都市IDを取得
[変更]
GetWeatherAsync: 都市コードを追加して天気予報を取得するように変更
GetCard: 指定した日付のみ表示されるように変更

LUIS による判定結果 (Intent & Entity) を取得する

LUIS による判定結果 (Intent および Entity) は result に格納されています。result.Entity を確認して、”Place” および "Day" と判定された (=Tag に”Place” または "Day" が入っている) Entity を取得します。

RootDialog.cs
private async Task GetWeatherAsync(IDialogContext context, LuisResult result)
{
    var selectedDay = "";
    var cityName = "";
    var cityId = "";

    // LUIS の判定結果から Entity を取得
    foreach (var entity in result.Entities)
    {
        if (entity.Type == "Place")
        {
            cityName = entity.Entity.ToString();

        }
        else if (entity.Type.Substring(0, 3) == "Day")
        {
            selectedDay = entity.Type.Substring(5);
        }
    }

    // 都市名から都市IDを取得

        :(後略)

都市の天気予報データを取得する

都市名から都市IDを取得

LUIS の Entity (Place) の値から取得した都市名を使って、都市IDを取得する、GetLocationAsync を追加します。
都市名と都市IDは Livedoor 天気予報 Weather Hacks全国の地点定義表(XML) で確認できます。こちらを直接参照するほか、今回はそれをJSON に置き換えた locationIdList.json を作成しましたので、これを利用します。

クラスファイルの作成

Adaptive Card 編の 天気予報データを格納するクラスを作成する と同じ方法で、新規クラスファイルを作成していきます。
Models フォルダーを右クリックして、[追加]>[クラス]をクリック、LoctaionModels.cs という名前で新規クラスファイルを作成します。

locationIdList.json を直接開く、またはダウンロードしてテキストエディタなどで開いて、内容をコピーします。

Visual Studio の 上部ツールバーから [編集]>[形式を選択して貼り付け]>[JSON をクラスとして貼り付ける] を選択して、コピーした内容を貼り付けます。

class Rootobject → class WeatherModel、Location → Location2 (WeatherModels.cs で利用済みのため。2か所) のように変更します。

都市名から都市IDの取得

この LocationModel を利用し、場所を入力すると Id を返す GetLocationAsync を以下のように作成します。念のため、都道府県名を入れてもリストの一番上にある都市のIDを返すようにしています。

RootDialog.cs
private async Task GetWeatherAsync(IDialogContext context, LuisResult result)
{
        :(中略)
}
private async Task<string> GetLocationAsync(string place)
{
    var client = new HttpClient();
    var locationResult = await client.GetStringAsync("https://raw.githubusercontent.com/a-n-n-i-e/CognitiveLUIS-AdaptiveCards-WeatherBot/master/LUISApp_WeatherBot.json");
    var locationStr = Uri.UnescapeDataString(locationResult.ToString());
    var locationModel = JsonConvert.DeserializeObject<LocationModel>(locationStr);

    var locationId = "";

    // 都市名に一致するコードを取得
    foreach (var location in locationModel.locations)
    {
        if (location.city == place)
        {
            locationId = location.city_id;
            break;
        }
    }
    if (locationId == "")
    {
        // 都道府県名に一致するコードを取得
        foreach (var location in locationModel.locations)
        {
            if (location.pref.Trim(new char[] { '都','府','県'}) == place)
            {
                locationId = location.city_id;
                break;
            }
        }
    }

    return locationId;
}

都市IDを指定して天気を取得

GetWeatherAsync を修整して、下記のように都市IDを指定して天気を取得できるようにします。

作成済みの GetWeatherAsync() を修整しても、新しく GetWeatherAsync(string cityId) を追加しても、どちらでも構いません。

RootDialog.cs
private async Task<WeatherModel> GetWeatherAsync(string cityId)
{
    // API から天気情報を取得
    var client = new HttpClient();
    //var result = await client.GetStringAsync("http://weather.livedoor.com/forecast/webservice/json/v1?city=140010");
    var result = await client.GetStringAsync("http://weather.livedoor.com/forecast/webservice/json/v1?city=" + cityId);

    // API 取得したデータをデコードして WeatherModel に取得
    result = Uri.UnescapeDataString(result);
    var model = JsonConvert.DeserializeObject<WeatherModel>(result);
    return model;

}

指定された日程でカードを作成する

取得した天気予報データを、指定した指定した日(今日、明日、明後日) のみの情報ををカードに生成します。

以下のように GetCard を修正して、日程を指定できるようにします。

RootDialog.cs
private static AdaptiveCard GetCard(WeatherModel model, string day)
{
    var card = new AdaptiveCard();
    //AddWeather(model, card);
    AddWeather(model, card, day);
    return card;
}

AddWeather も以下のように修整し、指定した日の情報のみをカードに表示するようにします。

RootDialog.cs
private static void AddWeather(WeatherModel model, AdaptiveCard card, string selectedDay)
{
    // タイトル作成
    var titleColumnSet = new ColumnSet();
    card.Body.Add(titleColumnSet);

    var titleColumn = new Column();
    titleColumnSet.Columns.Add(titleColumn);
    AddTextBlock(titleColumn, $"{model.location.city} の天気", TextSize.ExtraLarge, HorizontalAlignment.Center);

    // 本文作成
    // 天気情報をセット
    var mainColumnSet = new ColumnSet();
    card.Body.Add(mainColumnSet);


    foreach (var forcast in model.forecasts)
    {
        // Todayが取得出来ている場合は、dateLabel = "今日" の場合のみセット (=else の操作を行う)
        if (selectedDay == "Today" && forcast.dateLabel != "今日")
        {
        }
        else if (selectedDay == "Tomorrow" && forcast.dateLabel != "明日")
        {
        }
        else if (selectedDay == "DayAfterTomorrow" && forcast.dateLabel != "明後日")
        {
        }
        else
        {
            var mainColumn = new Column();
            mainColumnSet.Columns.Add(mainColumn);

            // 天気データの取得と加工
            string day = forcast.dateLabel;
            string date = DateTime.Parse(forcast.date).Date.ToString("M/d");

            // temperature が null の場合は "--" に変換
            string maxTemp, minTemp;
            try
            {
                maxTemp = forcast.temperature.max.celsius;
                minTemp = forcast.temperature.min.celsius;
            }
            catch
            {
                maxTemp = "--";
                minTemp = "--";
            }

            // データのセット
            AddTextBlock(mainColumn, $"{day}({date})", TextSize.Large, HorizontalAlignment.Center);
            AddTextBlock(mainColumn, $"{maxTemp} / {minTemp} °C", TextSize.Medium, HorizontalAlignment.Center);
            AddImage(mainColumn, forcast.image.url, ImageSize.Medium, HorizontalAlignment.Center);

        }

    }

}

確認

BOT の動作確認を行います。、F5 または デバック>デバックの開始 をクリックして、プロジェクトのビルドおよび起動を行います。ブラウザが起動して Bot Framework のデフォルト画面が表示されたら、Bot Framework Channel Emulator を起動してアクセスを行います。
Bot Framework Channel Emulator の上部中央にある Bot Url に、起動しているブラウザと同じ URL (デフォルトでは http://localhost:xxxx) に /api/messages を追加したアドレス (http://localhost:xxxx/api/messages) を指定します。

天気を尋ねる文章を送ると、指定した都市と日程の天気予報が返されるのを確認してください。

Appendix

ご参考まで、完成したコードを GitHub で公開しました。
https://github.com/a-n-n-i-e/CognitiveLUIS-AdaptiveCards-WeatherBot

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5