やりたいこと
Dify (https://github.com/langgenius/dify) で作成したチャットワークフローを Slack から使えると便利だなと思い、実際にやってみたので記事にしてみます。
Dify とは?
ChatGPT に聞いてみました。
Difyは、プログラミングの知識がなくても直感的にAIアプリケーションを開発できるオープンソースのプラットフォームです。 非エンジニアやビジネスユーザーでも、ノーコードでチャットボットやコンテンツ生成ツールなどの高度なアプリを構築できます。
Difyの主な特徴:
直感的なノーコードUI: ドラッグ&ドロップ操作でアプリケーションを構築でき、初心者でも手軽にAIアプリの開発が可能です。
多彩なAIモデルへの対応: OpenAI、Anthropic、Hugging Faceなど、さまざまなAIモデルプロバイダーをサポートしており、ユーザーのニーズに合わせて最適なモデルを選択できます。
RAGエンジンによる高度な機能: 外部データソースから必要な情報を効率的に取り込み、高精度な生成処理を実現します。
豊富なテンプレートとコンポーネント: チャットボット、タスクリスト、カレンダーなど、よく使われる機能をテンプレートとして提供しているため、ゼロからの開発は不要です。
Difyの利用方法:
クラウドでの利用: Difyの公式サイトにアクセスし、アカウントを作成することで、ブラウザ上で手軽に利用を開始できます。
ローカル環境での利用: オープンソースソフトウェアとして提供されているため、GitHubからリポジトリをクローンし、ローカル環境にインストールして利用することも可能です。
Difyの料金体系:
Difyは、無料プランから本格開発向けの有料プランまで、幅広い料金体系を提供しています。 無料プランでも基本的な機能を利用できますが、商用利用や高度な機能を必要とする場合は、有料プランの検討が必要です。
商用利用時の注意点:
Difyは基本的に商用利用が可能ですが、マルチテナントSaaSサービスの提供やロゴ・著作権情報の変更など、特定のケースでは商用ライセンスの取得が必要となります。
活用事例:
チャットボットの作成: Difyを活用して、企業独自のナレッジベースと連携した高度なチャットボットを開発することができます。
テキスト生成: マーケティングコピーの作成、製品説明文の自動生成、レポート作成支援など、さまざまなビジネスシーンで活用できるテキスト生成ツールを構築できます。
Difyは、AI開発の敷居を下げ、誰もが手軽に高度なAIアプリケーションを構築できるプラットフォームとして注目されています。 プログラミングの壁を越えて、誰もがAIの力を活用できる時代の到来を感じさせるDifyの世界をご覧ください。
回答してくれている通りですね。生成 AI アプリケーションをローコード・ノーコードで構築するためのプラットフォームです。
SaaS 版 と OSS 版 があり、基本的には OSS の内容が SaaS でも提供されているようです。
OSS は開発が活発で、だいたい毎週新しいバージョンがリリースされています。公式 Discord もあり、議論が活発に行われていたり、困りごとを誰かが回答してくれていたりします。
環境
- Slack bot
- Dify: 0.15.3
- localhost ではなくインターネットからアクセスできる必要があります
- SaaS 版を利用するか、AWS などにセルフホストしインターネットに公開してください
- 何らか API サーバ
- 今回は Azure Functions App を使います (他の FaaS でも 別途サーバを立てても可)
- ファイル保存用に Azure Storage Account もつかいます
構成
ざっと以下のような流れ。
今回はチャットワークフローを利用するので、Dify 側で発行される conversation_id を保持しておく必要があり、Azure の Storage Account で Slack の thread_ts と conversation_id とを紐づけて保存している。
(Optional) Slack ワークスペースの作成
以下から、今回の作業用にワークスペースを作成する。
Slack bot の作成
以下から新しいアプリを作成する。
右上の「Create New App」をクリックして、「From scratch」をクリック。
アプリの名前を入力して、使いたいワークスペースを選択する。今回用にワークスペースを作成した場合はそのワークスペースを選択する。
Create App をクリックして作成。
権限を付与する
アプリが作成されると以下のような画面になるので、左側のメニューから「OAuth&Permissions」をクリック。
少し下にスクロールしたところにある「Scopes」で以下の権限を追加する。
- app_mentions:read
- chat:write
他に権限が必要な場合は、用途に合わせて以下から必要な権限を追加する。
ページの先頭に戻り、「OAuth Tokens」をコピーして控える (Azure Functions App の中で利用する)。
イベントのサブスクリプション設定
今回は、アプリがメンションされた時に Azure Functions App を呼び出して Dify のチャットワークフローを実行するので、そのイベントを拾うための設定をする。
左側のメニューから「Event Subscription」をクリック。「Enable Events」のトグルボタンをクリックして、「On」にする。
アコーディオンメニューに「Subscribe to bot events」があるのでクリックすると、そのイベントを設定できる。「Add Bot User Event」をクリックして、「app_mention」を選択する。
「Request URL」のテキストボックスがあるが、ここに設定した URL に対してイベント発生が発生した際にリクエストが実行される。設定する初回は、その URL が正しいかの検証が行われる。具体的には、https://api.slack.com/events/url_verification の内容のリクエストが行われ、リクエストボディ内の "challenge"
に設定されている文字列がそのまま返ってくるか検証される。
他のイベントについて詳しくはここ↓↓↓
チャットワークフローの作成
Dify でチャットワークフローを選択した際のデフォルトのままで作成する。
サイドバーにある、API アクセスから API KEY を発行して、控えておく (Azure Functions App の中で利用する)
API の作成
今回は以下のような形で作成してみた。参考まで!
注意点としては以下。
- Events API はレスポンスが3秒以内に返ってくることを期待している
- 3秒以内に返ってこない場合、リトライが3回まで行われる
- LLM を使うとタイムアウトすることが多いので、今回はリクエストヘッダに
x-slack-retry-num
があれば無視するようにしている
- Azure Functions App ではいくつかの認証方式があるが今回は簡易的なので、認証なしにしている (
AuthLevel.ANONYMOUS
の部分) - 各環境変数は事前に設定する
- Storage Account についても事前に作成しておく
- 接続文字列を Azure Functions App 内で利用する
下記コードを Azure 上にデプロイして、発行される URL をテキストボックスに入力すると、検証用のリクエストが実行される。問題なければ、「Verified」のマークがつく。
Codes
import json
import logging
import os
import azure.functions as func
import requests
from azure.storage.blob import BlobServiceClient
app = func.FunctionApp()
# Environment variables
DIFY_CHAT_WORKFLOW_API_KEY = os.getenv("DIFY_CHAT_WORKFLOW_API_KEY", "")
DIFY_APP_BASE_URL = os.getenv("DIFY_APP_BASE_URL", "")
SLACK_OAUTH_TOKEN = os.getenv("SLACK_OAUTH_TOKEN", "")
STORAGE_ACCOUNT_CONNECTION_STRING = os.getenv("STORAGE_ACCOUNT_CONNECTION_STRING", "")
@app.route(route="slack/events", auth_level=func.AuthLevel.ANONYMOUS)
def slack_events(req: func.HttpRequest) -> func.HttpResponse:
# Ignore retry requests from Slack
if is_retry_request(req):
return func.HttpResponse(status_code=200)
logging.info("slack_events function was triggered")
req_body = req.get_json()
logging.info(f"request body: {req_body}")
# Handle Slack URL verification challenge
if "challenge" in req_body:
return func.HttpResponse(
body=slack_verification(req_body),
headers={"Content-Type": "text/plain"},
status_code=200,
)
# Extract necessary information from the request body
channel = req_body["event"]["channel"]
thread_ts = req_body["event"].get(
"thread_ts", req_body["event"]["ts"]
) # when start a conversation, thread_ts is not included and use ts instead
user = req_body["event"]["user"]
# Retrieve conversation ID from storage
conversation_id = get_conversation_id(thread_ts)
logging.info(f"conversation_id: {conversation_id}")
# Process the Slack app mention event
content, conversation_id = slack_app_mention(req_body, conversation_id)
store_conversation_id(thread_ts, conversation_id)
# Send a message to Slack
status = send_message_to_slack(content, channel, thread_ts, user)
msg = (
"the message was sent successfully"
if status == 200
else "failed to send the message"
)
return func.HttpResponse(body=msg, status_code=status)
def is_retry_request(req: func.HttpRequest) -> bool:
# Check if the request is a retry request from Slack
if "x-slack-retry-num" in req.headers:
logging.info(
f"ignore the retry request x-slack-retry-num: {req.headers['x-slack-retry-num']}"
)
return True
return False
def slack_verification(req_body: dict) -> str:
# Return the challenge token for Slack URL verification
logging.info(f"slack_verification executed")
return req_body["challenge"]
def slack_app_mention(req_body: dict, conversation_id: str) -> str:
# Handle Slack app mention event and call the workflow to get a response
logging.info(f"slack_app_mention executed")
query = req_body["event"]["blocks"][0]["elements"][0]["elements"][1]["text"]
req_msg = {
"inputs": {}, # 作成したチャットワークフローによって指定する
"query": query,
"response_mode": "blocking",
"conversation_id": conversation_id,
"user": "Azure Functions App",
"files": [],
}
return chat_messages_run(req_msg)
def chat_messages_run(req_msg: dict):
logging.info(f"chat_messages_run: {req_msg}")
request_headers = {
"Authorization": f"Bearer {DIFY_CHAT_WORKFLOW_API_KEY}",
"Content-Type": "application/json",
}
response = requests.post(
url=DIFY_APP_BASE_URL + "/chat-messages",
headers=request_headers,
json=req_msg,
)
logging.info(
f"Response Code from Dify app: {response.status_code}, {response.json()}"
)
return response.json()["answer"], response.json()["conversation_id"]
def send_message_to_slack(content: str, channel: str, thread_ts: str, user: str) -> int:
logging.info(
f"send_message_to_slack: {content[:100]} ..., {channel}, {thread_ts}, {user}"
)
url = "https://slack.com/api/chat.postMessage"
headers = {
"Authorization": f"Bearer {SLACK_OAUTH_TOKEN}",
"Content-Type": "application/json",
}
body = {
"channel": channel,
"text": f"<@{user}>\n{content}",
"thread_ts": thread_ts,
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"<@{user}>\n{content}",
},
}
],
}
response = requests.post(url=url, headers=headers, json=body)
logging.info(f"Response Code from Slack: {response.status_code}, {response.json()}")
return response.status_code
def get_conversation_id(thread_ts: str) -> str:
logging.info(f"get_conversation_id: {thread_ts}")
blob_service_client = BlobServiceClient.from_connection_string(
conn_str=STORAGE_ACCOUNT_CONNECTION_STRING
)
container_client = blob_service_client.get_container_client("func-data")
blob_name = f"{thread_ts}.json"
if blob_name in [blob.name for blob in container_client.list_blobs()]:
logging.info(f"blob name: {blob_name} is found")
blob_client = container_client.get_blob_client(blob=blob_name)
data = blob_client.download_blob().readall()
data = json.loads(data.decode("utf-8").replace("'", '"'))
return data["conversation_id"]
else:
logging.info(f"blob name: {blob_name} is not found")
return ""
def store_conversation_id(thread_ts: str, conversation_id: str):
logging.info(f"store_conversation_id: {thread_ts}, {conversation_id}")
blob_service_client = BlobServiceClient.from_connection_string(
conn_str=STORAGE_ACCOUNT_CONNECTION_STRING
)
container_client = blob_service_client.get_container_client("func-data")
blob_name = f"{thread_ts}.json"
data = json.dumps({"conversation_id": conversation_id})
if blob_name not in [blob.name for blob in container_client.list_blobs()]:
logging.info(f"blob name: {blob_name} is not found, create a new blob")
blob_client = container_client.get_blob_client(blob=blob_name)
blob_client.upload_blob(data)
実行してみる
Slack から対象の Bot に対してメンションしてメッセージを送ると Dify で作成したチャットワークフローが呼び出され、過去の内容も踏まえて回答してくれる!
ということで
Dify で作成したチャットワークフローを Slack Bot で呼び出せるようにしてみました。普段利用する機会の多い Slack で気軽に呼べるのはやはり便利ですね。
コード内の "inputs": {}, # 作成したチャットワークフローによって指定する
の部分が、Slack 上からだと指定するのが難しそうなので、何らか方法を考えたい箇所です。
Dify はアップデートが早く、今回は 0.15.3 を使いましたが 1.0.X がすでにリリースされプラグイン機能が追加されたことで、ますます利便性が向上しそうです!
以上です。