Edited at

Watson チャットボットの作り方 第2回目

More than 1 year has passed since last update.

第2回目は、チャットボットを聖徳太子の様に、同時にたくさんのユーザーからの問いかけに受け答えする機能について実装していきます。前回の Watson チャットボットの作り方 第1回目 では、LINEやFacebook とBluemixを連携する方法について、オウム返しのチャットボットを作ることで確認しました。 今回は前回のコードにセッション管理の機能を付け加えていきます。


セッション管理とは

はじめに、なぜセッション管理を考慮するのか考えてみましょう。 家電製品の故障問い合わせや銀行や保険会社のサービスなどで、コールセンターに電話をかける時を考えてみましょう。 電話をかけると、オペレータさんと会話するまでに、待たされる事が少なくないですよね。 電話のオペレータさんは、一度に一人づつしか応対できないので、順番を待たなければなりません。 もちろん空いていれば、待ち時間なしで繋がりますが、電話オペレータさんをたくさん雇うにはお金もかかるので、待たされるのが常ですよね。それに電話で対応となると、リアルタイムで両者の時間が拘束される点も効率が悪いですよね。

スクリーンショット 2017-06-05 23.19.53.png

これに対して、チャットボットを考えてみましょう。サーバーの処理能力は人間と比べて高速なので、同時に複数の相手と応対する事ができます。もちろん、それぞれ対話の内容が異なりますから、あたかも一人とだけに話している様に見せるため、相手によって話の流れを切り替える必要があります。 つまり相手ごとに独立した対話とするため、対話の流れを切り換える機能をセッション管理と呼んでいます。 例えばSiriの様な一問一頭の内容であれば、相手によって話の流れを切り替える必要は無いのですが、往復2回以上の会話が続く場合は、会話の続きが途切れない様に、セッションを管理する必要があります。

スクリーンショット 2017-06-05 23.20.05.png

この様な理由から、Watsonと繋ぐ前に、同時複数ユーザーと会話するためのセッション管理機能を実装します。これによりサーバーの処理能力を活かして複数人と同時並行で会話することができ、サーバーの能力を最大限活用し、ユーザーにとっては待ち時間の無い、快適な問い合わせが出来ることになります。


チャットボットのアーキテクチャー設計

これからチャットボットのプログラムの全体の作り方を考えていきましょう。最初は全体の処理が、プログラムコードを最もシンプルに書いて行ける様な構造、つまりアーキテクチャを考えていきます。 次の図は機能の一塊づつを箱で表した概念図です。このそれぞれの箱が、おおよそ必要な機能のひと塊りとなりますので、一個づつ機能を考慮していきます。

スクリーンショット 2017-06-07 17.03.44.png

この作業は。ソフトウェアのモジュールの機能、そして、主従や依存関係を考えていきます。 この考慮によって、重複の無い最も少ないコード量で、目的を達成し、拡張性を持たせ、性能が出る様にするためです。 しかし、アーキテクチャの設計は図式モデルを主体に考えるのではなく、具体的にコーディングするイメージで考慮していきます。コードを書いて、実行して確認して最適案が決まったら、解りやすい抽象的な図にして、人に伝えるといった設計の進め方を考えていきます。次から個々の箱について、設計した結果について書いていきます。

第一回目のWatson チャットボットの作り方 第1回目では、webhookの箱でメッセージ送信のメソッドを呼び出してオウム返していましたので、sessionCtrlm eventHandler, SessionStoreの3つを追加します。


webhook (メッセージ受信)

LINE や Facebook のメッセージング・システムから Webhook でメッセージの到着を通知されるモジュールです。 これは LINE、FacebookのRESTサービスに対応するAPIライブラリ( facebook-bot-messenger , line-msg-api ) を使って実装します。

