GASとChatGPTで偏差値3の買い物メモを使ってみた #リテールテックハッカソン #無駄づくり

## いつも同じものばかり買っている私
うちは大体週末に一週間分の買い物を済ませますが、買うのは毎週同じものばかり。マンネリ化した買い物はタスクでしかなく、毎週仕方なくイ●ンに通っています。

**たまにはワクワクするような買い物がしたい！**
もっと、**新鮮で**、**楽しくて**、**思いもよらない買い物体験**を！

そんな思いを実現すべく作ったサービスは**こちら**:information_desk_person:

## 偏差値3の買い物メモ
<img width="400" alt="S__389530192.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/b4770d2c-074a-9edd-e456-0ae60e2a6105.jpeg">

偏差値が低いので、**何にも覚えてくれない買い物メモ**です。役に立ちません。でも、**新しい世界を教えてくれます**。

## システム構成
![スクリーンショット 2023-06-10 145944.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2958610/2a927792-7078-548d-071b-8943cc73029f.png)
### ①買い物メモを登録する機能
- わたしがLINEで買い物メモを送る
- Google Apps ScriptでGoogleスプレッドシートに登録する
### ②買い物メモを見る機能
- LINEで「メモを見る」と送る
- Google Apps ScriptでGoogleスプレッドシートから買い物メモを取得する
- 取得した買い物メモをChatGPTで全く違う内容に改ざんする
- LINEで身に覚えのない買い物メモが返ってくる
### ③買い物メモを消す機能
- LINEで「買えた」と送る
- Google Apps ScriptでGoogleスプレッドシートの買い物メモを削除する

## つくりかた
### 使った技術
- Google Apps Script
- Googleスプレッドシート
- ChatGPT APT
- LINE Messaging API

### コード
<details><summary>Google Apps Scriptのコード</summary>

