チャットボットフレームワーク Ruboty を振り返る

Ruboty を利用したデプロイの様子
Ruboty を利用したデプロイの様子

この記事では、Slack や HipChat で動く Bot をつくるためのフレームワーク Ruboty の仕組みを振り返り、現状の実現方法を把握し、今後 ChatOps を改善するための足掛かりとしたい。

出勤、デプロイ

Ruboty というチャットボットフレームワークを数年前から開発しており、仕事でも Ruboty でつくった Bot を使った業務フローを導入・運用する機会が増えてきた。例えば、いま働いている会社では、Qiitan という Bot が出勤管理やデプロイに使われている。

_Qiitan に_最もよく投げかけられている発言は、「@qiitan 出勤」である。この発言を行うことで、自動的に社で利用している出勤管理サービスに対して、発言者が出勤した旨を代わりに登録してくれる。他によく使われる発言として「@qiitan デプロイしたい」がある。これらを始点として、Qiitan がどういう仕組みで動いているかについて考えたい。

WebHook

「@qiitan デプロイしたい」のような発言に対応して何らかの処理を行うようなプログラムをつくるには、まずその発言があったことを機械的に検知できなければならない。

Slack は、特定のチャンネルを指定して発言を監視することができるような WebHook の API を提供しており、また発言者の名前やアイコンを指定して発言を行うような API も提供している。よって、Bot が使えるチャンネルを特定のひとつのチャンネルだけに限れば、この機能を利用して Bot を実現することができる。

しかし、実際には様々なチャンネルで Bot を使いたいことが多く、この仕組みは利用しづらい。例えば、出退勤に関する発言は kintai というチャンネルで行われているが、アプリケーションのデプロイに関する発言は dev というチャンネルなどで行われている。チャンネルごとに WebHook を登録していく方法は、チャンネルや Bot の用途が今後増え続けることを考えると、あまり現実的ではない。

XMPP

複数のチャンネルで同じ Bot を運用したいときに Slack で使える手段として、 XMPP がある。これは、TCP 通信で XML をやり取りすることでメッセージングを行いましょうというプロトコルで、ノード間でメッセージングを行うための仕様であり、拡張仕様としてグループメッセージなどに関する仕様も存在している。

XMPP | XMPP Main

XMPP では、チャットサービスに必要な様々な事柄についても色々と網羅されており、例えばオンラインや離席中といった各アカウントの現在の状態の表明方法や、各アカウントの詳細なプロフィール情報を扱う電話帳のような概念についても仕様が定められている。

Slack では、この XMPP にユーザ名とパスワードを与えて接続することで、およそ Slack アカウントができる全ての操作を行えるようになっている。というよりかは、Slack クライアント自体が内部的に XMPP のようなものを利用しているような状態なので、同じく XMPP を適切に扱うプログラムを記述すれば、Bot をつくることができる。

Xrc

Ruby で XMPP を扱うライブラリとしては xmpp4r というものがあったが、当時 Ruby 1.9 以降でこのライブラリを使うと、マルチバイトの文字列を適切に扱うことができないことが確認できた。そこで、XMPP の仕様を読み、xrc という XMPP 用のライブラリをつくり、まずこの問題を解決した。

r7kamura/xrc

Ruboty

xrc を使って XMPP で Slack のサーバに接続することで、チャンネルに参加したり、参加しているチャンネルの発言を監視したり、チャンネルに対して発言したり、個人間で DM を送受信したりということが出来るようになった。これによって「@qiitan デプロイしたい」やその他のチャンネルに投稿される発言が受け取れるようになった。

次に必要なのは、特定の発言のパターンを検知し、何らかの処理を行い、必要であれば返事をする、というルールの集合と、それらと xrc を連携させる仕組みである。これを担当しているのが Ruboty である。

r7kamura/ruboty

Adapter

XMPP の他にも、IRC や、WebSocket、標準入出力など、メッセージを送受信できるものであれば対応できるようにしたい。これを抽象化した概念として、Ruboty ではアダプタというものを設けた。

