208
224

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LINEのBot開発 超入門(後編) メッセージの内容と文脈を意識した会話を実現する

Last updated at Posted at 2017-07-10

前編に続いてBotを開発していきます。今回は自然言語解析を用いたメッセージの理解と、文脈に応じて対話をおこなうところまでをカバーします。

開発の流れ

  • 自然言語解析サービスのDialogflowを設定し、理解すべきメッセージを理解できるように学習させます。
  • Botが受け取ったメッセージをDialogflowと連携してユーザーの意図を判定できるようにします。
  • 文脈に応じて対話するための機能をBot開発フレームワークを活用してBotに盛り込みます。

手順

自然言語解析の設定

Dialogflowは自然言語処理のサービスです。ユーザーが発話した文章が何を意図しているのかを特定するために利用します。また、その文章の中からパラメーターを抽出する機能も備えています。まずは下記Dialogflowのサイトにアクセスしてアカウントを作成してください。

Dialogflow

アカウントが作成できたらDialogflowにログインし、Agentを新規に作成します。必要な項目を入力して「Create」ボタンをクリックします。
create_agent.png

  • Agent name: 任意のAgent name
  • DEFAULT LANGUAGE: Japanese - ja

最初にEntityを登録します。Entityには二つの役割があります。

ひとつは表記揺れを吸収する役割です。例えば「寿司」という単語の場合、「寿司」と書かれる場合もあれば、「すし」の場合、あるいは「スシ」「鮨」などのバリエーションが考えられます。こういう場合に「sushi」というEntityを作成しておき、バリエーションとして前述の寿司、すし、スシ、鮨を登録しておくと、Dialogflowが「寿司」を理解しやすくなります。

もうひとつはユーザーのメッセージ中でBotが抽出したいパラメータを特定する役割です。例えば寿司の出前注文の場合、どのメニューを注文するのか特定する必要があります。「松」「竹」「梅」というメニューがあったとすると、ユーザーは「松を出前でお願いしたいんですが」というメッセージでオーダーをいれてくる可能性があります。この時、メニューは「松」であると特定する必要がありますので、「menu」というEntityを作成しておき、そのバリエーションとして松、竹、梅を登録しておくと、Dialogflowがメッセージからメニューを抽出してくれるようになります。

では早速Entityを作っていきます。今回はメニューだけ登録してみます。左サイドバーからEntitiesの+をクリックします。
plus_entities.png

Entityの作成・編集フォームに遷移します。まずmenuのEntityを作成します。必要な情報を入力して「SAVE」をクリックします。
entities_form.png

  • Entity name: menu
  • Define synonyms: チェックをはずす
  • value: 松、竹、梅 (一行に一単語を入力)

次にIntentを登録します。Intentはメッセージから「意図」と特定するための学習設定で、今回の場合だと「ユーザーは出前を注文しようとしている」という意図を特定できるようにする必要があります。学習プロセスはシンプルで、この意図を伝える際に使われる表現を例文としてどんどん登録していきます。

左サイドバーからIntentsの+をクリックします。
スクリーンショット_2018-05-04_10_32_24.png

Intentの作成・編集画面に遷移します。Intent nameに「出前注文」と入力し、ADD TRAINING PHRASESをクリックします。
スクリーンショット_2018-05-04_10_34_20.png

TRAINING PHRASESに例文をどんどん登録します。例えば下図のような文章です。実際に寿司を注文する際、ユーザーが電話で言いそうな表現を入れていくのです。
スクリーンショット 2018-05-04 10.37.39.png

例文を入れるほどにDialogflowは様々な表現でも出前注文を理解できるようになります。注目すべきなのは、「松を出前でお願いします」という例文の「松」が自動でハイライトされているところです。Entityに該当する単語があったため、自動的にmenu entityであることが検出されているわけです。これによって、例文では「松」ですが、「梅を出前でお願いします」という表現でも意図が出前注文であると理解することができ、かつ、menuは「梅」だと特定できるようになります。

同画面で「MANAGE PARAMETERS AND ACTION」をクリックします。
スクリーンショット_2018-05-04_10_39_56.png