```JavaScript:偏差値3の買い物メモ
// ↓↓↓↓↓ 自分で書き換えるところ ↓↓↓↓↓　=========================================================================================================
// LINE Bot 設定
const CHANNEL_ACCESS_TOKEN = '[CHANNEL_ACCESS_TOKEN]';

// AI設定
const openAIApiKey = '[openAIApiKey]';
// AIに演じてほしい役割の設定
const roleSetting = ''
// 補足 : トークン量（RPGでいう消費MP的なもの）は日本語より英語の方が少ない傾向にあります。
// 日本語で試行錯誤したあと、AIに与える指示が固まったら英訳して設定した方が省エネで利用できます。
// トークン量計算ツール -> https://platform.openai.com/tokenizer
// ↑↑↑↑↑ 自分で書き換えるところ ↑↑↑↑↑ =========================================================================================================

// その他設定
const logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('log');
var replyToken, json
const openAIEndPoint = "https://api.openai.com/v1/chat/completions";// 参考 -> https://platform.openai.com/docs/guides/chat
const openAIModel = "gpt-3.5-turbo";// 参考 -> https://platform.openai.com/docs/models

//ポストで送られてくるので、ポストデータ取得
function doPost(e) {
  // 動作確認用のログ出力
  // log_to_sheet("A", "doPost")

  // LINEBotから送られてきたデータを、プログラムで利用しやすいようにJSON形式に変換する
  json = JSON.parse(e.postData.contents);

  //返信するためのトークン取得
  replyToken = json.events[0].replyToken;
  if (typeof replyToken === 'undefined') {
    return;
  }

  // 返信するメッセージを作成
  // オウム返しができるようになったら、message = test_message()をコメントアウトし、messages = getAIAnswer(json.events[0].message.text)をコメントイン
  if (json.events[0].message.text == 'メモを見る') {
    const trueData = convertColData();
    messages = reply_message(trueData);
  } else if (json.events[0].message.text == '偏差値上げて') {
    const falseData = getColData();
    messages = reply_message(falseData);
  } else if (json.events[0].message.text == '買えた') {
    deleteColData();
    messages = get_message('消したよー！');
  } else {
    writeLogToSheet(json.events[0].message.text);
    messages = get_message('おぼえたよー！');
  }

  // line-bot-sdk-gas のライブラリを利用しています ( https://github.com/kobanyan/line-bot-sdk-gas )
  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);
}

// 動作確認用のオウム返しのメッセージを作成する関数
function test_message() {
  //送られたLINEメッセージを取得
  var user_message = json.events[0].message.text;

  //送られたメッセージをそのままオウム返し
  var reply_messages = [user_message];

  // メッセージを返信
  var messages = reply_messages.map(function (v) {
    return { 'type': 'text', 'text': v };
  });

  return messages
}

function get_message(message) {
  var user_message = message;

  //送られたメッセージをそのままオウム返し
  var reply_messages = [user_message];

  // メッセージを返信
  var messages = reply_messages.map(function (v) {
    return { 'type': 'text', 'text': v };
  });

  return messages
}

function reply_message(message) {
  var user_message = message;

  var reply_messages = [user_message];

  // メッセージを返信
  var messages = reply_messages.map(function (v) {
    return { 'type': 'text', 'text': v };
  });

  return messages
}

// AIから回答を得るための関数
function getAIAnswer(question) {
  // AIとの会話内容
  // 引数questionに情報を足してAIに渡したりもできます
  // 例： `${question}とはなんですか？小学生にも分かりやすいように教えてください`
  const openAISettingMessages = [
    {
      "role": "system", "content": roleSetting
    },
    { "role": "user", "content": `${question}を全く意味の異なる単語に変換してください。${question} → 変換後の単語の形にしてください。` }
  ]

  const options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      'Authorization': 'Bearer ' + openAIApiKey,
    },
    "payload": JSON.stringify({
      "model": openAIModel,
      "messages": openAISettingMessages,
      "max_tokens": 3000
    })
  }

  let answer = ''
  try {
    let data = JSON.parse(UrlFetchApp.fetch(openAIEndPoint, options).getContentText());
    answer = data["choices"][0]["message"]["content"]
  } catch (e) {
    // うまく動作しない場合は、エラーが発生していないか確認してみましょう
    // log_to_sheet("A", e)
    answer = 'エラーが発生しました'
  }
  // return [{'type':'text', 'text': answer}]
  return answer;
}

function writeLogToSheet(message = 'x') {
  // シート取得
  const sheet = SpreadsheetApp.getActiveSheet();
  // ループを回して1行ずつすでに記録されていないか確認していく
  let currentRow = 1;
  while (true) {
    // 記録されていない行が見つかったとき：
    if (!sheet.getRange(currentRow, 1).getValue()) {
      // 1列目に日付時刻を記録
      sheet.getRange(currentRow, 1).setValue(Utilities.formatDate((new Date()), 'Asia/Tokyo', 'yyyy/MM/dd hh:mm:ss'));
      // 2列目にメッセージ（引数値）を記録
      sheet.getRange(currentRow, 2).setValue(message);
      // ループ中断
      break;
    }
    // 次の行へ
    currentRow++;
  }
}

// 正しい買い物メモを取得する場合
function getColData() {
  let colData = '買うものは以下の通りです。';
  // シート取得
  const sheet = SpreadsheetApp.getActiveSheet();
  //シート最終行の値を取得する
  const lastRow = sheet.getLastRow();
  //指定したセル範囲を取得する
  try {
    const range = sheet.getRange(2, 2, lastRow - 1);
    //セル範囲の値を取得する
    const values = range.getValues();
    for (let value in values) {
      colData = colData + '\n' + values[value];
    }
  } catch (e) {
    colData = 'メモはありません。';
  }

  return colData;
}

// 偽りの買い物メモを取得する場合
function convertColData() {
  let colData = '';
  let replyColData = '買うものはこれだよー！\n';
  // シート取得
  const sheet = SpreadsheetApp.getActiveSheet();
  //シート最終行の値を取得する
  const lastRow = sheet.getLastRow();
  try {
    //指定したセル範囲を取得する
    const range = sheet.getRange(2, 2, lastRow - 1);
    //セル範囲の値を取得する
    const values = range.getValues();
    //ログ
    console.log(values);
    for (let value in values) {
      colData = colData + values[value] + '\n';
    }
    const aiAnswer = getAIAnswer(colData);
    console.log(aiAnswer);
    const lines = aiAnswer.split('\n');
    for (var i = 0; i < lines.length; i++) {
      // 空行は無視する
      if (lines[i] == '') {
        continue;
      }
      replyColData = replyColData + lines[i].split(' → ')[1] + '！';
    }
    console.log(replyColData);
  } catch (e) {
    replyColData='おなかへったねー！';
  }
  return replyColData;
}

function deleteColData() {
  // シート取得
  const sheet = SpreadsheetApp.getActiveSheet();
  //シート最終行の値を取得する
  const lastRow = sheet.getLastRow();
  sheet.deleteRows(2, lastRow);
}

// 処理の確認用にログを出力する関数
function log_to_sheet(column, text) {
  if (logSheet.getRange(column + "1").getValue() == "") {
    lastRow = 0
  } else if (logSheet.getRange(column + "2").getValue() == "") {
    lastRow = 1
  } else {
    var lastRow = logSheet.getRange(column + "1").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
    // 無限に増えるので1000以上書き込んだらリセット
    console.log("lastRow", lastRow)
    if (lastRow >= 1000) {
      logSheet.getRange(column + "1:" + column + "10").clearContent()
      lastRow = 0
    }
  }
  var putRange = column + String(lastRow + 1)
  logSheet.getRange(putRange).setValue(text);
}
```
</details>

