[LINE×GAS×Dify連携]領収書管理アプリを作ってみたpert2　〜GAS×Dify連携〜

## はじめに
前回の続きになります。

少し長いので3つに分割しています。
こちらはPert2 です。

↓Pert3（つづき） はこちら↓

https://qiita.com/mkthrkw/items/22fa54c10ca5ea0b3052

↓Pert1（前回） はこちら↓

https://qiita.com/mkthrkw/items/c3fd56a8cc21f13e3cb8

普段使っているLINE、無料で使えるGoogle Apps Script（以下、GAS）、簡単にLLMのアプリが作れると話題のDifyを連携して領収書管理アプリを作っていきます。


## 記事の内容
• LINEとGASを連携させ、ユーザーのアップロードした画像を処理する方法
• GASを使って画像をDifyに送信する方法
• 領収書の画像からデータを抽出し、Googleスプレッドシートに保存する方法
※Difyは最低限しか使っていない（使わなくてもよい）ので、詳しい使い方は他記事を参考にしてください。
※本格的というより、とりあえずやってみたいよーと思ってる方向けです。

## Step 4: Difyの設定
今回は前回の続きでDifyを設定していきます。
GASを書いているのでDifyのノーコード開発の良さが台無しになっていますが💦
使ってみたいという思いのみで無理やり組み込んでいきます

https://cloud.dify.ai/apps

### 4-1: 新しいアプリ作成
最初から作成を選択
チャットボットのChatflowで作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/5b618e92-5367-e68b-d6fe-06f805206bb5.png)


### 4-2: LLMの設定

作成されたワークフローの中のLLMの部分を設定していきます
モデルは画像を扱える必要があるため、デフォルトの```gpt-4o-mini```を使っていきます。

画像データを渡して内容を読み取って特定の形式で
返してくれるようにプロンプト部分を入れていきます
・XMLで記述
・何をするタスクかを記載する
・返却する形式を指定する
・細かな注意点や、実例も入れている

```xml
{{#context#}}
<instruction>
    <instructions>
        このタスクでは、レシートの画像から内容を読み取ってレシートに記載している情報をjson形式で抽出します。

        レシート画像からうまく情報が読み取れない場合は"画像読み取りNG"とだけ回答してください。
        レシートの情報は以下の形式で抽出してください：
        {
            "store":発行したお店や会社の名前,
            "purchase_date":発行した日付,
            "time":発行時間,
            "items": [
            {
                "name":商品名,
                "quantity": 個数,
                "price":価格(税込)
            },
            ],
            "total": 合計金額(税込),
            "payment_method": 支払い方法
        }




        レシートの内容を読み取る際には、文脈を考慮し、正確な情報を抽出するようにしてください。
        内容を計算して正しい情報を抽出出来ているかを確認してください。
        価格は税込みの価格を抽出してください。
        小計金額、合計金額も税込みの価格を抽出してください。
        割引金額はマイナスの値で抽出してください。
        支払い方法は、クレジットカード、デビットカード、電子マネー、現金などの情報を抽出してください。
        レシート内に該当のデータが存在しない場合は、そのデータを空欄としてください。
        例として、以下のようなデータを抽出してください。
        データ抽出出来た場合はjsonのみのデータを返却してください。返却データにはバッククォートやシングルクォートは含めないでください。

    </instructions>
    <examples>
        {
            "store": "とっても美味しいお弁当店",
            "purchase_date": "2024-09-26",
            "time": "20:17",
            "items": [
                {
                "name": "ステーキ弁当",
                "quantity": 2,
                "price": 1320
                },
                {
                "name": "和風ドレッシング",
                "quantity": 1,
                "price": 0
                },
                {
                "name": "バラエティセット",
                "quantity": 1,
                "price": 650
                }
            ],
            "total": 1720,
            "payment_method": "PayPay"
        }
    </examples>
</instruction>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3680973/20ad26e3-5f56-d0f9-9535-58a373a1262e.png)

出来上がったら公開をしてください。


## Step 5: GASからDifyにAPIを投げてみる

Difyに何かしらのメッセージとともに、ファイル（png、jpg、jpeg、webp、gif 形式）をアップロードできます。
流れとしては
①画像とuserを指定してアップロード
②レスポンスからIDを取得
③取得したファイルIDを指定してメッセージを送信
④メッセージが送られてくるとフローがスタート

### 5-1: 定数の設定

先にAPIに使う定数を設定しておきます
LINE_TOKENと同じようにトークンはプロジェクトのスクリプトプロパティに設定します。
USER名は画像とテキストを別々で送る時に同じもので送る必要があります。

```javascript:constants.gs
/**
 * Dify
 */
// Difyのアクセストークン
const DIFY_TOKEN = PropertiesService.getScriptProperties().getProperty('DIFY_TOKEN');
// DifyのURL
const DIFY_URL = 'https://api.dify.ai/v1';
// 毎回つけるヘッダー
const HEADERS = {
  'Authorization': 'Bearer ' + DIFY_TOKEN
}
// やりとりするためのUSER名
const DIFY_USER = 'GAS-app-owner';
```


### 5-2: 画像のアップロード

```multipart/form-data```で```file```と```user```をリクエストが必要となります。

https://cloud.dify.ai/app/521701c2-6832-41d1-819c-a275f61d4a7c/develop#file-upload

画像やファイルをアップロードする際によく使用される形式ですが、GASで```multipart/form-data```を簡単に書くことが出来ないので、下記を参考に書いていきます。

https://apidog.com/jp/blog/multipart-form-data/


```javascript:Dify.gs
/**
 * Difyアプリに画像をアップロードする
 * @param {Blob} imageBlob アップロードする画像のBlobオブジェクト
 * @param {string} fileName アップロードする画像のファイル名
 * @return {Object} チャットサービスからのJSON形式のレスポンス
 */
