「明日の横浜の天気は?」そんなナチュラルな入力を解釈する Chat Bot は簡単に作れます。Microsoft Bot Framework と Cognitive Services LUIS (Language Understanding Intelligent Service) を利用して、自然言語で入力された文章を分類し、キーワード(今回は日程と場所)を取得、Livedoor 天気予報 Weather Hacks の天気予報 API を利用してデータを取得、日付と気温と共に天気アイコンを表示するカードを作成、表示する BOT を作成します。
##手順
- 天気予報 BOT - Adaptive Card : 天気情報を取得して表示する
- 天気予報 BOT - Cognitive Services LUIS(1): 自然言語(文章)を解釈して、場所や日時を取得する
- 天気予報 BOT - Cognitive Services LUIS(2): 自然言語判定結果をBOTに組み込む (※このページ)
ご参考まで
- 天気予報 BOT - Adaptive Card 編で作成したアプリはこちらからダウンロードできます
Bot Framework × Adaptive Cards: WeatherBot アプリサンプル - 天気予報 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 への参照を追加します。
using Microsoft.Bot.Builder.LUIS;
using Microsoft.Bot.Builder.Luis.Models;
RootDialig クラスを IDialog から LuisDialig に変更します。
また、[Serializable] の前に LuisModel を追加します。AppID、EndpointKey は前回作成した LUIS App の API アクセス用 URL から抽出してコピー&ペーストします。
[LuisModel("[AppID]", "[EndpointKey]")]
[Serializable]
//public class RootDialog : IDialog<object>
public class RootDialog : LuisDialog<object>
{
:(後略)
StartAsync は使用しないため、コメントアウトまたは削除します。
LUIS により自然言語解析を行い、Intent が "None" または判定できなかった時の動作を Task None として以下のように追加します。ここでは初期メッセージを表示します。
//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 として下記のように追加します。
[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 を取得します。
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 編の [天気予報データを格納するクラスを作成する] (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 という名前で新規クラスファイルを作成します。
locationIdList.json を直接開く、またはダウンロードしてテキストエディタなどで開いて、内容をコピーします。
Visual Studio の 上部ツールバーから [編集]>[形式を選択して貼り付け]>[JSON をクラスとして貼り付ける] を選択して、コピーした内容を貼り付けます。
class Rootobject → class WeatherModel、Location → Location2 (WeatherModels.cs で利用済みのため。2か所) のように変更します。
###都市名から都市IDの取得
この LocationModel を利用し、場所を入力すると Id を返す GetLocationAsync を以下のように作成します。念のため、都道府県名を入れてもリストの一番上にある都市のIDを返すようにしています。
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) を追加しても、どちらでも構いません。
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 を修正して、日程を指定できるようにします。
private static AdaptiveCard GetCard(WeatherModel model, string day)
{
var card = new AdaptiveCard();
//AddWeather(model, card);
AddWeather(model, card, day);
return card;
}
AddWeather も以下のように修整し、指定した日の情報のみをカードに表示するようにします。
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