ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

Botkitで出てくるThread(スレッド)の概念を理解しようと頑張ってみる

こんにちは。

チャットボットフレームワークBotkitを使い始めて困惑したことが、自分なりに解決できたので何か書きます。困惑したのはThreadです。

私が最初に知っておけばよかったと思うことは 一度にユーザーに返せるのは1つのThread内のメッセージThreadはネストさせることはできない ということです。

↓こんなことはできない

user: hello
スレッドA -> bot: hello from thread A!
スレッドB -> bot: hello from thread B!

↓これもできない

user: hello
スレッドA -> bot: hello
user: oh
スレッドB -> bot: he
スレッドB -> bot: llo?
user: oh
スレッドA -> bot: bye!

注意: 「コード読めよ」という話ではありますが、理解できなかったので実験した結果です。気がついたことがあればコメントいただければと思います。

準備の話

いじれる環境を準備します。ネットで記事を探すと「slack + botkit」が目につきますが、サーバーに置くのが面倒なので、開発を自分のラップトップ内で完結させるように準備します。(あ、でもBotkit studioというホスティングサービスがあるのでサーバーに置くのはそれほど面倒ではないかもです)

hubotで言うところのadapterみたいなものの中に、consolebotというnodeのreplで動作するものが使用できるので、まずはその設定を行います。

まずはディレクトリを作ってyarn initなどをした後に、必要なライブラリを追加していきます。

$ yarn add --dev babel-cli babel-preset-env // 新し目の記法を使いたい
$ yarn add botkit // 本体

新し目の記法を使いつつ、コンソールで実行したいのでbabel-cliを追加しています。

package.json に次のように起動コマンドを追加します。

+  "scripts": {
+    "start": "babel-node ./index.js --presets env"
// or nodemonを使うともっとストレスがないかもです
+    "start": "nodemon index.js --exec babel-node --presets env"
+  }

動作確認のために何を言ってもHello worldしか言わないボットを作ります。

index.js

import Botkit from 'botkit';

const controller = Botkit.consolebot({ debug: true });
controller.on('message_received', (bot, message) => {
  bot.reply(message, 'Hello world!');
});
controller.spawn();

yarn startを実行するとボットが起動して対話が開始します。ただしボットからは何も言ってこないのでこちらから話しかけてあげてください。「Hello world!」って言ってくれればOKです。

(Botkit.consolebotの引数でデバッグモードにしていますが、あると結構みにくいのでfalseにしてしまってもいいと思います)

Conversationについて

質問や分岐などの複雑なユーザーとのやりとりのためにConversationというものを作成します。ThreadはConversation(に渡したコールバック)の中で使用することができます。ただしまだ使っていません。詳しくはドキュメントを参照ください。

index.js

...
 controller.on('message_received', (bot, message) => {
   bot.reply(message, 'Hello world!')
+  bot.startConversation(message, (err, convo) => {
+    if (err) throw err
+    convo.say('Conversation start')
+    convo.say('Conversation end')
+  })
 })
...

Threadを意図的に間違えて使ってみる

本題です。以下のようにコードを変更しました。自分としてはsayaddMessageで追加した文章が上から順々にコンソールに表示されればいいなーと思ったのですが全然ダメでした。

index.js

import Botkit from 'botkit'

const controller = Botkit.consolebot({ debug: false })

controller.on('message_received', (bot, message) => {
  bot.reply(message, 'Hello world!')
  bot.startConversation(message, (err, convo) => {
    if (err) throw err
    convo.say('Conversation start')
    convo.addMessage('Thread 1', '1')
    convo.gotoThread('1')
    convo.addMessage('Thread 2', '2')
    convo.gotoThread('2')
    convo.addMessage('Thread 3', '3')
    convo.gotoThread('3')
    convo.say('Conversation end')
  })
})

結果的に表示されたのは以下の文章です。

BOT: Hello world!
BOT: Thread 3
BOT: Conversation end

ここで何が起こっているか考えてみることにします。このコードによっていくつかのスレッド(default, 1, 2, 3)にメッセージが追加されました。それぞれ以下のようになっています。

  • default: ['Conversation start']
  • 1: ['Thread 1']
  • 2: ['Thread 2']
  • 3: ['Thread 3', 'Conversation end']

bot.replyで返されたConversationの外でのメッセージ「Hello world!」は考慮しないとして、ユーザーに 一度に返せるメッセージは1つのThread内のメッセージ であるとするならば、Conversationが最終的に位置している「3」Threadのメッセージが返されているのでは?と考えることができます。

色々考えてみる

