GPT-4o-mini(GPT-4omniへの切替機能付)とDALL-E3とDeepLを同時に使用できるLINE Bot

OpenAIとDeepLのAPIキーを利用して、文章生成AI「GPT-4o-mini」と画像生成AI「DALL-E3」、更にDeepLの翻訳機能(英訳)を同時に使用できるLINE BotのベースコードをGASで作りました。

英語への翻訳は、GPT-4o-mini経由でもできますが、トークンを使用する為、DeepLの無料アカウントで使用できるAPIキーを使用して、その部分は無料化することにしました。

下図のように、文章生成AIと画像生成AIとDeepLを一つのアカウントで同時に使用することが出来ます。
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/43ccdb5b-cea7-5337-713e-e5ebbf7dc2de.png)

ベースコードでは、「こんにちはGPT」という文字が「含まれる(部分一致)」する入力があった場合に、GPT-4o-miniと会話でき、

「絵を描いて」という文字が「含まれる(部分一致)する入力があった場合に、DALL-E3が絵を描いてくれ、

「英訳して」という文字が「含まれる(部分一致)する入力があった場合に、DeepLが英訳してくれるコード構成にしています。

それ以外の入力に対しては、単に「A」というテキストが返されるIF文構成になっています。

トリガーとなるキーワードや、仮に「A」としてある返しテキストに関しては、ご自身の環境に合わせて、自由に編集してください。

まず始めに、下のリンクからGSSファイルを開き、メニューファイルから「Make a copy」を選択して、ご自身のGoogle Driveにコピーしてください。
https://docs.google.com/spreadsheets/d/1MI0NPYix3bPwz9CF97xp2wFbYom5MKZgE3Tf4rp-QMg/edit?usp=sharing

GSSファイルをコピーしたら、下図のようにスクリプトを開いてください。
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/f296db85-c697-b3f2-9618-d15c164265b6.png)

スクリプトエディタを開いたら、所定の位置にLINEのアクセストークンとOpenAIのAPIキー、DeepLのAPIキーを入力してください。
![タイトルなし1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/e3c6864a-3dad-d1e5-6a9d-2624fe65aa0f.png)

それだけで、後はデプロイと初回認証を行い、LINE developersのWebhookにリンクさせるだけで使用できます。

DeepLのキーを入力しなくても、英訳機能以外のLINE Bot自体は動作します。
(GPT-3.5Turboとの会話機能の中でも、英訳自体は可能です。トークンは消費しますが)

尚、参考までにmain.gsファイルのコードは下のように作成しております。
```main.gs
// LINE Bot 設定
const CHANNEL_ACCESS_TOKEN = 'ここに入力する'; 

// AI設定
const openAIApiKey = "ここに入力する";

//英語翻訳機能を使う為のDEEL API Key
const DEEPL_API_KEY = 'ここに入力する';

const logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('log');
const DALLE_API = "https://api.openai.com/v1/images/generations";
var replyToken, json;

function doPost(e) {
  json = JSON.parse(e.postData.contents);
  replyToken = json.events[0].replyToken;
  if (typeof replyToken === 'undefined') {
    return;
  }

  var userMessage = json.events[0].message.text;
  var messages;

  if (userMessage.includes("こんにちはGPT")) {
    var replyText; // AIからの応答を保存する変数

    if (/こんにちはGPT、.*駅発、.*駅着/.test(userMessage)) {
    // 交通手段と費用に関するプロンプトを作成
    var prompt = userMessage + "前述の合理的な交通手段と費用を算出して1000字以内で出力してください。読みやすいように箇条書きにして、改行してください";
    replyText = getAIChatAnswer(prompt); // 関数を呼び出し、promptを渡す

    }else if (/.*、レシピ/.test(userMessage)) {
    // 交通手段と費用に関するプロンプトを作成
    var prompt = userMessage + "前述の合理的なレシピと予想される費用を概算で良いので算出し、1000字以内で出力してください。読みやすいように箇条書きにして、改行してください";
    replyText = getAIChatAnswer(prompt); // 関数を呼び出し、promptを渡す

    } else {
      // 通常の応答
      replyText = getAIChatAnswer(userMessage); // 関数を呼び出し
    }

    messages = [{'type': 'text', 'text': replyText}]; // LINEメッセージオブジェクトを作成
    
  } else if (userMessage.startsWith("絵を描いて")) {
    // 絵を描くリクエストに対して画像を生成
    messages = getAIImageAnswer(userMessage);
    
  } else if (userMessage.includes("英訳して")) {
    var textToTranslate = userMessage.replace("英訳して", "");
    var translatedText = translateTextWithDeepL(textToTranslate, "EN");
    messages = [{'type': 'text', 'text': translatedText}];

  } else {
    // それ以外のメッセージには「A」と返信
    messages = [{'type': 'text', 'text': 'A'}];
  }

  // LINEに返信する処理を呼び出す
  const linebotClient = new LineBotSDK.Client({ channelAccessToken: CHANNEL_ACCESS_TOKEN });
  try {
    linebotClient.replyMessage(replyToken, messages);
  } catch (e) {
    log_to_sheet("A", e);
  }

  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

//DALL-E3に関する関数
function getAIImageAnswer(text) {
  var imageURL = generateImageURL(text);
  var messages = [
    {'type':'text', 'text': '画像ができました！'},
    {'type':'image', 'originalContentUrl': imageURL, 'previewImageUrl': imageURL}
  ];
  return messages;
}

function generateImageURL(text) {
  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + openAIApiKey
    },
    "payload": JSON.stringify({
      "prompt": text,
      "model": "dall-e-3",
      "response_format": "url"
    })
  };
  var response = UrlFetchApp.fetch(DALLE_API, options);
  var data = JSON.parse(response.getContentText());
  return data.data[0].url;
}

//GPT-4o-miniに関する関数
function getAIChatAnswer(prompt, userId) {
  var requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + openAIApiKey
    },
    "payload": JSON.stringify({
      "model": "gpt-4o-mini",
      "messages": [
         {"role": "user", "content": prompt}
      ]
    })
  };
  var response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", requestOptions);
  var responseText = response.getContentText();
  var json = JSON.parse(responseText);
  return json.choices[0].message.content.trim();
}

function log_to_sheet(column, text) {
  var lastRow;
  if(logSheet.getRange(column + "1").getValue() == ""){
    lastRow = 0;
  } else if(logSheet.getRange(column + "2").getValue() == ""){
    lastRow = 1;
  } else {
    lastRow = logSheet.getRange(column + "1").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
    if(lastRow >= 1000){
      logSheet.getRange(column + "1:" + column + "1000").clearContent();
      lastRow = 0;
    }
  }
  var putRange = column + String(lastRow + 1);
  logSheet.getRange(putRange).setValue(text);
}

```
DeepL機能を動作させる為の関数は、別途deepl.gsファイルとして下のように作成しています。

