4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

chatGPTに、TechBlogのSummary作成をお願いした

Last updated at Posted at 2023-06-05

私自身の日常を効率的にする目的で、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. 構成

最終的な構成は以下の通りです。
image.png

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

資料はたくさんあります。面白いと感じたスライドをいくつか取り上げました。

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')
.env
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3.3 プロンプト作成

実装量は少ないです。メール本文に対して、どういう形式・ボリュームの要約を返してほしいか検討して、プロンプト文章を作成します。
(変数systemの内容を修正します)
実装は、冒頭に記載した「arxiv.orgのSummary作成記事」を参考にしています。
今回要約するメール本文は、トピックが大きく3つあり、各トピック3つほど記事があります。各記事に概要とURLが記載されています。
記事ごとに、日本語を用いた簡単な要約とURLを提示してくれる要約を目指しました。

実際に届いているメール

image.png

最初、以下のプロンプトを作成してみました。

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 スタックの実装内容

以下を参考に進めました。

フォルダ構成は以下の通りです。
image.png

LambdaとEventBridgeを定義
lib/gmailapi-stack.ts
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

ライブラリを定義します。

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フォルダに配置します。
配置後のフォルダ構成は以下の通りです。
image.png

4.3 デプロイ

デプロイを実行しましょう。Y/n形式で質問された場合、問題なければYを入力して、デプロイを継続します。

cdk deploy
4.4 動作確認

テストイベントを設定して実行すると、chatgptの出力結果がLambdaのコンソールに出力されました。
image.png

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のアクセストークン情報を追加します。

.env
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ LINE_ACCESS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

6. 最終動作確認

即時実行
ERRORなく、最後まで処理が完了しました。
image.png
トーク画面を見ると、文章が長くて途切れてしまいました。
image.png
要約文章をTopicごとに分割しました。
image.png
1回あたりのコストを出力しました。
image.png
メールを受信しなかった日は以下のように通知することにしました。
image.png
日によって異なりますが、月あたりの概算をしてみました。
月1ドルくらいで日々の情報収集が捗るのであれば、安いでしょう!

0.035/日 × 30日 = $1.05/月

定期実行
朝8時にスマホを見てみると、要約文が届いていることを確認しました。
image.png

7. まとめ

chatGPTを用いて何か実装したい、というところから始まりました。身近なメールを題材に、効率化するための仕組みを作れました。より効率化する場合に、気になった記事のURLからテキスト情報を取得し、その内容を要約することもできそうだと感じました。
chatGPTが起点でしたが、Lambda関数のLayer作成や・Dockerを用いたモジュール生成など、新しい知識を得られました。
プロンプト作成に関して、今回は期待する結果がそれほど苦労せず得られました。もう少し他のテーマでもやってみようと思います。
今後もchatGPTにお世話になる機会はあるので、少しずつ理解を深めていきたいと思います。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?