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

今回は、前々回の記事で60分で作成したチャットボットに
乗換案内機能を追加します。

以下はやりたいことのイメージです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/9561b7a9-0c17-9e9f-1f82-57d0062950f9.png)


今あるQ&A機能をそのまま残して
Q&A回答もできるし、ユーザーの言い回しを理解して、乗換案内も可能なチャットボットを実現します。


###①LUISのアカウント作成
**１．以下のURLにアクセスして、LUISのアカウントを作成しましょう**
https://www.luis.ai/user/tou

サブスクリプション、リージョン等の必要情報を入力していきます
最後にConfilmを選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/fb02fa34-2e4c-53af-4a2c-4f40fefc7d09.png)


**２．Confilmすると簡単なチュートリアルが表示されるので確認したら、Create  a LUIS app nowを選択**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/ec4659e8-1fb6-7524-f5eb-8dfeaecd43ae.png)


**３．LUIS Appの一覧画面が出てくるので、Create new appを選択します**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/10df18f2-16ac-1bb3-2c6e-4c4a61e12bd5.png)


###②LUIS Appの作成
**１．LUISを活用してやりたいことを下記に整理します**

ユーザーの発話を理解し、発話の意図が乗換案内であれば
出発地から目的地までの乗換案内を行います。

概念として
この際の意図をインテント
出発地、目的地に当てはまる単語をエンティティと呼びます

LUIS Appを作成するにあたりこのインテントとエンティティを定義していきます


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/77cf50fe-8016-866f-31de-04d67cf19fa3.png)



**２．必要情報を入力して、Appを作成します**
重要なのはDescriptionにあとで何かわかるような説明を書いておくこと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/92124aba-0cd9-49c8-bf66-bfbca8ef8115.png)




**３．出発地点、目的地点である駅名を理解するためのエンティティを作成します**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/64529f7c-96cc-bb53-cac9-efd6c241d662.png)




**４．List型のエンティティを作成します**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/5eda7e68-afc6-acde-0ce9-f7aa1d343ff6.png)




**５．json形式でリストを一括アップロードする場合のテンプレートを確認**
json形式で一括アップロードする場合はテンプレートを把握しておく必要があります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/a5ea1177-ee63-f726-a98b-fba9d7cc3ff5.png)





**６．本質と逸れるので詳細な手順は省きますが、csv形式の駅名一覧をWebから入手して、jsonに変換して活用します**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/e4b43a3c-aaeb-f8fa-0712-1a2833275d4a.png)





**７．作成した駅名リストをアップロードします**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/369d5847-32ef-19ab-da28-65d2c7559632.png)



**８．駅名にロールを設定します**
ユーザーの言い回しによりエンティティが出発点になるのか、終着点になるのか役割を与えるためにインテントにエンティティを設定する際に活用します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/01601fad-3b90-bfe7-ba41-b5fb81703c68.png)


**９．意図を理解するためのインテントを作成します**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/67f345ff-ceab-2ea7-b10d-87242dfb12c8.png)


**１０．インテント名を設定します**
今回は乗換案内なのでtransfer
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/fee0918d-a154-723c-00b8-9f658c9141bf.png)


**１１．想定するユーザーの言い回しでインテントを設定していきます**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/dc86a7e3-694f-7acb-844f-f62f7919f096.png)


**１２．想定する言い回しをある程度網羅して、fromとtoを設定したら完成**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/0074d09d-3bef-42bb-2be3-cbdba3506f4e.png)


**１３．トレーニングします**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/795205d7-84de-b1b8-7e73-335c2221fcaa.png)


**１４．テストします**
期待通りの理解をしているかの確認をします。
インテントがtransferでエンティティのfrom/toを理解できているかどうか確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/bead67a9-e495-5e1f-44e1-19516beedda8.png)


**１５．Publishします**
PublishすることでBot Serviceとの連携準備を行います
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/9a271134-f264-67b0-7bc3-b56654aa055d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/a2cfc33c-5916-1f4d-61c0-856759c82c06.png)


**１６．Publishが完了したら、エンドポイント情報を確認します**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/4ca37066-8faa-d887-74ec-84c4cdab3c62.png)


**１７． Starter_Key の  Example Queryをコピーします**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/1f94253a-8551-15d1-3b8d-12ae2b5e7fbb.png)


###③BOTからLUISを呼び出せるようにする

１．Bot Serviceのビルドからオンラインコードエディタを開きます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/ee783875-72d7-556f-a509-c654482b2a55.png)

※LUISのライブラリを使えば、もっと簡単に実装できるんですが
　今回は説明を簡略化したいがために手実装


**＜先頭行のほうに事前にコピーしておいたエンドポイントを設定＞**

```Javascript
// 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 = "＜ここにサンプルクエリ貼り付け＞";

```


**＜後方行にファンクション追加＞**


```Javascript
// ★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との連携処理を追加＞**



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

```

**２．ソースコードの変更を保存**
あとで変更内容がわかるように修正コメントをしっかり書いておきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/201a4d9a-3aaf-12e3-db49-c24eec9e6e66.png)




**３．保存したらRunします**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/369c558c-764d-f26b-479e-6281bf0f0da9.png)


**４． 一旦ここで 動作確認します**
これでユーザーの意図と出発地、目的地がチャットボットで把握できていることがわかります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/d67309d8-40c6-dbe3-4270-b661c7282eef.png)


**５．乗換案内を実装します**

**＜ファンクションを追加します＞**

```Javascript

// 乗換案内用に追加したファンクション
// 指定のエンティティを取得する
function getEntityByRole(entities,role){
    return entities.find(item => item.role === role).entity+ '駅';
}

```


**＜乗換案内用のURLを構築する処理を追加します＞**

乗換案内用のURLはお好みのサービスを使用してください
例ではGoogle Mapの乗換案内を活用します

```Javascript
// 乗換案内の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処理をさらに修正します

```Javascript

        // 乗換案内の実装に必要な情報はこの処理に入ったタイミングで揃っているはずなので
        // この処理に実際の乗換案内の為の処理を実装する

        // ユーザーが希望する出発地点と目的地の情報を取得する
        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;


```



**６．２と３の手順の保存＆Runして、動作確認します**
うまく動作していることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/a0f99ff4-59cf-1b87-6dec-5f5ee785d85c.png)