Difyからローカル環境のフォルダにファイル出力・読込


# 記事の概要

ローカルにDIfyを構築すると、docker上に構築することになり、ローカルにファイルを保存したり、ローカルのファイルを読み込むのがめんどうな構成となっている。

特にLLMを使ったアプリ開発などでは、Dify上で実行できるプログラムにはかなり制限があるため、ローカルにファイルを出力したくなる。

そこで、ローカルへのファイル出力と読込をpythonのライブラリであるflaskを利用して実現する。

ただし、Dify自体に外部へのファイル出力の機能が実装されるのも時間の問題と思うので、一時的な手順と考えている。

全体的な構成は以下の構成となっている。

```mermaid
graph TD;
    A --> B[Dify]
    B --> C
    C --> D[エンドポイント: /upload]
    C --> E[エンドポイント: /retrieve]
    D --> F[ローカルのフォルダ]
    E --> F
    
    subgraph local
        A
        C
        F
    end
    
    subgraph A[Docker]
        B
    end

    subgraph C[Flask]
        D
        E
    end
```

# ローカル側の設定

pythonがインストールされているWindowsのPC上であれば、以下のコマンドをコマンドプロンプトで実行すれば、ローカルPC側の構築は完了する想定。

```powershell
mkdir dify_external_folder
cd dify_external_folder

chcp 65001

echo # -*- coding: utf-8 -*- > app.py
echo from flask import Flask, request, jsonify, send_file >> app.py
echo import os >> app.py
echo. >> app.py
echo app = Flask(__name__) >> app.py
echo. >> app.py
echo @app.route('/upload', methods=['POST']) >> app.py
echo def upload_file(): >> app.py
echo     ^# JSONデータの取得 >> app.py
echo     data = request.json >> app.py
echo.    >> app.py
echo     ^# フォルダ名とファイル名の取得 >> app.py
echo     folder_name = data.get('folder_name') >> app.py
echo     file_name = data.get('file_name') >> app.py
echo     file_content = data.get('file_content') >> app.py
echo.    >> app.py
echo     ^# フォルダ名とファイル名が存在するかチェック >> app.py
echo     if not folder_name or not file_name: >> app.py
echo         return jsonify({'error': 'Folder name and file name are required'}), 400 >> app.py
echo.    >> app.py
echo     ^# 出力先のディレクトリを作成（存在しない場合） >> app.py
echo     output_dir = os.path.join('output', folder_name) >> app.py
echo     if not os.path.exists(output_dir): >> app.py
echo         os.makedirs(output_dir) >> app.py
echo.    >> app.py
echo     ^# ファイル内容を指定されたディレクトリに保存 >> app.py
echo     file_path = os.path.join(output_dir, file_name) >> app.py
echo     try: >> app.py
echo         with open(file_path, 'w', encoding='utf-8') as f: >> app.py
echo             f.write(file_content) >> app.py
echo     except Exception as e: >> app.py
echo         return jsonify({'error': str(e)}), 500 >> app.py
echo.    >> app.py
echo     return jsonify({'message': 'File saved successfully', 'path': file_path}), 200 >> app.py
echo.>> app.py
echo @app.route('/retrieve', methods=['GET']) >> app.py
echo def retrieve_file(): >> app.py
echo     ^# JSONデータの取得 >> app.py
echo     data = request.json >> app.py
echo.    >> app.py
echo     ^# クエリパラメータからフォルダ名とファイル名を取得 >> app.py
echo     folder_name = data.get('folder_name') >> app.py
echo     file_name = data.get('file_name') >> app.py
echo.    >> app.py
echo     ^# フォルダ名とファイル名が存在するかチェック >> app.py
echo     if not folder_name or not file_name: >> app.py
echo         return jsonify({'error': 'Folder name and file name are required'}), 400 >> app.py
echo.    >> app.py
echo     ^# ファイルパスを構築 >> app.py
echo     file_path = os.path.join('output', folder_name, file_name) >> app.py
echo.    >> app.py
echo     ^# ファイルが存在するかチェック >> app.py
echo     if not os.path.exists(file_path): >> app.py
echo         return jsonify({'error': 'File not found'}), 404 >> app.py
echo.    >> app.py
echo     ^# ファイルを送信 >> app.py
echo     return send_file(file_path, as_attachment=True) >> app.py
echo.>> app.py
echo if __name__ == '__main__': >> app.py
echo     app.run(host='0.0.0.0', port=5000, debug=True) >> app.py

mkdir output
python -m venv venv
venv\Scripts\activate

pip install flask
python app.py
```

実行すると、以下のようなファイルとフォルダが作成される。

```
project/
├── app.py
├── output/        # 出力ファイルを保存するフォルダ
└── venv/          # 仮想環境
```

