NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    DifyとSlackを連携したSlack Botをつくってみた

    こんにちは堤です。

    最近よくDifyを使って遊んでいます。使っていくなかで他のチャットツールと連携させる方法を知りたいと思ったので、今回はSlackと連携する方法を備忘がてらまとめてみました。

    Difyとは

    Difyは、オープンソースのLLMアプリケーション開発ツールで、ドラッグアンドドロップの簡単な操作で複雑なワークフローのアプリケーションを作ることができるのが特徴です。 コードを書くことなく、LangChainなどのフレームワークよりも簡単にLLMアプリを作成することができます。

    dify.ai

    主な特徴や機能をまとめてみました。

    • 幅広いモデルが選択できる

    OpenAI, Anthropic, Googleといった主要なプロバイダーのモデルはもちろん、オープンソースのモデルなど様々なモデルをサポートしているので、目的や用途にあった最適なモデルを選択することができます。

    https://github.com/langgenius/dify

    • RAGのサポート

    RAGもDify上で使うことができます。ドキュメントの取り込みからチャンキング、Embeddingや検索までDifyでサポートされているので、ドキュメントを登録するだけですぐ使えます。ハイブリッド検索やリランキングもサポートされており、カスタマイズ性が高いところも嬉しいです。

    • 様々なツールが使用できる。

    エージェント作成などで役に立つツールも様々なものがサポートされています。Web検索やCode Interprinter、計算ツールやスクレイピングツールなど一通りできることは網羅されており、カスタムツールも作成することが可能なので自作のAPIとの連携もできます。

    • ログやメトリクス取得機能が充実している。

    Difyではデフォルトでログやメトリクスが実装されているので、管理画面からすぐ利用状況を確認することができます。LangChainでもLangSmithというツールがありますが、デフォルトで利用状況が記録されており、すぐにメトリクスを確認できるのはとても便利だと思いました。利用料金の確認はもちろん、回答に対するフィードバックの確認もできます。

    Slack Botの作り方

    それではDifyとSlackを連携させたSlack Botを作っていきます。今回は、よくある「Botに対してメンションをしたら、そのスレッドで回答してくれる」ような仕組みのBotを作ってみました。 DifyとSlackだけでは完結しないため、間にAWS Lambdaを使用しています。 また今回はSlack BotやLambdaの詳細な説明は省略します。

    今回作成する簡単な構成図です。

    Slack Botの準備

    権限の付与

    「OAuth & Permissions」から以下の権限を付与しておきます。

    • app_mentions:read
    • channels:read
    • chat:write

    Lambdaの関数URLの作成

    今回は簡単に作りたいので、API Gatewayは作成せず、関数URLからLambdaを叩くようにします。 AWSコンソールからLambda関数を作成し、Lambda関数の「設定」の「関数URL」から設定を行いましょう。 今回は検証ということで認証を何もしていませんが、今のままではエンドポイントが分かったら誰でも叩けてしまうので注意してください。

    Event Subscriptionsの設定

    Slackの画面に戻り「Event Subscriptions」からイベントの設定を行います。Request URLには先程作成したLambdaの関数URLを入力します。 この際、challengeパラメータの検証が行われるので、それに合わせてLambdaも編集しておきます。基本的には入ってきたeventをそのままリターンすれば正しく検証されると思います。 「Subscribe to bot events」にはBotに対してメンションしたときに動作するようにしたいので、app_mentionを設定しておきます。

    最後に「Installed App Settings」から設定をインストールしてBot User OAuth Tokenを取得すれば準備は完了です。

    Difyのアプリ作成

    今度はDifyのほうでアプリケーションを作成していきましょう。自分でホストする方法もありますが、今回はクラウド版のDifyを使用しています。 まずはシンプルにLLM(今回はGPT-4o)に回答してもらうだけのアプリを作ってみました。

    作成できたら「公開する」からアプリを公開し、左の「APIアクセス」からAPIキーを取得しておきます。

    Lambda関数の作成

    先ほど作成したLambdaのコードを修正します。

    全体コード

    import os
    import json
    import urllib.request
    from urllib.error import URLError, HTTPError
    
    # 環境変数からSlackのボットトークンとDifyのAPIキーを取得
    slack_bot_token = os.environ["SLACK_BOT_TOKEN"]
    dify_api_key = os.environ["DIFY_API_KEY"]
    
    # Dify APIを使用してチャットメッセージを投稿する関数
    def post_chat_message(api_key, query, user_id, conversation_id=""):
        url = 'https://api.dify.ai/v1/chat-messages'
        headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
            'User-Agent': ''
        }
        data = {
            "inputs": {},
            "query": query,
            "response_mode": "blocking",
            "user": user_id,
            "conversation_id": conversation_id
        }
        
        request_data = json.dumps(data).encode('utf-8')
        request = urllib.request.Request(url, data=request_data, headers=headers, method='POST')
        
        try:
            with urllib.request.urlopen(request) as response:
                response_data = response.read()
                return json.loads(response_data)
        except HTTPError as e:
            print(f'HTTPError: {e.code} - {e.reason}')
            print(e.read().decode())  # サーバーからのエラーメッセージを表示
        except URLError as e:
            print(f'URLError: {e.reason}')
    
    # Slackイベントから必要な詳細を抽出する関数
    def extract_event_details(event):
        body = json.loads(event['body'])
        event_data = body.get('event', {})
        
        result = {
            "channel": event_data.get('channel'),
            "user_id": event_data.get('user'),
            "text": event_data.get('text'),
            "thread_ts": event_data.get('thread_ts', event_data.get('ts'))
        }
        
        return result
    
    # Slackのスレッドにメッセージを投稿する関数
    def post_message_to_thread(api_key, channel, text, thread_ts):
        url = 'https://slack.com/api/chat.postMessage'
        headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        }
        data = {
            "channel": channel,
            "text": text,
            "thread_ts": thread_ts
        }
        
        request_data = json.dumps(data).encode('utf-8')
        request = urllib.request.Request(url, data=request_data, headers=headers, method='POST')
        
        try:
            with urllib.request.urlopen(request) as response:
                response_data = response.read()
                return json.loads(response_data)
        except HTTPError as e:
            print(f'HTTPError: {e.code} - {e.reason}')
            print(e.read().decode())  # サーバーからのエラーメッセージを表示
        except URLError as e:
            print(f'URLError: {e.reason}')
    
    # Slackリトライヘッダーが存在するか確認する関数
    def has_slack_retry_header(event):
        headers = event.get('headers', {})
        if 'x-slack-retry-num' in headers:
            return True
        return False
    
    def lambda_handler(event, context):
        # リトライイベントを無視する
        if has_slack_retry_header(event):
            return {
                'statusCode': 200
            }
        
        # イベントから詳細を抽出
        result = extract_event_details(event)
        
        # Dify APIを使用して応答を取得
        dify_response = post_chat_message(dify_api_key, result["user_id"], result["text"])
        answer = dify_response["answer"]
        
        # Slackのスレッドに応答を投稿
        slack_response = post_message_to_thread(slack_bot_token, result["channel"], answer, result["thread_ts"])
        
        return {
            'statusCode': 200
        }
    

    基本的な流れとしては

    • 入力イベントから必要な情報を取得
    • Dify APIでアプリケーションを実行して返答を取得
    • 返答をSlack APIでメンションがあったスレッドに返す

    という処理になっています。

    環境変数にはSlackとDifyでそれぞれ取得したアクセストークンとAPIキーを設定しておきましょう。

    なお、SlackのAPIには3秒以内にレスポンスを返さないとタイムアウト扱いでエラーになってしまうという3秒ルールが存在します。LLMに対するリクエストだと3秒以上かかってしまうことがよくあり、その場合リトライ処理で複数のメッセージが送信されてしまいます。対策としては別のLambdaを叩くなどが考えられますが、今回はリトライヘッダーがあるかどうかで処理を分けてリトライ処理を走らせないようにしています。

    # Slackリトライヘッダーが存在するか確認する関数
    def has_slack_retry_header(event):
        headers = event.get('headers', {})
        if 'x-slack-retry-num' in headers:
            return True
        return False
        
    def lambda_handler(event, context):
        # リトライイベントを無視する
        if has_slack_retry_header(event):
            return {
                'statusCode': 200
            } 
    

    動作確認

    Slack上で動作確認してみたいと思います。 メンションを送ったら、Difyで実行された結果が返ってくることが確認できました。

    ただし、今回作成したLambda関数では会話履歴を参照することができません。 Difyには conversation_idというパラメータが存在し、それをリクエストの際指定することで会話履歴を含んだ応答を行うことができます。Slackであればスレッド単位で会話を分けたいので、thread_tsとconversation_idを紐づけ、それをDynamoDBなどに保存するという実装が考えられます。

    Bot作成例

    Difyを使ったSlack Botの例をいくつかご紹介します。

    1. 複数モデルを同時に実行するボット

    GPT-4o, Claude 3 Sonnet, Gemini 1.5 Proで同じ質問について回答させて、その結果をClaude 3 Haikuでまとめて回答させるSlack Botです。Difyでは現時点でまだ並列実行がサポートされていないので、少し時間はかかってしまうところがネックではあります。

    2. Web検索可能なボット

    TavilySearchというWeb検索APIを使用してWeb検索を行い、その結果をもとにGPT-4oに回答させるワークフローです。Web検索する前にユーザーの質問をクエリ変換する処理も入れてます。

    3. Pythonコード実行ボット

    最後はCode Interpreterツールを使ったPythonコードを実行できるワークフローです。 複雑な計算をさせたいときなどに使えそうです。

    Slack上からは確認できませんが、ログをみるとちゃんとPythonコードを実行して計算していることがわかります。

    まとめ

    今回はDifyを使ったSlack Botの作成方法をまとめてみました。LangChainで作るよりも圧倒的に楽ですし、バックエンドとLLMアプリケーションのロジックを分離できるのがいいなと思っています。Difyは個人的にとても好きなサービスなので、今後もアップデートなどを追っていきたいと思います。

    執筆者堤 拓哉

    インフラエンジニア。データ分析やデータ基盤周りの話に興味があります。