開発経緯
飛行機のフライト確定メールを見て、Googleカレンダーに予定を追加する時間を無くしたい。
今のAIであれば、メールをよしなに解析して登録するくらいのことはできるだろう。
しかし調査したところ、Google geminiの有料プランでなくてはこのGmail2GoogleCalenderの自動連携機能は使用できなかった。
金額的な話よりも、サブスクリプションが1つ増えることによる脳みそリソース圧迫の方が死活問題である。
というわけで、GASを用いて、GmailからchatGPTを経由して、GoogleCalenderにリクエストするシステムを作った。
(openaiに関しては、毎日バリバリに使っているのでサブスク契約していた)
同じようなことをしている人は他にもいたが、自分が連携したい送信元だけで、容易に連携先の追加が可能なことにこだわった
必須要件
- Gmailを解析して、Googleカレンダーに予定を追加する
- 簡単に新しい連携の追加が可能にしたい
プログラム
const CHATGPT_API_KEY = "API_KEY";
const CHATGPT_URL = "https://api.openai.com/v1/chat/completions";
// 作りたいカレンダーごとにtitleのフォーマットを変える
const ANA_PROMPT = "あなたは優秀なアシスタントです。以下のメールから予定情報を以下のJSON形式で抽出してください。ただしエスケープはしなくていいです。複数の場合は、この配列の中にカンマ区切りで指定してください。また、一つだけの場合でもこの配列で返却してください。絶対にjson以外回答しないでください。[{ \"title\"\: \"〇〇空港→〇〇空港\", \"start\": \"YYYY-MM-DDTHH:mm\", \"end\": \"YYYY-MM-DDTHH:mm\" }] メール本文:";
// ここにメール検索のパターンを追加していく: queryはメールの検索。handlerは専用関数名
const ROUTES = [
{
label: "ANAフライト",
query: 'ANA お支払い完了 予約',
handler: handleAnaFlight
}
];
/**
* ANAフライト予約
*
* 件名フィルターで検索結果から厳密にフィルタリングする。個別に色設定ができる
*/
function handleAnaFlight(message) {
const subject = message.getSubject();
// 件名フィルター
if (!subject.includes("ANA") || !subject.includes("お支払い完了のお知らせ")) {
Logger.log(`スキップ: ${subject} は対象外`);
return;
}
const body = message.getPlainBody();
const json = callChatGPT(ANA_PROMPT, body);
for (const item of json) {
createCalendarEvent(item, CalendarApp.EventColor.BLUE);
}
}
/**
* メイン関数
*
* トリガーを1時間に1回実行する前提で常に1時間前までのメールに検索をかける
*/
function autoSchedule() {
Logger.log(CalendarApp.getAllCalendars())
for (const route of ROUTES) {
// 1時間に一回実行させる
const date = new Date();
date.setHours(date.getHours() - 1);
const formattedDate = Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy/MM/dd');
// // 各ルートごとの検索実行
const threads = GmailApp.search(`${route.query} after:${formattedDate}`);
const messages = GmailApp.getMessagesForThreads(threads);
Logger.log(messages.length+"件");
for (const thread of messages) {
for (const message of thread) {
route.handler(message);
// レートリミット回避
Utilities.sleep(1);
}
}
}
}
/**
* chatgptに問い合わせ
*
* content: プロンプト
* body: メール本文
*/
function callChatGPT(content, body) {
const payload = {
"model": "gpt-4o",
"messages": [{
"role": "user",
"content": `${content} ${body}`
}],
"max_tokens": 500
};
const options = {
"method": "post",
"headers": {
"Authorization": `Bearer ${CHATGPT_API_KEY}`,
"Content-Type": "application/json"
},
"payload": JSON.stringify(payload)
};
const response = UrlFetchApp.fetch(CHATGPT_URL, options);
const json = JSON.parse(response.getContentText());
let gptReply = json.choices[0].message.content;
// コードブロック除去。gptはjsonで返答する場合でも必ずコードブロックで整形してしまうため。
gptReply = gptReply.replace(/```json|```/g, '').trim();
try {
return JSON.parse(gptReply);
} catch (e) {
Logger.log(`GPTのレスポンスが不正: ${gptReply}`);
throw new Error("GPT JSONパース失敗");
}
}
/**
* カレンダーに追加
*
* TODO: 説明の追加
*/
function createCalendarEvent(details, color) {
if (!details || !details.title || !details.start || !details.end) {
Logger.log("スキップ: detailsの中身が不完全");
return;
}
const calendar = CalendarApp.getDefaultCalendar();
const now = new Date();
const startDate = new Date(details.start);
const endDate = new Date(details.end);
// 日時不正
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
Logger.log("スキップ: 日付形式が不正");
return;
}
// 未来の日付のみ追加する
if (startDate < now) {
Logger.log(`スキップ: 過去の予定 - ${details.title}`);
return;
}
// 日時反転
if (startDate >= endDate) {
Logger.log(`スキップ: 終了が開始より前 - ${details.title}`);
return;
}
// 重複チェック
const events = calendar.getEvents(startDate, endDate, {search: details.title});
if (events.length > 0) {
Logger.log(`イベント「${details.title}」は既に存在するのでスキップ`);
return;
}
// イベント作成
const event = calendar.createEvent(details.title, startDate, endDate);
event.setColor(color);
Logger.log(`イベント作成: ${details.title}, 開始: ${startDate.toISOString()}, 終了: ${endDate.toISOString()}, イベントID: ${event.getId()}`);
}
OPEN APIのAPIキーをCHATGPT_API_KEYに設定してください。
この設定の仕方は、たくさん情報あると思うので割愛します。
連携先追加方法
ROUTESにjson形式で、メールの検索クエリと専用関数を定義します。
次に追加した専用関数を「handleAnaFlight」を参考に追加します。
件名フィルターとは、Gmailのクエリ検索では機能が柔軟であるがゆえにほかの関係ないメールも検索結果に引っかかってしまうため、ここで厳密なフィルタリング処理を行っています。
私は、まずANAの支払い後の予約確定メールからカレンダー追加したいので、handleAnaFlightを作りました。
後続の処理は同じです。
あと重要なのが、ANA_PROMPTです。
ANA_PROMPTを参考に他の通知を追加する場合、プロンプトを組み直します。
連携先個別にプロンプトを設定できる方がいいかなと思いました。
GASの設定
1時間に一回トリガー実行するように設定して、過去1時間のメールを探索します。
これでもれなくメールを調査して、過去の無駄なメールを捜査することを防ぎます。
困ったこと
私はgoogleアカウントを2つ使い分けています。
ANAのメールはAアカウントに届きますが、GoogleカレンダーはBアカウントで利用していました。
API連携どうするかとかごちゃごちゃトライしていたのですが、
そんなことしなくても、Googleカレンダーをアカウント間で連携して仕舞えば関係ありませんでした笑
まとめ
chatGPTを使って、GmailからGoogleCalederに予定を自動作成するシステムを作りました。
これでひとまずgeminiの契約はしなくてよさそうです。
2時間くらいでぱぱっと作ったものなので、今後よりエラーハンドリングや機能追加など検討します
ありがとうございました。