Slackでスレッドに返信するボットをローカルPCで動かそう #slack #ai #langgraph #azure #openai #llm #python
はじめに
弊社ではさまざまなコミュニケーションツールを利用しており、中でもSlackの利用率が高いです。Slackには既にSlack AIという機能があり、チャットやメニューでさまざまな仕事を任せられるようになっていますが、本稿ではAzure OpenAIと会話する独自のSlackチャットボットを作成し、ローカルPCで動かしてみます。
Slackチャットボットのワークスペース準備
注意:ここではSlackチャットボットを動かすために別途ワークスペースを作成して利用しています。テスト用には専用のワークスペースを準備すると良いと思います。もしお使いのSlackワークスペースでテストするなら、ボット作成権限がない場合は権限を持つ管理者にお問い合わせください。
Slackチャットボットは Slack アプリ の一つとして作成するので、Slackアプリの準備をしましょう。作成手順は Slack apps - Quickstart を参照してください。
まず Slack API: Applications を開き、「Create New App」ボタンを押します。ダイアログが出るので「From scratch」を選びます。ここでは「App Name」を「 chatbot-test 」と入力し、「Pick a workspace to develop your app in」ではSlackチャットボットをインストールするワークスペースを選択し、「Create App」ボタンを押します。
「Basic Information」画面に遷移するので、左のナビゲーションエリアにある「OAuth & Permissions」を選択します。下にスクロールして「Scopes」まで進み、「Bot Token Scopes」の「Add an OAuth Scope」ボタンを押します。「 app_mentions:read 」を選びます。これによりチャットボットが自身に向けたメンションを読み込むことができるようになります。再び「Add an OAuth Scope」ボタンを押し、「 chat:write 」を選びます。これでチャットボットはメッセージを書き込むことができるようになります。他にもさまざまな権限があるので Permission scopes を参照してください。
権限を追加したら左のナビゲーションエリアにある「Event Subscriptions」を選択します。「Enable Events」トグルスイッチをオンにします。「Subscribe to bot events」の折りたたみを開き、「Add Bot User Event」ボタンを押します。「 app_mention 」を選びます。これによりチャットボットは自身に向けたメンションのみをメッセージイベントとして読み取るようになります。右下の「Save Changes」ボタンを押して保存します。
イベントサブスクライブを設定したら左のナビゲーションエリアにある「Socket Mode」を選択します。この手順は Socket Mode を参照してください。「Enable Socket Mode」トグルスイッチをオンにします。すると「Generate an app-level token to enable Socket Mode」ダイアログが出るので、ここでは「Token Name」に「 bolt 」と入力し、「Generate」ボタンを押します(この bolt という名称については後述します)。表示される「Token」はアプリトークンなどと呼ばれるものです(xapp-
で始まる文字列です)。あとで利用するのでこれをコピーして保存し、一旦「Done」ボタンを押して閉じます。なおアプリトークンは再閲覧可能です。
ソケットモードを有効化したら左のナビゲーションエリアにある「Install App」を選択します。そして「Install to ワークスペース 」ボタンを押します。アプリインストールの同意画面が出るので「許可する」ボタンを押します。表示される「Bot User OAuth Token」はボットトークンなどと呼ばれるものです(xoxb-
で始まる文字列です)。あとで利用するのでこれをコピーして保存します。
ではいつものSlackの画面に戻りましょう。左のAppツリーに「 chatbot-test 」が追加されているはずです。アプリと会話したいチャンネルで「 /invite @chatbot-test
」を実行して招待しましょう。
Slackチャンネルにチャットボットがやってきました! しかし、実際に処理を行う部分を作っていないため、このチャットボットは抜け殻です。次項で処理部分を作っていきましょう。
Slackチャットボットの本体作成
本稿では Bolt for Python を使ってチャットボット本体の処理部分を作っていきます。先のソケットモードの設定時に名付けた「bolt」とはこれを意味しています。またGitHubレポジトリにはさまざまな examples があるのでこちらも参照してください。
ここではSlackチャットボットにメンションすると、スレッドで応答してくれるという処理を作ります。次のようなコードになります。
from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler import re import os from dotenv import load_dotenv load_dotenv() app = App(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 ) if __name__ == "__main__": SocketModeHandler(app, os.getenv("SLACK_APP_TOKEN")).start()
注目点を挙げていきましょう。
python-dotenv を使って環境変数を読み込んでいます。
SLACK_BOT_TOKEN
: 「Bot User OAuth Token」です(xoxb-
で始まる文字列)SLACK_APP_TOKEN
: 「Token」です(xapp-
で始まる文字列です)SLACK_BOT_ID
:@chatbot-test
のメンバーIDです。Slackプロフィールからコピーしておいてください。
スレッドで返信する際には thread_ts
と ts
という値が重要になります。
if 'thread_ts' in event: thread_ts = event['thread_ts'] else: thread_ts = event['ts']
チャットメッセージには ts
という値が振られています。これは「タイムスタンプ」を意味しています。例えばあるメッセージの ts
が 1234567890.123456
であるとします。このメッセージに対してスレッドで返信するには thread_ts
に 1234567890.123456
をセットして送信します。以降、同じスレッド内にメッセージを送信するには thread_ts
に 1234567890.123456
をセットしていきます。
msg = re.sub(f'<@{slack_bot_id}>', '', text)
Slackチャットボットへのメンション @chatbot-test
を削除しています。本節では特に関係ありませんが、今後このメッセージをAzure OpenAIに渡すときに邪魔になる可能性があるので削除しています。
say( text=f"Received mention from user: <@{user}>, text: {msg}, ts: {thread_ts}", thread_ts=thread_ts )
チャットメッセージを送信しています。前述の通り thread_ts
をセットして同一スレッド内に返信するようにしています。
では、このPythonスクリプトを実行してみましょう。ローカルPC上で構いません。
(langgraph) % ./31_slack_thread_reply.py ⚡️ Bolt app is running!
動き出しました。Slackのほうで @chatbot-test
にメンションしてみましょう。
スレッドとして返信が返ってきました! さらにスレッド内で会話を続けてみましょう。
スレッド内でのメンションなしのメッセージには反応しません。これはメンションされた場合のみ反応するように作成したためなので、想定通りの動作です。スレッド内でメンションすれば、きちんと応答してくれます。また、返信に記載されている thread_ts
が変わっていないことにも注目してください。これは最初にメンションしたメッセージの ts
を表しています。
SlackチャットボットとAzure OpenAIの連携
前項で作成したSlackチャットボットは簡単なサンプルなので、定型文の応答しかできませんでした。そこで Azure OpenAI と連携させて会話できるようにしてみましょう。今回はローカルPCで動作させるので、「LangGraphの会話履歴をSQLiteに保持しよう」を前提とします。
コードは次のようになります。
from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler import re from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage, AIMessage, HumanMessage from langchain_openai import AzureChatOpenAI import sqlite3 from langgraph.checkpoint.sqlite import SqliteSaver import os from dotenv import load_dotenv load_dotenv() # os.getenv("AZURE_OPENAI_API_KEY") # os.getenv("AZURE_OPENAI_ENDPOINT") llm = AzureChatOpenAI( azure_deployment = os.getenv("AZURE_OPENAI_MODEL_NAME"), api_version = os.getenv("AZURE_OPENAI_API_VERSION"), temperature = 0.95, max_tokens = 1024, ) class State(TypedDict): messages: Annotated[list, add_messages] def chatbot(state: State): res = llm.invoke(state["messages"]) return {"messages": res} graph_builder = StateGraph(State) graph_builder.add_node("chatbot", chatbot) graph_builder.set_entry_point("chatbot") graph_builder.set_finish_point("chatbot") conn = sqlite3.connect("checkpoints.sqlite", check_same_thread=False) memory = SqliteSaver(conn) graph = graph_builder.compile(checkpointer=memory) app = App(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) events = graph.stream( {"messages": [("user", msg)]}, {"configurable": {"thread_id": thread_ts}}, stream_mode="values" ) for event in events: message = event["messages"][-1] if type(message) == AIMessage: say( text=message.content, thread_ts=thread_ts ) if __name__ == "__main__": SocketModeHandler(app, os.getenv("SLACK_APP_TOKEN")).start()
注目点を挙げていきましょう。
msg = re.sub(f'<@{slack_bot_id}>', '', text)
前述の通り、Azure OpenAIに渡すときに邪魔になりそうなので、Slackチャットボットへのメンション @chatbot-test
を削除します。
events = graph.stream( {"messages": [("user", msg)]}, {"configurable": {"thread_id": thread_ts}}, stream_mode="values" )
LangGraphにおけるひとまとまりの会話を示す thread_id
には、Slackにおけるスレッドの識別子である thread_ts
を設定しています。これにより、LangGraphとSlackの両方でスレッドの範囲が一致します。
for event in events: message = event["messages"][-1] if type(message) == AIMessage: say( text=message.content, thread_ts=thread_ts )
thread_ts
を指定することでSlackにおけるスレッド内に返信します。
では、このPythonスクリプトをローカルPC上で実行し、
(langgraph) % ./32_slack_langgraph_sqlite.py ⚡️ Bolt app is running!
Slackのほうで @chatbot-test
にメンションしてみましょう。
スレッドとして返信が返ってきました! 引き続き会話を続けてみましょう。
見事にスレッド内で会話が成立しました。
では、別のスレッドで「プロフィールは何?」と聞いてみて、この会話内容を覚えているか見てみましょう。
想定通り、別スレッドの内容は知らないようです。元のスレッドで聞いてみると、
きちんとまとめてくれました。スレッドごとで会話が独立していることがわかります。
まとめ
本稿ではAzure OpenAIとSlackのスレッド内で会話するチャットボットをBolt for PythonとLangGraphで作成し、ローカルPCで動かしてみました。
まだ単純にAzure OpenAIとやりとりしているだけですが、LangGraphを組み合わせたことから、さまざまな応用ができるような形になっています。なお、エラー処理はまったく行っていませんし、Azure OpenAIとのやりとりに時間がかかっていることがわかりにくいなど、さらなる改善・改良の余地があります。さらに、今回はローカルPCで動作させているため、安定した動作場所の確保も必要となります。
本ブログでは引き続きAIチャットボットの改善・改良を進めていきます。