function uploadImageToDify(imageBlob, fileName){
  // Boundaryを生成
  const boundary = '----WebKitFormBoundary' + new Date().getTime();
  // multipart/form-dataの各パートを構築
  const delimiter = '--' + boundary;
  const closeDelimiter = '--' + boundary + '--';
  const blobByte = Utilities.newBlob(
    delimiter + '\r\n' +
    'Content-Disposition: form-data; name="user"\r\n\r\n' +
    DIFY_USER + '\r\n' +
    delimiter + '\r\n' +
    'Content-Disposition: form-data; name="file"; filename="' + fileName + '"\r\n' +
    'Content-Type: ' + imageBlob.getContentType() + '\r\n\r\n'
  ).getBytes().concat(
    imageBlob.getBytes()
  ).concat(
    Utilities.newBlob('\r\n' + closeDelimiter + '\r\n').getBytes()
  );

  const url = DIFY_URL + '/files/upload';
  const options = {
    'method': 'POST',
    'contentType': 'multipart/form-data; boundary="' + boundary + '"',
    'headers': HEADERS,
    'payload': blobByte,
    'muteHttpExceptions': true,
  };
  const response = UrlFetchApp.fetch(url, options);
  const responseJson = JSON.parse(response);
  if(!responseJson.id){
    throw new Error('画像をDifyへうまくアップロード出来ませんでした');
  }
  return responseJson;
}
```

### 5-3: メッセージの送信

画像から情報を抽出するのでメッセージは不要なんですが、
画像だけでフローがスタートしないので、メッセージ送信も設定していきます。

ファイルに関しては、URLを渡す```remote_url```と事前にアップロードする```local_file```という方法があり、今回は事前にアップロードする方法を選んでいます。

前段のファイルアップロードしたIDを受け取って、```files```に設定していきます。

```javascript:Dify.gs
/**
 * Difyアプリにチャットメッセージを送信する
 * @param {string} message ユーザーが送信するチャットメッセージ
 * @return {Object} チャットサービスからのJSON形式のレスポンス
 */
function sendMessageToDify(message, upload_file_id=''){
  const url = DIFY_URL + '/chat-messages';
  const payload = {
    'inputs': {},
    'query': message,
    'response_mode': 'blocking',
    'conversation_id': '',
    'user': DIFY_USER,
  }
  if(upload_file_id){
    payload.files = [
      {
        'type': 'image',
        'transfer_method': 'local_file',
        'upload_file_id': upload_file_id
      }
    ]
  }
  const options = {
    'method': 'POST',
    'contentType': 'application/json',
    'headers': HEADERS,
    'payload': JSON.stringify(payload),
    'muteHttpExceptions': true,
  }
  const response = UrlFetchApp.fetch(url, options);
  const responseJson = JSON.parse(response);
  const answer = JSON.parse(responseJson.answer);
  if(answer.error){
    throw new Error(answer.message);
  };
  return responseJson;
}
```



### 5-4: テスト

画像を実際にLINEからアップロードしてみて、Difyを通して返ってきたJsonデータを
GoogleDriveに出力してみます。
FOLDER_IDはどこか適当に指定してください。

```javascript
function doPost(e) {

  if (!e){
    Logger.log('None data');
    return ContentService.createTextOutput('データがありません');
  }

  // LINEからのメッセージデータ取得
  const json = JSON.parse(e.postData.contents);
  const replyToken = json.events[0].replyToken;
  const message = json.events[0].message;
  const messageType = message.type;

  if (typeof replyToken === 'underfined') {
    return ContentService.createTextOutput('リプライトークンが見つかりません');
  }
  if (messageType !== 'image'){
    replyMessageToLine(replyToken, '画像を送ってください');
  }

  try{
    // 画像データ準備
    const imageBlob =  getImageBlobByLineMessage(message);
    const extension = getExtensionByMimeType(imageBlob.getContentType());
    const fileName = generateTimestampedFileName(extension);

    // Difyアプリ利用
    const uploadResponse = uploadImageToDify(imageBlob,fileName);
    const messageResponse = sendMessageToDify('レシート画像', uploadResponse.id);
    const answerJson = JSON.parse(messageResponse.answer);

    const testFile = "テストファイル.txt";
    const folder = DriveApp.getFolderById(FOLDER_ID);
    folder.createFile(testFile, JSON.stringify(answerJson));
  } catch(error){
    replyMessageToLine(replyToken, error.message);
  }
}
```

無事取得出来ました！

```json
{
    "store":"とっても美味しいお弁当店",
    "purchase_date":"2023-10-25",
    "time":"20:51",
    "items":[
        {
            "name":"肉野菜炒め弁当",
            "quantity":1,
            "price":590
        },
        {
            "name":"鰻天丼",
            "quantity":1,
            "price":690
        },
        {
            "name":"から揚げ",
            "quantity":1,
            "price":180
        }
    ],
    "total":1430,
    "payment_method":"PayPay"
}
```

## さいごに

- 今回はここまで！
- 次回は取得したデータをスプレッドシートに書き込みをしていきます

## 続き

↓Pert3 はこちら↓

https://qiita.com/mkthrkw/items/22fa54c10ca5ea0b3052