Edited at

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

More than 1 year has passed since last update.

「明日の横浜の天気は?」そんなナチュラルな入力を解釈する 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