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

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

下記手順 3 まで行って作成できる 天気予報BOT↓
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_06.png)


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

>ご参考まで
1. 天気予報 BOT - Adaptive Card 編で作成したアプリはこちらからダウンロードできます
[Bot Framework × Adaptive Cards: WeatherBot アプリサンプル](https://msdnshared.blob.core.windows.net/media/2017/08/WeatherBot_AdaptiveCards.zip)
2. 天気予報 BOT - LUIS(1) で作成した App (JSON) はこちらからダウンロードできます。LUIS 管理画面から Import し、Train & Publish を行ってお使いください。
[WeatherBot LUIS App サンプル(JSON)](https://raw.githubusercontent.com/a-n-n-i-e/CognitiveLUIS-AdaptiveCards-WeatherBot/master/LUISApp_WeatherBot.json)

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

無償の Visual Studio 2017 Community or 2015 Community でOKなので、既存の環境がない場合は、ダウンロードしてインストールします。
[Visual Studio 2017 Community ダウンロードサイト](https://www.visualstudio.com/ja-jp/products/visual-studio-community-vs.aspx)
[Visual Studio 用の Bot Framework C# テンプレート](http://aka.ms/bf-bc-vstemplate)
[Bot Framework Channel Emulator (Windows版)](https://aka.ms/bf-bc-emulator) ※Mac/Linux は [Console版](https://aka.ms/bfemulator) をご利用ください
>Bot Framework 開発環境の作り方は、[Microsoft Bot Framework v3.0 からはじめる BOT 開発: Bot Framework を使うための開発環境](http://qiita.com/annie/items/edc26c0ee9603e84a2e4#bot-framework-%E3%82%92%E4%BD%BF%E3%81%86%E3%81%9F%E3%82%81%E3%81%AE%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83) をご覧ください。

#天気予報 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<object> から LuisDialig<object> に変更します。
また、[Serializable] の前に LuisModel を追加します。*AppID*、*EndpointKey* は前回作成した 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](http://weather.livedoor.com/weather_hacks/webservice) の [全国の地点定義表(XML)](http://weather.livedoor.com/forecast/rss/primary_area.xml) で確認できます。こちらを直接参照するほか、今回はそれをJSON に置き換えた [locationIdList.json](https://raw.githubusercontent.com/a-n-n-i-e/CognitiveLUIS-AdaptiveCards-WeatherBot/master/LUISApp_WeatherBot.json) を作成しましたので、これを利用します。

###クラスファイルの作成
Adaptive Card 編の [天気予報データを格納するクラスを作成する] (http://qiita.com/annie/items/3416fcf1ec275940979a#%E5%A4%A9%E6%B0%97%E4%BA%88%E5%A0%B1%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%A0%BC%E7%B4%8D%E3%81%99%E3%82%8B%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%9C%E6%88%90) と同じ方法で、新規クラスファイルを作成していきます。
Models フォルダーを右クリックして、[追加]＞[クラス]をクリック、LoctaionModels.cs という名前で新規クラスファイルを作成します。
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_01.png)
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_02.png)

[locationIdList.json](https://raw.githubusercontent.com/a-n-n-i-e/CognitiveLUIS-AdaptiveCards-WeatherBot/master/LUISApp_WeatherBot.json) を直接開く、またはダウンロードしてテキストエディタなどで開いて、内容をコピーします。
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_03.png)

Visual Studio の 上部ツールバーから [編集]＞[形式を選択して貼り付け]＞[JSON をクラスとして貼り付ける] を選択して、コピーした内容を貼り付けます。
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_04.png)
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_05.png)

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) を指定します。

天気を尋ねる文章を送ると、指定した都市と日程の天気予報が返されるのを確認してください。
![](https://msdnshared.blob.core.windows.net/media/2017/08/bluesky_20170816_06.png)

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