fbpx

SlackボットをAzure App Serviceで動かそう #slack #ai #langgraph #azure #openai #llm #python

はじめに

前回の「Slackでスレッドに返信するボットをローカルPCで動かそう」において、SlackのチャットボットをローカルPC上で動かし、Azure OpenAIと会話できるようにしました。しかしローカルPC上の動作なので、当然ながらローカルPCが起動しているときしかボットが動作しないという大きな欠点があります。

そこで本稿では、チャットボットを安定運用するためにサーバーレスアプリケーション環境である Azure App Service で動かしてみます。加えて、会話履歴はAzure Cosmos DB for PostgreSQL に記録するようにしてみます。

おさらい:ローカルPCで動かしたSlackチャットボット

実際のコードについては前回の「Slackでスレッドに返信するボットをローカルPCで動かそう」を参照してほしいのですが、次の特徴があります。

これらの特徴のいくつかは、サーバーレスアプリケーションで動かす際の障害となるため、作り変える必要があります。

HTTP Request URLs

ローカルPCで動作させていた際に採用していたソケットモードはステートフルで動作するため、サーバーレスアプリケーションとして動かすには不適です。そこでステートレスであるHTTP Request URLsを使用するモードに作り変える必要があります。ソケットモードとHTTPモードの詳しい比較については公式ドキュメント「Exploring HTTP vs Socket Mode」をご覧ください。

HTTPモードで動作させるには、Slackからのリクエストを受け付けるHTTPサーバーを起動する必要があります。とはいえ、HTTPサーバーと言ってもたくさんの種類があるため、どれを選べばよいのでしょうか? Bolt for Pythonのドキュメントを見ると、AdaptersFlaskという軽量なフレームワークが紹介されているので、これを利用することにしましょう。examplesにも簡単なコードがあります。

HTTPモードに変更した、スレッドで返信するボット

ソケットモードから、Flaskを使用したHTTPモードに変更したコードです。

さらに、会話履歴を Azure Cosmos DB for PostgreSQL に記録するようにしています。ここでは Azure Cosmos DB for PostgreSQL は最安の開発用プランで作成しています。なお、Azure Cosmos DB for PostgreSQL の作成手順などの詳細は省略していますので、公式ドキュメントを参照してください。

from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request

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
from psycopg import Connection
from langgraph.checkpoint.postgres import PostgresSaver

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")

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):
    with PostgresSaver.from_conn_string( os.getenv("DB_URI") ) as checkpointer:
        checkpointer.setup()
        graph = graph_builder.compile(checkpointer=checkpointer)

        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
                )

flask_app = Flask(__name__)
handler = SlackRequestHandler(app)

@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
    return handler.handle(request)

注目点は次の通りです。

app = App(
  signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
  token=os.getenv("SLACK_BOT_TOKEN")
)

SLACK_APP_TOKEN を使わず、代わりに SLACK_SIGNING_SECRET を使うようになっています。Slack API: Applicationsから、作成した chatbot-test を開きます。こちらの「Basic Information」から、「App Credentials」の「Signing Secret」を取得してください。これを以降の SLACK_SIGNING_SECRET として使用します。

    with PostgresSaver.from_conn_string( os.getenv("DB_URI") ) as checkpointer:
        checkpointer.setup()
        graph = graph_builder.compile(checkpointer=checkpointer)

PostgresSaverを使ってPostgreSQLに接続しています。詳細は How to use Postgres checkpointer for persistence をご覧ください。また DB_URI にはPostgreSQLに接続するための情報を入れます。ここでは Azure Cosmos DB for PostgreSQL の接続文字列「PostgreSQL connection URL」を指定しています。

ローカルPCでチャットボットを起動

サーバーレスアプリケーションとしてデプロイする前に、ローカルPCで確認してみましょう。しかし、LAN内にいるローカルPCでHTTPモードのチャットボットを起動しても、インターネット上のSlackからのリクエストを受け取ることはできません。そこで ngrok を使って、簡単にトンネリングしてみましょう。

Quickstart | ngrok の手順に従い、アプリケーションバイナリをインストールし、サインアップします。認証トークンを取得・設定したら、次のようにHTTPで8000ポートを公開するように起動します。

