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

私自身の日常を効率的にする目的で、chatGPTを用いた仕組みを作ってみました。
TLDRという英語のTechBlogがあります。最新情報が得られますが、一から全て読むのがちょっと大変です。（週5件ほどメールが届く）
**chatGPTが作成した要約文を確認し、気になるものはメール本文やURLを確認する、といった形で情報収集することにしました。**

##### 取り組むきっかけ
arxiv.org（機会学習関連の論文が掲載されたサイト）のAPIで取得した論文情報に対して、chatGPTでサマリーを生成し、SlackBotで通知を行う、といった内容でした。この記事を読んで、今回のGmail APIとchat GPTの構成を実現してみることにしました。

https://zenn.dev/ozushi/articles/ebe3f47bf50a86

# 0.読んでいただく前に
プロンプト作成自体は、chatGPTに関する専門的な知識は要りません。
以下の経験があると、スムーズに進められると思います。
- AWSのLambdaの利用：テスト実行とログ確認を行ったことがある
- AWSのLayerの作成：Layerを作成し、Lambda関数に対して紐づける
- AWS CDKの利用：デプロイまで実施したことある（言語はTypescript以外でもOKです！）

ソースコードです。適宜ご利用ください。

https://github.com/eno-conan/gmail-summary-stack

:::note warn
**API KEYの扱いについて**
API KEYの値をgitHubに公開しないように注意してください。
コードに直接記載しない、環境変数で読み込む場合も`.env`ファイルを`.gitignore`に設定する、といった対策とってください。
:::

# 1. 構成
最終的な構成は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/5c09bb02-5634-b3e5-8746-55677585c48c.png)

# 2. Gmail API設定とメール内容取得の実装

##### 2.1 プロジェクト作成
以下参考に進めました。
「プロジェクト名」は任意の値を設定します。「場所」は個人で進める場合、「組織なし」のままで「作成」をクリックします。

https://www.y-hakopro.com/entry/google_oauth_api

##### 2.2 Gmail APIの有効化とOAuthクライアントの作成
以下を参考に進めました。最後にダウンロードする`credentials.json`は、今回の作業で使用します。

https://developers.google.com/gmail/api/quickstart/python?hl=ja

##### 2.3 サンプルコード動作確認
認証情報を用いて、Gmailのラベル一覧を取得するサンプルコードです。Gmailの本文情報取得のベースです。以下を参考に進めました。細かい説明は省きますが、初回実行時に、ブラウザで認証に関する確認作業があるので、対応してください。

https://developers.google.com/gmail/api/quickstart/python?hl=ja

##### 2.4 本文抽出のためのコード修正
以下の条件を固定して実装しました。
- 送信元がTLDR
- 前日に受信したメール
- 未開封のメール

<details><summary>Gmail APIを用いたメール本文取得の実装</summary>

```python
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()
```
</details>

# 3. chatGPTで要約依頼のプロンプト実装
##### 3.1 chatGPT
資料はたくさんあります。面白いと感じたスライドをいくつか取り上げました。
- [生成AI周回遅れキャッチアップ勉強会！(約50ページ)](https://speakerdeck.com/minorun365/sheng-cheng-aizhou-hui-chi-rekiyatutiatuhumian-qiang-hui)
  - 生成LLMという仕組みを利用して、chatGPTを機能させています、といった話です。 
- [ChatGPTによって描かれる未来とAI開発の変遷(約100ページ)](https://speakerdeck.com/hirosatogamo/0421dsxie-hui-chatgptniyotutemiao-kareruwei-lai-toaikai-fa-nobian-qian?slide=6)
    - GPTとは何か・GPTを用いた開発・これからの機械学習開発、といった内容が記載されています。
- [New Era at Computing - ChatGPT がもたらした新時代(約150ページ）](https://speakerdeck.com/dahatake/new-era-of-computing-chatgpt-gamotarasitaxin-shi-dai-3836814a-133a-4879-91e4-1c036b194718)
    - chatGPTの説明、またはBingChatというMicrosoft発の対話型AIチャットツールも組み合わせて、より実践的な開発、といった内容です。 
##### 3.2 API KEYの取得
実装前に、OPENAIのサイトでAPI KEYを取得します。以下を参考に進めました。

https://auto-worker.com/blog/?p=6988

プロンプト作成部分の実装です。（環境変数：`OPEN_API_KEY`の値は、`.env`ファイルから取得）
```python
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')
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/def0a420-7ed2-d8f5-bd9d-26643c3712d1.png)

</details>

最初、以下のプロンプトを作成してみました。
```python
system = """与えられたテキストについて、項目ごと（ARTICLES & TUTORIALS、OPINIONS & ADVICE、LAUNCHES & TOOLS）に100文字程度で要点して、
以下のフォーマットで日本に訳して出力してください。URLがある場合は、出力してください```
・ARTICLES & TUTORIALSの内容
    ・まとめた内容
    ・URL一覧
・OPINIONS & ADVICEの内容
    ・まとめた内容
    ・URL一覧
・LAUNCHES & TOOLSの内容
    ・まとめた内容
    ・URL一覧
```"""
```
<details><summary>出力結果（URLがない・・・）</summary>

```
・ARTICLES & TUTORIALS
　・APIやネットワークへのアクセス回数を制限するレート制限についての解説
　・Postgresデータベース内で地図を生成する方法の紹介
　・アセンブリ言語でGUIを作成する方法の解説

・OPINIONS & ADVICE
　・Zig言語の学習が難しい理由とその価値についての考察
　・プロジェクトの見積もりが甘くなる理由と改善策の提案
　・AIが経営者の役割を置き換える可能性についての議論

（以下略）
```
</details>

一発では望んだ結果にならないので、プロンプトを修正し、期待する出力形式を目指します。**プロンプトを工夫することで、時間がかからずに、期待結果が得られました。（1時間未満で完了）**
```python
system = """与えられたテキストについて、各項目（'ARTICLES & TUTORIALS','OPINIONS & ADVICE','LAUNCHES & TOOLS'）、
それぞれについて、100文字程度で要点して、以下のフォーマットで日本に訳して出力してください。
各項目で取得できるURL情報も合わせて返してください。
・ARTICLES & TUTORIALSの内容
    ・まとめた内容[URL]
・OPINIONS & ADVICEの内容
    ・まとめた内容[URL]
・LAUNCHES & TOOLSの内容
    ・まとめた内容[URL]
```"""
```
<details><summary>出力結果（いい感じ！！）</summary>

```
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]

（以下略）
```
</details>

# 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関数のコンソールからは設定可能](https://dev.classmethod.jp/articles/aws_lambda_support_python310/)となりました。

##### 4.1 スタックの実装内容
以下を参考に進めました。

https://zenn.dev/yosemat/articles/01c83132a0c705

フォルダ構成は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/8cba4e2a-2d7f-471c-faf4-4ea6475181ad.png)

<details><summary>LambdaとEventBridgeを定義</summary>

```typescript: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));
  }
}
```
</details>


##### 4.2 外部モジュールの用意
この作業はUbuntu上で行いました。
Lambda関数上で、Gmailの本文取得に必要なライブラリをimportできるようにします。仮想環境を構築して、必要なライブラリのモジュールを用意します。
仮想環境構築については、以下を参考にしました。

https://camp.trainocate.co.jp/magazine/venv-python/

`tutorial-env`フォルダは仮想環境関連なので、自分で用意したのは`requirements.txt`のみです。
```
.
├── tutorial-env
    ├── bin
    ├── include
    ├── lib
    └── lib64