あらわれたAction nameに「handle-delivery-order」と入力します。これはメッセージがこの意図だと判定されたときに何を実行するべきか示すための項目です。この値がどのように使われるのかは後ほど。
スクリーンショット_2018-05-04_10_43_23.png

この項目はBotへの指示になりますが、詳しくはまた後ほど。

ここで忘れずに画面上の「SAVE」ボタンをクリックして設定を保存してください。

右サイドバーでメッセージを入力して、意図を特定できるかどうか、パラメーターを抽出できるかどうかをテストすることができます。
スクリーンショット_2018-05-04_10_46_56.png

Botに自然言語解析を組み込む

DialogflowのSDKをインストールします。このSDKを使って、DialogflowのAPIにアクセスすることになります。

$ npm install --save dialogflow

APIアクセスするためにはいくつかの情報が必要になります。今回は下記3つの情報を利用します。

  • Project ID
  • Service Account
  • Private Key

Project IDとService AccountはDialogflowのAgent設定画面のGeneralタブで確認できます。
スクリーンショット_2018-05-04_10_52_03.png

Private Keyの取得はちょっと手間がかかります。同画面のService AccountのリンクをクリックしてGoogle Cloud Platformのサービスアカウントのページに遷移します。このページでDialogflow Integrationsの右にあるメニューから「キー作成」を選択します。
スクリーンショット_2018-05-04_10_56_14.png

JSONを選択して「作成」ボタンをクリックします。するとJSON形式の鍵ファイルがダウンロードされます。
スクリーンショット_2018-05-04_10_57_18.png

この鍵ファイルを開くと、「private_key」という項目があります。この値をPrivate Keyとして利用します。

環境変数に確認したこれらの値をセットします。 値をシングルクォートで囲んでいることに注意してください。

$ heroku config:set GOOGLE_PROJECT_ID='あなたのProject ID'
$ heroku config:set GOOGLE_CLIENT_EMAIL='あなたのService Account'
$ heroku config:set GOOGLE_PRIVATE_KEY='あなたのPrivate Key'

index.jsを下記のように更新してSDKのインポート、DialogflowのAPIアクセスに必要な情報の設定、および自然言語解析に応じて返答をおこなう機能を追加します。

index.js
// -----------------------------------------------------------------------------
// モジュールのインポート
const server = require("express")();
const line = require("@line/bot-sdk"); // Messaging APIのSDKをインポート
const dialogflow = require("dialogflow");

// -----------------------------------------------------------------------------
// パラメータ設定
const line_config = {
    channelAccessToken: process.env.LINE_ACCESS_TOKEN, // 環境変数からアクセストークンをセットしています
    channelSecret: process.env.LINE_CHANNEL_SECRET // 環境変数からChannel Secretをセットしています
};

// -----------------------------------------------------------------------------
// Webサーバー設定
server.listen(process.env.PORT || 3000);

// APIコールのためのクライアントインスタンスを作成
const bot = new line.Client(line_config);

// Dialogflowのクライアントインスタンスを作成
const session_client = new dialogflow.SessionsClient({
    project_id: process.env.GOOGLE_PROJECT_ID,
    credentials: {
        client_email: process.env.GOOGLE_CLIENT_EMAIL,
        private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n")
    }
});