% ngrok http 8000

「Forwarding」で示されたURLが外部からリクエストを受け付けるURLとなります。これをメモしておきましょう。

そしてボットアプリを起動します。

% FLASK_APP=app.py flask run -p 8000

FLASK_APP で指定するのはボットアプリのファイル名とします。 -p 8000 は8000番ポートで待ち受けするという意味になります。

試しに curl コマンドでアクセスしてみましょう。URLは先程メモしたもので、パスを /slack/events としてみてください。

% curl -X POST https://★-★-★-★-★.ngrok-free.app/slack/events
{"error": "invalid request"}

エラーとなっていますが、アプリを起動したターミナルでもアクセスを受け付けたログが表示されているはずです。

127.0.0.1 - - [25/Nov/2024 18:17:43] "POST /slack/events HTTP/1.1" 401 -

これでボットアプリをローカルPCで起動し、インターネット側からアクセスする準備ができました。

Slack側でアプリをソケットモードからHTTPモードに切り替え

Slack側のアプリ設定を行いましょう。「Slackでスレッドに返信するボットをローカルPCで動かそう」で有効化したソケットモードを無効化することで、HTTPモードを有効化します。

Slack API: Applicationsから、作成した chatbot-test を開きます。次に左のナビゲーションエリアにある「Socket Mode」を「Enable Socket Mode」トグルスイッチをオフにします。

Slack側でアプリのリクエストURLを設定

SlackからボットアプリのHTTPサーバーにアクセスするURLを設定します。

Slack API: Applicationsから、作成した chatbot-test を開きます。次に左のナビゲーションエリアにある「Event Subscriptions」を開き、「Request URL」に前項でcurlでアクセスしてみた「 https://★-★-★-★-★.ngrok-free.app/slack/events 」を入力してエンターを押します。しばらく待つと Verified ✔ と表示されるので、右下の「Save Changes」ボタンを押します。

もし Verified ✔ が表示されずに「 Your URL didn't respond with the value of the challenge parameter 」のようなエラーが出る場合は入力したURLが間違っていないか、アプリが起動しているか、ngrokが起動しているか、確認してください。

Slackで確認

ではSlackでチャットボットに話しかけてみましょう。

スレッド内で返信がありました! HTTPモードでボットが動いていることが確認できました。

Azure App Service の準備

ボットアプリをローカルPCで動かすことと同じように、Azure Virtual Machines上で動かすという手もあるかもしれません。一緒にPostgreSQLも仮想マシン内にまとめてしまえば、もっと安上がるでしょう。反面、仮想マシンのメンテナンスはやっぱり大変です。そこで、サーバーレスアプリケーション環境である Azure App Service で動かしてみましょう。こちらなら仮想マシンのメンテナンスは必要ありません。

Azure App Service のデプロイ

Azure App Serviceの作成手順を見ていきます。特に重要な点のみをピックアップしていきます。詳細は Azure App Service公式ドキュメントを参照してください。

Azure マーケットプレイスから「Webアプリ」を選択します。まぎらわしいですがAzure App Serviceではありません。次の設定で作成します。なお、要点のみ記載しており、開発用の最低限の設定であることに注意してください。

  • 基本
    • 公開: コード
    • ランタイムスタック: Python 3.12
    • オペレーティングシステム: Linux
  • データベース
    • なし
  • デプロイ
    • なし
  • ネットワーク
    • パブリックアクセスを有効にする: オン
    • ネットワークインジェクションを有効にする: オフ
  • 監視とセキュリティ保護
    • なし

アプリの設定

作成したWebアプリの「設定」→「環境変数」で、ローカルPCではpython-dotenvで設定していた環境変数を設定します。

  • AZURE_OPENAI_API_KEY
  • AZURE_OPENAI_API_VERSION
  • AZURE_OPENAI_ENDPOINT
  • AZURE_OPENAI_MODEL_NAME
  • SLACK_BOT_ID
  • SLACK_BOT_TOKEN
  • SLACK_SIGNING_SECRET
  • DB_URI

「設定」→「構成」の「全般設定」タブで、「スタックの設定」の「スタートアップコマンド」に次を指定し、「保存」します。

