LoginSignup
15
15

あなたの冷蔵庫の食材でシェフGPTは何を作る!?【LINE✖️GAS✖️スプレッドシート✖️ChatGPT】

Last updated at Posted at 2023-03-31

ピンク 緑 黄色 サロン LINE リッチメニュー (4).png

ChatGPTで何作ろう?

今流行りのChatGPT・・・
せっかく答えが返ってくるなら何か自分にとって有益な情報が欲しい!
ChatGPTなら晩ご飯のレシピも考えてくれるんじゃ?
でも食材入れてレシピ検索するならレシピサイトでいいのでは?
じゃ、冷蔵庫にある食材全部ほりこんで、その中から食材選んでレシピ作ってもらうのはどうか???

デモとしてはこんな感じ

ユーザー側で行うこと

  1. LINEで「友だち登録」する。
  2. 買い物をしたら食材を登録しておく。
  3. レシピを聞く(少し待つ)。
  4. 使い終わった食材は消す(LINEから可能)。

構成

非エンジニアがLINEで少し生活を豊かにする方法.jpg

・ユーザーがLINEで食材を送信→Webhookwを介してGASが動きスプレッドシートに登録
・スプレッドシートからGASをChatGPT APIに与える情報を抽出してレシピを作成してもらう
・作成結果をLINEで送信

※LINE✖️GASの基礎的な操作が知りたい方はこちら

スプレッドシートの準備

masterシートを作成します。
スクリーンショット 2023-04-01 2.19.36.png

あなたの冷蔵庫でシェフGPTは何作る!?.png
A-F列は整理された食材リストで、ChatGPTへの情報用や食材整理時のクイックリプライに使用します。
ユーザーからのメッセージは一旦K-P列に保存されます。
K列で「卵」と「豚こま肉」の間が空白なのは、食材整理の際に食材が削除されたからです(食材整理項目参照)。
空白があると何かと不便なので(詳細は後述)、空白のないリストを作っておきます。

※空白を消したリストの作り方
A3セル以降に「=IFERROR(INDEX(K:K,SMALL(IF(K:K<>"",ROW(K:K)),ROW())),"")」を入力。
(B列にはKをLに変更した式、C列にはKをMに変更した式、・・・・)

ユーザーごと(LINE IDで区別)にシート作成

//user_idをシート名にして新しいシートを作成
function addnewSheet(user_id) {
  let mySheet = SpreadsheetApp.openById(
   "スプレッドシートID"
  ).getSheetByName("master");
  //スプレッドシートに新しいシートを追加挿入
  newSheet = mySheet.copyTo(
    SpreadsheetApp.openById("スプレッドシートID")
  );
  //追加挿入したシートに名前を設定
  newSheet.setName(user_id);
}

食材の登録

if (userMessage == "肉・魚・卵を登録する!") {
      sheet.getRange("H1").setValue(1);
      messages = [
        {
          type: "text",
          text: "登録します。1品目ずつ入れてください。",
        },
      ];
      replyToLine(replyToken, messages);
}

ユーザーが入力した食材が肉・魚・卵の列(K列)に収納されるよう、H1セルの数字を1にします。
H1セルの数字によってユーザーのメッセージが収納される場所が決まります。

食材の整理(削除)

ユーザーが整理したいカテゴリーを選んだ後、クイックリプライで今登録されている食材が表示されます。

スプレッドシートからクイックリプライを作るコード

let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 1).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];

ここでセルに空白があるとクイックリプライを作るところでエラーが出るので、上述の通り空白のないリストを作成しました。
選択された食材は下記の関数で処理されます。

// 使った食材を削除する
function wordsearch(sheet, item) {
  const searchRange = sheet.getRange("K3:P100");
  // 検索する文字列
  const searchString = item;
  // 置換する文字列
  const replaceString = "";
  // 置換
  searchRange.createTextFinder(searchString).replaceAllWith(replaceString);
}

ChatGPT APIへ情報を入れる