// -----------------------------------------------------------------------------
// ルーター設定
server.post('/bot/webhook', line.middleware(line_config), (req, res, next) => {
    // 先行してLINE側にステータスコード200でレスポンスする。
    res.sendStatus(200);

    // すべてのイベント処理のプロミスを格納する配列。
    let events_processed = [];

    // イベントオブジェクトを順次処理。
    req.body.events.forEach((event) => {
        // この処理の対象をイベントタイプがメッセージで、かつ、テキストタイプだった場合に限定。
        if (event.type == "message" && event.message.type == "text"){
            events_processed.push(
                session_client.detectIntent({
                    session: session_client.sessionPath(process.env.GOOGLE_PROJECT_ID, event.source.userId),
                    queryInput: {
                        text: {
                            text: event.message.text,
                            languageCode: "ja",
                        }
                    }
                }).then((responses) => {
                    if (responses[0].queryResult && responses[0].queryResult.action == "handle-delivery-order"){
                        let message_text
                        if (responses[0].queryResult.parameters.fields.menu.stringValue){
                            message_text = `毎度!${responses[0].queryResult.parameters.fields.menu.stringValue}ね。どちらにお届けしましょ?`;
                        } else {
                            message_text = `毎度!ご注文は?`;
                        }
                        return bot.replyMessage(event.replyToken, {
                            type: "text",
                            text: message_text
                        });
                    }
                })
            );
        }
    });

    // すべてのイベント処理が終了したら何個のイベントが処理されたか出力。
    Promise.all(events_processed).then(
        (response) => {
            console.log(`${response.length} event(s) processed.`);
        }
    );
});

Herokuにデプロイします。

$ ./deploy.sh
または
$ git add . && git commit -m 'Improve' && git push

これで、「出前をお願いします」というメッセージを送ると注文を聞くメッセージが返信され、「竹を出前でお願いします」という注文を含んだメッセージを送ると、注文はすでに理解した旨のメッセージが返信されます。また、出前以外のメッセージを送ると無視されます。
S__704514.jpg

文脈に応じた対話機能を追加する

私たちは普段あまり意識せずとも文脈を加味して対話をしています。例えば下記のスレッドを見てみてください。

お客さん:「出前をお願いします」
寿司屋:「毎度!ご注文は?」
お客さん:「梅」
寿司屋:「あいよ!梅ね。どちらにお届けしましょ?」

何の違和感もありません。では次のスレッドはどうでしょうか。

お客さん:「梅」
寿司屋:「あいよ!梅ね。どちらにお届けしましょ?」

寿司屋がややエスパー気味になっています。「梅」と言われただけで「あーー、出前のことですよね」と推察するのはやや難度が高いと思われます。つまり「梅」というのは出前を頼んでいるという文脈があるからこそ1単語で成立するメッセージであり、文脈がなければ解釈できません。

というわけでBotにも文脈を理解させておくことが重要です。そしてこのあたりからBot開発はカオスになってきます。同じメッセージが来ても文脈によって反応を変える必要がでてきます。単純に「自然言語解析」=>「特定のリアクション」というわけにはいかず、膨大な条件分岐が生まれてくるからです。

今回はこの部分をbot-expressというNode.js用のBot開発フレームワークを活用してオフロードしようと思います。

まず現在のindex.jsを別名にしておきます。

$ mv index.js index.old.js

bot-expressをインストールします。

$ npm install --save bot-express

> bot-express@0.5.2 postinstall /Users/nkjm/code/bootcamp-sushi-bot/node_modules/bot-express
> node script/postinstall.js

May I create skill directory and index.js for you? (y/n): yを入力

すると現在のディレクトリ配下に新たにindex.jsとskillディレクトリが作成されます。

$ ls
Procfile	index.js	node_modules	package.json
deploy.sh	index.old.js	package-lock.json	  skill

自動生成されたindex.jsスクリプトにはbot-expressの初期設定が含まれています。今回はこれ以上変更する必要はありませんが、詳細な設定オプションについてはbot-expressのドキュメントを参照してみてください。

また、skillディレクトリにはBotのスキルを1スキル1ファイルという形で作成していくことになります。今回の場合、DialogflowでIntentとして設定した「出前注文」に対応するスキルを作成する必要があります。このスキルファイルを作成するときの規則として、Intentで設定したaction名.jsというファイル名で作成してください。今回は handle-delivery-order.js というファイル名になります。

skill/handle-delivery-order.js
"use strict";

