Line
Watson
ibmcloud
linebot
chatbot
IBM CloudDay 13

IBM Cloud Lite で LINE と Watson Assistant (旧Conversation) の連携よる接客チャットボットをつくってみた

このエントリは、IBM Cloud Advent Calendar 2017 13日目の記事です。

変更点

  • Watson Conversation --> Watson Assistant (本文テキストのみで,コードサンプルは変更していません)
  • LINE flex スタイルの追加

はじめに

チャットボットの会話エンジンとして Watson Assistant を活用し、フロントエンドに LINE を用いて連携させた、とあるレストランの接客チャットボットを作成します。今回の例は、2017年11月から提供がはじまった IBM Cloud ライト(Lite)・アカウント で無料でも作成することができます。ぜひお試しください。

作成するチャットボットのイメージ

単純なテキストメッセージだけでなく、LINE の Messaging API でサポートされているいろいろなテンプレートを活用して、チャットボットの返答を作成したいと思います。

また、一問一答形式のやりとりだけでなく、Watson Assistant で作成した対話ツリーを LINE からの入力でたどっていけるように、会話の状態 (context) を LINE と Watson Assistant の間で Cloudant データベースを用いて共有できるようにします。

画面例1 画面例2
LINE screen 1 LINE screen 2

今回紹介する LINE と Watson Assistant 間のデータフローは次のようになります。

データフロー

# 内容
1 ユーザーがLINE アプリからテキストを入力
2 LINE サーバから、webhook に登録した URL が呼ばれる
3 最新のコンテキストがある場合は、コンテキストをDBからロード後付与する
4 Watson Developer Cloud API で Watson Assistant サービスにメッセージを送る
5 Watson Assistant サービスから返答がくる
6 コンテキストを DB に保存
7 LINE Messaging API を用いて返答を送出
8 ユーザの LINE アプリ上で表示

用意するもの

サービスのアカウント

  1. LINE@ MessagingAPI のアカウント 作成方法はこちらなどを参考に https://qiita.com/yoshizaki_kkgk/items/bd4277d3943200beab26
  2. IBM Cloud ライト・アカウント 作成方法はこちらなどを参考に https://qiita.com/tokida/items/13408a1d4230608f182f

使用するクラウドサービス

  1. LINE@ MessagingAPI (LINE)
  2. SDK for Node.js (IBM Cloud, Node.js Cloudant DB Web Starterに含まれます)
  3. Cloudant NoSQL DB (IBM Cloud, 同上)
  4. Watson Assistant (IBM Cloud)

ツール類

  1. Bluemix CLI
  2. Node.js ランタイムおよび NPM -こちら などから入手できます
  3. 開発環境 (Visual Studio Codeなど。IBM Cloud 用のエクステンション(英語ページ)もあります)

LINE を用いるにあたり、Watson Developer Cloud には Botkit Middleware という、いろいろなチャットサービスに接続するための Node.js アプリ集があるのですが、記事初回公開時点(2017年12月)では LINE のサポートがないため、LINE 自身が提供する Node.js 用 BOT SDK のサンプルを改変しながら実装をしてみます。

IBM Cloud ライト・アカウントには、サービスの料金プラン「ライトプラン」が用意されています。サービスによってはいくつが制限がかけられていることがあります。今回紹介する例では、この制限の範囲内でもきちんと動作することを確認しています。

おおまかな手順

  • IBM Cloud ライト・アカウントで IBM Cloud にログインし、Node.js Cloudant DB Web Starter でアプリのひな形を作成します。アプリ名(例: linewatsonconvdemo) のほかはデフォルトのまま(ドメイン: mybluemix.net, デプロイする地域の選択: 米国南部, 組織の選択: メールアドレス, スペースの選択: dev)で指定し、作成をクリックします

  • IBM Cloud ダッシュボードで Watson Assistant サービスを追加し、の会話フローを作成します (のちほど、次節の「Watson Assistant の会話データに LINE の選択肢をどう挿入するか」を参考に、LINE で選択肢を表示するための工夫を入れ込みます)

  • LINE@ MessagingAPI を有効化し、アクセストークン(ロングターム)Channel Secret を生成します

  • Node.js 用の Cloudant SDK をインストールします (package.json は自動更新)

