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を意図的に間違えて使ってみる
本題です。以下のようにコードを変更しました。自分としてはsay
とaddMessage
で追加した文章が上から順々にコンソールに表示されればいいなーと思ったのですが全然ダメでした。
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')
も表示されないのはかなり混乱しました。say
とaddMessage
の違いはThreadを指定しないことだけだということで、てっきりsay
はいつでも表示されると思っていたからです。このメッセージはdefaulに追加されているので、Conversationのコールバックが終わった後の最終的な位置は「3」Threadのため表示されないということでした。つまり Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される と考えることができるのではないでしょうか?
また、チャットボットの特性かもしれませんが、だいたいの処理がユーザー駆動になっています。ユーザーの入力があったときにcontrollerでメッセージを受け取るとコールバックが起動されて、各Threadにメッセージが追加されます。あとでわかったことですが Threadに追加したメッセージは原則変更できない ということも意外と重要なことかもしれません。ask
、addQuestion
など、Conversation内でユーザーからの入力を再度受け付けるためのメソッドが存在しますが、この 入力結果を使用して動的にメッセージを生成するにはask
、addQuestion
のコールバック内で呼び出す必要がある ことに注意しなければいけないと思います。(特に{{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
は基本的にask
かaddQuestion
内のコールバックでしか使わない。
まとめ
- 一度にユーザーに返せるのは1つのThread内のメッセージ
- Threadはネストさせることはできない
- Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される
- Threadに追加したメッセージは原則変更できない
- 入力結果を使用して動的にメッセージを生成するには
ask
、addQuestion
のコールバック内で呼び出す必要がある - Threadは再利用できる形で作っておく
convo.gotoThread
は基本的にask
かaddQuestion
内のコールバックでしか使わない。
感想
- めちゃくちゃ煩雑で雑多な内容になりました。
ask
とかaddQuestion
の説明記事を書いた方が自分と世の中のためになった気がする - なんかやったことをとりあえず並べていっているせいか、記事がチュートリアルっぽくなりがち
- 結局記事のターゲットはBotkitで複雑なことをしたいと思って色々やってよくわかんないってなった人(自分)
- 「仕組み上何ができないか?」ということがはっきりわかると大変助かるので、そういう部分を探って行きたいと思います。
- マルチプラットフォーム前提のチャットボットフレームワークがあれば知りたい。コアの部分とインタフェースがしっかり別れていて、コアを使いまわせるものが欲しい。。。messengerとLINEとslackで同じチャットボットと対話できるとめちゃくちゃ広がりそう。チャットプラットフォームがブラウザで、ボットがWebページみたいな世界。
- チャットボットに将来性をすごく感じているので何か仕事があればとか思う日々を送っています。