小ネタ:Slackボットの応答待ちでクルクルの代用に絵文字リアクションをつけよう #slack #python #llm #ai

はじめに
「SlackボットをAzure App Serviceで動かそう」では、Slackボット経由でAzure OpenAIと会話できるようにしました。
ただ、返事の生成処理に時間がかかるとなかなか応答が返ってこず、「本当に処理されているのかな?」とか「もしかしてボットが止まってる?」とか、不安になってしまうでしょう。ウェブアプリやコンソールアプリであれば、クルクルを出して処理中であることを示すことができるでしょう(参考: 小ネタ:yaspinでターミナルにクルクルを出そう)。しかし、Slackではクルクル(spinner)そのものを出すのは難しそうです。
そこで、クルクルを出す代わりに、問いにメッセージに絵文字リアクションをつけて、処理中であることを表すようにしてみました。本稿ではこれまで同様、Bolt for Python を利用してボットプログラムを作成していきます。
おさらい:おうむ返しするSlackチャットボット
ボットにメンションするとスレッドで返答するボットプログラムです。
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
import re
import os
from dotenv import load_dotenv
load_dotenv()
app = App(
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
token=os.getenv("SLACK_BOT_TOKEN")
)
slack_bot_id = os.getenv("SLACK_BOT_ID")
@app.event("app_mention")
def mention_reply(event, say):
user = event['user']
text = event['text']
if 'thread_ts' in event:
thread_ts = event['thread_ts']
else:
thread_ts = event['ts']
msg = re.sub(f'<@{slack_bot_id}>', '', text)
say(
text=f"Received mention from user: <@{user}>, text: {msg}, ts: {thread_ts}",
thread_ts=thread_ts
)
flask_app = Flask(__name__)
handler = SlackRequestHandler(app)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
- Slackアプリとして作成。
- HTTP Request URLsモードで起動し、Slackからアプリへ通信。
- Bolt for Pythonをフレームワークとして使用。
- ngrokを使ってローカルPCにトンネリング。
という建付けです。「SlackボットをAzure App Serviceで動かそう」もご覧ください。
このプログラムでは、メンションに対してすぐリプライが返ってくるので、動いているかどうかといった心配をする必要がないでしょう。しかし構造が単純なので、こちらを基礎プログラムとして、待ち時間に絵文字リアクションをつける処理を追加していきます。
絵文字リアクションをつけるには
絵文字リアクションをつけるSlack APIは reactions.add です。Bolt for Pythonでは reactions_add メソッドになります。
また、スコープとして reactions:write の設定が必要ですので、追加してください。具体的な手順は「Slackでスレッドに返信するボットをローカルPCで動かそう」を参照してください。
Pythonの並列処理
処理を待っている間に、絵文字リアクションをつけるという別の処理をするには、並列処理が必要になります。ここではPythonの並列処理として concurrent.futures モジュールを利用します。
まずボット関係なしに、concurrent.futuresモジュールを使った並列処理を見てみましょう。
from concurrent.futures import ThreadPoolExecutor
import time
moonspinner_stop = False
def moon_spinner():
global moonspinner_stop
moon_emojis = [
'🌘 waning_crescent_moon', # 🌘
'🌗 last_quarter_moon', # 🌗
'🌖 waning_gibbous_moon', # 🌖
'🌕 full_moon', # 🌕
'🌔 waxing_gibbous_moon', # 🌔
'🌓 first_quarter_moon', # 🌓
'🌒 waxing_crescent_moon', # 🌒
'🌑 new_moon', # 🌑
]
for moon_emoji in moon_emojis:
if moonspinner_stop:
break
time.sleep(5)
print(moon_emoji)
def long_work(r,t):
global moonspinner_stop
for i in range(1,r):
print(".", end="", flush=True)
time.sleep(t)
moonspinner_stop = True
with ThreadPoolExecutor(2) as executor:
executor.submit(long_work, 30, 1)
executor.submit(moon_spinner)
1秒ごとに . を30回出力する for ループを、時間のかかる処理としています。それと並列して、 moonspinner 関数では5秒ごとに月の満ち欠け絵文字とそれを示す文字列を出力します。実際の動きを見てみましょう。