$ npm install cloudant --save

  • こちらのGithub リポジトリから、LINE 自身が提供する Node.js 用 BOT SDK のサンプルをチェックアウトします

    $ git clone https://github.com/line/line-bot-sdk-nodejs

  • examples/echo-bot の例をベースに、index.js, package.json をコピーします。既存のアプリケーションコードに追加する場合の注意点としましては、body-parser が使われているとアクセストークン等の認証が失敗することがあります。body-parser を用いたサーバーコードとは分けておくほうが無難です。

body-parserを除去またはコメントアウト
//var bodyParser = require('body-parser')
//
//// parse application/x-www-form-urlencoded
//app.use(bodyParser.urlencoded({ extended: false }))
//
//// parse application/json
//app.use(bodyParser.json())
  • manifest.yml を用意します
manifest.yml
applications:
- path: .
  memory: 256M
  name: <appname> (IBM Cloud 上で作成時に指定したもの)
  host: <appname>
  services:
  - <appname>-cloudantNoSQLDB
  env:
    CHANNEL_ACCESS_TOKEN: <LINE Messaging API のアクセストークン>
    CHANNEL_SECRET: <LINE Messaging API のシークレット>
    CLOUDANT_URL: <Cloudant DB の URL>
    CONVERSATION_USERNAME: <Watson Assistant の username>
    CONVERSATION_PASSWORD: <Watson Assistant の password>
    CONVERSATION_WORKSPACE: <Watson Assistant のワークスペースID>

  • Webhook URL を決定します

    (例) https://<appname>.mybluemix.net/api/linewebhook

  • index.js の中で、Webhook URL を指定します

index.js
// register a webhook handler with middleware
// about the middleware, please refer to doc
app.post('/api/linewebhook', line.middleware(lineConfig), (req, res) => {
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result));
});
  • LINE@ の Channel 基本設定ページで、この Webhook URL を指定します

line-webhook.png

  • Node.js 用のLINE SDKをインストールします (package.json は自動更新)

$ npm install @line/bot-sdk --save

  • アプリをデプロイします。 IBMid は IBM Cloud ライト・アカウント作成時に指定した ID です

$ bluemix login -a api.ng.bluemix.net -u <IBMid> -o <IBMid> -s dev
API endpoint: https://api.ng.bluemix.net

Password> <ここにパスワードを入力します>
Authenticating...
OK

$ bluemix app push <appname>

  • echo-bot の動作確認。LINE アプリからテキストメッセージを送って、それがそのまま返ってくる(echo back)することを確認します。ここまでで、LINE と IBM Cloud がつながることを確認できます。

  • Watson Developer Cloud SDK をインストールします (package.json は自動更新)

$ npm install watson-developer-cloud --save

  • Watson Developer Cloud API を使うコードを挿入します
WatsonDeveloperCloudAPI
var ConversationV1 = require('watson-developer-cloud/conversation/v1');

var watsonConfig = {
  username: process.env.CONVERSATION_USERNAME,
  password: process.env.CONVERSATION_PASSWORD,
  version_date: ConversationV1.VERSION_DATE_2017_05_26  
};