```deepl.gs
//自動英語翻訳機能を成立させる為の追加関数
function translateTextWithDeepL(text, targetLang) {
  const deepLUrl = `https://api-free.deepl.com/v2/translate?auth_key=${DEEPL_API_KEY}&text=${encodeURIComponent(text)}&target_lang=${targetLang}`;
  const response = UrlFetchApp.fetch(deepLUrl, {
    'method': 'post'
  });
  const jsonResponse = JSON.parse(response.getContentText());
  if (jsonResponse.translations && jsonResponse.translations.length > 0) {
    return jsonResponse.translations[0].text;
  } else {
    return "翻訳に失敗しました。";
  }
}
```

また、GPT-4o-miniに対する応用的な用途として、ユーザーの入力に対して、プロンプトを追加してGPT-4o-miniに送信するコードも２つ、実装しています。

それぞれ、ご自身の用途に合わせてプロンプトやトリガー条件を工夫してみてください。

その2つのコードに関しては上のコードからご確認いただけますが、

まず1つめは、こんにちはGPT、〇〇駅発、〇〇駅着という形式の入力があった時に、「前述の合理的な交通手段と費用を算出して1000字以内で出力してください。読みやすいように箇条書きにして、改行してください」というプロンプトを追加して送信するものです。

実際に動作させると、下図の様な応答になります。
![タイトルなし1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/d72673ad-8c54-90f7-f50c-790ec6575500.png)

2つめは、こんにちはGPT、〇〇(料理名)、レシピという形式の入力があった時に、「前述の合理的なレシピと予想される費用を概算で良いので算出し、1000字以内で出力してください。読みやすいように箇条書きにして、改行してください」というプロンプトを追加して送信するものです。

実際に動作させると、下図の様な応答になります。
![タイトルなし1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/a027f4b5-7753-fff8-a33b-aba36762744d.png)

これらのコードは、「ユーザーの入力に対して、管理者がプロンプトを追加してGPT-4o-miniに送信する」形を構築する為のサンプルとして実装しています。

...

2024年5月13日、更に、改良を加えたGPT-4omniへの切り替え機能を持つベースコードも合わせて作成しました。

OpenAIによれば、GPT4ominはGPT-4Turboと同等の精度を持ち、かつトークン代はGPT-4Turboの更に半額に抑えられているモデルです。

改良版については、下のリンクからGSSファイルを開き、メニューファイルから「Make a copy」を選択して、ご自身のGoogle Driveにコピーしてください。
https://docs.google.com/spreadsheets/d/1EhpTnlfhI7sYf8Z2ixFGGmxtsY7d3gMOZi0kQ_rwuPg/edit?usp=sharing

その後の設定に関しては、上記解説した通常版と同じです。

参考までに、改良版のmain.gsファイルのコードは下のように作成しております。

```main.gs
// LINE Bot 設定
const CHANNEL_ACCESS_TOKEN = 'ここに入力する'; 

// AI設定
const openAIApiKey = "ここに入力する";

//英語翻訳機能を使う為のDEEL API Key
const DEEPL_API_KEY = 'ここに入力する';

const logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('log');
const DALLE_API = "https://api.openai.com/v1/images/generations";
var replyToken, json;

function doPost(e) {
  json = JSON.parse(e.postData.contents);
  replyToken = json.events[0].replyToken;
  if (typeof replyToken === 'undefined') {
    return;
  }

  var userMessage = json.events[0].message.text;
  var messages;

  if (userMessage.includes("こんにちはGPT")) {
    var replyText; // AIからの応答を保存する変数
    var model = "gpt-4o-mini"; // デフォルトモデル

    if (userMessage.includes("こんにちはGPT4")) {
      model = "gpt-4o-2024-05-13"; // GPT-4モデルを指定
    }

    if (/こんにちはGPT[4]、.*駅発、.*駅着/.test(userMessage)) {
      var prompt = userMessage + "前述の合理的な交通手段と費用を算出して1000字以内で出力してください。読みやすいように箇条書きにして、改行してください";
      replyText = getAIChatAnswer(prompt, model); // 関数を呼び出し、promptとモデルを渡す

    } else if (/.*、レシピ/.test(userMessage)) {
      var prompt = userMessage + "前述の合理的なレシピと予想される費用を概算で良いので算出し、1000字以内で出力してください。読みやすいように箇条書きにして、改行してください";
      replyText = getAIChatAnswer(prompt, model); // 関数を呼び出し、promptとモデルを渡す

    } else {
      replyText = getAIChatAnswer(userMessage, model); // 関数を呼び出し
    }

    messages = [{'type': 'text', 'text': replyText}]; // LINEメッセージオブジェクトを作成
    
  } else if (userMessage.startsWith("絵を描いて")) {
    messages = getAIImageAnswer(userMessage);
    
  } else if (userMessage.includes("英訳して")) {
    var textToTranslate = userMessage.replace("英訳して", "");
    var translatedText = translateTextWithDeepL(textToTranslate, "EN");
    messages = [{'type': 'text', 'text': translatedText}];

  } else {
    messages = [{'type': 'text', 'text': 'A'}];
  }

  const linebotClient = new LineBotSDK.Client({ channelAccessToken: CHANNEL_ACCESS_TOKEN });
  try {
    linebotClient.replyMessage(replyToken, messages);
  } catch (e) {
    log_to_sheet("A", e);
  }

  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

//DALL-E3に関する関数
function getAIImageAnswer(text) {
  var imageURL = generateImageURL(text);
  var messages = [
    {'type':'text', 'text': '画像ができました！'},
    {'type':'image', 'originalContentUrl': imageURL, 'previewImageUrl': imageURL}
  ];
  return messages;
}

function generateImageURL(text) {
  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + openAIApiKey
    },
    "payload": JSON.stringify({
      "prompt": text,
      "model": "dall-e-3",
      "response_format": "url"
    })
  };
  var response = UrlFetchApp.fetch(DALLE_API, options);
  var data = JSON.parse(response.getContentText());
  return data.data[0].url;
}

//GPTに関する関数
function getAIChatAnswer(prompt, model) {
  var requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + openAIApiKey
    },
    "payload": JSON.stringify({
      "model": model,
      "messages": [
         {"role": "user", "content": prompt}
      ]
    })
  };
  var response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", requestOptions);
  var responseText = response.getContentText();
  var json = JSON.parse(responseText);
  return json.choices[0].message.content.trim();
}

function log_to_sheet(column, text) {
  var lastRow;
  if(logSheet.getRange(column + "1").getValue() == ""){
    lastRow = 0;
  } else if(logSheet.getRange(column + "2").getValue() == ""){
    lastRow = 1;
  } else {
    lastRow = logSheet.getRange(column + "1").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
    if(lastRow >= 1000){
      logSheet.getRange(column + "1:" + column + "1000").clearContent();
      lastRow = 0;
    }
  }
  var putRange = column + String(lastRow + 1);
  logSheet.getRange(putRange).setValue(text);
}

```

この改良版では、下図のように「こんにちはGPT」と部分一致するテキスト入力をした際にはGPT－4o-miniと会話し、「こんにちはGPT4」と部分一致するテキスト入力をした際にはGPT－4omniと会話する分岐を行います。
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/ecf6c7c3-dedf-3d22-053f-16512632df51.png)

ユーザーの入力に対して、プロンプトを追加してGPTに送信する2つのコードにおいても、GPT-4o-miniとGPT-4omniを切り替えて使用することが出来ます。

まず1つ目の機能では、下図のように「こんにちはGPT、〇〇駅発、〇〇駅着」と入力した時は、GPT-4o-miniが回答し、「こんにちはGPT4、〇〇駅発、〇〇駅着」と入力した時は、GPT-4omniが回答します。
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/0f12f4d6-4d9d-8d6e-318b-5bf2472ff2a6.png)

料理のレシピを出力させる機能についても下図のように、同様に使用モデルをGPT-4o-miniとGPT-4omniを切り替えることが出来ます。
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3661150/5f3ed185-0735-3998-99bb-8ab80657f1b3.png)