モーグルとカバとパウダーの日記

モーグルやカバ(EXカービング)山スキー(BC)などがメインの日記でした。今は仕事のコンピュータ系のネタが主になっています。以前はスパム対策関連が多かったのですが最近はディープラーニング関連が多めです。

ローカルのdifyに独自の組み込みツールを作成する

この記事はDifyアドベントカレンダー2024用に書いた記事です。

ローカルのdifyに独自の組み込みツールを作成する

difyには組み込みツールという各種SaaSなどを呼び出すようなツールが準備されています。
difyは元々SaaSでのサービスをメインに作られている(と思われる)ため、例えば基本的にファイルへの出力がサポートされていないとか、(まだ)動画や音声を扱うようなものがあまりなかったりします。
しかしローカルで動かしてているdifyでは、生成されたテキストをファイルに書き出したいとか、動画をffmpeg的なもので編集したいとかが結構あります。

そこで、ファイルへのテキスト書き込みだけをする非常に簡単な組み込みツールを作ることを題材に、ローカルのdifyに独自の組み込みツールを作成して運用するための手順を説明します。

公式では下記ページにビルトインツールの作り方について説明があります。この記事と合わせて参照してみてください。
クイック統合ツール | Dify https://docs.dify.ai/ja-jp/guides/tools/quick-tool-integration

組み込みツールの場所と構成

組み込みツールのおいてある場所は、ソース内の以下のディレクトリにあります。

dify/api/core/tools/provider/builtin/

この中に、組織名(サービス名)でディレクトリを作ります。

今回は公開するものではなく自分用のツールを作るという体で「mytools」というディレクトリにしました。つまりこの場合

dify/api/core/tools/provider/builtin/mytools/

というフォルダに自前のツールを作っていくことになります。
この組織名(サービス名)は、ディレクトリ名と同一になるので、当然ユニークである必要があります。

difyは頻繁にアップデートされていきますが、多くの場合gitでpullしてアップデートしていると思います。
その際にgitでのコンフリクトが起きないように、組み込みツールのフォルダのみを追加すれば動くように設計されています。

.gitignoreの設定

それでもgitで変更が出てくるのが気持ち悪いと思うので、builtinフォルダの中にgitignoreを設定しておきます。

dify/api/core/tools/provider/builtin/.gitignore

 .gitignore
 mytools

組み込みツールの中身

このディレクトリ内には

  • _assets/
  • tools/
  • mytools.yaml
  • mytools.py

を準備します。
ここでフォルダ名と同じ名前のついている「mytools.yaml」や「mytools.py」は、全体についての設定と基本のクラス設定のみです。

tools/ ディレクトリ内に、具体的に機能ごとのスクリプトとその設定のYAMLファイルを作っていきます。
このとき、このツールごとのファイル名は他のツールのファイル名ともユニークである必要がある、ように思います。たぶん…

_assets/ ディレクトリ以下には、アイコンやスクリプトから使われるファイルが置かれます。アイコンが不要ならばこのディレクトリ自体がなくても大丈夫です。

ツールの作成方法

具体的にファイルを作成していきます。
新しく作るときには、関連しそうなツールをコピーして、その中身を変更するのが楽だと思います。

mytools.yaml

組織名(サービス名)自体の設定を書きます。
名前と作者の設定、どの種別のツールかだけです。

identity:
  author: stealthinu
  name: mytools
  label:
    en_US: My Tools
  description:
    en_US: My Tools
  icon: icon.svg
  tags:
    - utilities

mytools.py

ほぼ決め打ちで、class名をファイル名と同じ名前でつけます。
なので名前は他と被らないように注意します。

from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController

class MyToolsProvider(BuiltinToolProviderController):
    def _validate_credentials(self, credentials: dict) -> None:
        pass

tools/file_writer.yaml

ツールの名前や作者、説明、パラメータの説明を書きます。
パラメータの説明には、どんなパラメータが必要で、それが何か、データの型や必須条件などを記述します。 ただしここに設定した内容は、あくまで説明のためのもので、プログラム的には後述のget_runtime_parametersの中で指定するようになっています。

identity:
  name: file_writer
  author: stealthinu
  label:
    en_US: File Writer
description:
  human:
    en_US: Write content to a file.
  llm: Write content to a file.
