LUISを使って頭の悪いLINE Botを作ってみよう!:特集: 新たなアプリ「ボット」の時代(3/4 ページ)
LUIS(自然言語解析サービス)とロケスマWeb(お店発見Webサービス)とGoogle Geocoding APIを使って、ユーザーが探しているお店を教えてくれるLINE Botを作ってみよう!
このLINE Botで行っていること
ここでは前述したLUISに加えて以下の2つのサービスを利用している。
ロケスマWebはPCやスマートデバイスのブラウザを利用して、現在位置(あるいは指定した位置)の近傍にある特定のお店(「カフェ」のように大ざっぱに指定したり、「スターバックス」などの特定のチェーン店を指定したりできる)を表示してくれるというWebサービスだ。このサービスはREST APIを提供しているので、LINEユーザーとLINE Botの対話の中で「ユーザーが何かのお店を探している」というインテントをLUISアプリが検出したら、ロケスマWebが提供しているAPIを利用して、LUISアプリが抽出した「Category」エンティティ(探しているお店の種類)をロケスマWebで利用できるIDへと変換している(例:「カフェ」というCategoryを「food/cafe」のように変換)。
もう1つのGoogle Geocoding APIは、LUISアプリが抽出した「Location」エンティティ(どの辺のお店を探しているか)を緯度経度情報に変換するために使用している。ただし、今回は手抜きでGoogle Geocoding APIを利用してしまったが、これは本来、静的な利用=事前に緯度経度情報を取得してアプリ内にキャッシュしておくような利用を念頭に置いたものなので、プロダクトレベルで動的=その場その場でユーザーの要求に応えて緯度経度情報を取得するのであればGoogle Maps JavaScript API Client Geocoderなどを利用するのが正しい(今回はあくまでもサンプルということでご容赦願いたい)。
ロケスマWebでは「https://www.locationsmart.org/?id=ロケスマのカテゴリID&lat=緯度情報&lon=経度情報」のようにすることで、特定の地点の周辺にあるお店をブラウザ上に表示できる。ロケスマWebのREST APIとGoogle Geocoding APIを利用することで、これに必要な情報を取り出して、最終的にこのようなURLを作成して、ユーザーに提供するというのがこのアプリの狙いとなる。
まとめると、本稿で作成するBotでは次のような手順でユーザーの入力を処理している。
- ユーザーからのメッセージを全てLUISアプリで解析する
- ユーザーがお店を探しているとLUISアプリが判断したら以下の処理を行う
- ロケスマWeb APIでCategoryエンティティをロケスマで利用可能なIDに変換
- Locationエンティティがあれば(「新宿のカフェで待ち合わせ」の「新宿」など)、Google Geocoding APIを利用して緯度経度情報を取得
- 3と4で得た情報を基にロケスマWebでお店を表示するためのURLを作成してユーザーに送信
Google Geocoding APIを利用するにはAPIキーの取得も必要になるが、この辺りの話についてはGeocoding APIのドキュメントを参照してほしい。呼び出し自体は「https://maps.googleapis.com/maps/api/geocode/json?key=取得したAPIキー&address=地名/住所など」の形式で行うだけだ。
次にこれらの処理から幾つかの部分を抜き出して、実際のコードを見ていこう。
Line Bot Applicationプロジェクトテンプレートが生成するコード
Line Bot Applicationプロジェクトテンプレートで生成されるコードで、今回、実際に手を入れた部分は少ない。というのはここではユーザーが入力したテキストのみを処理対象としているからだ(スタンプや写真などをユーザーが入力しても、何の処理も行わない)。そこでまずはプロジェクトテンプレートにより生成されたコードの中でテキスト処理に関係ある部分だけを以下に示しておこう。本稿のBotはこのコードを改変して、各種のWebサービスを利用するためのコードを追加しているだけだ。
public async Task<HttpResponseMessage> Post(HttpRequestMessage request)
{
…… 省略 ……
foreach (Event lineEvent in activity.Events)
{
LineMessageHandler handler = new LineMessageHandler(lineEvent);
Profile profile = await handler.GetProfile(lineEvent.Source.UserId);
switch (lineEvent.Type)
{
case EventType.Beacon:
await handler.HandleBeaconEvent();
break;
…… 省略 ……
case EventType.Message: // メッセージの処理
Message message =
JsonConvert.DeserializeObject<Message>(lineEvent.Message.ToString());
switch (message.Type)
{
case MessageType.Text: // テキストメッセージの処理
await handler.HandleTextMessage();
break;
case MessageType.Audio:
case MessageType.Image:
…… 省略 ……
break;
case MessageType.Location:
await handler.HandleLocationMessage();
break;
}
break;
…… 省略 ……
case EventType.Unfollow:
await handler.HandleUnfollowEvent();
break;
}
}
return Request.CreateResponse(HttpStatusCode.OK);
}
強調書体で示した部分がポイントだ。簡単にはテキストが入力されたら「handler.HandleTextMessage」メソッドを呼び出しているということになる。実際のテキスト処理は次のコードで行っている。
public async Task HandleTextMessage()
{
var textMessage = JsonConvert.DeserializeObject<TextMessage>(lineEvent.Message.ToString());
Message replyMessage = null;
if (textMessage.Text.ToLower() == "buttons")
{
…… 省略 ……
}
else if (textMessage.Text.ToLower() == "confirm")
{
…… 省略 ……
}
else if (textMessage.Text.ToLower() == "carousel")
{
…… 省略 ……
}
else
{
replyMessage = new TextMessage(textMessage.Text);
}
await Reply(replyMessage);
}
このコードではユーザーが入力したテキストに応じて処理を振り分けている。例えば「buttons」と入力すれば、次のような返答を行うようになっている。が、これはプロジェクトテンプレートが用意してくれたデモ的な機能であり、ここでは必要ない。自分でBotを実装する際にはこれらのコードは削除してしまうか、最後のelse節内で独自のテキスト処理を行えばよい。
LineBotWithLUISにおけるユーザー入力の処理
というわけで、ここではシンプルに以下のようなコードとした。
public async Task HandleTextMessage()
{
var textMessage =
JsonConvert.DeserializeObject<TextMessage>(lineEvent.Message.ToString());
Message replyMessage = null;
if (textMessage.Text == "sleep")
sleep = true;
else if (textMessage.Text == "wakeup")
sleep = false;
if (sleep) return;
var luisres = await GetLuisResponseAsync(textMessage.Text);
if (luisres.entities.Count(x => x.type == "Category") > 0)
{
var locasmadata = await GetLocaSmaResponseAsync(luisres);
var locandcat = await GetLocationAndCategoryAsync(luisres, locasmadata);
var message = MakeMessage(locandcat);
replyMessage = new TextMessage(message);
}
await Reply(replyMessage);
}
最初にやっているsleepプロパティのオン/オフはBotがうざく感じるときに「黙らせたり、再度発言を可能にしたり」するための処理だ。sleepプロパティの値がfalseの間は(基本的には)Botは口をつぐんでいるようになる(はずなのだが、原因不明でたまに口を挟んでくるときがあった)。
その後は、前ページの最後で述べたステップ1〜5の手順に即した形で処理を行っている。それがGetLuisResponseAsync、GetLocaSmaResponseAsync、GetLocationAndCategoryAsync、MakeMessageの各メソッド呼び出しだ(実際には、ステップ4と5は微妙に入り交じっている。GetLoationAndCategoryAsyncメソッドでは、ユーザー入力に位置を示すエンティティがあるかどうかに関係なく、その後のMakeMessage呼び出しで各種情報をひとまとめに利用できるようなオブジェクトを返し、MakeMessageメソッドではそれを利用してURLを生成している)。
ステップ2の「ユーザーがお店を探しているとLUISアプリが判断」する部分は、LINQのCountメソッドでLUISアプリが返送してきたデータに「Category」エンティティが含まれているかで判定している。LUISアプリが返送してくるデータは例えば、次のようなものになる(ここでは「新宿で天ぷらでも食べない?」と入力があったものとする)。
{
"query": "新宿で天ぷらでも食べない?",
"topScoringIntent": {
"intent": "findCategory",
"score": 0.979055166
},
"intents": [
{
"intent": "findCategory",
"score": 0.979055166
},
{
"intent": "None",
"score": 0.0737134
}
],
"entities": [
{
"entity": "天ぷら",
"type": "Category",
"startIndex": 3,
"endIndex": 5,
"score": 0.984412551
},
{
"entity": "新宿",
"type": "Location",
"startIndex": 0,
"endIndex": 1,
"score": 0.983726442
}
]
}
JSONデータの先頭にあるtopScoringIntentのintentプロパティを基に判断を行ってもよいのだが、ここではentitiesプロパティに「typeプロパティの値がCategoryとなるエンティティ」が含まれているかどうかを見るようにした(これはサンプルを作り出した時点からそうしており、intentプロパティを見ればよかったじゃんと思わないでもない。ただし、地名のみを含んだユーザーの入力をLUISアプリが「お店を探している」と判断した場合には対処に困る。そういう意味では地名はなくても、「カフェ」などのカテゴリが含まれていれば、お店を探しているのだと判断するのはそう悪くはないことだ思われる)。
次ページでは、このJSONデータを取得するためのREST API呼び出しについて見ていく。
Copyright© Digital Advantage Corp. All Rights Reserved.