💰

LINE MessagingAPI + GASの家計簿アプリにChatGPTによる分析を組み込んだ

2023/03/29に公開1

概要

  • なんぼなんでもChatGPT開発やらんとあかんやろ
  • → ChatGPTのAPIを触ってみようキャンペーン
  • 自分用に作っていたLINE上の家計簿ツールがあるので、そのインターフェース上でChatGPTに自分の支出を分析する機能をつけた
  • 家計簿ツールは、LINE公式アカウント + LINE Messaging APIを用いて実装されており、その公式アカウントをメンバーとして含むグループで品名と金額を投稿すると、投稿主と投稿時刻と共にSpreadsheetに記録されている
    • Spreadsheetは月ごとに分かれて集計されている
  • 「最近電気代高くね?」みたいなことを聞きたい

前提

以下のような形式で、毎月の支出がまとまっています。
家計簿の毎月のシート
「支払い日, 支払った人, 品目, 金額」
ちなみに、「支払った人」はlineの userId を使って別テーブルを参照しています

main.gs
function doPost(e) {
    const json = JSON.parse(e.postData.contents);

    const reply_token = json.events[0].replyToken;
    if (typeof reply_token === 'undefined') {
        return;
    }
    const userId = json.events[0].source.userId // 投稿主

    const user_message = json.events[0].message.text; // 投稿メッセージ
    const message_parameter = user_message.split(/\r\n|\n/);
    let post_message = ""; 
    const date = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy年MM月dd日');

    if (message_parameter.length == 2) { // 2行なら記録
      post_message = writeData(message_parameter, userId, date);
    }
    else if (.........){ // 色々な条件分岐と処理
	    ....
    }....
    else {
      post_message = "ぽへー"
      // post_message = chatGpt(message_parameter[0]); // ここでやりたい
    }

    UrlFetchApp.fetch(URL, {
      'headers': {
          'Content-Type': 'application/json; charset=UTF-8',
          'Authorization': 'Bearer ' + ScriptProperties.getProperty('LINE_BOT_ACCESS_TOKEN'),
      },
      'method': 'post',
      'payload': JSON.stringify({
          'replyToken': reply_token,
          'messages': [{
              'type': 'text',
              'text': post_message,
          }],
      }),
    });
    return ContentService.createTextOutput(JSON.stringify({ 'content': 'post ok' })).setMimeType(ContentService.MimeType.JSON);
}

今まで条件に当てはまらないメッセージは「ぽへー」と返していたのですが、ここにChatGPTを噛ませようと思いました。

試行

今回はモデルとして gpt-3.5-turbo を使いました。

ChatGPTのAPIでは user ロールと assistant ロールに分けて以前の会話の文脈を指定できるようなので、先に手動でその文脈を指定します。(system ロールでのプロンプトは今回は試しませんでした)
https://platform.openai.com/docs/guides/chat

事前データの提供

ChatGPTは事前に成功体験を積ませることが大事なようなので、最初に型を与えて、理解した感じにしておきます。
ChatGPTに、渡すデータの型を教えている様子

[user]
これからある一家の支出の一覧を与えます。データはカンマ区切りで、一行ごとに「支払い日, 支払った人, 品目, 金額」という形式で与えられます。

例えば、このような形です。

2023/03/01, 太郎, 食料品, 5000
2023/03/03, 花子, 光熱費, 10000
2023/03/05, 太郎, 雑誌代, 1500

次のメッセージから実際のデータを渡します。わかりましたか?
[assistant]
はい、わかりました。この例では、2023年3月3日に花子が10000円を光熱費に使ったことがわかります。また、太郎は全部で6500円を支出しました。それでは実際のデータを送ってください。
[user]
${data}
[assistant]
ありがとうございます。これらのデータを使って一家の支出を分析することができます。質問を送ってください。

質問

基本的にはLINEから飛んできたメッセージをそのまま質問文にしようとしたのですが、例えば「先月の電気代は?」といった質問に対して、今が何年何月なのかという情報がないので答えられません。

なので、質問の前に以下のように今月の情報を教える必要がありました。

[user]
今月は${YEAR}年 ${MONTH}月です。

回答

普通に「今月いくら使ったか教えて」と言うと、まず該当するデータを全て示してから、それを足し算している様子を示し、その後でようやく最後の答えを示すので、「回答のみを簡潔に出力してください。」みたいなことを伝える必要がありました。

普通に質問するとすごく長い回答を送ってくる