parameters:
  - name: content
    type: string
    required: true
    label:
      en_US: Content
      ja_JP: 内容
    human_description:
      en_US: The content to write to the file.
      ja_JP: ファイルに書き込む内容。
    llm_description: The content to write to the file.
    form: llm

tools/file_writer.py

ここに実際の処理を書きます。絶対に必要なものは以下の2関数です。

  • get_runtime_parameters : 取得するパラメータとその型や説明を設定します。
  • _invoke : ノードで処理する内容。実際にやりたい処理を書きます。

パラメータの渡し方だけ注意すればあとは普通にpythonを書けばよいです。
ただし、ファイルの出力についてはdify側にデータを渡して処理してもらうような書き方が正しいようで、そのあたりが注意が必要です。 ここではcreate_blob_messageを使って、組み込みツール用のディレクトリにテキストファイルを出力するようにしています。

tools/file_writer.py

from typing import Any

from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter
from core.tools.tool.builtin_tool import BuiltinTool


class FileWriterTool(BuiltinTool):
    def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> list[ToolInvokeMessage]:
        content = tool_parameters.get("content")
        
        if not content:
            return [self.create_text_message("No content provided")]

        try:
            # コンテンツをUTF-8でエンコード
            binary_content = content.encode('utf-8')

            # Difyシステムにファイルを返す
            return [
                self.create_text_message("Successfully prepared content"),
                self.create_blob_message(
                    blob=binary_content,
                    meta={"mime_type": "text/plain"},
                    save_as=self.VariableKey.CUSTOM.value,
                ),
            ]
            
        except Exception as e:
            return [self.create_text_message(f"Failed to process file: {str(e)}")]

    def get_runtime_parameters(self) -> list[ToolParameter]:
        parameters = []

        # コンテンツパラメータ
        parameters.append(
            ToolParameter(
                name="content",
                label=I18nObject(
                    en_US="Content",
                    ja_JP="内容"
                ),
                human_description=I18nObject(
                    en_US="The content to write to the file.",
                    ja_JP="ファイルに書き込む内容。"
                ),
                type=ToolParameter.ToolParameterType.STRING,
                form=ToolParameter.ToolParameterForm.LLM,
                required=True
            )
        )

        return parameters

dockerの運用方法

difyの運用はdockerで行われていることが多いのではないかと思います。
組み込みツールを入れて docker compose up しても、組み込みツールが入った状況で dify が動くことはありません。これは docker-compose で動いている dify-api が、dokcer hubから落としてきたdockerイメージを利用するためです。
そこで、ローカルで dify-api のdockerイメージを作成して、docker-compose のオーバーライド設定で、その docker-image を使うように設定してやります。

dify-apiは下記のようにしてローカルに「dify-api」というdockerイメージで作成します。

$ cd dify/api
$ docker build . -t dify-api

そのままdocker-composeを使うと、ローカルのdify-api dockerイメージが利用されないため、以下のようなdocker-compose.override.yamlを作成します。

dify/docker/docker-compose.override.yaml

services:
  api:
    image: dify-api:latest 

  worker:
    image: dify-api:latest

この状況で

$ docker compose up -d

すると組み込みツールが入った状況でdifyが起動します。

動作確認

difyが起動したら、「ツール」を確認して「My Tools」が存在しているか確認してください。
さらに「My Tools」を選択すると「File Writer」があること、パラメータがContent文字列が必須であることが確認できます。

スタジオでワークフローを作成し、「開始」の入力フィールドに「query」という文字列入力のパラメータを追加します。
開始 -> + -> ツール -> My Tools/File Writer を選択し、「内容」に「開始/(x)query」を選択します。 File Writer -> + -> 「終了」で、出力変数に「FIle Writer/(x)files」を選択します。

dify-filewriter

組み込みツールがファイルを出力する場所

ツールがファイルを出力する場所は、difyをdockerで動かしている場合、以下のdocker用のディレクトリの中に、さらにUIDでディレクトリが作られて、その中に出力されます。

dify/docker/volumes/app/storage/tools/

例えば以下のようなファイル名でテキストファイルが作成されます。

dify/docker/volumes/app/storage/tools/de4a84eb-ac6e-45d3-a8b6-49f776dfc900/cf4ff00b019a4f9c81cce53cc0137aa3.txt