次のスニペットは、第一回目のオウム返しするだけのコード( https://github.com/takara9/chatbot-echo-facebook/blob/echo/echoback_bot.js ) ですが、メッセージが到着すると、bot.on が呼び出されます。受け取ったメッセージを、そのまま送信用メソッドにセットして送信します。関数の引数の部分にさらに関数を定義するというコールバック関数は、慣れていない人にとっては、不思議な書き方ですが、後ほど解説したいと思います。 前述の図では、webhook(bot.on)の中から、sessionCtrl の関数を呼び出す事を表しています。

// メッセージ到着

bot.on(MessengerPlatform.Events.MESSAGE, function(userId, message) {
<中略>
// エコー応答
bot.sendTextMessage(userId,message.getText());
});


sessionCtrl (セッション管理)

この部分が今回の話の中心であるsessionCtrl(セッション管理)の箱で、複数の相手と同時に会話するための端末の管理機能です。具体的には、どこまで会話していたのか、相手の返信先アドレス、最後にメッセージが来た時刻、エージェントの種類等々の情報を保存、検索、削除を受け持ちます。主要な機能を箇条書きにすると以下の三点です。


  • 最初のメッセージを受けた時に、ユーザーIDをKeyとして、前述の情報を作成して保存します。

  • 次にメッセージを受信した時に、ユーザーIDをKeyとして、KVS (Key Value Store) 型のデータベースから情報を引き出し活用します。

  • 2日間ほどアクセスが無い場合、KVSからセッションの情報を自動削除します。

次のスニペットは、LINEからメッセージを受けた時に、 オウム返しに返すモジュールに、sessionCtrl のコールを追加したものです。そして、前述の図の様に、sessionCtrl の コールバック関数の中から、eventHandler を呼び出しています。 それから、図の中に描くと、ごちゃごちゃしてしまうので、描いていませんが、sessionCtrl の関数の下から揚がってきたエラーを errorHandler で処理して、スマホにエラーメッセージを返す様にします。

bot.on(function (message) {

userId = message.events[0].source.userId;
agent = "LINE"
sc.sessionCtrl( agent, userId, message, function(err, session) {
if (err) {
errorHandler(agent, userId, message, "内部エラー", err);
} else {
eventHandler(message, session, function(err,session) {
sc.sessionUpdate(session, function(err,session) {});
});
}
});
});


eventHandler (イベント処理)

SNSのアプリから送られてくるメッセージやイベントを処理するモジュールです。これは、それぞれのSNSアプリに仕様に応じて処理内容を書く必要があります。 このため次のスニペットの様に、agent で判別して、それぞれに対応したモジュールを呼び出しています。

function eventHandler(session, message, callback) {

if (session.agent == "LINE") {
_eventHandlerLINE(session,message,function(err,session) {
callback(err,session);
});
} else if (session.agent == "facebook") {
_eventHandlerFB(session,message,function(err,session) {
callback(err,session);
});
}
}


Watson APIs (Watsonインタフェース)

これから Watson API を呼び出す場所を次のスニペットに示します。受け取ったメッセージを Watson API で Watson に送って回答を受け取り、アプリの応答送信のメソッドを使って、ユーザーに応答します。 こうやってみると、チャットボットでは、Watson の APIの部分は、ほんの少しであることが、解りますね。

// Facebookイベント処理                                                                                                     

function _eventHandlerFB( session, message, callback) {
// エコーバック (将来、ここに Watson API を組み込む)
if (message.isTextMessage()) {
session.recvMsg = message.getText();
session.replyMsg = session.recvMsg;
bot.sendTextMessage(session.user_id, session.replyMsg);
}
callback(null, session);
}


Session Store (セッション・データベース)

ユーザーIDで一意に対応するデータを保存、取出し、削除を実行するため、特性として、KVS型NoSQLデータベースが軽く早くスケールするため、適合すると考えられます。 そこで Cloudant を利用することにします。 Cloudant は、Lite プランでは無料で利用できますから、開発や技術の習得には絶好と思います。

以上で主要な構成要素の考慮は、一旦完了で、次に実装について必要な技術を考えていきます。


詳細実装技術


セッション管理データベース

セッションを保存しておくデータベースは、Cloudant (Apache Couch DB) を利用します。 Node.js から クラウダントをアクセスする方法は、Node.jsから Cloudant へのアクセスパターン に整理したので、参照していただけると、理解が進むと思います。


セッションの開始

次のスニペットが、セッションを開始する入り口の部分です。 SNSのユーザーIDで、ゲットして存在しなければ、新しいセッションとしてデータを作成して登録します。sessionOpen() のコールバック関数の中で、callback(err,session)としてるsessionに、データベースから取得したJSON形式のデータが入っています。

//  セッション管理

//  複数の端末と同時並行で処理する
exports.sessionCtrl = function(agent, userId, message, callback) {
// ユーザーIDでセッションの存在をチェック
dbSession.get(userId, function(err,session) {
if (err) {
if (err.error == 'not_found') {
// セッション開始
sessionOpen(agent, userId, message, function(err, session) {
callback(err, session);
});
} else {
callback(err,session);
}
} else {
callback(err, session);
}
});
}


セッション更新

次のチャットボットの会話(セッション)を開始して、2回目以降のメッセージを受信した場合の処理です。 対話の回数 session.count をインクリメントして、最終更新時刻 sesion.last を更新します。 この最終更新時刻は、セッションの終了を判定するために利用します。

// セッション更新

function sessionUpdate(session, callback) {
console.log("セッション UPDATE");
date = new Date();
session.last_unixTime = date.getTime();
session.count++;
dbSession.insert(session,session.user_id, function(err, res) {
callback(err,session);
});
}


セッション終了 タイムアウト処理

セッションの開始は、初めてのメッセージが到着することが切欠となるのですが、セッション終了は明確なものが無く、会話が途切れてから、十分な時間が経過して、忘れた頃がセッションの終了となるはずです。 そこで、最終更新時刻を定期的に確認しておき、十分に時間が経過したものから、セッション情報を削除します。

次にスニペットは、定期的にセッションを削除するための処理を実行するタイマーをセットするものです。 PruningInterval が定期的に実行する間隔時間です。 この値がタイムアウトすると、sessionPruning() が実行され、更新されていないセッション情報を削除していきます。

// 放置されたセッションを削除する

const SessionTimeOut = 60*60*48; // 2 days
const PruningInterval = 60*60; // by 1 hour
var timer = setTimeout( function() {
date = new Date();
sessionPruning(date.getTime() - 1000 * SessionTimeOut);
}, 1000 * PruningInterval);


JavaScript コールバック関数

なんで Node.js なんですか? Javaじゃダメですか? と言われる事があるのですが、Watson関連のRedBook の殆どのサンプルコードが、Node.js となっているので、マスターしておくと良いと思います。 Node.js が嫌いというエンジニアさんは、ほとんど100パーセント ノンブロッキングI/O と コールバック地獄が嫌だという方では無いかと思います。私も最初は、プログラムの実行の流れが把握できず、びっくりしました。 でも、理解してしまえば、極めて効率の良い実行モデルなので、利用価値が高いと思います。

独立の記事として、Node コールバック関数とノンブロッキングI/O の動作 に、サンプルコードを書いて試した内容をまとめていますから、参照いただけると、Node.js の特殊な動きの理解に役立つと思います。


ローカルとBluemix をサポートする実装

Bluemixがいくら便利といっても、コードを追加したり修正するたびに、デプロイする必要があれば、とても時間がかかり、生産性は良くありません。この課題に対して、CloudFoundryは、コードの開発と単体レベルのテストは、ローカルまたは仮想サーバー環境で実施する様にサポートしています。参考資料(1)(2)(3)のcfenvを利用することで、その内容を知ることができます。

次のスニペットは、Bluemix上では、VCAP環境変数からサービスを利用するための情報を取得し、ロカール環境では、JSON形式のファイルから取得するものです。 下記のわずかなコードで、実現することができます。


sessionCtrl.js

// ローカルVCAP設定と資格情報の読込み

const cfenv = require("cfenv");
var vcapLocal;
try {
vcapLocal = require("./vcap-local.json");
console.log("Loaded local VCAP", vcapLocal);
} catch (err) {
console.log(err.message);
}
const appEnvOpts = vcapLocal ? { vcap: vcapLocal} : {}
const appEnv = cfenv.getAppEnv(appEnvOpts);

次のスニペットは、ローカル環境からBluemixのサービスを利用するためのJSONファイルです。 CLIまたはポータルからサービス資格情報を取得して、セットします。


vcap-local.json

{

"services": {
"cloudantNoSQLDB": [
{
"credentials": {
"username": "",
"password": "",
"host": "",
"port": 443,
"url": ""
},
"label": "cloudantNoSQLDB"
}
]
}
}


コードの入手

今回のコードは、次のGitHub に登録してあります。 今回用にブランチを切ってあり、 git clone -b multi http://xxx とすることで、セッション管理機能が追加されたコードをローカルにコピーすることができます。

Bluemix へのプロビジョニング方法は、上記のREADME.md に説明してあります。


まとめ

今回は、チャットボットのプログラムの基本構造を設計する作業を実施しました。 次は、Watson Natural Language Classifier (NLC) を使って受けたメッセージの意味判別して、簡単な返事を返す部分に進めていきたいと思います。


参考資料

(1) cfenv - easy access to your Cloud Foundry application environment https://github.com/cloudfoundry-community/node-cfenv

(2) npm cfenv https://www.npmjs.com/package/cfenv

(3) Bluemixの資格情報取得にはcfenvを使おう http://qiita.com/blackaplysia/items/a354826520b92d85f63f