60分で組み立てたQnA MakerベースのチャットボットにLUISと連携した乗換案内機能を追加する

今回は、前々回の記事で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を作成するにあたりこのインテントとエンティティを定義していきます

image.png


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を呼び出せるようにする

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();
        });



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して、動作確認します
うまく動作していることが確認できます

いいね (←参考になった場合はハートマークを押して評価お願いします)
読み込み中...

注意事項・免責事項

※技術情報につきましては投稿日時点の情報となります。投稿日以降に仕様等が変更されていることがありますのでご了承ください。

※公式な技術情報の紹介の他、当社による検証結果および経験に基づく独自の見解が含まれている場合がございます。

※これらの技術情報によって被ったいかなる損害についても、当社は一切責任を負わないものといたします。十分な確認・検証の上、ご活用お願いたします。