【忙しい人必見⚡】Difyを利用した次世代型ToDoリスト Part2(Dify×Llama3.1編)

今回は[2週間前の記事](https://qiita.com/vanchan2625/items/18791b8873c94dee139a
)の発展ということで、まだご覧になっていない方は先にご覧いただくことをおすすめします。

https://qiita.com/vanchan2625/items/18791b8873c94dee139a

## 今回は何をするか（おさらい）
前回は、**AppSheet(ToDoリスト)に新規項目を追加した際に、その項目名をそのままLLMに入力して、何を買うべきか回答を生成**させていました。

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/c48e8421-c3a2-d7b2-446a-c750a904dfae.png">

ですので、 [**ハルシネーション**](https://atmarkit.itmedia.co.jp/ait/articles/2303/30/news027.html) と呼ばれる誤った情報を出力する場合がありました。
それを解決するためには [**RAG(Retrieval-Augmented Generation：検索拡張生成)**](https://atmarkit.itmedia.co.jp/ait/articles/2403/13/news035.html) の仕組みを活用することが有効であるとお伝えしました。

具体的には**入力文をいったんGoogleなどに検索をかけ、入力文とWeb検索結果を併用してLLMから回答を得る仕組みに修正**していきたいと考えています。

## 作成したワークフロー図
早速ですが、今回作成した[Dify](https://cloud.dify.ai/apps)のワークフロー図です。

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/28739b9c-5452-9a3e-2246-a65572d0ee63.png">

フローとしては、以下のようになります。
1. 新規項目を追加
1. 1.をGoogle翻訳で英語に変換
1. 2.のイメージ画像を`Stable Diffusion`で生成
1. 「2.の内容＋recommend」でGoogle検索で検索
1. 2.の内容に4.の内容を加えて`LLM`におすすめを提案させる
1. 5.の内容をGoogle翻訳で日本語に変換
1. 3.の画像と6.の内容を出力させる

## 今回利用したLLM
前回はLLMにGoogle社の`Gemini 1.5 Pro`を利用しました。
今回は違ったLLMに挑戦しようということで、Meta社から一週間前(7月23日)に発表されたばかりの`Llama3.1`を利用することにしました。

GPT-4o超えとの噂もありますが、今回は軽量で高速処理が特徴の`8Bモデル`を利用しました。

https://note.com/kakeyang/n/n43f40ac5af61

`Llama3.1`は日本語でプロンプトを入力しても`null`しか返してきませんでした。

ですので、先述の[作成したワークフロー図](https://qiita.com/vanchan2625/items/20e246d91ae9cd71273c#%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3)のように、`Llama3.1`への入力を日本語⇒英語にし、出力を英語⇒日本語にするためにLLMの前後にGoogle翻訳のブロックを挟んでいます。

`Llama3.1`に入力するプロンプトは以下の通りです。
前回のプロンプトをベースに英語で作成しました。
```:プロンプト
## Commands
* You are a "moving pro".

A user is considering buying "{{#1722207567888.text#}}".

* Provide quick recommendations on what to buy based on the following criteria and "Google search results".
"Google search results": {{#1721200265906.text#}}

* Suggest specific manufacturers and product names.

## Criteria
- Couple in their 20s
- 1LDK
- Love Scandinavian style
- Minimalist
- Save money
```

## Llama3.1をどうやって利用するか
私のような初学者にとってはここが一番のハードルではないかと思います。

できるだけ丁寧に説明したいと思いますが、結論として2024年7月30日現在`Llama3.1`を[Hugging Face](https://huggingface.co/)のInference APIで利用するには<font color="red"><strong>[Pro Account](https://huggingface.co/pricing)($9/month)</strong></font>に登録する必要があります。
Freeプランであれば前モデルの[Llama3](https://huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct)が使えると思うので、読み替えてください。

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/e8a47600-fd9c-8bdf-858d-0faaa26da68b.png">

### Llama3.1の利用申請
手順としては、まず以下のHugging Faceの`Meta-Llama-3.1-8B-Instruct`モデルのページに移動します。

https://huggingface.co/meta-llama/Meta-Llama-3.1-8B-Instruct

そこで、`Llama3.1`モデルの利用申請をSubmitします。
私の場合、数十分で承認のメールがきました。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/e1e26108-b8a9-2be1-4781-e2f0fd6978f7.png">

### Hugging Faceのアクセストークン発行
続いて、Hugging Faceのアクセストークンを発行します。
* Hugging Faceの右上メニューからSettings
↓
* Access Tokens
↓
* Create new token
↓
* Token typeとして`Read`を選択
↓
* 任意のToken nameを入力
↓
* Create token
↓
* `hf`から始まるアクセストークンをCopy
↓
* Done
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/c8ada691-3a44-9a84-af56-033424b69dce.png">

### DifyにLlama3.1追加
続いて、Difyに`Llama3.1`を追加します。
* Difyの右上メニューから設定
↓
* モデルプロバイダー
↓
* Hugging Faceのモデルの追加
↓
* Model Typeは`LLM`
↓
* Model Nameは`meta-llama/Meta-Llama-3.1-8B-Instruct`
↓
* Endpoint Typeは`Hosted Inference API`
↓
* API Tokenは先ほどHugging Faceでコピーしたものを貼り付け
↓
* 保存
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/4270ad7d-5c9c-b59b-c35e-70fc4bb1c2bb.png">

### Llama3.1のトークン数の設定
最後に`Llama3.1`のトークン数を設定します。デフォルトは`20`ですが、出力文が非常に短くなってしまいます。
トークン数が多くし過ぎると回答の生成が遅くなってしまうので適度な値に設定します。
私は今回`101`に設定しました。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/6c597440-4186-9336-4a60-9583253f378c.jpeg">

**これでセットアップ完了です。**
あとは、先述の[作成したワークフロー図](https://qiita.com/vanchan2625/items/20e246d91ae9cd71273c#%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3)の通りにブロックを追加して接続していきます。

## なぜStable Diffusionを挟んでいるのか
前回から相違点としてLLMの違いの他に、今回新たに`Stable Diffusion`のブロックを挟んでいます。
これは、AppSheet(ToDoリスト)の各項目のサムネイル用として、アイキャッチ画像を生成したいと考えて追加しました。

ちなみにStable DiffusionのAPIは従量課金制のため、クレジットをもっていなければ[Stability AI Developer Platform](https://platform.stability.ai/account/credits)から課金をする必要があります。

今回は生成スピード重視で`Stable Diffuison 3 Turbo`を使用しました。
`Stable Diffuison 3 Turbo`も割と最近(2024年4月18日)にAPIが公開された高性能なモデルになります。

<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/8291437d-c3de-bdbb-3c0e-c0b5c9a8f4dd.png">

APIキーはログインして以下から`＋ Create API Key`で発行します。

https://platform.stability.ai/account/keys

### Google Apps Scriptコードの修正
Stable Diffusionで生成した画像のURLをAppSheetに紐づいたGoogleスプレッドシートに書き込む必要があります。

ですので前回のコードに、コメントを含めて以下の**6行だけ**追加すればOKです。
```js:追加するコード
    // 画像URLの抽出と書き込み（新規追加）
    if (outputs.files && outputs.files.length > 0) {
      const imageUrl = outputs.files[0].url;
      // H列に画像URLを書き込み
      sheet.getRange('H' + lastRow).setValue(imageUrl);
    }
```

<details><summary>←全体のコードは三角マークを押すと確認することができます。</summary>

```js:Google Apps Scriptコード
function sendChildrenQuestion() {
  // スプレッドシートのIDは、スプレッドシートのURLに含まれています。
  // 例えば、URLが https://docs.google.com/spreadsheets/d/abcd1234efgh5678ijkl90mnopqrstuv の場合、
  // abcd1234efgh5678ijkl90mnopqrstuv がスプレッドシートのIDです
  const spreadsheetId = 'YOUR_SPREADSHEET_ID';  // TODO スプレッドシートのIDを書き換え
  const sheetName = 'シート1';

  const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  const sheet = spreadsheet.getSheetByName(sheetName);

  // B列のデータをすべて取得
  const data = sheet.getRange('B:B').getValues();

  // 最後の非空の行を特定
  let lastRow = data.length;
  while (lastRow > 0 && data[lastRow - 1][0] === '') {
    lastRow--;
  }

  // 最後の非空の行の質問を取得
  const question = sheet.getRange('B' + lastRow).getValue();

  const url = 'https://api.dify.ai/v1/workflows/run';  // リクエストを送るURL

  // リクエストのヘッダー情報
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer {YOUR_DIFY_API_KEY}'  // TODO DifyワークフローのAPIキーで書き換え
  };

  // リクエストのボディデータ
  const payload = {
    'inputs': {Input:question},  // TODO Difyのワークフローの開始で設定した変数名
    'response_mode': 'blocking',
    'user': 'user123'
  };

  // オプションの設定
  const options = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(payload),
    'muteHttpExceptions': true  // これをtrueにすると、エラーが発生しても例外がスローされません
  };

  // HTTP POSTリクエストの送信
  try {
    const response = UrlFetchApp.fetch(url, options);
    // Logger.log(response.getContentText());  // レスポンスの内容をログに出力
    const jsonResponse = response.getContentText()

    // JSONをオブジェクトに変換
    const responseObject = JSON.parse(jsonResponse);

    // 必要なデータを取得
    const taskId = responseObject.task_id;
    const workflowRunId = responseObject.workflow_run_id;
    const responseData = responseObject.data;
    const status = responseData.status;
    const outputs = responseData.outputs;
    const answer = outputs.text;

    // ログに出力
    Logger.log('Task ID: ' + taskId);
    Logger.log('Workflow Run ID: ' + workflowRunId);
    Logger.log('Status: ' + status);
    Logger.log('Outputs Text: ' + answer);

    // G列に返却された値を書き込み
    sheet.getRange('G' + lastRow).setValue(answer);

    // 画像URLの抽出と書き込み（新規追加）
    if (outputs.files && outputs.files.length > 0) {
      const imageUrl = outputs.files[0].url;
      // H列に画像URLを書き込み
      sheet.getRange('H' + lastRow).setValue(imageUrl);
    }
    
  } catch (e) {
    Logger.log('Error: ' + e.message);  // エラーメッセージをログに出力
  }

  // LINE Notify用の処理を追加
  const lineNotifyToken = 'YOUR_LINE_NOTIFY_TOKEN'; // TODO: LINE Notifyのトークンを設定してください
  const lineNotifyUrl = 'https://notify-api.line.me/api/notify';
  
  // B列（質問）とG列（回答）の最新の内容を取得
  const latestQuestion = sheet.getRange('B' + lastRow).getValue();
  const latestAnswer = sheet.getRange('G' + lastRow).getValue();
  
  // LINE Notifyに送信するメッセージを作成
  const message = '\n【' + latestQuestion + '】 が追加されました！' + '\n\n【AI提案】\n' + latestAnswer + '\n"ご自身のAppSheetの共有リンク"';
  
  const lineNotifyOptions = {
    'method': 'post',
    'headers': {
      'Authorization': 'Bearer ' + lineNotifyToken,
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    'payload': 'message=' + encodeURIComponent(message)
  };
  
  // LINE Notifyに通知を送信
  try {
    const lineResponse = UrlFetchApp.fetch(lineNotifyUrl, lineNotifyOptions);
    Logger.log('LINE Notify Response: ' + lineResponse.getContentText());
  } catch (error) {
    Logger.log('LINE Notify Error: ' + error.message);
  }
}
```
</details>

## デモ

https://x.com/vanchan2625/status/1818212007260340346

## あとがき
Hugging FaceのInference APIを利用してみたくてLLMを`Llama3.1`にしてみましたが、デモ動画からわかる通り、出力結果は**翻訳感の強い**残念な結果となってしまいました。。。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3597730/61e897d8-37fb-ebd3-ee63-65d94a0921f3.jpeg">

翻訳を使わずとも日本語入力に対応できれば、また可能性は広がりそうですね！


## これまで作製したプロダクトのご紹介
https://qiita.com/vanchan2625/items/3445b546e44649742c71

https://qiita.com/vanchan2625/items/3892184ef8b37bf40ce9

https://qiita.com/vanchan2625/items/00b9caeb1dadd36d6f5e

https://qiita.com/vanchan2625/items/3d49e33ce0815369debf