既存のアダプタ実装としては、Slack と接続するための ruboty-slack アダプタや、HipChat と接続するための ruboty-hipchat アダプタ (同じ XMPP を使うが Slack と HipChat では微妙に作法が異なるのだ…)、IRC サーバと接続するための ruboty-irc などがある。前述した通り、ruboty-slack や ruboty-hipchat の中では xrc が使われている。

r7kamura/ruboty-slack

Ruboty 本体のコードにも、サンプルとして標準入出力用のアダプタが同梱されている。プラグインを開発するときのデバッグ用途などで重宝することが多い。

アダプタに求められる役割は、サービスに接続してメッセージを待ち受け、メッセージを受け取ると Ruboty 本体に通知し、また Ruboty 本体から要求があればサービスに対してメッセージを投稿するというものである。

コードのレベルで具体的に説明するならば、 Ruboty から求められるインターフェースを実装し、#run メソッドが呼ばれたときにサービスと接続して無限にメッセージを待ち受け、メッセージを受け取ったときには robot.receive(…) を実行し、また #say メソッドが呼ばれたときにはサービスに対してメッセージを投稿する、というようなものである。

Handler

通信関係の低レイヤとのインターフェースを抽象したものがアダプタであったのに対して、発言のパターンを検査して処理を行うようなルール集合を定義するのがハンドラである。

例えば、GitHub の Pull Request や Issue を操作するルールを提供する ruboty-github や、勤怠管理サービスを操作するルールを提供する ruboty-jobcan などがある。アダプタは種類が少ないのに対して、ハンドラは rubygems.org で「ruboty」で検索すると大量に出てくる。

search | RubyGems.org | your community gem host

ハンドラの設計は、正規表現とメソッド名の組をルールとして登録してもらって、そのパターンに一致したときに指定されたメソッドを実行する、というものである。Ruboty はメッセージを受け取ると、登録された順にルールに一致するものがないか走査していって、一致するものがあれば対応する処理を実行する。

実際に運用してみると、「他のルールに一致しなかったときに一致するルールを定義したい」「mention されたときだけ一致するルールを定義したい」「すべての発言について何らかのパターンが含まれていたら反応するルールを定義したい」「登録されているルール集合を一覧したい」「ルールそれぞれに対して説明を記述したい」といった要求が出てきたため、開発が進むにつれて、それぞれのルールにオプションが追加されたり、登録されているルールをランタイムで取得できるような API が追加されたりしていった。

Plug-in

上述したアダプタやハンドラのようなプラグインは、基本的には Gem として提供されることが想定されている。Ruboty は、起動時の初期化処理として Gemfile に記述されている Gem を全て読み込むようになっている。結果的に、利用者は Gemfile を書くだけでプラグインを追加できる。

Slack への接続に使う認証情報など、各プラグインに必要な設定は、環境変数を介して与えられるようになっている。JSON ファイルや起動時のコマンドラインオプションなどで設定を与えられるようにするという選択肢もあったが、The Twelve-Factor App の「設定を環境変数に格納する」に従い、環境変数を利用することにしている。

The Twelve-Factor App (日本語訳)

Brain

アダプタとハンドラ以外に、永続化を行うバックエンドを差し替えるための、ブレインと呼ばれる類のプラグインも存在する。最もよく使われるのが ruboty-redis で、Redis をバックエンドにしてデータを永続化するためのプラグインである。

r7kamura/ruboty-redis

例えば、「@qiitan デプロイしたい」というコマンドでは、内部的には GitHub の Pull Request を作成する Web API が利用されている。このコマンドを利用するには、事前に GitHub のアクセストークンを Ruboty に教えておいて、Slack のアカウントと GitHub のアクセストークンとの対応関係を記憶させておく必要がある。

デフォルトの状態では Ruboty のプロセスから参照できるメモリに保存されているので、Ruboty のプロセスが終了するとそこでデータが揮発してしまう。ここでデータを永続化させるために利用されているのが、ruboty- redis である。

Ruboty のデータ層は単純な KVS として実現されているので、Redis 以外にも Memcache や Google Spreadsheet などをバックエンドにしたプラグインをつくることは可能である。しかし Heroku で運用する場合、無料で Redis のアドオンを利用できるので、ruboty-redis が使われることが多い。

