この記事は更新から24ヶ月以上経過しているため、最新の情報を別途確認することを推奨いたします。
今回は、前々回の記事で60分で作成したチャットボットに
乗換案内機能を追加します。
以下はやりたいことのイメージです
今あるQ&A機能をそのまま残して
Q&A回答もできるし、ユーザーの言い回しを理解して、乗換案内も可能なチャットボットを実現します。
①LUISのアカウント作成
1.以下のURLにアクセスして、LUISのアカウントを作成しましょう
https://www.luis.ai/user/tou
サブスクリプション、リージョン等の必要情報を入力していきます
最後にConfilmを選択します
2.Confilmすると簡単なチュートリアルが表示されるので確認したら、Create a LUIS app nowを選択
3.LUIS Appの一覧画面が出てくるので、Create new appを選択します
②LUIS Appの作成
1.LUISを活用してやりたいことを下記に整理します
ユーザーの発話を理解し、発話の意図が乗換案内であれば
出発地から目的地までの乗換案内を行います。
概念として
この際の意図をインテント
出発地、目的地に当てはまる単語をエンティティと呼びます
LUIS Appを作成するにあたりこのインテントとエンティティを定義していきます
2.必要情報を入力して、Appを作成します
重要なのはDescriptionにあとで何かわかるような説明を書いておくこと
3.出発地点、目的地点である駅名を理解するためのエンティティを作成します
4.List型のエンティティを作成します
5.json形式でリストを一括アップロードする場合のテンプレートを確認
json形式で一括アップロードする場合はテンプレートを把握しておく必要があります
6.本質と逸れるので詳細な手順は省きますが、csv形式の駅名一覧をWebから入手して、jsonに変換して活用します
7.作成した駅名リストをアップロードします
8.駅名にロールを設定します
ユーザーの言い回しが出発点になるのか、終着点になるのかエンティティに役割を与えるためにインテントにエンティティを設定する際に活用します
9.意図を理解するためのインテントを作成します
10.インテント名を設定します
今回は乗換案内なのでtransfer
11.想定するユーザーの言い回しでインテントを設定していきます
12.想定する言い回しをある程度網羅して、fromとtoを設定したら完成
13.トレーニングします
14.テストします
期待通りの理解をしているかの確認をします。
インテントがtransferでエンティティのfrom/toを理解できているかどうか確認
15.Publishします
PublishすることでBot Serviceとの連携準備を行います
16.Publishが完了したら、エンドポイント情報を確認します
17. Starter_Key の Example Queryをコピーします
③BOTからLUISを呼び出せるようにする
2020/07/13追記
最新のサンプルコードで記事の通りにファンクションの差し込みができない場合、こちらのサンプルコードをご参考ください。
https://github.com/microsoft/BotBuilder-Samples/tree/master/samples/javascript_nodejs/11.qnamaker/
※上記サンプルコードも今後の変更等で内容が変わる可能性があります。
1.Bot Serviceのビルドからオンラインコードエディタを開きます
※LUISのライブラリを使えば、もっと簡単に実装できるんですが
今回は説明を簡略化したいがために手実装
<先頭行のほうに事前にコピーしておいたエンドポイントを設定>
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { ActivityHandler } = require('botbuilder');
const { QnAMaker } = require('botbuilder-ai');
// ★LUIS用の追加
const request = require(`request`);
const luisurl = "<ここにサンプルクエリ貼り付け>";
<後方行にファンクション追加>
// ★LUIS用に追加したファンクション
// LUISにメッセージを投げ、解釈の結果を取得する
function doRequest(options) {
return new Promise(function (resolve, reject) {
request(options, function (error, res, body) {
if (!error && res.statusCode == 200) {
resolve(JSON.parse(body));
} else {
reject(error);
}
});
});
}
// ★LUIS用に追加したファンクション
// 確信度 80%以下のインテントは意図として扱わない
function getIntent(res) {
if(res.topScoringIntent.score >= 0.8){
return res.topScoringIntent.intent;
} else {
return 'None';
}
}
// ★乗換案内用に追加したファンクション
// ルールベースで動作する処理
// 情報が不足していたら質問を返す
function createTransferMassage(res){
if(res.entities.length == 1){
if(res.entities[0].role == 'to' || res.entities[0].role == undefined){
return '乗換案内ですね! 〇〇駅から'+ res.entities[0].entity +'駅に行きたい のように聞いてもらえたらご案内しますよ!!!';
} else {
return '乗換案内ですね! '+ res.entities[0].entity +'駅から△△駅に行きたい のように聞いてもらえたらご案内しますよ!!!';
}
} else if (res.entities.length == 2){
// 乗換案内の実装に必要な情報はこの処理に入ったタイミングで揃っているはずなので
// この処理に実際の乗換案内の為の処理を実装する
if(res.entities[0].role == 'to'){
return '乗換案内ですね! '+ res.entities[1].entity +'駅から'+ res.entities[0].entity +'駅に行きたいんですか?';
} else {
return '乗換案内ですね! '+ res.entities[0].entity +'駅から'+ res.entities[1].entity +'駅に行きたいんですか?';
}
} else {
return '乗換案内ですね! 〇〇駅から△△駅に行きたい のように聞いてもらえたらご案内しますよ!!!';
}
}
<既存のonMessage処理にLUISとの連携処理を追加>
// When a user sends a message, perform a call to the QnA Maker service to retrieve matching Question and Answer pairs.
this.onMessage(async (context, next) => {
this.logger.log('Calling QnA Maker');
// ★LUISとの連携の為に追加した処理 ここから
const res = await doRequest({url: luisurl+encodeURIComponent(context.activity.text),method: "get"});
switch (getIntent(res)) {
case 'transfer':
// 乗換案内
this.logger.log(res);
await context.sendActivity(createTransferMassage(res));
break;
case 'None':
// ★LUISとの連携の為に追加した処理 ここまで
const qnaResults = await this.qnaMaker.getAnswers(context);
// If an answer was received from QnA Maker, send the answer back to the user.
if (qnaResults[0]) {
await context.sendActivity(qnaResults[0].answer);
// If no answers were returned from QnA Maker, reply with help.
} else {
await context.sendActivity('本当にごめんなさい!質問が理解できませんでした');
}
// ★LUISとの連携の為に追加した処理 ここから
}
// ★LUISとの連携の為に追加した処理 ここまで
// By calling next() you ensure that the next BotHandler is run.
await next();
});
2020/07/13 追記
使用の中サンプルコードにonMessage処理がない場合は、メッセージを受け取る処理の以降であればどの箇所でも問題ありません。目的や仕様に合った場所に処理を追加する必要があります。
※業務利用の多くの場合は、[メッセージ受け取り処理] → [認証処理] → → [業務処理] → [Luisによる分類処理] → [業務処理] になるのではないでしょうか。
2.ソースコードの変更を保存
あとで変更内容がわかるように修正コメントをしっかり書いておきます
3.保存したらRunします
4. 一旦ここで 動作確認します
5.乗換案内を実装します
<ファンクションを追加しまs>
// 乗換案内用に追加したファンクション
// 指定のエンティティを取得する
function getEntityByRole(entities,role){
return entities.find(item => item.role === role).entity+ '駅';
}
<乗換案内用のURLを構築する処理を追加します>
乗換案内用のURLはお好みのサービスを使用してください
例ではGoogle Mapの乗換案内を活用します
// 乗換案内のURL
var norikaeurl = "https://www.google.com/maps?saddr=<STATION_FROM>&daddr=<STATION_TO>&ie=UTF8&f=d&sort=def&dirflg=r&hl=ja";
既存のonMessage処理をさらに修正します
// 乗換案内の実装に必要な情報はこの処理に入ったタイミングで揃っているはずなので
// この処理に実際の乗換案内の為の処理を実装する
// ユーザーが希望する出発地点と目的地の情報を取得する
const stationFrom = getEntityByRole(res.entities,'from');
const stationTo = getEntityByRole(res.entities,'to');
// 乗換案内用のURL作成
norikaeurl = norikaeurl.replace('<STATION_TO>',encodeURIComponent(stationTo ));
norikaeurl = norikaeurl.replace('<STATION_FROM>',encodeURIComponent(stationFrom));
// メッセージの作成
return '乗換案内ですね! ' + stationFrom + 'から' + stationTo + 'へのご案内~\n'+norikaeurl;
6.2と3の手順の保存&Runして、動作確認します
うまく動作していることが確認できます