このように . の出力と月の満ち欠けが並列して実行されました。
処理待ち中に月の満ち欠け絵文字リアクションをつけるSlackチャットボット
では、ボットにメンションするとスレッドで返答するボットプログラムに、待っている間に絵文字リアクションをつける処理を組み込んでみましょう。前述の通り、おうむ返しで処理待ちは発生しないので、意図的に待ち時間を20秒挟むことにします。
また、メンションをつけられたら即、目玉の絵文字リアクションをつけるようにしています。
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
import re
from concurrent.futures import ThreadPoolExecutor
import time
import os
from dotenv import load_dotenv
load_dotenv()
app = App(
signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
token=os.getenv("SLACK_BOT_TOKEN")
)
slack_bot_id = os.getenv("SLACK_BOT_ID")
moonspinner_stop = False
def add_reaction(event, client, emoji):
client.reactions_add(
channel=event['channel'],
timestamp=event['ts'],
name=emoji
)
def moon_spinner(event, client):
global moonspinner_stop
moon_emojis = [
'waning_crescent_moon', # 🌘
'last_quarter_moon', # 🌗
'waning_gibbous_moon', # 🌖
'full_moon', # 🌕
'waxing_gibbous_moon', # 🌔
'first_quarter_moon', # 🌓
'waxing_crescent_moon', # 🌒
'new_moon', # 🌑
]
for moon_emoji in moon_emojis:
if moonspinner_stop:
break
time.sleep(5)
add_reaction(event, client, moon_emoji)
def say_thread_reply(event, say):
global moonspinner_stop
user = event['user']
text = event['text']
if 'thread_ts' in event:
thread_ts = event['thread_ts']
else:
thread_ts = event['ts']
msg = re.sub(f'<@{slack_bot_id}>', '', text)
###
time.sleep(20)
###
say(
text=f"Received mention from user: <@{user}>, text: {msg}, ts: {thread_ts}",
thread_ts=thread_ts
)
moonspinner_stop = True
@app.event("app_mention")
def mention_reply(event, say, client):
global moonspinner_stop
moonspinner_stop = False
add_reaction(event, client, "eyes") # 👀
with ThreadPoolExecutor(2) as executor:
executor.submit(say_thread_reply, event, say)
executor.submit(moon_spinner, event, client)
flask_app = Flask(__name__)
handler = SlackRequestHandler(app)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
これをローカルPCで起動し、ngrokでトンネリングし、Slackと接続します。具体的な手順は「Slackでスレッドに返信するボットをローカルPCで動かそう」を参照してください。
では、実際の動きを見てみましょう。

このように、
- ボットにメンションをつけると即 👀 の絵文字リアクションが追加される。
- 5秒ごとに、月の満ち欠けの絵文字リアクションが追加される。
- 20秒後に、ボットからスレッドに返信がつく。
想定通りの動きとなりました。最後にタイミングがちょっとズレてしまって、ボットの返信がきてから月の満ち欠け絵文字リアクションが追加されましたが、そこはご愛嬌ということで…。
まとめ
本稿では concurrent.futures モジュールを利用して、Slackチャットボットの応答待ち時間中に絵文字リアクションを追加するようにしてみました。ウェブアプリやコンソールアプリのクルクル(spinner)のように処理中であることがわかりやすくなり、応答を待っている間の不安が少しは軽減されると思います。今回は単純なおうむ返しにスリープを入れただけですが、実際にAzure OpenAIの応答待ちであればより効果が実感できるはずです。
また、今回のスクリプトには例外処理を入れていなかったり、月の満ち欠け絵文字の数ぶんしかリアクションを追加しなかったりと、そのまま利用するには不安が残る点があると思います。時計の絵文字(🕛)に差し替えたり、絵文字リアクションを追加するだけでなく削除したり、さらなる改善・改良の余地があります。
本稿がSlackチャットボット作成のヒントとなれば幸いです。
