##はじめに
前回のポケモンBotでGo - Botからグラフデータベース (Grakn.ai) に問い合わせてみる(クエリ編)ではグラフデータベース(Grakn.ai)側での作業としてポケモンBotで使用するデータの整備やクエリの準備を行ったが、今回は実際にボットと繋げるあたりについて書きたいと思う。とは言ってもLINEとDialogflowを使ったボットの情報は沢山あるので、今回は特にwebhook側、特にGraknと繋げる上で必要な設定やNode SDKの使用例について主に記載する。
##今回のお題
尚、既出にはなるがポケモンBotの対応シナリオ及び各種ライブラリのバージョンは以下の通り:
####シナリオ
- ポケモンの身長/体重を答える
- 特定の弱点(2つ指定)をもつポケモンを挙げる。
- ポケモンの進化について答える
####各種ライブラリバージョン
- Grakn:0.17.1
- Node:6.10.1
- express:4.16.2
- body-parser:1.18.2
- socket.io:2.0.4
##インテグレーションの全体像
今回はLINEとDialogflowは直接繋げ、DialogflowでIntent(何を聞きたいか)と質問に必要な情報を収集した上、webhookを通してウェブサービスに問い合わせる形をとる。Cloud Functions for Firebaseは今回使用しない。HerokuにてホスティングするNodeのRest APIサービスをwebhookの接続先として用意する。
Graknは別途AzureにあるUbuntuのVMにてホスティングしており、こちらはGraknが提供するNode SDKを使用してHerokuからアクセスする。
##Dialogflow側
####Entity
Entityは二種類。PokemonとPokemon_Type。
前回の投稿で記載した通り、今回のサンプルデータには第一世代にあたる151体のポケモンデータが格納されている。本来であればこれら151体分はEntityとして登録し、多少のゆらぎをカバー(「ピカチュー」で「ピカチュウ」をヒットさせる)必要があるのだが、今回は30体分程度に抑えた。また、マスタの二重管理を避けるためにもここは最低限の登録数で充分な精度を保ちたいところ。(まぁ、全部登録するのが面倒だったのも当然あるのだが。)これでもある程度知らないポケモンでも文脈から「ポケモンだろう」と推測してくれる。
一方、ポケモンタイプはポケモンずかんに記載のある通り全てその通り登録した。実はサンプルデータにはポケモンずかんにも記載のない「Unknown」と「Shadow」というタイプ定義があったのだが、実際サンプルデータにこれらタイプを参照しているものは無かったので無視する。ゆらぎや「かな/カナ/漢字」をエンティティでカバーするのは大事だが、今回は割愛。
ちなみに、Entityの定義は案外一般ユーザーが「このBotって結構賢い」と思う重要な要素であり、またEntityの判別精度が上がるとIntentの判定精度ももあがるので本来は労力を惜しんではいけないところ。Intentが上手く特定出来ないときに、Entityを整理したり増強したりすることで解消する事も多い。
####Intent
Intentはシナリオ3つに対して4つ用意。WeightとHeightは1つに纏めることも可能だったが、多少精度に問題が出たのと、あとここは頑張って纏めるメリットが薄いところなので今回は分けて定義した。このあたりの柔軟性はIntent定義では重要で、何をおいてもDialogflowのIntent判定精度が低ければエージェントは機能しない。
実際のIntent判定の具合や問題点を確認しながらIntentを詰めていく作業は非常に大事で、作業前の仕様通りで上手く判定できない時には思い切って構成を変えるべき。この「結果オーライ」アプローチはDialogflowの精度や見えない仕様に依存する多少危険なアプローチではあるが、機械学習的なパッケージを扱う上では避けて通れないグレーエリアに則っているので妥当だ、と思っている。
今回は全てのシナリオを通してPokemonInquiryというContextを指定している。これはポケモン名が既出の場合に2度聞かないようにする為の配慮。(「ピカチュウの高さは?」に答えた後に「重さは?」と聞くとピカチュウの事だと判断)
今回Intentの例が少ないが、ここは常に頑張るべきところ。ほとんど同じような文言であっても列挙することでIntentの判定精度は上がる。これは時折不思議に思う事もあるんだが、経験上数は多ければ多いだけ精度が上がる。
全てのIntentではFulfillmentは当然Onにしておく
slot-fillingは使わない。回答は全てNodeサーバー側で定義することとした。賛否両論あると思うが、原則この方法を周到している。その理由はサーバー側で定義すると:
- チャット(CUI)と音声(VUI)で回答内容を変えれる。(音声の場合もっと短くする)
- カルーセル等チャット側のリッチレスポンスを使用する場合に特定フォーマットのJSONが要求される。
ダイアログに関する定義がDialogflowとwebhook側のサーバーに分散されるのは正直あまり気持ちの良いものではないが、あまりDialogflow側で何でもしようとする事もリスク。ものすごくざっくり言うと、webhookを通るダイアログの返答はwebhook側ですべて制御した方が裏目に出たときに詰まない。
Action名は大事で、この文字列を元にサーバー側でIntentのマッピングを行う。1つのエージェントに対して1つのwebhookしか登録できない。当然1つのサービスでそのエージェントのIntentを全て判定しないといけない。
文字列としての分かりやすさより一意となる工夫と曖昧さの排除を優先すべき。
##webhook側
まずはHerokuにサービスを作成。
npm init --yes
npm install --save --save-exact express socket.io body-parser grakn
git init
git add .
git commit -m "Initial"
heroku create pokemon-graph-bot
heroku ps:scale web=1
####Grakn Node SDK
Grakn Node SDKといってもGraknのREST APIを呼ぶ上でのユーティリティレイヤーなので構造はいたってシンプル。ソースはここ (GitHub)にあるが、30行に満たない。GitHub上ではREADMEには「Grakn 0.18が必要」とあり実際のソースには「Grakn 0.17対応」と書いてあるが、どちらでも動く。確認していないが、コードを見る限り0.16以前でも動きそう。
以下でクライアントインスタンスを生成する:
// external libraries
const Grakn = require('grakn');
// Grakn Server
const grakn = new Grakn(process.env.GRAKN_URL,process.env.GRAKN_KEYSPACE);
...
クライアント生成時にはGraknサーバーのURLとキースペースの定義しか出来ないが、デフォルトでInference RuleはOn、Materialize Inference(ルールから導き出されるRelationshipを永続化する)はOffとなっている。ほとんどの使用ケースではこの定義で問題ない。実際0.18からGrakn本体でもInference RuleのデフォルトはOnとなっている。また、マテリアライズすると重くて現時点では実用に耐えられない。マテリアライズについては深く調べていないが、クエリを投げるだけでも重さは実感できる。
ちなみにクライアントはPromiseを返す。
Graph Processor
グラフへのアクセス箇所の例は以下:
...
exports.get_pokemon_height = (pokemon) => {
return new Promise((resolve, reject) => {
//Grakn query - Finding illness names matching a parameter.
let query_get_pokemon = `match ` +
`$pokemon isa pokemon has name_jp "${pokemon}" ` +
`has height $pokemon_height;` +
`get $pokemon_height;`;
console.log('query:' + query_get_pokemon);
grakn.execute(query_get_pokemon).then((res) => {
resolve(get_attribute(res, 'pokemon_height'));
});
});
}
...
クエリはmatchもinsertも同じくexecute
によって実行する。
先程Inference RuleはデフォルトOn、Materialize InferenceはOffとなっていると書いたが、このexecute実行時に引数に指定する事により変更する事が出来る。例えばInferenceをOffにしたい場合は
...
grakn.execute(query_get_pokemon, false).then((res) => {
resolve(get_attribute(res, 'pokemon_height'));
});
...
とすれば良い。true, true
とすればマテリアライズもOnになる。
####Server
Graph Processorを呼び出すServer側も至って単純だが、一応参考の為抜粋して記載する:
// external libraries
const express = require('express');
const bodyParser = require('body-parser');
// user defined modules
const Graph_Processor = require('./graph_processor');
// Dialogflow Actions
const ACTION_INQUIRY_HEIGHT = 'pokemon.height';
const ACTION_INQUIRY_WEIGHT = 'pokemon.weight';
const ACTION_INQUIRY_WEAKNESS = 'pokemon.weakness';
const ACTION_INQUIRY_EVOLUTION = 'pokemon.evolution';
//server configuration
const server = express();
server.use(bodyParser.urlencoded({
extended: true
}));
server.use(bodyParser.json());
server.post('/bot_endpoint', (req, res) => {
console.log('inside webhook. Before processing.');
let pokemon = req.body.result.parameters['pokemon'];
let type1 = req.body.result.parameters['type1'];
let type2 = req.body.result.parameters['type2'];
console.log('pokemon:' + pokemon);
console.log('type1' + type1);
console.log('type2' + type2);
let answer = null;
if (pokemon != null) {
Graph_Processor.isa_pokemon(pokemon).then((resut) =>{
if(result.length == 0) {
answer = `${pokemon}という名前のポケモンは見つかりませんでした。` +
`もしかしたら私が知らないポケモンかも知れません…`;
}
return return_response(req, res, answer);
});
}
if (req.body.result.action == ACTION_INQUIRY_HEIGHT) {
console.log(`inside ${ACTION_INQUIRY_HEIGHT}`);
let pokemon = req.body.result.parameters['pokemon'];
Graph_Processor.get_pokemon_height(pokemon).then((result) => {
let answer = '';
console.dir(result);
if (result != null) {
answer = `${pokemon}の高さは${result * .1}メートルです。`;
} else {
answer = `ごめんなさい。${pokemon}の高さが分かりませんでした…`;
}
console.log("answer:" + answer);
return return_response(req, res, answer);
});
} else if (req.body.result.action == ACTION_INQUIRY_WEIGHT) {
...
サンプルとしての可読性を考慮してクラアントへの返答もここに書き込んでいるが、本来webhookのエンドポイントとなるサーバーでは多種多様なfulfillmentコールを一手に受け持つ必要性が出てくるので、ここでは純粋にトラフィックコントローラーの役割だけさせるのが正しい。ダイアログの返信文言をwebhook側にも保存するとして、であればプロダクション環境ではもう少し賢い管理方法が望ましい。
ちなみに、身長と体重にはGraknからの戻り値 x 0.1としているが、これは何故かデータが x 10として登録されているからである。(おそらく単純にlongとして値を保存したかっただけだとは思うが)一応ポケモン図鑑に対していくつかランダムチェックを行ったが、特にルールはなく単純に値が10倍されているだけだった。
あれ、Contextからポケモン名を取るところが出来てない… これは後で調べよう。
##さいごに
今回はGraknの紹介も兼ねてBotを作ってみたが、今後はプロセスをGraknでモデリングしたり、ユーザーエクスペリエンスをグラフという形で格納して分析したり、「リレーショナルでは出来ないグラフ」の真価を、しかもより直感的に扱えるプラットフォームとしてGraknを調査したいと思う。
少しでも興味を持っていただけると幸いである。