ruboty-template

Gemfile と環境変数だけ指定すれば Bot を動かせるということで、Heroku で Ruboty を動かすためのプロジェクトの雛形として、ruboty-template というリポジトリを公開している。

r7kamura/ruboty-template

このリポジトリを fork して、Gemfile とGemfile.lock を変更し、Heroku Button を押して必要な環境変数を入力すれば、Heroku に Ruboty を利用した Bot をデプロイできる仕組みになっている。

実際、Qiitan も ruboty-template を fork したリポジトリになっている。Public なリポジトリとして公開しているので、Gemfile を見ると、利用されているプラグインも調べられる。

increments/qiitan-rb

例えば、ruboty-github という GitHub API 操作用のプラグインを Qiita のデプロイフロー用に少し改変した ruboty-qiita-github というプラグインを使っていて、これによって「@qiitan デプロイしたい」という発言で Pull Request をつくれるようになっている。他には ruboty-jobcan という勤怠管理サービスの API を操作するためのプラグインを使っていて、これによって「@qiitan 出勤」で出勤記録を残せるようになっている。

ruboty-template の fork のグラフを見ると、Ruboty が使われている様子が可視化できる。

ruboty-template の fork Network
ruboty-template の fork Network

WebSocket

Slack は WebSocket を利用した API も提供していて、XMPP の代わりにこれを利用することもできる。Slack の WebSocket API 用のアダプタもあるので、ruboty-slack の代わりにこれを利用することもできる。

WebSocket API を使うと、Bot 用に Slack アカウントをつくらなくても利用できるというメリットがある。Slack はアカウントの数で課金されるので、コストを抑えるという意味では WebSocket API を使って実現する方が良い。

rosylilly/ruboty-slack_rtm

今後の展望

ここまでは既存の Ruboty の実装について説明したが、今後 Bot を改善するならどうするかということについて考えたい。

WebHook API では特定の一つのチャンネルの発言しか取得できないと前述したが、最近では Slack App としてアプリケーションを登録すると、すべてのチャンネルのイベントに反応するような WebHook を用意することができる。これを利用すれば、WebHook だけで Bot を実現することも可能である。更に Slack Apps では、Interactive Buttons というボタンを提供する機能も利用できるので、Bot の表現力の幅を広げられる。特にモバイルアプリからの利用では、テキストを入力するよりもボタンを押下する方が楽なケースが多い。

Ruboty はアダプタを交換することで他のサービスでも同じ機能を提供できるという利点があるので、Slack Apps に特化した機能をつくるのは難しく、Ruboty では Slack Apps と WebHook を利用した実装は提供していない。とはいえ Slack はシェアの大きいチャットサービスであるし、今後チャットボットフレームワークをつくる場合は、Slack Apps に対応しつつ WebHook ベースで動作する実装にした方が良いかもしれない。

WebHook ベースで動作させる場合、Heroku で動かすのではなくて、AWS Lambda と API Gateway を組み合わせて実装する方法も考えられる。またハンドラの内部実装も、Lambda の 1 function として実装を切り離すような設計も考えられる。サーバレス的な仕組みは少しずつ使われるようになってきているので、そういう仕組みに完全に乗っかった Bot システムも面白いかもしれない。

一方で、プロセスを常時稼働させておくことができないシステムだと、現在 ruboty-cron などで行っているような類の仕事は難しくなる。例えば、毎日設定した時刻に日報を書いてもらうようにアナウンスしてもらうとか、定期的に Twitter をエゴサーチして検索結果を表示してもらうというようなことは難しい。そのため、サーバレスなシステムベースで実装するときには、この手の処理の実現方法を別途用意してあげる必要がある。

参考記事

Ruboty 関係で他に参考になりそうな記事を挙げておく。

Ruby + Bot = Ruboty - ✘╹◡╹✘

天下一bot武闘会で発表しました - ✘╹◡╹✘

XMPP界 - ✘╹◡╹✘