function callOpenAI(ingred, theme, must) {
  const url = "https://api.openai.com/v1/chat/completions";

  const content = `
${ingred}を使って2つおかずを作ってください。1つは野菜のみのおかずにしてください。${theme}でお願いします。${must}

# 条件
- 簡潔に
- 文字数:8000字まで
`;

  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${OPEN_AI_API_KEY}`,
    },
    payload: JSON.stringify({
      model: "gpt-3.5-turbo",
      messages: [{ role: "user", content: content }],
    }),
  };

  const response = UrlFetchApp.fetch(url, options);
  const result = JSON.parse(response.getContentText());
  const text = result.choices[0].message.content.trim();
  return text;
}

model: "gpt-3.5-turbo"は適宜新しいモデルに変更してください。
ちなみに引数のingred, theme, mustは以下のとおりです。

let ingred = sheet.getRange("J3").getValue();
let theme = userMessage;
let must = sheet.getRange("Q2").getValue() + "を必ず使ってください。";

J3セルは全ての食材をつないだ文字列になっています。
(私はI列で各カテゴリーをつないだ上で=I1&" "&I2&" "&I3&" "&I4&" "&I5&" "&I7としましたが、他の方法でもよいです。)

使ってみた感想

・そこそこ期待通りのレシピが帰ってくる
・ただし、ない食材も含まれていることもあり
・製作段階で離乳食のレシピも頼んだところ”とりあえずなんでもミキサーにかける”的なレシピが出てきたのでボツに。
・毎日使ってたらマンネリ化しないかが心配
・使ってみての感想やこうした方がいいんじゃないかというご意見があればぜひぜひお願いいたします。

protopediaにも投稿中。

全体のコード

const LINE_CHANNEL_ACCESS_TOKEN =
  PropertiesService.getScriptProperties().getProperty(
    "LINE_CHANNEL_ACCESS_TOKEN"
  ) ||"LINEチャンネルアクセストークンを入力";
const OPEN_AI_API_KEY =
  PropertiesService.getScriptProperties().getProperty("OPEN_AI_APY_KEY") ||
  "openAIのAPI keyを入力";
let messages;

function callOpenAI(ingred, theme, must) {
  // 食材なければ登録を促す

  const url = "https://api.openai.com/v1/chat/completions";

  const content = `
${ingred}を使って2つおかずを作ってください。1つは野菜のみのおかずにしてください。${theme}でお願いします。${must}

# 条件
- 簡潔に
- 文字数:8000字まで
`;

  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${OPEN_AI_API_KEY}`,
    },
    payload: JSON.stringify({
      model: "gpt-3.5-turbo",
      messages: [{ role: "user", content: content }],
    }),
  };

  const response = UrlFetchApp.fetch(url, options);
  const result = JSON.parse(response.getContentText());
  const text = result.choices[0].message.content.trim();
  return text;
}

