LINE Messaging API と Oracle Digital Assistant を併用して LINE から呼び出せるチャットボットを構築する

以前、LINE Messaging API を用いて、オウム返しするだけのボットを作った。

また、Oracle Digital Assistant というモノを使うと、ユーザの発言を解釈して複雑な会話フローを実現できることを学んだ。

今回は、これら2つを連携して、Digital Assistant のフロントエンドに LINE アプリを使ってみる。

目次

前提

Digital Assistant Channel を用意する

まずは、作成してある Skill を外部から呼び出せるよう、Channel というモノを作る。

「Oracle Digital Assistant Designer UI」画面に移動したら、ハンバーガーメニュー → Development → Channels と進み、「+ Channel」ボタンからチャネルを新規作成する。

この内容で「Create」ボタンを押下する。

次の画面で、「Route To」項目から、作成した Skill を選択しておく。

ココまで出来たら、この画面に表示されている「Secret Key」と「Webhook URL」の情報を控えておく。

Digital Assistant 連携用のライブラリをインストールする

以降は Express サーバの実装拡張。

まずは Digital Assistant と連携するためのライブラリをインストールする。Oracle 公式が出している、@oracle/bots-node-sdk というパッケージを使う。

$ npm install -S @oracle/bots-node-sdk

LINE から受信したメッセージを Digital Assistant Channel に連携する

続いて、LINE からメッセージを受信するパスの処理。ココで、LINE から受け取ったメッセージを、先程作った Digital Assistant の Webhook URL に向けて送信しよう。

const express = require('express');

// Express サーバを生成する
const app = express();

// LINE からのメッセージ受信部分
app.use('/callback', require('./line-router'));

