2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Dify のチャットワークフローを Slack Bot 化してみる

Posted at

やりたいこと

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」をクリック。

image.png

少し下にスクロールしたところにある「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 でチャットワークフローを選択した際のデフォルトのままで作成する。

image.png

サイドバーにある、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」のマークがつく。
image.png

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 で作成したチャットワークフローが呼び出され、過去の内容も踏まえて回答してくれる!

image.png

ということで

Dify で作成したチャットワークフローを Slack Bot で呼び出せるようにしてみました。普段利用する機会の多い Slack で気軽に呼べるのはやはり便利ですね。
コード内の "inputs": {}, # 作成したチャットワークフローによって指定する の部分が、Slack 上からだと指定するのが難しそうなので、何らか方法を考えたい箇所です。

Dify はアップデートが早く、今回は 0.15.3 を使いましたが 1.0.X がすでにリリースされプラグイン機能が追加されたことで、ますます利便性が向上しそうです!

以上です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?