仕事の依頼メールをGoogle ToDoリストへ自動登録
1. やることのポイント
特定のサブアドレスに届いたメールをGASで処理
AIを使った期限の読み取り
Google ToDoリストに、上記の期限、メールの件名、メールへのリンクを登録
2. 背景(ぼやき)
- 現状認識
私立大学の教員は、常に、10ぐらいの仕事が同時に走っている。週に5〜6コマの授業が走っているだけでなく、ゼミ対応、卒研対応、経費精算、委員会の仕事、もし引き受けていれば外部の仕事、…。そのような状況で、期限を切られて仕事が新規に舞い込んでくるのは、結構なストレスだ。(読み直して「研究」が抜けていることに、あとから気づいた。)
しかも悪いことに、仕事の依頼はGmailで来る。もともとメールは、お手紙のようなコミュニケーション手段であったはずが、いつの間にか、情報収集手段、業務依頼の窓口になっている。コロナでそれに拍車がかかった。本来別の目的の連絡が全部、メールで届く。その上、最悪なことに、Gmailは「あいまい検索」になっていて、ちょっと間違って検索しても表示される=余計なメールがいっぱい表示される仕様になっている。困った。 - 対策案
AIとGASを組み合わせて、ToDoリストへの自動登録を試みる。ToDoには RFC822 のMessage-ID を使ったメールへのリンクを仕込む。これによって、期日の書き間違い、メールと何かを同時に開く面倒、メールを再び検索する面倒、他の人と「どのメールか」を伝える面倒を回避する。
※ Googleカレンダーに対して同様のことをしたのが「Gmailで届いた会議、研究会のスケジュールを、メール転送するだけでGoogleカレンダーに登録する」の記事です。
3. 準備
3-1. Open AIのAPI
- 機密保持
OpenAIのAPIを使った場合には、データは学習には利用されない。もちろん、情報漏洩のリスクがないわけではないが、メールの処理をするには相対的には良い。そこで、OpenAIのAPIをGASで使って、メールから期日を抽出することにする。 - 登録
次に OpenAIのAPIを使うことについては、各所に掲載されているので、それを参照する。たとえばOpenAI APIとは?使い方を実際の手順を紹介しながら解説など。私はいつの間にか初期のサービス分が尽きていたようなので10ドル入金した。 - モデルの種類
Open AI は、いろいろなモデルを供出している。(cf.OpenAI API で提供されている モデル まとめ) ここではgpt-3.5-turboを使う。
3-2. サブアドレス
- サブアドレスとは
hoge@example.com に対して、hoge+todo@example.com のようなアドレスのこと。RFC5233で定められている。+todo の部分が余分でも、同じユーザーに届く。Gmailでも当然対応している。届いたメールアドレスを元になにか処理するときに使える。 - テストしておく
これを予め、自分で自分のサブアドレスにメールを送って確かめておく。
3-3. GAS とGoogle ToDo リスト
これを使うために、あらかじめGASでTasks(ToDo)追加するを読んでおく。なお、「ワンマンカー」というのが和製英語であるように(?)、Google ToDoリストは日本語の表記で、英語では Google Tasks のようである。(cf. Google ToDo リストの概要)
3-4. Googleスプレッドシートの作成
あらかじめ、スタンドアロンのGASの書き出し用のGoogleスプレッドシートを作っておく。2行目以下は空欄にしておく。
4. プログラム
4-1. メールから情報抽出してスプレッドシートへ記載
スタンドアロンのGASを書いた。ポイントはいくつかある。
- ChatGPT3 は今日の日付を答えられない。「明日まで」を理解するためには基点がわかる必要がある。そこで、メールのヘッダから日付を取得して与える。
- スクリプトプロパティを使った。プログラムを公開するときに安心。移植性もちょっと良くなる。
- Gmailのスレッドの最後のメールを使っている。これについては後ほど活用・応用のところでコメントする。
- 初回は手動で1回実行する。実行許可が問われるので許可する。
- トリガーを時間主導で1分おきに動くようにする。
- Gmailへのリンクは、RFC822の Message ID を利用している。私だけでなく、送信者や、同報で送られた人も(Gmailを使っていれば)使えるURLなので利便性が高い。
サブアドレスに送られてきた仕事依頼のメールの以下の情報をスプレッドシートに書き出す
// サブアドレスに送られてきた仕事依頼のメールの以下の情報をスプレッドシートに書き出す。
// ・件名
// ・仕事の期限
// ・メッセージIDに基づくGmailの検索URL
// Ver.01 : 2024-02-02
// 左側の 「プロジェクトの設定」(歯車マーク)> スクリプト プロパティ で、次の2つを設定、入力する
// 1) スプレッドシートのID : SID
// 2) OpenAI の APIキー : ChatGPTAPI
// 3) あなたのメールアドレス: EMailAddress
// 関数本体
function processEmails() {
var spreadsheetId = PropertiesService.getScriptProperties().getProperty('SID'); // スクリプトプロパティによるシートのID
var subaddress = PropertiesService.getScriptProperties().getProperty('EMailAddress'); // スクリプトプロパティによるメールアドレス
subaddress = subaddress.replace('@', '+todo@'); // メールのサブアドレス hoge+todo@foo.com を設定。
var sheet = SpreadsheetApp.openById(spreadsheetId).getActiveSheet();
const ProcessedLabel = 'ToDoProcessed'; // 処理済みとわかるためのラベル名。任意。
var processedLabel = GmailApp.getUserLabelByName(ProcessedLabel);
if (!processedLabel) { // ラベルが存在しない場合は作成
processedLabel = GmailApp.createLabel(ProcessedLabel);
}
var query = 'to:' + subaddress + ' -in:draft -label:' + ProcessedLabel; // 未送信(下書き)、処理済ラベルがついたものは対象としない。
var threads = GmailApp.search(query);
for (var i = 0; i < threads.length; i++) {
var messages = threads[i].getMessages();
var message = messages[messages.length - 1]; // スレッド内の最後(最新)のメッセージを取得
if (!message.isInTrash()) {
// メールから取り出すのは3つ。件名、メッセージID、日付
var subject = message.getSubject();
var messageId = message.getHeader('Message-ID');
var dateHeader = message.getDate();
// AI で期限を取得
var dateDeadLine = getDeadLineWithOpenAI(message.getPlainBody().slice(0,999), dateHeader);
// スプレッドシートに追記(4列のデータ)
sheet.appendRow([
new Date(), // 登録日時
'', // シートのコンテナバインドGASの処理状況欄。最初は空欄。
dateDeadLine, // 期限・期日
subject, // メールの件名
'https://mail.google.com/mail/#search/in%3Aanywhere+'+encodeURIComponent('rfc822msgid:'+messageId)]); // メールのスレッドへのURL(検索)
threads[i].addLabel(processedLabel); // 処理済ラベルを付ける
}
}
}
// OpenAI APIを使用してメールから期限・期日を抽出する関数
// AI は、「今日」を認識しないので、メールから取り出したメールの日付を活用する。
function getDeadLineWithOpenAI(emailBody, emailDate) {
const apiKey = PropertiesService.getScriptProperties().getProperty('ChatGPTAPI'); // スクリプトプロパティによるAPIキー
const apiUrl = 'https://api.openai.com/v1/chat/completions'; //ChatGPTのAPIのエンドポイントを設定
//ChatGPTに投げるメッセージを定義(ユーザーロールの投稿文のみ)
const messages = [{'role': 'user', 'content': 'メールの内容を分析し、仕事の期限(締め切り)の日時を、文章を用いず、yyyy-MM-dd 形式で答えよ。理由などの記述は省く。\
ただし、メールの書かれた日時は、' + emailDate + 'である。\
また、メールの内容は次のとおりである。:\
' + emailBody + ' \
'}];
//OpenAIのAPIリクエストに必要なヘッダー情報を設定
const headers = {
'Authorization':'Bearer '+ apiKey,
'Content-type': 'application/json',
'X-Slack-No-Retry': 1
};
//ChatGPTモデルやトークン上限、プロンプトをオプションに設定
const options = {
'muteHttpExceptions' : true,
'headers': headers,
'method': 'POST',
'payload': JSON.stringify({
'model': 'gpt-3.5-turbo',
'max_tokens' : 2048,
'temperature' : 0.0,
'messages': messages})
};
//OpenAIのChatGPTにAPIリクエストを送り、結果を変数に格納
const response = JSON.parse(UrlFetchApp.fetch(apiUrl, options).getContentText());
Logger.log(response);
//ChatGPTのAPIレスポンスをログ出力
Logger.log(response.choices[0].message.content);
return response.choices[0].message.content; // メールから取り出した期限・期日
}
4-2. スプレッドシートからToDoリストへ登録
上記のGASを実行した結果は、スプレッドシートに保管される。そのスプレッドシートにコンテナバインドのGASを追加する。
ポイントとしては、次のようなところがある。
-
taskListId
のところはvar taskListId = '@default';
とすれば、IDを調べる必要はない。個人名のついたToDoリストに登録される。 - 手作業でシートにToDo を書き込んでも良いように、GASを2つに分けている。シートの3列目(期日)を入力し直して修正、2列めを空欄にすれば、再びToDoリストにエントリーできる。
- このスクリプトも1回手作業で実行し、権限を与えてから、トリガーで1分ごとに起動するようにする。
スプレッドシートからToDoリストへ登録
// スプレッドシートから ToDoリスト に仕事を登録する。
// Ver.1 2024-02-02
//
// ※ シートは、メールの +todo のサブアドレス宛のメールを受信して、1分ごとのトリガーで追加・更新されている。
// ※ このシートに手書きすることを考えて、上の作業を実行するGASとは別に、ここで1分ごとのトリガーで起動する。
// ※ 準備として、ToDoリストのIDを取得する。次のURLにアクセスして、Execute
// https://developers.google.com/tasks/reference/rest/v1/tasklists/list?hl=ja
// リストは複数作れる。あらかじめ、"ByMail"というリストを作ってある。IDさえ取得できれば、名前は何でも構わない。
// (cf. https://taibonn.com/gsuite/gas-auto-add-tasks/ )
// 取得した ID は、「プロジェクトの設定」>「スクリプトプロパティ」に taskID_ByMail として保存した。
// ※ 左側の「サービス」をクリックして、「Google TASKS API」を選んで「追加」をクリックする。左側に「Tasks」という表示が現れる。
// 1回、手動で実行して、各種パーミッションを許可してから、トリガーで1分ごとに実行するようにする。
// 必要なスクリプトプロパティ
// 1) taskID_ByMail : Google の TODO List のID
function processSpreadsheetForTasks() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = sheet.getDataRange().getValues();
var urlGS = SpreadsheetApp.getActiveSpreadsheet().getUrl();
// Google Tasks APIを扱うためのToDoリストIDを取得して指定。
//
// デフォルトのリストの場合
// var taskListId = '@default';
// ByMail というToDoリストのIDは、スクリプトプロパティから取得
var taskListId = PropertiesService.getScriptProperties().getProperty('taskID_ByMail');
for (var i = 1; i < data.length; i++) { // 1行目(ヘッダー行)をスキップ
var row = data[i];
if (row[1] === '' && isValidDateTime(row[2])) {
var dateDL = new Date(row[2]);
var dateDLstring = Utilities.formatDate(dateDL, "JST", "yyyy-MM-dd")+ "T00:00:00.000Z";
try {
var task = {
title: row[3],
notes: '■ 詳細のメールはこちら:\n' + row[4] +'\n■ スプレッドシートはこちら: ' + urlGS,
due: dateDLstring
};
// タスクを追加
var createdTask = Tasks.Tasks.insert(task, taskListId);
// スプレッドシートに「Order」をマーク
sheet.getRange(i + 1, 2).setValue('Ordered');
} catch(e) {
// エラー処理
console.error('Error adding task: ' + e.message);
}
}
}
}
// 日時または日付として妥当かどうかを判定
function isValidDateTime(value) {
var date = new Date(value);
return !isNaN(date.getTime()); // 日時として解釈可能ならtrueを返す
}
5. 活用・応用
- 周りの人に「私に期限付きの仕事を振るときには、このメールアドレスに送ってね!♡」なんて言っても送ってくれいないだろう。そういうメールが来そうなメーリングリストへの登録を、サブアドレスにしておく可能性がある。期日をAIが判定できなければ、登録されない。
- 自分でメールを読んでいて、「あ、これ登録しなきゃ」とおもったら、自分のサブアドレスに転送するとよい。登録される。メールへのURLもスレッドへのURLになるので、転送メールと転送前の着信メールと、両方開ける。
- プログラムではスレッドを取得している。繰り返しになるが、Gmailでは、転送されたメールも同じスレッドになる。だから、件名に Fwd: がある場合には、転送メールと判断し、一つ前のメールを登録すると良いかもしれない。
- Googleカレンダーの方が他の人と共有できていいのかもしれない。
6. 感想
OpenAIのAPIは安い。まだ1ドルも使っていない。こんなに安ければ、活用したくなる。業務の効率化は、アイデア次第で色々できそうだ。しかし、数年すれば、こんなスクリプト書かなくても、標準サービスで自動でやってくれそうな気もするし、そういった方向への進化を期待している。
もっとスタイリッシュにやっている人がいそうな気もしますが、自分の備忘録を兼ねて。