また、以前の「ぽへー」を残したかったので、メッセージが質問ではなかった場合のハンドリングを記述しました。

[user]
以下に、質問または質問ではない文章を与えます。
質問には、その回答を出力してください。回答までの途中経過は含めず、回答のみを簡潔に出力してください。
質問ではない文章に対しては、「ぽへー」の3文字だけを出力し、それ以外は出力しないでください。

実装

以下の記事を大いに参考にしました。
https://auto-worker.com/blog/?p=7438

chatGpt.gs
const ANALYZE_MONTH_COUNT = 5
const SPREADSHEET_ID = "your_spreadsheet_id"

function chatGpt(message) {
  const apiKey = ScriptProperties.getProperty('CHAT_GPT_KEY');
  const apiUrl = 'https://api.openai.com/v1/chat/completions';

  const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);

  const sheetNames = getRecentMonths(ANALYZE_MONTH_COUNT).map(getRecentSheet)

  const data = sheetNames.map(sheetName => {
    const sheet = spreadsheet.getSheetByName(sheetName)
    const lastRow = sheet.getLastRow();
    return sheet.getRange(2, 1, lastRow-1, 4).getValues().map(e => {
      const[date, ...rest] = e
      return [`${date.getFullYear()}/${date.getMonth()}/${date.getDate()}`, ...rest].join(",")
    }).join("\n")
  })

  const messages = [
    {'role': 'user', 'content': `
      これからある一家の支出の一覧を与えます。データはカンマ区切りで、一行ごとに「支払い日, 支払った人, 品目, 金額」という形式で与えられます。

      例えば、このような形です。

      2023/03/01, 太郎, 食料品, 5000
      2023/03/03, 花子, 光熱費, 10000
      2023/03/05, 太郎, 雑誌代, 1500

      次のメッセージから実際のデータを渡します。わかりましたか?
    `},
    {'role': 'assistant', 'content': `
    はい、わかりました。この例では、2023年3月3日に花子が10000円を光熱費に使ったことがわかります。また、太郎は全部で6500円を支出しました。それでは実際のデータを送ってください。
    `},
    {'role': 'user', 'content': `${data}`},
    {'role': 'assistant', 'content': 'ありがとうございます。これらのデータを使って一家の支出を分析することができます。質問を送ってください。'},
    {'role': 'user', 'content': `
      今月は${new Date().getFullYear()}${new Date().getMonth()+1}月です。

      以下に、質問または質問ではない文章を与えます。
      質問には、その回答を出力してください。回答までの途中経過は含めず、回答のみを簡潔に出力してください。
      質問ではない文章に対しては、「ぽへー」の3文字だけを出力し、それ以外は出力しないでください。

      ${message}
    `},
    ];
  
  const headers = {
    'Authorization':'Bearer '+ apiKey,
    'Content-type': 'application/json',
    'X-Slack-No-Retry': 1
  };

  const options = {
    'muteHttpExceptions' : true,
    'headers': headers, 
    'method': 'POST',
    'payload': JSON.stringify({
      'model': 'gpt-3.5-turbo',
      'max_tokens' : 1024,
      'temperature' : 0.9,
      'messages': messages})
  };

  const response = JSON.parse(UrlFetchApp.fetch(apiUrl, options).getContentText());
  if (response.error) return (JSON.stringify(response.error)) // エラーハンドリングはお好きに
  return response.choices[0].message.content;
}

シート名の取得の部分( getRecentMonths )は以下のような実装です。

シート名
シート名は yyyy-mm 形式で、今月のものだけ recent にしています

utils.gs
const getRecentMonths = (n) => new Array(n).fill().map((e, index) => (new Date().getMonth() - index + 12) % 12 + 1)

function getRecentSheet(month) {
  const now = new Date()
  const nowYear = now.getFullYear()
  const nowMonth = now.getMonth()+1
  if (nowMonth === month) return 'recent'
  else if (nowMonth > month) return `${nowYear}-${monthZeroPadding(month)}`
  else return `${nowYear-1}-${monthZeroPadding(month)}`
}

function monthZeroPadding(month) {
  return ('00' + month).slice(-2);
}

結果

電気代の推移
いい感じ(データも合っている)

今月の支出
むちゃむちゃ

ぽへー
ぽへー
というか比較しておくれよ

まとめ

  • データを抜き出すだけならギリいける
  • 分析系は結構やばい
  • GPT-4に期待

...

アドバイス

  • 何のためにお前がおんねん

Discussion