FLASK_APP=app.py flask run -p 8000 -h 0.0.0.0

アプリのプッシュ設定

本稿ではローカルPC上に Git レポジトリを作り、それを Azure にプッシュするという形を取ります。Azure App Service へのローカル Git デプロイ も参照してください。なお、App Service デプロイで基本認証を無効にする によれば、基本認証の利用は推奨されていないようです。しかしローカル Git は基本認証が無効では利用できません。基本認証を利用しないデプロイ方法は今後の検討課題とします。

「設定」→「構成」の「全般設定」タブで、「プラットフォームの設定」で「SCM基本認証の発行資格情報」と「FTP基本認証の発行資格情報」を両方オンにし、「保存」します。

「デプロイ」→「デプロイセンター」で「設定」タブを開きます。「ソース」のプルダウンメニューから「 ローカル Git 」を選択し、一旦「保存」します。

Git プッシュ先の「Git Clone URI」が出てくるので、これをメモしておきます。

「ローカルGitまたはFTPSの資格情報」タブを開きます。「アプリケーションスコープ」の「ローカルGitユーザー名」と「パスワード」を用いて、先の「Git Clone URI」に対してレポジトリがプッシュできるようになります。

アプリのプッシュ

本稿の最初で作成したPythonスクリプトを app.py という名前でローカルのGitレポジトリに格納しましょう。ここでは webapp-slackbot ディレクトリの中に入れていきます。また、次の requirements.txt も一緒に含めます。

langgraph
python-dotenv
openai
langchain_openai
psycopg
psycopg-pool
langgraph-checkpoint-postgres
slack_bolt
Flask
% mkdir webapp-slackbot
% cd webapp-slackbot
% git init
% git branch -M main master
% cp .../app.py .../requirements.txt .
% git add app.py requirements.txt
% git commit -m "initial import." .
% git remote add origin 【Git Clone URI】
% git push origin master

【Git Clone URI】は先にメモした「Git Clone URI」を入力してください。ここで求められるユーザー名とパスワードは先の「ローカルGitユーザー名」と「パスワード」を入力してください。

正しくプッシュされれば、後は自動的にビルド・デプロイしてくれます。これでAzure App Serviceでサーバーレスアプリケーションとしてチャットボットが動き始めました。

Slack側でアプリのリクエストURLを再設定

SlackからボットアプリのHTTPサーバーにアクセスするURLを、ngrokからAzure App Serviceに切り替えます。作成したWebアプリの「概要」の「規定のドメイン」をコピーしておきましょう。

Slack API: Applicationsから chatbot-test を開きます。次に左のナビゲーションエリアにある「Event Subscriptions」を開き、「Request URL」に先にコピーしたホスト名を入力します。「 https://★.★.azurewebsites.net/slack/events 」のようにパスは補います。入力してエンターを押し、しばらく待つと Verified ✔ と表示されるので、右下の「Save Changes」ボタンを押します。

Slackで確認

では、Slackのほうで @chatbot-test にメンションしてみましょう。

スレッド内で会話が成立しました。

Azure Cosmos DB for PostgreSQL の接続文字列には psql コマンドもあるので SELECT thread_id, checkpoints FROM checkpoints; など発行してみてください。会話履歴が保存されていることがわかると思います。

まとめ

本稿では次の特徴を持つSlackチャットボットアプリを作成しました。

これでサーバーレスアプリケーションとなり、ローカルPCで実行する必要がなくなり、常時安定稼働が期待できます。文中でも述べた通り、起動に必要な最低限の設定しか行っていないため監視などはなく、料金節約のため開発用の最低スペックなので、高アクセス時には問題が発生するかもしれません。しかしながら逆に言うと、最低限の土台ができたので、ここから本番向けにさまざまな実際の要望を組み込んでいくことができるようになったとも言えるでしょう。この先さらに改善・改良を進めたいと思います。

Author

Chef・Docker・Mirantis製品などの技術要素に加えて、会議の進め方・文章の書き方などの業務改善にも取り組んでいます。「Chef活用ガイド」共著のほか、Debian Official Developerもやっています。

Daisuke Higuchiの記事一覧

新規CTA