それにしても、convo.say('Conversation start')も表示されないのはかなり混乱しました。sayaddMessageの違いはThreadを指定しないことだけだということで、てっきりsayはいつでも表示されると思っていたからです。このメッセージはdefaulに追加されているので、Conversationのコールバックが終わった後の最終的な位置は「3」Threadのため表示されないということでした。つまり Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される と考えることができるのではないでしょうか?

また、チャットボットの特性かもしれませんが、だいたいの処理がユーザー駆動になっています。ユーザーの入力があったときにcontrollerでメッセージを受け取るとコールバックが起動されて、各Threadにメッセージが追加されます。あとでわかったことですが Threadに追加したメッセージは原則変更できない ということも意外と重要なことかもしれません。askaddQuestionなど、Conversation内でユーザーからの入力を再度受け付けるためのメソッドが存在しますが、この 入力結果を使用して動的にメッセージを生成するにはaskaddQuestionのコールバック内で呼び出す必要がある ことに注意しなければいけないと思います。(特に{{vars.hogehoge}}や、convo.extractResponseの値など)

Threadで色々遊んでみる

一応、ある程度の使い方がわかったのでどんなことができるか、色々遊んでみることにしました。

index.js をユーザーの入力から動的に数珠つなぎのスレッドを生成するようにする(全体)

...
    const loop = message.text
    for (let i = 1; i < loop; i++) {
      convo.addMessage(`Thread ${i}`, `${i}`)
      convo.addQuestion(`Do you wanna go to thread ${i + 1}?`, [
        {
          pattern: bot.utterances.yes,
          callback: (res, convo) => {
            convo.gotoThread(`${i + 1}`)
          }
        },
        {
          pattern: bot.utterances.no,
          callback: (res, convo) => {
            convo.gotoThread('complete')
          }
        },
        {
          default: true,
          callback: (res, convo) => {
            convo.repeat()
            convo.next()
          }
        }
      ], {}, `${i}`)
    }
...

index.js を前のスレッドを再利用かつ、すぐに戻って来れるようにする(全体)

...
    const loop = message.text
    let jump // ここと
    for (let i = 1; i < loop; i++) {
      convo.addMessage(`Thread ${i}`, `${i}`)
      convo.addQuestion(`Do you wanna go to thread ${i + 1}?`, [
        {
          pattern: bot.utterances.yes,
          callback: (res, convo) => {
            convo.gotoThread(jump || `${i + 1}`) // ここと
          }
        },
        {
          pattern: bot.utterances.no,
          callback: (res, convo) => {
            convo.gotoThread('complete')
          }
        },
        {
          default: true,
          callback: (res, convo) => {
            convo.repeat()
            convo.next()
          }
        }
      ], {}, `${i}`)
    }
...
    // ここら辺
    convo.addQuestion('Which thread do you like?', [
      ...((l) => {
        const arr = []
        for (let i = 1; i < l; i++) {
          arr.push({
            pattern: `${i}`,
            callback: (res, convo) => {
              jump = `${loop}`
              convo.gotoThread(`${i}`)
            }
          })
        }
        return arr
      })(loop),
      {
        default: true,
        callback: (res, convo) => {
          convo.gotoThread('complete')
        }
      }
    ], {}, `${loop}`)
...

Threadは再利用できる形で作っておく のがポイントだと感じました。あと convo.gotoThreadは基本的にaskaddQuestion内のコールバックでしか使わない。

まとめ

  • 一度にユーザーに返せるのは1つのThread内のメッセージ
  • Threadはネストさせることはできない
  • Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される
  • Threadに追加したメッセージは原則変更できない
  • 入力結果を使用して動的にメッセージを生成するにはaskaddQuestionのコールバック内で呼び出す必要がある
  • Threadは再利用できる形で作っておく
  • convo.gotoThreadは基本的にaskaddQuestion内のコールバックでしか使わない。

感想

  • めちゃくちゃ煩雑で雑多な内容になりました。askとかaddQuestionの説明記事を書いた方が自分と世の中のためになった気がする
  • なんかやったことをとりあえず並べていっているせいか、記事がチュートリアルっぽくなりがち
  • 結局記事のターゲットはBotkitで複雑なことをしたいと思って色々やってよくわかんないってなった人(自分)
  • 「仕組み上何ができないか?」ということがはっきりわかると大変助かるので、そういう部分を探って行きたいと思います。
  • マルチプラットフォーム前提のチャットボットフレームワークがあれば知りたい。コアの部分とインタフェースがしっかり別れていて、コアを使いまわせるものが欲しい。。。messengerとLINEとslackで同じチャットボットと対話できるとめちゃくちゃ広がりそう。チャットプラットフォームがブラウザで、ボットがWebページみたいな世界。
  • チャットボットに将来性をすごく感じているので何か仕事があればとか思う日々を送っています。