この記事は BrainPad AdventCalendar 2017 12日目の記事です。
※データサイエンス的な記事が続いたので、ちょっと箸休め的な話題でも。
はじめに
こんにちは。@yagizoです。自社サービスの開発担当をしている普通のエンジニアです。
ところで皆さんは、チャットツールって使ってますか?
弊社では数年前から全社的に業務でのチャットツール(Hipchat)の利用がはじまりました。最初はコミュニケーションツールとして使っていたのですが、そのうちに色々なチャットボットを作って、開発支援に利用したり、運用状況のレポートを問い合わせるのに使ったり、契約者情報を調べるのに使ったりと、いろいろと役に立つボットが誕生してきました。今ではそのボットの機能が使えないと業務の効率に影響するくらい、なくてはならない存在になっています。
前置きが長くなってしまいましたが、12月は何かと飲み会が多い時期です。今回の記事は、「チャットボットを使って宴会を盛り上げちゃおう!」という内容で、簡単なチャットボットの作り方(遊び方)をお持ち帰りいただこうという企画です。
※苦しいこじつけからの企画スタート。
Hubotの基礎知識
今更「チャットボット?何っ?」て聞けない人のためのウォームアップの章です。Hubotを知っているっていう人は読み飛ばして、次行っちゃいましょう。
ボット(bot)って何さ
チャットツールに常駐してチャット経由でコマンドを待ち受けて特定の機能を実行したり、決められた条件に従ってチャットに発言してチャットの参加者に通知したりするようなプログラムです。
※ボットかと思っていたら、たまにボットのフリをする中の人だったりする場合があるので、騙されないように気をつけましょう(私は体験済み)w
ボットを一から作るのもいいのですが、チャットツールと機能の間を取り持ってくれるボットの基本機能を提供してくれるフレームワークがあり、今回はその中の1つであるHubotを使って「宴会アシストボット」を作っていきます。
Hubotって何さ
GitHub社が開発しMITライセンスで公開しているNode.js(CoffeeScript)でbotを作り動かすためのフレームワークです。
- https://hubot.github.com/ ... 本家サイト
Hubotについてもっと詳しく知りたい人は、ちょっと古い記事ですが以下の連載を読むとよいでしょう。
※インストール手順は変わっているので、最新の手順については本家のサイトの情報を確認しましょう。
このフレームワークを使うと何が嬉しいかというと、Adapterという概念があって、使用するアダプターを差し替えることで、機能を変更することなく、いろいろなチャットツールと連携することができる点です。
普通にSlack、HipChat、IRC(ちょっと懐かしい)などに対応したアダプターがあるだけじゃなく、アダプターを実装するための仕様も公開されています。
そのため、ちょっとネットで探すと、下記のように誰かが欲しいアダプターを既に作ってくれていることが多いのも魅力です。
今回は環境構築についての話は割愛します。
※「Getting Started With Hubot - Hubot Documentation」の手順に従って導入するのが最短ルートだと思われます。
Hubotのきほんの「き」
Hubotは起動時に環境変数を読み込み、指定されたアダプターを通じて対象のチャットツールを監視します。常駐させたいルームなんかもここで指定します。
Hubotの使い方ですが、robotオブジェクトのメソッドを通じて、コールバック関数を登録することによって「チャット上で誰かが特定のキーワードを発言したとき」や「チャットルームに誰かが新たに参加したとき」など様々な状況ごとにHubotの振る舞いを定義することができます。
./scriptsディレクトリの下に、任意の処理を実装したcoffeeスクリプトを放り込むだけで、ボットにどんどん機能を追加することができるので、とても楽です。
個人的に気に入っているのは、Hubot自体がhttpdとしての機能を有していて、REST APIを通じて、Hubotへ命令を送ることができます。この機能のおかげで、かなり楽に外部機能との連携ができて、アイディア次第で結構面白いことができます。
前置きはこのくらいにして、さっそく例のやつの設計にかかりましょう。
宴会アシスト(ロ)ボット
宴会アシスト(ロ)ボットとは
「宴会に参加した人(私)を助け、場を盛り上げること」を目的としたサービスです。
はい。なんのことやら、さっぱりわからないですね。。。私もわからないです(汗)。
※一人ハッカソン状態でアドベントカレンダーの現在進行形で考えながらネタに挑んでいる状況
このままでは埒が明かないので、もう少し具体的になるようなイメージ(機能)を考えてみます。
- 盛り上げる → 効果音
- レシピ:外部のプログラムを呼び出す
- 場を繋ぐ → ネタを提供(私、日本酒が好きなのでそれ系で)
- レシピ:外部のサービス(Rest APIを呼び出す)と連携する
- 幹事を助ける → なかなか言い出しづらい、おひらき(終了)宣言をする
- レシピ:Hubotを呼び出す(連携する)
- せっかくなんでロボット感を出す(カッコイイ) → 物理的に設置する、喋らせる
- レシピ:次のネタを探すための実験を入れ込むという野心を盛り込む
- HipChatと連携 → 会社で飲むときに実践投入するためw
- レシピ:怠惰なエンジニアは一石二鳥を狙う
(... できた!!)
ぱっと見、仕掛けも何もない人形にしか見えない!!のに、まさかこの人形にこんな機能が隠されているとは!!と周りから驚愕の声が聞けること間違いなしです♪
(...ええ。あなたが言いたいことは何となくわかりますよ。もう少し工作の時間があれば、何とかなったんだぁ。。。理想の形状は、脳内変換で置き換えてください。すんません)
「そもそもなんだけど、このサービスでチャットツール使う必要あるの?」っていう鋭いツッコミが、どこからともなく聞こえてきた...
も、も、も、も、もちろん。そ、それは宴会チャットルームを用意することで、話題に入りそびれた人が後からトピックを追うことができたり、チャットルームで、この「宴会アシスト(ロ)ボット2017」を知ってもらって遊んでもらうなどの舞台装置として、立派に役割があるんですよ...?
※優しさは必要です。粗を探すのではなく、良いところ評価してくださいね。
では、早速作っていきましょう。
用意するもの
宴会アシスト(ロ)ボット2017を作るのに必要な材料。
- 外観となるロボット的な人形
- RaspberryPi + WiFi
- バッテリー、スピーカー
もうお分かりかと思いますが、RaspberryPiのLinux環境にHubotをインストールして、上記の機能を実装していきます。
機能実装
Hubotをインストールしたディレクトリは以下のようになっています。
-rw-r--r-- 1 pi pi 26 12月 10 13:12 Procfile
-rw-r--r-- 1 pi pi 7880 12月 10 13:12 README.md
drwxr-xr-x 2 pi pi 4096 12月 10 13:12 bin
drwxr-xr-x 2 pi pi 4096 12月 10 13:43 data
-rw-r--r-- 1 pi pi 19 12月 10 13:12 external-scripts.json
-rw-r--r-- 1 pi pi 2 12月 10 13:12 hubot-scripts.json
drwxr-xr-x 2 pi pi 4096 12月 10 13:40 log
drwxr-xr-x 16 pi pi 4096 12月 10 13:12 node_modules
-rw-r--r-- 1 pi pi 641 12月 10 13:23 package.json
drwxr-xr-x 3 pi pi 4096 12月 10 13:12 scripts
binの下に起動クリプト(起動設定を含む)や連携ツールを収録し、dataの下にはネタに必要なデータを収録しています。機能実装したものはどんどんscriptに投げ込んでいく感じで、作っていきます。
効果音で盛り上げる
チャットに何か投稿されたら、効果音とともにメッセージを返す機能です。
時間がないので(←おい)ルールベースで使えそうな状況を想定し、特定のキーワードがきたらメッセージと共に(ロ)ボットから、音ネタを出力することにします。
実装はなりふり構わず、音ネタはwebでフリー素材を拾ってきて、コマンドツールで再生します。apt-getで簡単にインストールできるアプリとして、今回は以下を使用しました。
- mpg321 ... mp3音源の再生
- aplay ... wav音源の再生
以下は実装例です。
# Description
# 効果音付きのメッセージを返すよ
#
# Commands:
# hubot 拍手 - パチパチというメッセージと共に効果音を流す
#
# Author:
# yagizo
child_process = require 'child_process'
module.exports = (robot) ->
robot.hear /(拍手|パチパチ|ぱちぱち|おー)/i, (msg) ->
s = ['cheer1.mp3', 'cheer2.mp3', 'cheer3.mp3']
msg.send "パチパチパチ"
child_process.exec "mpg321 /home/pi/mybot/data/" + msg.random s , (error, stdout, stderr) ->
robot.hear /(発表します)/i, (msg) ->
msg.send "(ドキドキドキ)"
child_process.exec "mpg321 /home/pi/mybot/data/drumroll.mp3" , (error, stdout, stderr) ->
if !error
msg.send "なんと!!"
robot.hear /(質問|クエスチョン|Question)/i, (msg) ->
child_process.exec "aplay /home/pi/mybot/data/question1.wav" , (error, stdout, stderr) ->
if !error
msg.send "えっと、、"
robot.hear /(正解)/i, (msg) ->
child_process.exec "mpg321 /home/pi/mybot/data/correct1.mp3" , (error, stdout, stderr) ->
msg.send "おおおおおおっ"
robot.hear /(違います|はずれ|違う|ちがう)/i, (msg) ->
child_process.exec "mpg321 /home/pi/mybot/data/incorrect1.mp3" , (error, stdout, stderr) ->
msg.send "ぶぶー"
robot.hear /(爆発|どかーん|ドカーン|地雷|ボン)/i, (msg) ->
s = ['bomb1.mp3','bomb2.mp3','cannon1.mp3','cannon2.mp3','thunder2.mp3']
child_process.exec "mpg321 /home/pi/mybot/data/" + msg.random s , (error, stdout, stderr) ->
msg.send "(boom)"
robot.hear /(誕生日|[bB]irthday)/i, (msg) ->
msg.send "!!"
child_process.exec "mpg321 /home/pi/mybot/data/Happy_Birthday_To_You.mp3" , (error, stdout, stderr) ->
if !error
msg.send "おめでとう〜♪"
child_process.exec "mpg321 /home/pi/mybot/data/cheer1.mp3" , (error, stdout, stderr) ->
robot.hear /(うるさい|静かに|黙れ|ご静粛に)/i, (msg) ->
child_process.exec "kill $(pgrep mpg321)" , (error, stdout, stderr) ->
msg.send "しーん..."
ちょっと解説すると、robot.hearメソッドで入室しているルームの投稿を監視し、正規表現と合致したら、実行する仕組みです。またchild_process.execメソッドで外部のコマンドを呼び出せます。msg.sendメソッドでチャットを投げることができるので、そこで気の利いたメッセージでリアクションすればOK。msg.randomメソッドでリストからランダムにピックすることができるので、適当な変化球を投げたい時に便利です。爆発の返信の「(boom)」はHipchatの絵文字(emoticons)です。他のアダプターを使うと微妙ですが、特定のチャットツールに投げることがわかっている場合はそちらの機能も混ぜ込むと、テンションが上がります。
インタラクションとして、音を持ち込むだけで随分と反応がよくなるので面白い機能です。実装例を元に自分だけのネタを詰め込んでくださいね。
※仕込む側はネタに気づいてもらえないとガッカリしますが、ネタが発火すると思わずニヤリと口元が緩むこと請け合いです。
小ネタを提供し、盛り上げる
どうでもいいんですが、私は日本酒を飲むのが大好きなのです。飲むことが好きなのと、日本酒に造形が深いことは本来なら無関係なはずなのですが、誰かと会話をしている時、世の中ではしばしば
「〜が好き」 → 「〜に詳しい、〜が上手、〜に強い」
に変換(誤解)されて話が進んでしまい、メンドくさいなぁと思うことがあります。
そんな私を助けてくれる機能をボットに実装したのが今回の話です。ネットで調べると、酒蔵や銘柄を検索できるREST APIがあったので、
これを使って、何か日本酒について聞かれたら、ボットに丸投げして答えてもらいます。この機能、実は遥か昔に実装していて、Githubに公開しています。
詳細はそちらを参照してもらうことにして、機能の解説をしましょう。
# Description
# 日本酒の情報を取得する
# https://www.sakenote.com/access_tokens (Sakenote Database API)
#
# Commands:
# hubot <text(都道府県)>の酒 - <text(都道府県)>のお酒を探す
# hubot <text>ってお酒 - <text>についての日本酒情報を表示する
# hubot <text>なんとか - <text>っぽい日本酒を探す
http = require 'http'
APP_ID = process.env.CONOMI_SAKE_TOKEN
table = {
"北海道":1, "ほっかいどう":1,
"青森県":2, "青森":2, "あおもり":2,
"岩手県":3, "岩手":3, "いわて":3,
"宮城県":4, "宮城":4, "みやぎ":4,
:
"宮崎県":45, "宮崎":45, "みやざき":45,
"鹿児島県":46, "鹿児島":46, "かごしま":46,
"沖縄県":47, "沖縄":47, "おきなわ":47
}
module.exports = (robot) ->
robot.respond /(.+)(の情報|のつく酒|って酒|って日本酒|のお酒|のおさけ|の酒|のさけ|なんとか|何とか)$/i, (msg) ->
get_sake_info msg, msg.match[1]
get_sake_info = (msg, keyword="") ->
p = "/api/v1/sakes"
p = p + '?token=' + APP_ID
if keyword of table == true
p = p + '&prefecture_code=' + table[keyword]
else
p = p + '&sake_name=' + encodeURIComponent(keyword)
console.log "http://www.sakenote.com" + p
req = http.get { host:'www.sakenote.com', path:p }, (res) ->
contents = ""
res.on 'data', (chunk) ->
contents += "#{chunk}"
res.on 'end', () ->
j = JSON.parse contents
# console.log j
if j['sakes'].length == 0
msg.send "..."
return
rep = ""
for i, value of j['sakes']
rep += "\n『" + value['sake_name'] + " (" + value['sake_furigana'] + ")』 \n"
if "maker_name" of value == true
rep += " 蔵元: " + value['maker_name'] + " "
if "maker_postcode" of value == true
rep += "〒" + value['maker_postcode'] + " "
if "maker_address" of value == true
rep += value['maker_address']
rep += "\n"
if value['maker_url'] != null
rep += " URL: " + value['maker_url'] + "\n"
msg.send rep
req.on "error", (e) ->
msg.send "(boom)ぅぅぅ... {e.message}"
簡単に解説を。今回のイディオムは、「特定のメッセージを受けたら、外部のREST APIを叩いて情報(JSON)を引いてくる」です。
httpライブラリをインポートするだけで、簡単にhttp.getメソッドでリクエストを投げて、レスポンスのBodyはそのままJSONオブジェクトとして使用できるので、このレシピさえ知っていれば何にでも応用できます。ちなみにrobot.respondは直接話しかけられた(メンション付き)場合に呼ばれます。
※このコードをみると、当時もかなり適当だったことがわかる。都道府県検索はかなり強引に、マッピングテーブルを作って力技で解決w
幹事を助けてあげよう(+そして人形が喋る!)
宴会の幹事になって困るのが、すごい盛り上がっている場を閉会する儀式ですね。そこでなかなか言い出しづらい、おひらき(終了)の宣言を手伝う機能を作りましょう。
チャットで、「20:50にお開き」と終了時刻を設定すると、その時刻になると幹事に代わって「宴もたけなわでございますが、〜」と切り出してくれます。あ、まだ喋らせてないので、ここで(ロ)ボットに発話させます。
メッセージを拾ってスケジュール登録する機能は、Hubotのスクリプトからcronに閉会メッセージを飛ばす外部プログラムを呼び出すことで簡易に実装します。
# Description
# 閉会を切り出してくれる
#
# Commands:
# hubot <hh:mm>にお開き - hh:mmに閉会のメッセージを流す
#
child_process = require 'child_process'
module.exports = (robot) ->
robot.respond /(¥d{2}):(¥d{2})にお開き/i, (msg) ->
child_process.exec "/home/pi/mybot/bin/register_closeup.sh " + msg.match[1] + " " msg.match[2] , (error, stdout, stderr) ->
msg.send "らじゃ"
スケジュール実行機能はcronに任せたということで、実行時に呼び出すスクリプトをセットする機能をサクッと実装します。なおこの(ロ)ボットでは、他にスケジュール機能を使わないという前提で、呼び出されたらスケジュールも消しちゃうという楽観的な実装で解決します。
#!/bin/bash
MY_HOME=/home/pi/mybot
EXEC=$MY_HOME/bin/execute_closeup.sh
echo "$2 $1 * * * $EXEC" > /tmp/tmp.cron
crontab /tmp/tmp.cron
rm /tmp/tmp.cron
exit 0
チャットルームへの投稿はHubotのREST APIを叩いてメッセージを送ります。REST APIの実装は以下の通り。
module.exports = (robot) ->
robot.router.post '/hubot/mybot/msg', (req, res) ->
data = if req.body.payload? then JSON.parse req.body.payload else req.body
room = data.room
message = data.msg
robot.messageRoom room, "#{message}"
res.send 'OK'
Hubotに対して、/hubot/mybot/msg にroomとメッセージをセットしてPOSTすると、このスクリプトがチャットツールにメッセージを繋いでくれる仕組みです。
外部からHubotをハブにしてチャットルームと連携する時にこのイディオムを割とよく使います。個人的にはHubotを使ったサービスを作るときは、使い勝手がいいのでチャットツール以外にもハブとして、別のサービス呼び出し用途に利用したりしてます(ちょっと悪ノリ感はありますがw)。
そして、閉会時刻に閉会メッセージを飛ばして喋る機能を実装します。今時のAlexa的な発話APIを呼べばカッコイイのですが、手軽に遊べるOpen JTalkを使って、(ロ)ボットが遂に...!!
※環境構築は下記の記事が参考になります。
#!/bin/bash
ROOM="xxxxxx_test@conf.hipchat.com"
URL="http://127.0.0.1:18089/hubot/mybot/msg"
MSG="みなさま、宴もたけなわでございますが、そろそろ閉会の時間がやってまいりました。それでは本日の功労者である幹事さんにバトンタッチします"
MSG_FILE=hoge.txt
VOICE_FILE=hoge.wav
OPEN_JTALK_PATH=/var/lib/mecab/dic
echo "$MSG" > $MSG_FILE
open_jtalk -x $OPEN_JTALK_PATH/dic -m $OPEN_JTALK_PATH/voice/mei/mei_normal.htsvoice -ow $VOICE_FILE $MSG_FILE
afplay $VOICE_FILE &
curl -X POST -H "Content-Type: application/json" -d "{\"room\":\"${ROOM}\", \"msg\":\"${MSG}\"}" $URL
rm $MSG_FILE $VOICE_FILE
crontab -r
定刻になると、上記スクリプトが実行され、宴会に参加しているメンバーが「なんだなんだ」と(ロ)ボットに注目したところで、幹事に振ってくれるので、最後は幹事自身が用意したとっておきのサプライズネタ(←ここはこの記事では責任をもたないけど)で会を閉めれば、大成功間違いなしですよ!!
というわけで
以上、宴会アシストボットの製作を通じて、いくつかのボットを使った機能実装レシピをお届けしましたが、いかがだったでしょうか?
一つでも気に入って頂けたネタがあれば、幸いです。
最後に蛇足
実は、会社では業務時間後に、こういう(くだらない)サービスを(お酒を飲みながら)作る放課後サークル的な会が(ほぼ週1で)開催されていて、何か新しい技術(ネタ)を知った時、まずは作ってみる、動かしてみるという試行をしています。
この活動を通じて、「こういうことをしたい(考えている)んだけど」というより、「こんなもの作ってみたんだけど」という方が、仲間を集めたり、人を動かしやすいことを学びました。
何かしたいけど、もやもやしていた方へ。まずは手を動かしてプロトタイプを作ってみることをはじめませんか。
※一歩踏み出すことで、あなたのプロジェクトは勝手にはじまりますよw
それでは、引き続きBrainPad AdventCalendar 2017をお楽しみくださいね〜。
最後までお付き合い頂きまして本当にありがとうございました。
またどこかで、お会いしましょう。ではでは。