私自身の日常を効率的にする目的で、chatGPTを用いた仕組みを作ってみました。
TLDRという英語のTechBlogがあります。最新情報が得られますが、一から全て読むのがちょっと大変です。(週5件ほどメールが届く)
chatGPTが作成した要約文を確認し、気になるものはメール本文やURLを確認する、といった形で情報収集することにしました。
取り組むきっかけ
arxiv.org(機会学習関連の論文が掲載されたサイト)のAPIで取得した論文情報に対して、chatGPTでサマリーを生成し、SlackBotで通知を行う、といった内容でした。この記事を読んで、今回のGmail APIとchat GPTの構成を実現してみることにしました。
0.読んでいただく前に
プロンプト作成自体は、chatGPTに関する専門的な知識は要りません。
以下の経験があると、スムーズに進められると思います。
- AWSのLambdaの利用:テスト実行とログ確認を行ったことがある
- AWSのLayerの作成:Layerを作成し、Lambda関数に対して紐づける
- AWS CDKの利用:デプロイまで実施したことある(言語はTypescript以外でもOKです!)
ソースコードです。適宜ご利用ください。
API KEYの扱いについて
API KEYの値をgitHubに公開しないように注意してください。
コードに直接記載しない、環境変数で読み込む場合も.env
ファイルを.gitignore
に設定する、といった対策とってください。
1. 構成
2. Gmail API設定とメール内容取得の実装
2.1 プロジェクト作成
以下参考に進めました。
「プロジェクト名」は任意の値を設定します。「場所」は個人で進める場合、「組織なし」のままで「作成」をクリックします。
2.2 Gmail APIの有効化とOAuthクライアントの作成
以下を参考に進めました。最後にダウンロードするcredentials.json
は、今回の作業で使用します。
2.3 サンプルコード動作確認
認証情報を用いて、Gmailのラベル一覧を取得するサンプルコードです。Gmailの本文情報取得のベースです。以下を参考に進めました。細かい説明は省きますが、初回実行時に、ブラウザで認証に関する確認作業があるので、対応してください。
2.4 本文抽出のためのコード修正
以下の条件を固定して実装しました。
- 送信元がTLDR
- 前日に受信したメール
- 未開封のメール
Gmail APIを用いたメール本文取得の実装
from __future__ import print_function
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from datetime import datetime, timedelta
import base64
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
def main():
"""Shows basic usage of the Gmail API.
Lists the user's Gmail labels.
"""
creds = None
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
token.write(creds.to_json())
try:
# Call the Gmail API
service = build('gmail', 'v1', credentials=creds)
today = datetime.today().date()
three_days_ago = today - timedelta(days=3)
start_date = three_days_ago.strftime("%Y/%m/%d")
# メールの検索クエリを構築する
query = f'from:"TLDR WEB DEV" is:unread after:{start_date}'
# メールを検索する
response = service.users().messages().list(userId='me', q=query).execute()
messages = response.get('messages', [])
# UnicodeEncodeError: 'cp932' codec can't encode character '\xa0' in position 120: illegal multibyte sequence
with open('contents.txt', 'w', encoding='utf-8') as contents:
for message in messages:
message_id = message['id']
msg = service.users().messages().get(userId='me', id=message_id, format='full').execute()
if 'parts' in msg['payload']:
parts = msg['payload']['parts']
for part in parts:
if part['mimeType'] == 'text/plain':
data = part['body']['data']
# base64エンコードされた本文をデコードする
body = base64.urlsafe_b64decode(data).decode('utf-8',)
body = body.replace('\n','')
# 必要な文章を抽出
contents.write(body.split('🧑💻')[1].split('🎁')[0])
# print(body)
elif 'body' in msg['payload']:
data = msg['payload']['body']['data']
body = base64.urlsafe_b64decode(data).decode('utf-8')
# print(body)
except HttpError as error:
# TODO(developer) - Handle errors from gmail API.
print(f'An error occurred: {error}')
if __name__ == '__main__':
main()
3. chatGPTで要約依頼のプロンプト実装
3.1 chatGPT
資料はたくさんあります。面白いと感じたスライドをいくつか取り上げました。
-
生成AI周回遅れキャッチアップ勉強会!(約50ページ)
- 生成LLMという仕組みを利用して、chatGPTを機能させています、といった話です。
-
ChatGPTによって描かれる未来とAI開発の変遷(約100ページ)
- GPTとは何か・GPTを用いた開発・これからの機械学習開発、といった内容が記載されています。
-
New Era at Computing - ChatGPT がもたらした新時代(約150ページ)
- chatGPTの説明、またはBingChatというMicrosoft発の対話型AIチャットツールも組み合わせて、より実践的な開発、といった内容です。
3.2 API KEYの取得
実装前に、OPENAIのサイトでAPI KEYを取得します。以下を参考に進めました。
プロンプト作成部分の実装です。(環境変数:OPEN_API_KEY
の値は、.env
ファイルから取得)
from dotenv import load_dotenv
load_dotenv()
import os
os.getenv('OPENAI_API_KEY')
import openai
system = """与えられたテキストについて、XXXXしてください```
・要約結果
・ポイント1
・ポイント2
・ポイント3
```"""
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{'role': 'system', 'content': system},
{'role': 'user', 'content': text} #textにはまとめたい文書を設定
],
temperature=0.01,
)
summary = response['choices'][0]['message']['content']
title, *body = summary.split('\n')
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3.3 プロンプト作成
実装量は少ないです。メール本文に対して、どういう形式・ボリュームの要約を返してほしいか検討して、プロンプト文章を作成します。
(変数system
の内容を修正します)
実装は、冒頭に記載した「arxiv.orgのSummary作成記事」を参考にしています。
今回要約するメール本文は、トピックが大きく3つあり、各トピック3つほど記事があります。各記事に概要とURLが記載されています。
記事ごとに、日本語を用いた簡単な要約とURLを提示してくれる要約を目指しました。
最初、以下のプロンプトを作成してみました。
system = """与えられたテキストについて、項目ごと(ARTICLES & TUTORIALS、OPINIONS & ADVICE、LAUNCHES & TOOLS)に100文字程度で要点して、
以下のフォーマットで日本に訳して出力してください。URLがある場合は、出力してください```
・ARTICLES & TUTORIALSの内容
・まとめた内容
・URL一覧
・OPINIONS & ADVICEの内容
・まとめた内容
・URL一覧
・LAUNCHES & TOOLSの内容
・まとめた内容
・URL一覧
```"""
出力結果(URLがない・・・)
・ARTICLES & TUTORIALS
・APIやネットワークへのアクセス回数を制限するレート制限についての解説
・Postgresデータベース内で地図を生成する方法の紹介
・アセンブリ言語でGUIを作成する方法の解説
・OPINIONS & ADVICE
・Zig言語の学習が難しい理由とその価値についての考察
・プロジェクトの見積もりが甘くなる理由と改善策の提案
・AIが経営者の役割を置き換える可能性についての議論
(以下略)
一発では望んだ結果にならないので、プロンプトを修正し、期待する出力形式を目指します。プロンプトを工夫することで、時間がかからずに、期待結果が得られました。(1時間未満で完了)
system = """与えられたテキストについて、各項目('ARTICLES & TUTORIALS','OPINIONS & ADVICE','LAUNCHES & TOOLS')、
それぞれについて、100文字程度で要点して、以下のフォーマットで日本に訳して出力してください。
各項目で取得できるURL情報も合わせて返してください。
・ARTICLES & TUTORIALSの内容
・まとめた内容[URL]
・OPINIONS & ADVICEの内容
・まとめた内容[URL]
・LAUNCHES & TOOLSの内容
・まとめた内容[URL]
```"""
出力結果(いい感じ!!)
ARTICLES & TUTORIALS
・APIやネットワークへのアクセス回数を制限するレート制限についての解説[https://open.substack.com/pub/bytebytego/p/rate-limiting-fundamentals]
・Postgresデータベース内で地図を生成する方法についての記事[https://www.crunchydata.com/blog/svg-images-from-postgis]
・アセンブリ言語でGUIを作成する方法についてのチュートリアル[https://gaultier.github.io/blog/x11_x64.html]
OPINIONS & ADVICE
・新しいプログラミング言語Zigの学習についての記事[http://ratfactor.com/zig/hard]
・プロジェクトの見積もりが狂う理由についての解説[https://davestewart.co.uk/blog/the-work-is-never-just-the-work/]
・AIが経営者の役割を置き換える可能性についての記事[https://www.hamiltonnolan.com/p/automate-the-ceos]
(以下略)
4. CDKでインフラ構築とデプロイ
「Lambda関数にEventBridgeの定期実行を設定する」構成をCDKで作ります。実装経験ある言語を選びやすいという理由でCDK(TypeScript)
で進めました。以下コマンドで土台を作成します。
cdk init --language=typescript
LambdaとEventBridgeの定義
-
Lambda関数
- ランタイム:PYTHON3.9
- 環境変数:OPENAI_API_KEY、LINE_ACCESS_TOKEN
- メモリ:256MB
- タイムアウト:2分(テストを通じて得た処理秒数の2倍の時間)
-
EventBridge
- 日本時間午前6時にLambda関数を定期実行
※CDKでpython3.10
は設定不可ですが、Lambda関数のコンソールからは設定可能となりました。
4.1 スタックの実装内容
以下を参考に進めました。
LambdaとEventBridgeを定義
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as dotenv from 'dotenv';
import * as path from 'path';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
export class GmailapiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
dotenv.config();
const lambdaLayer = new LayerVersion(this, "GmailSummaryLayer", {
code: AssetCode.fromAsset("python"),
compatibleRuntimes: [Runtime.PYTHON_3_9],
});
// Lambda関数の作成
const lambdaFunction = new lambda.Function(this, 'GmailSummaryLambda', {
functionName: 'gmail_summary',
runtime: lambda.Runtime.PYTHON_3_9,
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')),// Lambda関数のコードディレクトリ
handler: 'main.handler',
environment: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY || 'default-value',
LINE_ACCESS_TOKEN: process.env.LINE_ACCESS_TOKEN || 'default-value',
},
memorySize: 256,
timeout: cdk.Duration.seconds(120),
layers: [lambdaLayer],
});
// EventBridgeルール作成
const rule = new events.Rule(this, 'DailyRule', {
schedule: events.Schedule.cron({ hour: '23', minute: '0' }), // 毎日午前8時(日本時間)に実行
});
rule.addTarget(new targets.LambdaFunction(lambdaFunction));
}
}
4.2 外部モジュールの用意
この作業はUbuntu上で行いました。
Lambda関数上で、Gmailの本文取得に必要なライブラリをimportできるようにします。仮想環境を構築して、必要なライブラリのモジュールを用意します。
仮想環境構築については、以下を参考にしました。
tutorial-env
フォルダは仮想環境関連なので、自分で用意したのはrequirements.txt
のみです。
.
├── tutorial-env
├── bin
├── include
├── lib
└── lib64
└── requirements.txt
ライブラリを定義します。
python-dotenv
google_auth_oauthlib
google-api-python-client
google-api-core
openai
以下記事を参考に、Dockerを使って必要なモジュールを揃えました。
public.ecr.aws/sam/build-python3.9
イメージを使用し、requirements.txt
に記述したライブラリをインストールして、python/lib/python3.9/site-packages
フォルダに各モジュールを配置します。
docker run -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.9" /bin/sh -c "pip install -r requirements.txt -t python/lib/python3.9/site-packages/; exit"
モジュールが揃ったらzip化します。(必須ではないです。今後zipファイルをアップロードする可能性がある場合は、実行します。)
zip -r mypythonlibs.zip python > /dev/null
python/lib/python3.9/site-packages
フォルダをlayer
フォルダに配置します。
配置後のフォルダ構成は以下の通りです。
4.3 デプロイ
デプロイを実行しましょう。Y/n
形式で質問された場合、問題なければY
を入力して、デプロイを継続します。
cdk deploy
4.4 動作確認
テストイベントを設定して実行すると、chatgptの出力結果がLambdaのコンソールに出力されました。
5. LINE通知
スマートフォンで確認しやすいのでLINE Botにしました。以下記事を参考にしています。
chatGPTによる要約文をもとに、LINEへの通知を行います。
実装内容はこちら
LINE_NOTIFY_API = "https://notify-api.line.me/api/notify"
LINE_ACCESS_TOKEN = os.environ["LINE_ACCESS_TOKEN"]
'''
中略
'''
def notify_to_line(message: str):
# Topicごとに文章を分割
articles_tutorials = '\n' + message.split('OPINIONS & ADVICE')[0]
opinions_advice = '\nOPINIONS & ADVICE\n' + message.split('OPINIONS & ADVICE')[1].split('LAUNCHES & TOOLS')[0]
launches_tools = '\nLAUNCHES & TOOLS\n' + message.split('OPINIONS & ADVICE')[1].split('LAUNCHES & TOOLS')[1].split('API Call Cost:')[0]
# コスト通知
cost = '\nAPI Call Cost : $' + message.split('API Call Cost:')[1]
method = "POST"
headers = {"Authorization": "Bearer " + LINE_ACCESS_TOKEN}
try:
for each_topic in [articles_tutorials, opinions_advice, launches_tools,cost]:
payload = {"message": each_topic}
payload = urllib.parse.urlencode(payload).encode("utf-8")
req = urllib.request.Request(
LINE_NOTIFY_API, data=payload, method=method, headers=headers)
urllib.request.urlopen(req)
print('Success Notify')
return message
except Exception as e:
return e
.env
に、LINEのアクセストークン情報を追加します。
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ LINE_ACCESS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
6. 最終動作確認
即時実行
ERRORなく、最後まで処理が完了しました。
トーク画面を見ると、文章が長くて途切れてしまいました。
要約文章をTopicごとに分割しました。
1回あたりのコストを出力しました。
メールを受信しなかった日は以下のように通知することにしました。
日によって異なりますが、月あたりの概算をしてみました。
月1ドルくらいで日々の情報収集が捗るのであれば、安いでしょう!
0.035/日 × 30日 = $1.05/月
定期実行
朝8時にスマホを見てみると、要約文が届いていることを確認しました。
7. まとめ
chatGPTを用いて何か実装したい、というところから始まりました。身近なメールを題材に、効率化するための仕組みを作れました。より効率化する場合に、気になった記事のURLからテキスト情報を取得し、その内容を要約することもできそうだと感じました。
chatGPTが起点でしたが、Lambda関数のLayer作成や・Dockerを用いたモジュール生成など、新しい知識を得られました。
プロンプト作成に関して、今回は期待する結果がそれほど苦労せず得られました。もう少し他のテーマでもやってみようと思います。
今後もchatGPTにお世話になる機会はあるので、少しずつ理解を深めていきたいと思います。