└── requirements.txt

```
ライブラリを定義します。
```txt:requirements.txt
python-dotenv
google_auth_oauthlib
google-api-python-client
google-api-core
openai
```
以下記事を参考に、Dockerを使って必要なモジュールを揃えました。

https://nisshingeppo.com/ai/aws-lambda-library-install/

`public.ecr.aws/sam/build-python3.9`イメージを使用し、`requirements.txt`に記述したライブラリをインストールして、`python/lib/python3.9/site-packages`フォルダに各モジュールを配置します。

```sh
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/e94aaa22-a2bc-1419-4d67-5d2d47ddf5d6.png)

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

##### 4.4 動作確認
テストイベントを設定して実行すると、chatgptの出力結果がLambdaのコンソールに出力されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/12719bef-3313-4664-aab2-bb3d8d3d32b3.png)

# 5. LINE通知
スマートフォンで確認しやすいのでLINE Botにしました。以下記事を参考にしています。

https://qiita.com/grapefruit1030/items/727bedb2867d90c7d1a6

chatGPTによる要約文をもとに、LINEへの通知を行います。
<details><summary>実装内容はこちら</summary>

```python
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
```
</details>

`.env`に、LINEのアクセストークン情報を追加します。
```diff_awk:.env
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+ LINE_ACCESS_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

# 6. 最終動作確認
**即時実行**
ERRORなく、最後まで処理が完了しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/33adda2b-1e19-257e-1bd3-d9b93888e641.png)
トーク画面を見ると、文章が長くて途切れてしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/4d197518-33cf-cf76-1e1f-13c3f9bbbbda.png)
要約文章をTopicごとに分割しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/65f43279-d57f-4105-9537-cb9365b4aee3.png)
1回あたりのコストを出力しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/88f7e5f0-8f7f-2117-d80d-d485f977fa03.png)
メールを受信しなかった日は以下のように通知することにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/68cf051a-251b-d835-bc6c-322a4d4b030d.png)
日によって異なりますが、月あたりの概算をしてみました。
**月1ドルくらいで日々の情報収集が捗るのであれば、安いでしょう！**
> 0.035/日 × 30日 = $1.05/月

**定期実行**
朝8時にスマホを見てみると、要約文が届いていることを確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/a5531ba2-9025-a43a-8e17-d41fc7f55007.png)
    
# 7. まとめ
chatGPTを用いて何か実装したい、というところから始まりました。身近なメールを題材に、効率化するための仕組みを作れました。より効率化する場合に、気になった記事のURLからテキスト情報を取得し、その内容を要約することもできそうだと感じました。
chatGPTが起点でしたが、Lambda関数のLayer作成や・Dockerを用いたモジュール生成など、新しい知識を得られました。
プロンプト作成に関して、今回は期待する結果がそれほど苦労せず得られました。もう少し他のテーマでもやってみようと思います。
今後もchatGPTにお世話になる機会はあるので、少しずつ理解を深めていきたいと思います。