LINE MessagingAPI + GASの家計簿アプリにChatGPTによる分析を組み込んだ
概要
- なんぼなんでもChatGPT開発やらんとあかんやろ
- → ChatGPTのAPIを触ってみようキャンペーン
- 自分用に作っていたLINE上の家計簿ツールがあるので、そのインターフェース上でChatGPTに自分の支出を分析する機能をつけた
- 家計簿ツールは、LINE公式アカウント + LINE Messaging APIを用いて実装されており、その公式アカウントをメンバーとして含むグループで品名と金額を投稿すると、投稿主と投稿時刻と共にSpreadsheetに記録されている
- Spreadsheetは月ごとに分かれて集計されている
- 「最近電気代高くね?」みたいなことを聞きたい
前提
以下のような形式で、毎月の支出がまとまっています。
「支払い日, 支払った人, 品目, 金額」
ちなみに、「支払った人」はlineの userId
を使って別テーブルを参照しています
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
ロールでのプロンプトは今回は試しませんでした)
事前データの提供
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文字だけを出力し、それ以外は出力しないでください。
実装
以下の記事を大いに参考にしました。
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
にしています
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
GPT-3でもプロンプトによって結構嘘をつかせないことができるっぽいので試してみたい