5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

公開!週末研究ノート12 ー Dify カスタムツールを作る!

Last updated at Posted at 2024-10-06

はじめに ー 週末研究ノートとは?

個人的に研究的な活動をやるにあたり、オープンにしてみたら面白いかもと思い、自分が興味を持っている ざっくりテーマについて、これから、ゆるい週末研究を公開していこうと思います。(有識者の方のアドバイスも、ちょっとというかかなり期待してます!笑)

どこかの権威的な学会やジャーナルなどで発表する予定はないため、万が一、私の記事を利用する際には自己責任でお願いします。そんな人はいないと思いますが、念のため。

今回のサマリ (TL; DR)

  • FastAPI 等で独自に作ったAPIをカスタムツール化するのってできるんだっけ?という疑問に答えます!
  • もちろん、できます!
  • では、どうやって?というのは、本記事に記載していますので、ご覧ください☆彡

環境

  • Fast API 実行環境
    • OS: Ubuntu 24.04.1 LTS
      • IP: 192.168.2.5 (仮)とします
    • Docker version 27.3.0, build e85edf8
    • Python 3.10.12 / Poetry (version 1.8.3)
      • FastAPI 0.115.0
    • コードは、こちら
      • Docker 環境の構築手順
        git clone https://github.com/tkosht/experiment.git
        cd experiment
        make
        
        ※ 約1時間程度かかります
        docker コンテナ構築後の Pythonライブラリのインストールに時間を要します
        なので、backend/pyproject.toml のインストール対象をコメントアウトすることで、時間短縮が可能です
  • Dify 実行環境
    • OS: Ubuntu 24.04.1 LTS
    • Docker version 27.3.0, build e85edf8
    • Dify github
    • 2024/10/06 時点での最新版

今回の週末研究ノート

FastAPI の作成

Dify ドキュメントのAPI 拡張のサンプルコードをそのまま使います

from fastapi import Body, FastAPI, Header, HTTPException
from pydantic import BaseModel

app = FastAPI()


class InputData(BaseModel):
    point: str
    params: dict = {}


@app.post("/api/dify/receive")
async def dify_receive(data: InputData = Body(...), authorization: str = Header(None)):
    """
    DifyからのAPIクエリデータを受信します。
    """
    expected_api_key = "123456"  # TODO このAPIのAPIキー
    try:
        auth_scheme, _, api_key = authorization.partition(" ")

    except Exception as e:
        raise HTTPException(status_code=401, detail=f"Unauthorized {e}")

    if auth_scheme.lower() != "bearer" or api_key != expected_api_key:
        raise HTTPException(status_code=401, detail="Unauthorized")

    point = data.point

    # デバッグ用
    print(f"point: {point}")

    if point == "ping":
        return {"result": "pong"}
    if point == "app.external_data_tool.query":
        return handle_app_external_data_tool_query(params=data.params)
    # elif point == "{point name}":
    # TODO その他のポイントの実装

    raise HTTPException(status_code=400, detail="Not implemented")


def handle_app_external_data_tool_query(params: dict):
    app_id = params.get("app_id")
    tool_variable = params.get("tool_variable")
    inputs = params.get("inputs")
    query = params.get("query")

    # デバッグ用
    print(f"app_id: {app_id}")
    print(f"tool_variable: {tool_variable}")
    print(f"inputs: {inputs}")
    print(f"query: {query}")

    # TODO 外部データツールクエリの実装
    # 返り値は"result"キーを持つ辞書でなければならず、その値はクエリの結果でなければならない
    if inputs.get("location") == "London":
        return {
            "result": "City: London\nTemperature: 10°C\nRealFeel®: 8°C\nAir Quality: Poor\nWind Direction: ENE\nWind "
            "Speed: 8 km/h\nWind Gusts: 14 km/h\nPrecipitation: Light rain"
        }
    else:
        return {"result": "Unknown city"}

ここで、Dify のカスタムツールから実行されるときは、point が app.external_data_tool.query であることに注意しておく

また、expected_api_key の値は、Dify のカスタムツールを設定するときに使います

FastAPI を起動

make poetry
make[1]: ディレクトリ '/home/xxxx/pj/experiment' に入ります
docker compose up -d
[+] Running 1/0
 ✔ Container experiment.app  Running                                                                                                                                                     0.0s
