こんにちは堤です。
最近よくDifyを使って遊んでいます。使っていくなかで他のチャットツールと連携させる方法を知りたいと思ったので、今回はSlackと連携する方法を備忘がてらまとめてみました。
Difyとは
Difyは、オープンソースのLLMアプリケーション開発ツールで、ドラッグアンドドロップの簡単な操作で複雑なワークフローのアプリケーションを作ることができるのが特徴です。 コードを書くことなく、LangChainなどのフレームワークよりも簡単にLLMアプリを作成することができます。
主な特徴や機能をまとめてみました。
- 幅広いモデルが選択できる
OpenAI, Anthropic, Googleといった主要なプロバイダーのモデルはもちろん、オープンソースのモデルなど様々なモデルをサポートしているので、目的や用途にあった最適なモデルを選択することができます。
- 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は個人的にとても好きなサービスなので、今後もアップデートなどを追っていきたいと思います。