// サーバを起動する
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Listening on ${port}`);
});
const express = require('express');
const line = require('@line/bot-sdk');
const OracleBot = require('@oracle/bots-node-sdk');

// ルータ・モジュールを作成する
const router = express.Router();

// LINE クライアントを生成する
const client = new line.Client({
  channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.LINE_CHANNEL_SECRET
});

// Digital Assistant クライアントを生成する
const digitalAssistantClient = new OracleBot.Middleware.WebhookClient({
  channel: {
    url: process.env.DIGITAL_ASSISTANT_WEBHOOK_URL,
    secret: process.env.DIGITAL_ASSISTANT_WEBHOOK_SECRET
  }
});

// [/callback] : LINE からのメッセージ受信部分
router.post('/', line.middleware(config), (req, res) => {
  Promise.all(req.body.events.map((event) => {
    return handleEvent(event);
  })
    .then((result) => {
      res.status(200).json({}).end();
    });
});

/**
 * イベント1件を処理する
 * 
 * @param {*} event イベント
 * @return {Promise} テキストメッセージイベントの場合は client.pushMessage() の結果、それ以外は null
 */
function handleEvent(event) {
  // メッセージイベントではない場合、テキスト以外のメッセージの場合は何も処理しない
  if(event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }
  
  // LINE ユーザ ID
  const userId = event.source.userId;
  // LINE から受信したテキスト
  const text = event.message.text;
  
  // Digital Assistant に送信するメッセージを組み立てる
  const message = {
    userId: userId,
    messagePayload: OracleBot.Lib.MessageModel.textConversationMessage(text)
  };
  
  // Digital Assistant に送信する
  return digitalAssistantClient.send(message);
}

module.exports = router;

こんな感じ。

Digital Assistant における会話セッションの特定には、message.userId プロパティが使用される。ココに LINE のユーザ ID (ユーザが普段目にするモノとは別の ID) を指定すれば OK。送信するメッセージは messagePayload プロパティに渡すが、ココに設定するオブジェクトの組み立て方が分かりづらいので、@oracle/bots-node-sdk が提供する textConversationMessage() というメソッドを使う。

あとは @oracle/bots-node-sdk から生成した WebhookClientsend() メソッドでメッセージを送れば OK。

Digital Assistant Channel からの応答を受け取り LINE Push API で返信する

コレで LINE → Express サーバ → Digital Assistant という流れが組み立てられた。Digital Assistant の Webhook Channel は、設定画面で設定した「Outgoing Webhook URI」に対して POST リクエストを投げることで、応答メッセージを伝えてくる。コレを受け取って、LINE に向かって返信してやれば良いワケだ。

// …前略…

// LINE からのメッセージ受信部分
app.use('/callback', require('./line-router'));

// ↓以下を追加
// Digital Assistant からの応答メッセージ受信部分
app.use('/outgoing', require('./digital-assistant-router'));

// …後略…
const express = require('express');
const OracleBot = require('@oracle/bots-node-sdk');

// LINE クライアントを生成する
const client = new line.Client({
  channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN,
  channelSecret: process.env.LINE_CHANNEL_SECRET
});
// Digital Assistant クライアントを生成する
const digitalAssistantClient = new OracleBot.Middleware.WebhookClient({
  channel: {
    url: process.env.DIGITAL_ASSISTANT_WEBHOOK_URL,
    secret: process.env.DIGITAL_ASSISTANT_WEBHOOK_SECRET
  }
});

// ルータ・モジュールを作成する
const router = express.Router();
// Digital Assistant 用のパーサを組み込む
OracleBot.init(router);

// [/outgoing] : Digital Assistant からの応答メッセージ受信部分
router.post('/', digitalAssistantClient.receiver());
// Digital Assistant から応答メッセージを受信した時の処理 : 応答メッセージを処理して LINE に返信する
digitalAssistantClient.on(OracleBot.Middleware.WebhookEvent.MESSAGE_RECEIVED, (message) => {
  // LINE ユーザ ID
  const userId = message.userId;
  // Digital Assistant からの応答メッセージ
  const text = message.messagePayload.text;
  
  // LINE に返信するメッセージを組み立てる
  const message = {
    type: 'text',
    text: text
  };
  
  // Push API で返信する
  lineClient.pushMessage(userId, message);
});

// Digital Assistant との処理エラー時 : コンソール出力する
digitalAssistantClient.on(OracleBot.Middleware.WebhookEvent.ERROR, (error) => {
  console.error(error);
});

module.exports = router;

こんな感じ。

特徴的なのは、OracleBot.init() 関数。line.middleware() と同様に、リクエストヘッダに含まれる署名の検証や JSON パースなどを行う Express ミドルウェアを挟んでくれる。

公式のドキュメントでは、以下のように記述されている。

const app = express();
OracleBot.init(app);

しかし、Express サーバ全体に init() 処理を適用してしまうと、LINE 用の /callback ルータ側にもミドルウェアが設定されてしまい、LINE からのメッセージが正しく処理できなくなってしまう (TypeError: Data must be a string or a buffer といったエラーが発生する)。

そこで、express.Router() でルータ・モジュールを分割し、Digital Assistant からの受信部分であるこのルータ・モジュールにのみミドルウェアを設定するようにした。

ルーティング定義である router.post() 部分には WebhookClient#receiver() を挟んでいるだけ。Digital Assistant の Webhook に関する動作は、全て WebhookClient#on() メソッドで定義した各種イベントのコールによって実行される。つまり、LINE へ返信する実処理は WebhookClient#on(MESSAGE_RECEIVED) イベントで定義した関数に実装する。

Webhook イベントはこの他に、OracleBot.Middleware.WebhookEvent.MESSAGE_SENT というモノがある。/callback ルーティングにて、WebhookClient#send() を実行した後、Webhook Channel への送信が成功したことを知らせるイベントだ。適宜設定すると良いだろう。

LINE への返信には Push API を使っている。コレは、Digital Assistant にメッセージを送り、応答メッセージを受け取るという流れの中で、「リプライトークン」を保持しておけなかったため。受信したメッセージデータをどこかに蓄えておき、Digital Assistant から応答メッセージを受信したタイミングで、メッセージデータからリプライトークンを抜き取って使用する、みたいな機構が作れれば、Reply API でも返信できるだろう。しかし、何らかの DB が必要になる他、同一ユーザからの連続した投稿などに対応しきれなさそうなので、Digital Assistant における会話セッションを作るために指定した「LINE ユーザ ID」をそのまま利用して、Push API で返信するように実装した。

デプロイして動作確認

という実装ができたら、このサーバアプリを Heroku などにデプロイしよう。

それぞれのパス指定に誤りがないか、よく確認すること。いずれも HTTPS でないと通信できない。

デプロイと設定ができたら、LINE トーク画面から Digital Assistant を呼び出せそうな文言を投稿してみよう。Digital Assistant を経由して、何らかの返信が得られれば OK だ。

諸課題

LINE、Oracle Digital Assistant ともに、イマイチ API 仕様が明らかでなかったり、一応のドキュメントはあるものの物凄く分かりづらかったりする。ココまでで分かっている「課題」と思われる事項を挙げておく。

インテント解析のために日本語の投稿を翻訳する

Digital Assistant のインテント解析において、日本語をサンプルフレーズに使えない (精度が低く不安定) ことは以前の記事でも触れた。

現状のインテント・エンジンは英語と中国語くらいにしか対応していないようで、それ以外の言語を使う際は、Google か Microsoft の翻訳 API サービスを組み込んであげないといけない。

インテント・エンジンは、単語の出現頻度くらいしかチェックしていないようなので、「do」と「do not」のように意味が逆転するような言葉が出てこない限り、機械翻訳の精度で十分と思われる。特定の日本語を翻訳する際に、対応する英語の類語をいくつかサンプルフレーズに入れてあれば問題ないだろう。

翻訳 API サービスは、Skill の設定画面から組み込んで使用できるが、別の方法として、LINE からのメッセージを受け取る Express サーバ内で、特定の日本語文言を受け取ったら対応する英語のフレーズに置換して Digital Assistant に送信する、というやり方も考えられる。

Digital Assistant からの応答メッセージを整形する

Digital Assistant からの応答メッセージに System.List コンポーネントを使った選択肢提示なんかを組み込んだ場合。上述の実装だと、ユーザは選択肢が分からず、正しく答えられないだろう。

選択肢情報は、message.messagePayload の中に、actions プロパティとして格納されていたりする。

digitalAssistantClient.on(OracleBot.Middleware.WebhookEvent.MESSAGE_RECEIVED, (message) => {
  // 応答メッセージ本文
  let text = text = message.messagePayload.text;
  
  // 選択肢があったら、改行と中黒 (箇条書き記号) を付与して本文に繋げる
  message.messagePayload.actions.forEach((action) => {
    text += `\n・${action.label}`;
  });
});

こんな風にすれば、

あなたの性別は?
・男性
・女性

といったテキストメッセージを LINE に返信できたりはする。

だが、実際は Flex Message なんかを使ってリッチに見せたかったりすると思う。

今のところ、Digital Assistant からの応答メッセージを機械的に Flex Message なんかに変換する方法はないので、

といった実装にするしかないだろう。

ココらへん、良い方法があれば教えてほしい。

なお、Webhook Channel 作成時の「Channel Type」で「Facebook Messenger」が選べるとおり、Facebook Messenger をフロントに使う場合は良い感じに応答メッセージが整形されるっぽい。LINE に関してもこういう対応をしてくれたら嬉しいのだが、ゴリゴリの米国企業が日韓ぐらいでしかシェアのないアプリに対応してくれることはまずないだろうな…。

以上

ひとまずこんな感じで、LINE トーク画面から Oracle Digital Assistant を使う実装ができた。