runnning task @ backend: poetry
docker compose exec app bash -c "cd backend && make poetry"
Spawning shell within /home/devuser/workspace/backend/.venv
devuser@fb6f06346a6f:~/workspace/backend$ . /home/devuser/workspace/backend/.venv/bin/activate
(experiment-backend-py3.10) devuser@fb6f06346a6f:~/workspace/backend$

作業ディレクトリを変更して起動スクリプトを実行します

cd app/dify/
sh bin/webapi.sh
INFO:     Will watch for changes in these directories: ['/home/devuser/workspace/backend/app/dify/tools']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [21526] using WatchFiles
INFO:     Started server process [21528]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

FastAPI への接続確認

ターミナルで、以下のスクリプトを実行します

#! /usr/bin/sh

url=http://192.168.2.5:8000/api/dify/receive
api_key="123456"

curl -X POST -sSL $url \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $api_key" \
     -d '
{
    "point": "ping"
}
'
{"result":"pong"}

{"result":"pong"} が表示されたらOKです

Dify でカスタムツールを設定する

Dify にログイン後、「カスタム」タブを選択します

image.png

↑で、「+カスタムツールを作成する」を実行すると、↓のような画面がでてきます

image.png

ツールの名前を設定

「名前」欄を任意に設定します
※ ここでは、「tools-test」という名称を設定します

image.png

スキーマを設定

「スキーマ」欄に、JSON 形式の定義データを設定します。

image.png

スキーマの作成

ここで、現時点では、「+ URLからインポートする」から openapi.json を指定しても、スキーマチェックでエラーになるため、修正済の以下の JSON を使います(※ 上記FastAPI のサーバIPを 192.168.2.5、ポートを 8000 として設定しています )