var conversation = new ConversationV1(watsonConfig);
  • Echo サンプルを拡張して、Response のフックを作成します (次節の「Cloundat DB にコンテキストをどう保存して取り出すか」を参考に、イベントハンドラに LINE Messaging API, Cloudant, および Watson Developer Cloud API を用いたコードを入れます
イベントハンドラ全体
function handleEvent(event) {

  if (event.type === 'message' && event.message.type !== 'text') {
    // ignore non-text-message event
    return Promise.resolve(null);
  }

  var responsemsg = 'default message';
  return new Promise(function(resolve, reject){

    // Cloudantからロードするコード ここから
    var lastcontext = {};
    var params = {reduce:true,group:true,keys:[event.source.userId]};
    mydb.view('docs','byUserId',params,function(err1,body1){
      if (!err1) {
        var lasttimestamp = 0;
        if (body1.rows.length==1) {
          lasttimestamp = body1.rows[0].value.max;          
        }
        var selector = {userId:event.source.userId,timestamp:lasttimestamp};
        mydb.find({selector:selector},function(err2,result2){
          if (err2) {
            throw err2;
          }
          if (result2.docs.length==1) {
            lastcontext = result2.docs[0].context;
          }
          // Cloudantからロードするコード ここまで

          if (event.type === 'message') {
            if (is_conversation_start(event.message.text)) {
              lastcontext = {};
            }
            // Watson Assistant に入力テキストを転送
            conversation.message({
              input: { text: event.message.text },
              context: lastcontext,
              workspace_id: process.env.CONVERSATION_WORKSPACE
            }, function(err3,response3) {
               if (err3) {
                 reject(err3);
                 return;
               } else {
                  // Cloudant に保存するコード ここから
                  if (response3.context) {
                    mydb.insert({timestamp:event.timestamp, userId:event.source.userId, 
                      context:response3.context},
                        function(err4,body4){
                          if (!err4) {
                            console.log('context inserted to '+dbName);
                          }
                    });                      
                  }
                  // Cloudant に保存するコード ここまで

                  // line_templateの挿入 ここから
                  var responsemsg = {};
                  if (response3.output.line_template) {
                    responsemsg = {
                      type: 'template',
                      altText: 'template alt text',
                      template: response3.output.line_template
                    };
                  } else if (response3.output.line_flex) {
                      responsemsg = {
                        type: 'flex',
                        altText: 'flex alt text',
                        contents: response3.output.line_flex
                      };
                  } else {
                    var tmsg = response3.output.text.join('\n');
                    responsemsg = {
                      type: 'text',
                      text: tmsg
                    };
                  }
                  // line_templateの挿入 ここまで

                  // LINE に replyMessage を送信
                  var rr = client.replyMessage(event.replyToken, responsemsg);      
                  resolve(rr);
               }
            });
          }
        });
      }     
    });
  });
}
  • Watson Assistant の context 情報を保存するために、Cloudant DB "mydb" にインデックスを追加します 新規に "docs" というドキュメントを追加し、そのあと Search Indexes を2つ("timestamp" と "userId")追加します。
docs/timestamp
function (doc) {
  index("timestamp", doc.timestamp);
}
docs/userId
function (doc) {
  index("userId", doc.userId);
}

続いて、"docs" に "byUserId" という名前で View を作成し、Reduce を _stats に設定します。

docs/byUserId
function (doc) {
  if (doc.userId && doc.timestamp) {
    emit(doc.userId, doc.timestamp);
  }
}
  • Cloudant DB にアクセスするコードがない場合は、以下コードを参考に挿入します
CloudatDBへ接続
var Cloudant = require('cloudant');
var cloudant = Cloudant({url:process.env.CLOUDANT_URL});

var dbName = 'mydb';

cloudant.db.create(dbName, function(err, data) {
  if(!err) //err if database doesn't already exists
    console.log("Created database: " + dbName);
});

var mydb = cloudant.db.use(dbName);
  • アプリを再度デプロイします。

$ bluemix app push <appname>

  • 動作確認をします。 LINEアプリから Watson Assistant の会話ツリーにあわせてテキストメッセージを送信して、期待どおりの返答が返ってくるか確認します

主な考慮点

ここが、本エントリの核心の部分になります。

Watson Assistant の会話データに LINE の選択肢をどう挿入するか

LINEが解釈できるJSONデータを、ノード内に組み込みます。ダイアログエディタではサポートされていないので、Open JSON Editor ボタンで直接 JSON を編集します。
"output" が、Watson から返答が送られるテキストなどが入る部分です。ここの "text" の兄弟として、"line_template" または "line_flex" というオブジェクトを追加します。中身の書式は、LINE Messaging API で使うデータそのままです。

JSONエディタの内容
"output": {
  "text": {
      "values": ["いらっしゃいませ。ランチとディナーどちらのご注文でしょうか?"],
      "selection_policy": "sequential"
  },
  "line_template": {
    "text": "いらっしゃいませ。ランチとディナーどちらのご注文でしょうか?",
    "type": "buttons",
    "actions": [{
       "text": "ランチ",
       "type": "message",
       "label": "ランチ"
      }, {
       "text": "ディナー",
       "type": "message",
       "label": "ディナー"
      }]
  }
},

各メニューは画像や説明を用いて Flex コンテナをつかって表現することもできます。

flexコンテナの例
  "line_flex":{
    "type":"carousel",
    "contents":[
      {"body": {
         "type":"box",
         "layout":"vertical",
         "spacing":"md",
         "contents":[
          {"size":"xl",
           "text":"Aランチ",
           "type":"text",
           "align":"start",
           "color":"\\#0000ff",
           "weight":"bold"},
          {"type":"separator"},
          {"text":"【内容】Aランチの内容\n【価格】680円",
           "type":"text",
           "wrap":true}]},
       "hero":{
         "url":"<Aランチ画像のURL>",
         "size":"full",
         "type":"image",
         "aspectRatio":"1:1"},
       "type":"bubble",
       "footer":{
         "type":"box",
         "layout":"horizontal",
         "contents":[
           {"type":"button",
            "style":"primary",
            "action":{
              "text":"Aランチ",
              "type":"message",
              "label":"Aランチを選択"}}]}
      },
      { // Bランチの内容  },
      { // Cランチの内容  },
      {"type":"bubble",
        "footer":{
          "type":"box",
          "layout":"horizontal",
          "contents":[
            {"type":"button",
             "style":"secondary",
             "action":{
               "text":"キャンセル",
               "type":"message",
               "label":"キャンセル"}}]},
        "header":{
          "type":"box",
          "layout":"vertical",
          "contents":[{
            "text":"キャンセル",
            "type":"text"}]}}
    ]}

いっぽう、これを Node アプリケーションのなかで認識して取り出す処理をいれるには次のようにします。"output.line_template" があればその中身をセットし、なければ通常のテキストメッセージとします。

line_templateの挿入
var responsemsg = {};
if (response3.output.line_template) {
  responsemsg = {
    type: 'template',
    altText: 'template alt text',
    template: response3.output.line_template
  };
} else if (response3.output.line_flex) {
  responsemsg = {
    type: 'flex',
    altText: 'flex alt text',
    contents: response3.output.line_flex
  };
} else {
  var tmsg = response3.output.text.join('\n');
  responsemsg = {
    type: 'text',
    text: tmsg
  };
}

Cloundat DB にコンテキストをどう保存して取り出すか

Cloudant には、Watson からの返答のうち、タイムスタンプ、LINEのユーザID、返答内の context の組を保存します。

Cloudant に保存するコード
if (response3.context) {
  mydb.insert({timestamp:event.timestamp, userId:event.source.userId,
   context:response3.context},function(err4,body4){
      if (!err4) {
        console.log('context inserted to '+dbName);
        }
  });                      
}

LINEのメッセージを Watson へ転送するときには、ユーザごとに最新のタイムスタンプを取得し、それをもとにコンテキストデータを検索します。

Cloudantからロードするコード
var lastcontext = {};
var params = {reduce:true,group:true,keys:[event.source.userId]};
mydb.view('docs','byUserId',params,function(err1,body1){
  if (!err1) {
    var lasttimestamp = 0;
    if (body1.rows.length==1) {
      lasttimestamp = body1.rows[0].value.max;          
    }
    var selector = {userId:event.source.userId,timestamp:lasttimestamp};
    mydb.find({selector:selector},function(err2,result2){
      if (err2) {
        throw err2;
      }
      if (result2.docs.length==1) {
        lastcontext = result2.docs[0].context;
      }
      ...
    });
  }     
});

会話の終了と初期化

対話フローでは通常「会話の終了」は定義しませんが、Watson Assistant には、会話の開始点と認識する機能があります。(条件名:conversation_start)。
これと同じ状態を Node アプリの中で作り出すことで、会話フローをリセットすることができます。
たとえば、ユーザがLINEから「リセット」や「はじめから」などの特定のキーワードを入力すると、コンテキストを消去(クリア)し、最初のノードからの会話に誘導します。

会話リセットの例
// conversation_start
function is_conversation_start(message) {
  var value = false;
  if (message==='リセット') {
    value = true;
  }

  return value;
}
...

 if (is_conversation_start(event.message.text)) {
   lastcontext = {};
 }
 conversation.message({
   input: { text: event.message.text },
   context: lastcontext,
   workspace_id: process.env.CONVERSATION_WORKSPACE
 },
...

おわりに

今回は、LINE と Watson Assistant サービスを IBM Cloud Node.js アプリを用いて連携させる例を紹介しました。
この例では、会話に登場するメニュー等は Watson Assistant の会話エディタの中でハードコードしていますが、Watson Developer Could API (Conversation/V1) を用いると、updateDialogNode などの API を経由して、会話の中の context や、上記で導入した line_template などのデータの部分を動的に変更することができます。例えば、会話の開始時に、Cloudant DB からメニューのデータを読み込んで対話フローのデータを更新する、といった処理を挿入します。

IBM Cloud ライトアカウントを作成して、チャットボットサービスの構築にチャレンジしてみましょう!

LINE を接点としたチャットボットサービスの開発に本記事がお役に立つことができれば幸いです。