//LINEにリプライメッセージを送る
function replyToLine(replyToken, messages) {
  const url = "https://api.line.me/v2/bot/message/reply";
  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${LINE_CHANNEL_ACCESS_TOKEN}`,
    },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: messages,
    }),
  };
  UrlFetchApp.fetch(url, options);
}

function doPost(e) {
  var json = JSON.parse(e.postData.contents);
  const eventData = json.events[0];
  const replyToken = eventData.replyToken;
  const userId = eventData.source.userId;
  getSpreadSheet(userId);
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(userId);

  if (json.events[0].type == "message") {
    const userMessage = eventData.message.text.trim();
    if (userMessage == "肉・魚・卵を登録する!") {
      sheet.getRange("H1").setValue(1);
      messages = [
        {
          type: "text",
          text: "登録します。1品目ずつ入れてください。",
        },
      ];
      replyToLine(replyToken, messages);

      //
    } else if (userMessage == "野菜・果物を登録する!") {
      sheet.getRange("H1").setValue(2);
      messages = [
        {
          type: "text",
          text: "登録します。1品目ずつ入れてください。",
        },
      ];
      replyToLine(replyToken, messages);
      //
    } else if (userMessage == "豆類・乳製品を登録する!") {
      sheet.getRange("H1").setValue(3);
      messages = [
        {
          type: "text",
          text: "登録します。1品目ずつ入れてください。",
        },
      ];
      replyToLine(replyToken, messages);
      //
    } else if (userMessage == "調味料を登録する!") {
      sheet.getRange("H1").setValue(4);
      messages = [
        {
          type: "text",
          text: "登録します。1品目ずつ入れてください。",
        },
      ];
      replyToLine(replyToken, messages);
      //
    } else if (userMessage == "冷凍庫に入れる!") {
      sheet.getRange("H1").setValue(5);
      messages = [
        {
          type: "text",
          text:
            "登録します。1品目ずつ入れてください。※注意:冷凍庫の食材はレシピには自動で含まれないため、もし使いたい場合はレシピ作成時に使いたい食材で入力してください。",
        },
      ];
      replyToLine(replyToken, messages);
      //
    } else if (userMessage == "その他を登録する") {
      sheet.getRange("H1").setValue(6);
      messages = [
        {
          type: "text",
          text: "登録します。1品目ずつ入れてください。",
        },
      ];
      replyToLine(replyToken, messages);
    } else if (userMessage == "食材を登録する") {
      messages = [
        {
          type: "text",
          text: "どこに登録しますか?",
          quickReply: {
            items: [
              {
                type: "action",
                action: {
                  type: "message",
                  label: "肉魚卵",
                  text: "肉・魚・卵を登録する!",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "野菜果物",
                  text: "野菜・果物を登録する!",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "豆・乳製品",
                  text: "豆類・乳製品を登録する!",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "調味料",
                  text: "調味料を登録する!",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "冷凍",
                  text: "冷凍庫に入れる!",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "その他",
                  text: "その他を登録する",
                },
              },
            ],
          },
        },
      ];
      replyToLine(replyToken, messages);
    } else if (userMessage == "今日のレシピを提案してください。") {
      let noingred = sheet.getRange("G9").getValue();
      if (noingred == 12) {
        messages = [
          {
            type: "text",
            text: "冷蔵庫が空っぽです!何か食材を登録してください。",
          },
        ];
        replyToLine(replyToken, messages);
      } else {
        sheet.getRange("H1").setValue(7);
        messages = [
          {
            type: "text",
            text: "必ず使いたい食材はありますか?",
            quickReply: {
              items: [
                {
                  type: "action",
                  action: {
                    type: "postback",
                    label: "ある",
                    text: "ある",
                    data: "must",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "postback",
                    label: "ない",
                    text: "ない",
                    data: "choice",
                  },
                },
              ],
            },
          },
        ];
        replyToLine(replyToken, messages);
      } //
    } else if (userMessage == "食材を見る(整理する)") {
      sheet.getRange("H1").setValue(10);
      messages = [
        {
          type: "text",
          text: "整理するジャンルを選んでください。",
          quickReply: {
            items: [
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "肉・魚・卵",
                  text: "肉・魚・卵",
                  data: "protein",
                },
              },
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "野菜・果物",
                  text: "野菜・果物",
                  data: "vege",
                },
              },
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "豆類・乳製品",
                  text: "豆類・乳製品",
                  data: "bean",
                },
              },
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "調味料",
                  text: "調味料",
                  data: "season",
                },
              },
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "冷凍食品",
                  text: "冷凍食品",
                  data: "frozen",
                },
              },
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "その他",
                  text: "その他",
                  data: "otherclear",
                },
              },
            ],
          },
        },
      ];
      replyToLine(replyToken, messages);
    } else {
      // 自由入力の挙動 各列に収納していく
      let number = parseInt(sheet.getRange("H1").getValue());
      if (number == 9) {
        let count = sheet.getRange("S2").getValue()
        sheet.getRange("S2").setValue(count + 1)
        let ingred = sheet.getRange("J3").getValue();
        let theme = userMessage;
        let mustornot = sheet.getRange("J7").getValue();
        if (mustornot == 1) {
          let must = sheet.getRange("Q2").getValue() + "を必ず使ってください。";
          let text = callOpenAI(ingred, theme, must);
          messages = [
            {
              type: "text",
              text: "できました!こんなのどうでしょう?",
            },
            {
              type: "text",
              text: text,
            },
          ];
          replyToLine(replyToken, messages);
          sheet.getRange("J7").setValue(0);
        } else {
          let must = " ";
          let text = callOpenAI(ingred, theme, must);
          messages = [
            {
              type: "text",
              text: "できました!こんなのどうでしょう?",
            },
            {
              type: "text",
              text: text,
            },
          ];
          replyToLine(replyToken, messages);
          sheet.getRange("J7").setValue(0);
        }
      } else if (number == 10) {
        wordsearch(sheet, userMessage);
        messages = [
          {
            type: "text",
            text: "整理しました!",
          },
        ];
        replyToLine(replyToken, messages);
      } else if (number == 8) {
        sheet.getRange("H1").setValue(9);
        sheet.getRange("Q2").setValue(userMessage);
        messages = [
          {
            type: "text",
            text:
              "料理のジャンルを選んでください。ジャンルを入れるとレシピ案が出ます(少し時間がかかります...)",
            quickReply: {
              items: [
                {
                  type: "action",
                  action: {
                    type: "message",
                    label: "和食",
                    text: "和食",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "message",
                    label: "洋食",
                    text: "洋食",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "message",
                    label: "イタリアン",
                    text: "イタリアン",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "message",
                    label: "中華料理",
                    text: "中華料理",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "message",
                    label: "韓国料理",
                    text: "韓国料理",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "message",
                    label: "タイ料理",
                    text: "タイ料理",
                  },
                },
                {
                  type: "action",
                  action: {
                    type: "postback",
                    label: "その他ジャンル",
                    text: "その他ジャンル",
                    data: "other",
                  },
                },
              ],
            },
          },
        ];
        replyToLine(replyToken, messages);
      } else {
        let lastRow = sheet
          .getRange(1, number + 10)
          .getNextDataCell(SpreadsheetApp.Direction.DOWN)
          .getRow();
        sheet.getRange(lastRow + 1, number + 10).setValue(userMessage);
        messages = [
          {
            type: "text",
            text: "登録しました!",
          },
        ];
        replyToLine(replyToken, messages);
      }
    }
  } else if (json.events[0].type == "postback") {
    if (json.events[0].postback.data == "must") {
      sheet.getRange("H1").setValue(8);
      sheet.getRange("J7").setValue(1);
      messages = [
        {
          type: "text",
          text:
            "使いたい食材を送信して下さい。(複数も可。例:「豚肉、キャベツ」)",
        },
      ];
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "choice") {
      sheet.getRange("H1").setValue(9);
      messages = [
        {
          type: "text",
          text: "料理のジャンルを選んでください。",
          quickReply: {
            items: [
              {
                type: "action",
                action: {
                  type: "message",
                  label: "和食",
                  text: "和食",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "洋食",
                  text: "洋食",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "イタリアン",
                  text: "イタリアン",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "中華料理",
                  text: "中華料理",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "韓国料理",
                  text: "韓国料理",
                },
              },
              {
                type: "action",
                action: {
                  type: "message",
                  label: "タイ料理",
                  text: "タイ料理",
                },
              },
              {
                type: "action",
                action: {
                  type: "postback",
                  label: "その他ジャンル",
                  text: "その他ジャンル",
                  data: "other",
                },
              },
            ],
          },
        },
      ];
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "other") {
      messages = {
        type: "text",
        text: "料理についての希望を書いてください。",
      };
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "protein") {
      let list = sheet.getRange("G2").getValue();
      if (list == 2) {
        messages = {
          type: "text",
          text: "登録された食べ物がありません。",
        };
      } else {
        sheet.getRange("H1").setValue(10);
        let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 1).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];
      }
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "vege") {
      sheet.getRange("H1").setValue(10);
      let list = sheet.getRange("G3").getValue();
      if (list == 2) {
        messages = [
          {
            type: "text",
            text: "登録された食べ物がありません。",
          },
        ];
      } else {
        let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 2).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];
      }
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "bean") {
      sheet.getRange("H1").setValue(10);
      let list = sheet.getRange("G4").getValue();
      if (list == 2) {
        messages = [
          {
            type: "text",
            text: "登録された食べ物がありません。",
          },
        ];
      } else {
        let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 3).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];
      }
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "season") {
      let list = sheet.getRange("G5").getValue();
      if (list == 2) {
        messages = [
          {
            type: "text",
            text: "登録された食べ物がありません。",
          },
        ];
      } else {
        sheet.getRange("H1").setValue(10);
        let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 1).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];
      }
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "frozen") {
      let list = sheet.getRange("G6").getValue();
      if (list == 2) {
        messages = [
          {
            type: "text",
            text: "登録された食べ物がありません。",
          },
        ];
      } else {
        sheet.getRange("H1").setValue(10);
        let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 1).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];
      }
      replyToLine(replyToken, messages);
    } else if (json.events[0].postback.data == "otherclear") {
      let list = sheet.getRange("G7").getValue();
      if (list == 2) {
        messages = [
          {
            type: "text",
            text: "登録された食べ物がありません。",
          },
        ];
      } else {
        sheet.getRange("H1").setValue(10);
        let items = [];
        for (let i = 3; i < list + 1; i++) {
          let food = sheet.getRange(i, 1).getValue();
          item = {
            type: "action",
            action: {
              type: "message",
              label: food,
              text: food,
            },
          };
          items.push(item);
        }
        messages = [
          {
            type: "text",
            text:
              "以下が入っています。既に使ったものがあれば選択すれば削除できます。",
            quickReply: { items },
          },
        ];
      }
      replyToLine(replyToken, messages);
    }
    // proteinvegebeanseasonotherclear
    else if (messageType !== "text") {
      messages = [
        {
          type: "text",
          text: "テキストのみ受付可能です。",
        },
      ];
      replyToLine(replyToken, messages);
    }
  }
}
//user_idのシートを取得する(なければ新規作成する)
function getSpreadSheet(user_id) {
  let ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(user_id);
  //読み込んだシート名が存在するかif文で確認
  if (!ss) {
    return addnewSheet(user_id);
  } else {
    return SpreadsheetApp.openById(
      "スプレッドシートID"
    ).getSheetByName(user_id);
  }
}

//user_idをシート名にして新しいシートを作成
function addnewSheet(user_id) {
  let mySheet = SpreadsheetApp.openById(
   "スプレッドシートID"
  ).getSheetByName("master");
  //スプレッドシートに新しいシートを追加挿入
  newSheet = mySheet.copyTo(
    SpreadsheetApp.openById("スプレッドシートID")
  );
  //追加挿入したシートに名前を設定
  newSheet.setName(user_id);
}

// 使った食材を削除する
function wordsearch(sheet, item) {
  const searchRange = sheet.getRange("K3:P100");
  // 検索する文字列
  const searchString = item;
  // 置換する文字列
  const replaceString = "";
  // 置換
  searchRange.createTextFinder(searchString).replaceAllWith(replaceString);
}

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15