【Dify】flask-openapi3による独自処理のカスタムツール化

# 概要
flask-openapi3を使って、独自処理をDifyのカスタムツール化する方法のメモです。

# 背景
[Dify](https://dify.ai/jp)はLLMアプリケーションをノーコードで作れるツールです。とても便利ですが、組み込みツールだけだと、実現できる処理の自由度に限界があります。
ローカルでAPIを作ってカスタムツールとして登録できれば、開発の自由度が高まることが期待されます。
Difyでは、OpenAPIの仕様に沿って作られたAPIは簡単にカスタムツールとして登録することができます。そこで、flask-openapi3を使って、pythonでローカル実装された関数を、Difyのカスタムツールに登録することを試みました。

# 環境

- [flask-openapi3](https://pypi.org/project/flask-openapi3/) バージョン3.1.3

  ```
  pip install flask-openapi3
  ```

- M1 Mac
- pythonは3.11.2
- Difyのクラウド版
- [ngrok](https://ngrok.com/)（クラウド版からローカルのサーバーに到達できるようにするために使用）

  ```sh
  brew install ngrok
  ```
  
# 手順
## ngrokの起動
ポート5000でAPIサーバを起動することを前提にngrokを実行します。このあと登場するpythonコードにngrokのURLを記載する必要があるので、先に実行します。

```sh
ngrok http 5000
```

実行後、`Forwarding`という行に記載されたhttpsのURLをコピーしておきます。
ここでは仮に"https://ngrokaddress.app" としておきます。

## flaskサーバの実行

flask-openapi3のgithubの[servers_demo.py](https://github.com/luolingchun/flask-openapi3/blob/master/examples/servers_demo.py)を参考に、ユーザの入力をオウム返しするAPIを作ってみます。

```Python
from pydantic import BaseModel

from flask_openapi3 import Info, Tag
from flask_openapi3 import OpenAPI, Server

info = Info(title="Echo API", version="1.0.0")
servers = [
    Server(url="https://ngrokaddress.app")
]
app = OpenAPI(__name__, info=info, servers=servers)

echo_tag = Tag(name="echo", description="echo your input")


class Query(BaseModel):
    input_text: str

@app.get("/echo", tags=[echo_tag])
def echo_input(query: Query):
    """echo the text you input
    to check server response
    """
    return {
        "code": 0,
        "message": "ok",
        "data": [
            {"response": f"you input {query.input_text}" }
        ]
    }


if __name__ == '__main__':
    app.run(debug=True) # デフォルトポートは5000
```

実行します。

```sh
python app.py
```

エラーなく、実行できたらngrokのアドレスにブラウザからアクセスしてみます、ngrokを実行していなかったら実行してください。
ngrokのアドレス末尾に"/openapi"を付ける必要があることに注意してください。
（例："https://ngrokaddress.app/openapi"）

pypiのページにあるようなswagger, ReDocなどを選択できるページが表示されれば成功です。
"https://ngrokaddress.app/openapi/openapi.json"にアクセスするとAPIの仕様を表すjsonが見られます。Difyにはこれを登録します。

# Difyのカスタムツールの作成

Dify > ツール > カスタム > カスタムツールを作成する > +URLからインポートする
と遷移し、表示されたテキストボックスに"https://ngrokaddress.app/openapi/openapi.json" を入力し、OKを押下します。その後「スキーマ」のテキストボックスにjsonが入力されたら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/3d34ab1e-d9fa-dc69-993e-28cd7f721783.png)

「利用可能なツール」のところに「echo_input_echo_get」という名前のメソッドが追加されていると思います。ソースコードの対応を整理すると先頭の"echo_input"がメソッド名、次のecho"がURLのパス、最後の"get"はリクエストのメソッド名に対応しているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/7dfa68bf-06c2-8a38-3cd1-4423f7716500.png)

適当な名前をつけて、登録を完了したら、ワークフローなどの作成画面から作成したカスタムツールが選べるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/84e24dec-87e8-3b24-89a6-79a603356722.png)

# おわりに
flaskで作ったAPIをDifyのカスタムツールに登録することができました。Difyにpythonの独自関数をツールとして登録できると、やれることの幅が大きく広がるので、実装方法がわかってよかったです。flask-openopi3のお陰で、実装量を大幅に減らせたため開発者の方に感謝です。

今後の課題としては、無料版のngrokだと、URLがngrokの実行ごとに変わってしまって使いにくいので、なにか対応を考えたほうが良さそうです。

本記事がどなたかのお役に立てましたら幸いです。

# 補足
## flask-openapi3のSimple Exampleそのままだとエラーになる

[flask-openapi3](https://pypi.org/project/flask-openapi3/)のPyPIのトップページにあるSimple Exampleをそのままサーバとして用いると、カスタムツールからの登録に失敗します。invalid schema...みたいなメッセージが出ました。少し調べると、server(のurl)の情報がschemaに含まれていないためエラーになるようです。

```Python:app.py
info = Info(title="Echo API", version="1.0.0")
servers = [
    Server(url="https://ngrokaddress.app")
]
app = OpenAPI(__name__, info=info, servers=servers)
```

本記事のapp.pyのようにServerクラスを使って、urlを与えることで、エラーを出さなくすることができました。