module.exports = class SkillHandleDeliveryOrder {

    constructor(){
        this.required_parameter = {
            menu: {
                message_to_confirm: {
                    type: "template",
                    altText: "出前のメニューは松、竹、梅の3種類になっとりますけどどちらにしましょっ?",
                    template: {
                        type: "buttons",
                        text: "ご注文は?",
                        actions: [
                            {type: "message", label: "", text: ""},
                            {type: "message", label: "", text: ""},
                            {type: "message", label: "", text: ""}
                        ]
                    }
                },
                parser: async (value, bot, event, context) => {
                    if (["", "", ""].includes(value)) {
                        return value;
                    }
                    
                    throw new Error();
                },
                reaction: async (error, value, bot, event, context) => {
                    if (error) return;
                    
                    bot.queue({
                        type: "text",
                        text: `あいよっ!${value}ね。`
                    });
                }
            },
            address: {
                message_to_confirm: {
                    type: "text",
                    text: "どちらにお届けしましょっ?"
                },
                parser: async (value, bot, event, context) => {
                    if (typeof value == "string"){
                        return value;
                    } else if (typeof value == "object" && value.type == "location"){
                        return value.address;
                    }
                    
                    throw new Error();
                }
            }
        }
    }

    async finish(bot, event, context){
        await bot.reply({
            type: "text",
            text: `あいよっ。じゃあ${context.confirmed.menu}を30分後くらいに${context.confirmed.address}にお届けしますわ。おおきに。`
        });
    }

}

例によってHerokuにデプロイしておきます。

$ ./deploy.sh
または
$ git add . && git commit -m 'Improve' && git push

まずコンストラクターにこのスキルで必要となるパラメーターをthis.required_parameterのプロパティとして設定しています。ここに設定したパラメーターはすべて集まるまでbot-expressがユーザーにメッセージを送信して確認(質問)します。

そのときに送信するメッセージを各パラメーターごとにmessage_to_confirmプロパティで設定できます。このプロパティはLINEのMessage Objectのフォーマットでそのまま記述できます。メッセージフォーマットについては公式APIリファレンスを参照ください。

次にfinishメソッドを実装しています。このメソッドは必要なパラメーターが全部揃ったら実行されます。このメソッドには5つの引数が与えられており、中でも特に重要なのが第三引数のcontextです。このオブジェクトには確定したパラメーターの値が収められています。今回の例だとmenu, addressの情報がcontext.confirmedに入っています。開発者はこの情報を利用して最終処理を記述することができます。

また、第一引数のbotにはreply()メソッドが用意されており、Messaging APIのreplyを使って返信することができます。

テスト

こんな形でBotが注文を受付してくれれば完成です。

デバッグ

うまく動かない、という場合はDEBUGしましょう。
環境変数にDEBUGを追加して下記のように設定します。

$ heroku config:set DEBUG=bot-express:*

そしてHeroku上のログをheroku logsコマンドにてリアルタイムに眺めます。

$ heroku logs --tail

この状態でBotにメッセージを送信してみてください。どこかでエラーになっている可能性が高いと思います。
よくある設定ミスを下記に挙げておきます。

  • 環境変数が正しくセットされていない => heroku configコマンドで環境変数名とその値をチェックしてください。
  • ソースコードがShift-JISになっている => Windowsの方は要注意。すべてUTF8で保存してください。
  • ソースのファイル名がまちがっている => 拡張子、大文字小文字、ファイルの場所をチェックしてください。

どうしても動かない場合

今回の完成形のソースコードがこちらにあります。手元のソースコードと見比べてみてください。
https://github.com/nkjm/bootcamp-sushi-bot

また、最終手段としてHeroku Buttonをご用意しておきました。こちらをクリックすればご自身のHeroku環境に完動品のBotがデプロイされます。Channel SecretやTokenなどはご自身の環境の値をデプロイ時に入力ください。
Deploy

まとめ

今回はBotが自然言語からユーザーが求めていることを理解し、文脈も加味しながら対話できるまでを実装しました。

この後はいろいろなメッセージに対して適切に反応できるように理解力とスキルを広げていく、精度をあげていくということが求められると思います。Bot開発はまだまだ確立した開発手法というものは存在せず、開発者の腕にかかっている分野で、そこがまた魅力的なところですね。

付録

LINEのBot開発にあたり有用なライブラリ・ツールをご紹介しておきます。

208
224
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
208
224

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?