また、上記のコマンドを実行すると「python app.py」このコマンドでflaskが起動しており、以下のようなログが出力される。
この状態であれば、DifyからのHTTPリクエストを受け付けている状態となる。

```
(venv) C:\XXXXXXXXXXXXXXXX\project>python app.py
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.XXX.XXX:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 805-336-953
```

（再起動後などに）再度、flaskを起動する場合は、以下のコマンドを実行する。

```powershell
venv\Scripts\activate # pythonの仮想環境もactivateが必要な場合のみ
python app.py
```

構築用のコマンドを実行すれば、自動で作成されている想定だが、念のためpythonファイルを記載しておく。

```python:app.py
# -*- coding: utf-8 -*- 
from flask import Flask, request, jsonify, send_file 
import os 
 
app = Flask(__name__) 
 
@app.route('/upload', methods=['POST']) 
def upload_file(): 
    # JSONデータの取得 
    data = request.json 
    
    # フォルダ名とファイル名の取得 
    folder_name = data.get('folder_name') 
    file_name = data.get('file_name') 
    file_content = data.get('file_content') 
    
    # フォルダ名とファイル名が存在するかチェック 
    if not folder_name or not file_name: 
        return jsonify({'error': 'Folder name and file name are required'}), 400 
    
    # 出力先のディレクトリを作成（存在しない場合） 
    output_dir = os.path.join('output', folder_name) 
    if not os.path.exists(output_dir): 
        os.makedirs(output_dir) 
    
    # ファイル内容を指定されたディレクトリに保存 
    file_path = os.path.join(output_dir, file_name) 
    try: 
        with open(file_path, 'w', encoding='utf-8') as f: 
            f.write(file_content) 
    except Exception as e: 
        return jsonify({'error': str(e)}), 500 
    
    return jsonify({'message': 'File saved successfully', 'path': file_path}), 200 

@app.route('/retrieve', methods=['GET']) 
def retrieve_file(): 
    # JSONデータの取得 
    data = request.json 
    
    # クエリパラメータからフォルダ名とファイル名を取得 
    folder_name = data.get('folder_name') 
    file_name = data.get('file_name') 
    
    # フォルダ名とファイル名が存在するかチェック 
    if not folder_name or not file_name: 
        return jsonify({'error': 'Folder name and file name are required'}), 400 
    
    # ファイルパスを構築 
    file_path = os.path.join('output', folder_name, file_name) 
    
    # ファイルが存在するかチェック 
    if not os.path.exists(file_path): 
        return jsonify({'error': 'File not found'}), 404 
    
    # ファイルを送信 
    return send_file(file_path, as_attachment=True) 

if __name__ == '__main__': 
    app.run(host='0.0.0.0', port=5000, debug=True) 
```

# Dify側の設定

フロー自体に意味はないが、Dify側の設定のイメージを持つために以下のフローを構築する。

![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/3d5b36c7-2043-78a1-101e-4940546dad50.png)

### 「開始」ノード

「input_text」にはLLMへの質問事項を入力する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/7698c788-b228-251c-2d4d-4b9de515015a.png)

### 「LLM」ノード

LLMの出力である必要はないが、基本的にLLMの出力をファイル出力する想定で作成している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/40c97908-0ba7-5548-e9af-2b7f7edd8d14.png)

```
日本語で出力してください。
入力がどのような形式でも日本語のみで出力してください。

「ユーザーの質問」：の質問について回答してください。
「ユーザーの質問」：{{#1715394478158.input_text#}}
```

### 「HTTPリクエスト（upload）」ノード

LLMの出力をローカルに保存する。
「192.168.XXX.XXX」にはipconfigなどで「IPv4 アドレス」を入力する。

![upload_node.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/3dcefaac-ecb6-7c25-ba7d-1a18386dc5bc.png)

```
{
  "folder_name": "test_folder",
  "file_name": "text.txt",
  "file_content": "{{#1716114640530.text#}}"
}
```

### 「HTTPリクエスト（retrieve）」ノード

ローカルからファイル内のデータを取得する。
「192.168.XXX.XXX」にはipconfigなどで「IPv4 アドレス」を入力する。

![retrieve_node.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/4e8d4753-c495-c42a-41d6-0acbacb80360.png)

```
{
  "folder_name": "test_folder",
  "file_name": "text.txt"
}
```

### 「終了」ノード

ローカルに保存したファイルの内容を出力する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/8d0540eb-dc9c-948a-6013-c28b0aa5ead0.png)

# 結果

以下のように、各種アウトプットが想定通り出力されている。

### Difyの出力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/a14061ca-5953-a18a-fa9c-31b8d7c7f69d.png)

### ローカルのフォルダ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/4ea5c855-58d7-1e71-0f4e-1cd399e6dd2d.png)

### ローカルのファイル

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619695/1cca8c9f-da4e-a13c-4c69-ce13af363982.png)