{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "servers": [
    {
      "url": "http://192.168.2.5:8000"
    }
  ],
  "paths": {
    "/api/dify/receive": {
      "post": {
        "summary": "Dify Receive",
        "description": "DifyからのAPIクエリデータを受信します。",
        "operationId": "dify_receive_api_dify_receive_post",
        "parameters": [
          {
            "name": "authorization",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "title": "Authorization"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InputData"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "InputData": {
        "properties": {
          "point": {
            "type": "string",
            "title": "Point"
          },
          "params": {
            "type": "object",
            "title": "Params"
          }
        },
        "type": "object",
        "required": [
          "point"
        ],
        "title": "InputData"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      }
    }
  }
}

openapi.json の変更点

http://192.168.2.5:8000/openapi.json へアクセスし、"server" セクションを追加します

  • 変更前
{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/api/dify/receive": {
      "post": {
      :
  • 変更後
{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "servers": [
    {
      "url": "http://192.168.2.5:8000"
    }
  ],
  "paths": {
    "/api/dify/receive": {
      "post": {
      :

components/schemas/InputData の "params" の "default" を削除します(型チェックエラーになるため)

  • 変更前
  "params": {
    "type": "object",
    "title": "Params",
    "default": {

    }
  }
  • 変更後
  "params": {
    "type": "object",
    "title": "Params"
  }

認証情報を設定

次に認証情報を設定します

image.png

  • 「認証タイプ」は「APIキー」、「ベアラー」を選択します
  • 「値」に、「123456」を設定します
    • この値は、FastAPI のコード上でハードコードで設定していた値です

image.png

上記設定後、「保存」します

image.png

認証設定の動作確認

「利用可能なツール」の「テスト」を開きます

image.png

  • 「point」パラメータの値に、「ping」を設定し、
  • 「params」パラメータの値に、「{}」(空の辞書)を設定します
  • 「テスト」を実行します

image.png

image.png

「テスト結果」に、{"result": "pong"} が表示されたら接続OKです
「テスト」ダイアログを閉じて、「保存」しておきます

image.png

以上で、カスタムツールの作成が完了です

ワークフローから作成したカスタムツールを設定する

では、実際に以下のようなワークフローを作成して、カスタムツールを利用してみます

image.png

※ 「DIFY_RECEIVE_API_DIFY_RECEI...」というノードが今回作成したツールです

ワークフローの yaml は、以下の通りです。
こちらからダウンロードできます

app:
  description: FastAPIへ接続するワークフロー
  icon: 🤖
  icon_background: '#FFEAD5'
  mode: workflow
  name: FastAPI接続ワークフロー
  use_icon_as_answer_icon: false
kind: app
version: 0.1.2
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      image:
        enabled: true
        number_limits: 3
        transfer_methods:
        - local_file
        - remote_url
    opening_statement: ''
    retriever_resource:
      enabled: true
    sensitive_word_avoidance:
      enabled: false
    speech_to_text:
      enabled: false
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: false
    text_to_speech:
      enabled: false
      language: ''
      voice: ''
  graph:
    edges:
    - data:
        isInIteration: false
        sourceType: tool
        targetType: code
      id: 1728113691816-source-1728113825420-target
      source: '1728113691816'
      sourceHandle: source
      target: '1728113825420'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: code
        targetType: end
      id: 1728113825420-source-1728114165252-target
      source: '1728113825420'
      sourceHandle: source
      target: '1728114165252'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: start
        targetType: code
      id: 1728113681511-source-1728114754455-target
      source: '1728113681511'
      sourceHandle: source
      target: '1728114754455'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: code
        targetType: tool
      id: 1728114754455-source-1728113691816-target
      source: '1728114754455'
      sourceHandle: source
      target: '1728113691816'
      targetHandle: target
      type: custom
      zIndex: 0
    nodes:
    - data:
        desc: ''
        selected: false
        title: 開始
        type: start
        variables:
        - label: location
          max_length: 48
          options:
          - London
          - NewYork
          - Tokyo
          required: true
          type: select
          variable: location
      height: 68
      id: '1728113681511'
      position:
        x: 80
        y: 282
      positionAbsolute:
        x: 80
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: ''
        provider_id: e139d00c-cc53-46c2-b15c-334832c15f13
        provider_name: api-tools
        provider_type: api
        selected: false
        title: dify_receive_api_dify_receive_post
        tool_configurations: {}
        tool_label: dify_receive_api_dify_receive_post
        tool_name: dify_receive_api_dify_receive_post
        tool_parameters:
          authorization:
            type: mixed
            value: '123456'
          params:
            type: mixed
            value: '{{#1728114754455.params#}}'
          point:
            type: mixed
            value: app.external_data_tool.query
        type: tool
      height: 41
      id: '1728113691816'
      position:
        x: 684
        y: 282
      positionAbsolute:
        x: 684
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        code: "import json\n\n\ndef main(posted: str) -> str:\n    jsn = json.loads(posted)\n\
          \    return {\n        \"result\": jsn[\"result\"],\n    }\n"
        code_language: python3
        desc: ''
        outputs:
          result:
            children: null
            type: string
        selected: false
        title: コード Pickup the Result
        type: code
        variables:
        - value_selector:
          - '1728113691816'
          - text
          variable: posted
      height: 41
      id: '1728113825420'
      position:
        x: 988
        y: 282
      positionAbsolute:
        x: 988
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: ''
        outputs:
        - value_selector:
          - '1728113825420'
          - result
          variable: result
        selected: false
        title: 終了
        type: end
      height: 68
      id: '1728114165252'
      position:
        x: 1292
        y: 282
      positionAbsolute:
        x: 1292
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        code: "import json\n\n\ndef main(location: str) -> dict:\n    return {\n \
          \       \"params\": json.dumps({\n            \"app_id\": \"dify-6d61f5c0-54af-471b-ba55-443c9219579a\"\
          ,\n            \"inputs\": {\"location\": location}\n        }),\n    }\n"
        code_language: python3
        desc: ''
        outputs:
          params:
            children: null
            type: string
        selected: false
        title: コード Convert  Location to Params
        type: code
        variables:
        - value_selector:
          - '1728113681511'
          - location
          variable: location
      height: 41
      id: '1728114754455'
      position:
        x: 384
        y: 282
      positionAbsolute:
        x: 384
        y: 282
      selected: true
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    viewport:
      x: -22
      y: 66.5
      zoom: 1

上記、yaml をファイルに保存して、「DSLファイルをインポート」で取り込むことでワークフローの作成をショートカットできます

image.png

YAMLファイルのインポート後、↓のように、「FastAPI接続ワークフロー」が作成されます

image.png

開いて実行してみます

image.png

「Location」にて、「London」を選択します

image.png

「実行を開始」を実行します

image.png

以下のように表示されれば、ワークフローの実行は成功です!

image.png

※ 「NewYork」や「Tokyo」を選択すると「Unknown city」が表示されます

まとめ

  • FastAPI のhttp://192.168.2.5:8000/openapi.json がそのまま使えないのが少しハマりました
  • openapi.json 以外は、特に躓きポイントはなかったです
  • 今回省略しましたが、作ったワークフローをさらにツールとしても使えるようにできますので、幅が広がります

参考文献

5
5
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
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?