ChatGPTには以下のプロンプトを投げています。
レスポンスの形を指定することで、加工しやすくしています。
```
${question}（入力値）を全く意味の異なる単語に変換してください。${question} → 変換後の単語の形にしてください。
```

## 使ってみた
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【偏差値3の買い物メモ】<br>LINEで使える買い物メモですが、何も覚えられません。でも、思いもよらない買い物を提案してくれます。<a href="https://twitter.com/hashtag/LINEDC?src=hash&amp;ref_src=twsrc%5Etfw">#LINEDC</a><a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a><a href="https://twitter.com/hashtag/%E3%83%AA%E3%83%86%E3%83%BC%E3%83%AB%E3%83%86%E3%83%83%E3%82%AF%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#リテールテックハッカソン</a> <a href="https://t.co/keATlI5qTT">pic.twitter.com/keATlI5qTT</a></p>&mdash; ちーろってぃ🐶通勤をハッピーに (@chiiirotty) <a href="https://twitter.com/chiiirotty/status/1667472270783307776?ref_src=twsrc%5Etfw">June 10, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

何一つ正しいメモが返ってきません:joy:
いかにも頭が悪そうな受け答えが愛らしいですね:relaxed:

**たまには予定に縛られない買い物を楽しむのも悪くない！**
それにしても、ペンギンはどこで買えるのだろうか・・・:rolling_eyes: :penguin:

## （おまけ）機能追加
実は:point_up:のコード、偏差値を上げる機能が付いてます！
つまり、普通の買い物メモってこと:slight_smile:
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【偏差値3の買い物メモ】<br>偏差値を上げる機能を追加しました！<a href="https://twitter.com/hashtag/LINEDC?src=hash&amp;ref_src=twsrc%5Etfw">#LINEDC</a><a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a><a href="https://twitter.com/hashtag/%E3%83%AA%E3%83%86%E3%83%BC%E3%83%AB%E3%83%86%E3%83%83%E3%82%AF%E3%83%8F%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#リテールテックハッカソン</a> <a href="https://t.co/Z1kRVezobc">pic.twitter.com/Z1kRVezobc</a></p>&mdash; ちーろってぃ🐶通勤をハッピーに (@chiiirotty) <a href="https://twitter.com/chiiirotty/status/1667516953853054977?ref_src=twsrc%5Etfw">June 10, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 参考にした記事
- [5分でできるLINE Bot + AI応答](https://qiita.com/cog1t0/items/cc7779345a01192d8f01)・・・この記事はマジで有用です！LINE BotでChatGPTを使いたいときにまず参考にすべき記事です！私もまずはこの記事通りにオウム返しLINE Bot⇒ChatGPT連携LINE Botを作ってから自分の作りたい機能を追加しました！