geminiを使用して返信が必要なメールを判定する

# はじめに
本記事では生成系AIでgoogle社が提供しているgeminiを利用して、送信が必要なメールのみを抽出する方法を検討します。近年生成系AIの台頭が凄まじく、技術のキャッチアップを目的としてコードを書いてその方法を共有することが目的となります。
# コード
開発環境はjupyter notebookで行います。
### configの設定
まずはgmail, geminiで使用するconfigの設定を行います。
```python
# configの設定
import os
# gmailの設定例(SMTP, SMTPポート等を正しく設定すればgmail以外でも実行可能)
IMAP = "imap.gmail.com"
USER = "XXXX@gmail.com"
PASSWORD = "XXXX"
SMTP = "smtp.gmail.com"
SMTP_PORT = 587
FROM_MAIL_ADDRESS = "XXXX@gmail.com"
TO_MAIL_ADDRESS = "XXXX@gmail.com"

# gemini apiの設定
GOOGLE_API_KEY = "XXXX"
def set_google_api_key():
    os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY
```
gmailでの環境を想定しているため、SMTPやSMTP_PORT等はgmailで使用するもので記入しています。
gmail以外でも正しく設定すればメールの送受信を行うことができます。
XXXXに関しては各自の設定を記入してください。

### モジュールのインストール
必要なモジュールのインストールを行います。
```python
! pip install email
! pip install langchain_google_genai 
! pip install langchain
```
### メールの受信
pythonからメールを受信し未閲覧のみを表示するコードを作成します。
```python
import imaplib
import email
from email.header import decode_header

def get_emails(imap_server, imap_user, imap_password, only_unread=False):
    try:
        mail = imaplib.IMAP4_SSL(imap_server)
        mail.debug = 0
        mail.login(imap_user, imap_password)
        mail.select("INBOX")

        search_criteria = 'UNSEEN' if only_unread else 'ALL'
        _, data = mail.search(None, search_criteria)

        if data[0]:
            for num in data[0].split():
                _, data = mail.fetch(num, '(RFC822)')
                msg = email.message_from_bytes(data[0][1])

                subject = decode_header(msg["Subject"])[0][0]
                if isinstance(subject, bytes):
                    try:
                        subject = subject.decode()
                    except UnicodeDecodeError:
                        subject = subject.decode('latin-1')
                print("件名:", subject)

                sender = decode_header(msg.get("From"))[0][0]
                if isinstance(sender, bytes):
                    try:
                        sender = sender.decode()
                    except UnicodeDecodeError:
                        sender = sender.decode('latin-1')
                print("送信者:", sender)
                print("-" * 20)

                if msg.is_multipart():
                    for part in msg.walk():
                        if part.get_content_type() == "text/plain":
                            body = part.get_payload(decode=True).decode()
                            print("本文:", body)
                            break
                else:
                    body = msg.get_payload(decode=True).decode()
                    print("本文:", body)
                print("-" * 20)
                mail.store(num, '-FLAGS', '\\Seen')
        else:
            print("メールボックスにメールがありません。")

        mail.close()
        mail.logout()

    except imaplib.IMAP4.error as e:
        print(f"IMAPエラー: {e}")
        print(dir(e))
        print(e.args)
    except Exception as e:
        print(f"その他のエラー: {e}")
        print(dir(e))
        print(e.args)

# メールの取得する動作確認
get_emails(IMAP, USER, PASSWORD, only_unread=True)

```
get_emails関数でメールを取得し表示します。only_unreadは未読メールのみを取得するboolで、Trueに設定することで未読のみをフィルターします。(Falseの場合は既読含めすべてのメールを表示します)
メールのタイトルと本文が取得できることが確認できます。これらの情報をgeminiに渡すことでタイトルと本文から返信が必要か判定するようにします。

### fewshot prompt
geminiに渡すpromptに返信が必要な場合のメールと不要な場合のメール例を渡すことで精度の向上を検討したいと思います。
メールの返信が必要か不要かを判定する処理をgeminiに渡して判定するようにしますが、今後のプログラムの拡張性を考慮し、返信が必要な場合にTrue、返信が不要な場合にFalseを返すようにしたいです。fewshotプロンプトの回答例を指定することでTrueもしくはFalseで返答するように形式を指定します。
```python
# fewshot promptで使用する質問と回答の例
no_reply_json1 = {"title": "【広報】新製品「〇〇」発表のお知らせ", "label": False,  "body": """
報道関係各位

平素は格別のご高配を賜り、厚く御礼申し上げます。

本日、弊社は新製品「〇〇」を発表いたしました。〇〇は〇〇という特徴を持ち、〇〇の分野において革新的な製品となると確信しております。

製品の詳細情報は、下記URLにて公開しておりますので、ご参照ください。
[製品情報URL]

本件に関するお問い合わせは、下記までお願いいたします。
[問い合わせ先メールアドレス]

なお、本メールへのご返信は不要です。

今後とも弊社製品にご注目いただきますよう、お願い申し上げます。
"""}

no_reply_json2 = {"title": "ウェブサイト リニューアルのお知らせ", "label": False,  "body": """
関係各位

いつも弊社ウェブサイトをご利用いただき、誠にありがとうございます。

この度、弊社ウェブサイトを全面リニューアルいたしましたことをお知らせいたします。今回のリニューアルでは、デザインの一新に加え、情報へのアクセス性向上、スマートフォンへの最適化などを実施いたしました。

新しいウェブサイトはこちらからご覧いただけます。
[新ウェブサイトURL]

今後とも弊社ウェブサイトをご愛顧賜りますようお願い申し上げます。
"""}

no_reply_json3 = {"title": "〇〇財団への寄付実施のご報告", "label": False,  "body": """
関係各位

平素は格別のご高配を賜り、厚く御礼申し上げます。

弊社は、社会貢献活動の一環として、〇〇財団へ〇〇円の寄付を実施いたしましたことをご報告申し上げます。この寄付金は、〇〇の活動に役立てられます。

詳細については、下記URLをご覧ください。
[詳細URL]

今後ともご理解ご協力のほど、よろしくお願い申し上げます。
"""}

reply_json1 = {"title": "製品〇〇の仕様について", "label": True,  "body": """
〇〇株式会社 営業部 〇〇様

いつもお世話になっております。△△株式会社の□□です。

貴社製品「〇〇」について、以下の点についてお伺いしたくご連絡いたしました。

〇〇の最大処理能力はどの程度でしょうか。
〇〇の対応OSを教えてください。
〇〇の導入事例をご紹介いただけますでしょうか。
お忙しいところ恐縮ですが、〇月〇日（〇）までにご回答いただけますと幸いです。

お手数をおかけいたしますが、よろしくお願い申し上げます。
"""}

reply_json2 = {"title": "【日程調整のお願い】〇〇プロジェクト 定例会議", "label": True,  "body": """
〇〇株式会社 プロジェクトメンバー各位

いつもお世話になっております。△△株式会社の□□です。

〇〇プロジェクトの定例会議について、下記日程で調整させていただきたくご連絡いたしました。

下記日程でご都合の良い日時をお知らせください。

〇月〇日（〇）午前10時～12時
〇月〇日（〇）午後1時～3時
〇月〇日（〇）午前10時～12時
上記以外の日程をご希望の場合は、ご遠慮なくお申し付けください。

お忙しいところ恐縮ですが、〇月〇日（〇）までにご返信いただけますと幸いです。

よろしくお願いいたします。
"""}

reply_json3 = {"title": "〇〇プロジェクトへの参加について", "label": True,  "body": """
〇〇株式会社 〇〇様

いつもお世話になっております。△△株式会社の□□です。

〇〇プロジェクトへのご参加を以前お願いしておりましたが、改めてご承諾いただきたくご連絡いたしました。

本プロジェクトは〇〇を目的としており、〇〇様のお力添えをいただけると大変助かります。

ご多忙中恐縮ですが、ご承諾いただけるかどうか、〇月〇日（〇）までにご返信いただけますと幸いです。

よろしくお願いいたします。
"""}

FEW_SHOW_PROMPTS = [
    no_reply_json1,
    no_reply_json2,
    no_reply_json3,
    reply_json1,
    reply_json2,
    reply_json3
]
```
上記のfewshowプロンプトの例はchatGPTで作成した返信が必要な場合と不要な場合の例をそれぞれ3つ作成しました。titleはメールのタイトル、bodyはメールの本文、labelは返信が必要か不要かのboolでモデルが回答する例になります。

### geminiでの判定
それではgeminiを使用してメールの返信が必要かを判定するプログラムを作成します。

```python
import os
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.output_parsers import StrOutputParser
from langchain.schema import HumanMessage, SystemMessage
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
# APIの保存
if "GOOGLE_API_KEY" not in os.environ:
    set_google_api_key()

examples = FEW_SHOW_PROMPTS

def is_mail_reply(title:str, body:str) -> bool:
    # LLMテンプレート
    prompt_template = """
    以下の件名と本文から、返信が必要かの有無をtrueかfalseで判定してください。
    必ず指定した形式を守ってください。

    件名:
    {title}

    本文: 
    {body}
    """
    example_prompt = PromptTemplate(
        input_variables=["title", "body"],
        template=prompt_template,
    )
    # few-shotプロンプトの作成
    few_shot_prompt = FewShotPromptTemplate(
        examples=examples,
        example_prompt=example_prompt,
        suffix="タイトル: {title}\n本文: {body}",
        input_variables=["title", "body"],
    )
    llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash-latest", temperature=0)
    output_parser = StrOutputParser()

    # Chain
    chain = few_shot_prompt | llm | output_parser
    # 実行
    response = chain.invoke({"title": title, "body": body})
    return response.splitlines()[-1]

print("返信が不要の場合の動作確認")
noreply_response = is_mail_reply(title = "【リマインダー】チームミーティングのお知らせ", body = "皆様\n来週の月曜日、1月1日午前10時からチームミーティングを通常通り行います。オンラインでの開催となりますので、ご確認のほど、よろしくお願いいたします。")
print(noreply_response)
print("返信が必要の場合の動作確認")
reply_response = is_mail_reply(title = "成果物の納品について", body = "お世話になっております。〇〇です。ご依頼いただいておりますソースコードについて相談したいことがありましてご連絡いたしました。google meetで会議をお願いしたく、こちらの候補日の中からご都合の良い日を教えていただけますでしょうか。2025/1/1, 1/2. 1/3 以上よろしくお願いいたします。")
print(reply_response)

>>>
返信が不要の場合の動作確認
false
返信が必要の場合の動作確認
true
```
is_mail_replyを呼び出しメールのタイトルと本文を送信し、返信が不要な場合と必要な場合の例を作成しました。簡単な例での動作確認となっていますが、正しく判定できました。

geminiで判定する関数に実際のメールを送るようにソースコードを修正すれば、メールの内容に対して返信の要不要を判定するコードを作成できますが、その際にgeminiにapiでメールのタイトルと本文を送付することになり、メールに含まれる機密情報を送ってしまう可能性があるためソースコードの記載は控えようと思います。

# 感想
geminiを使用してメールの返信が要不要を判定するようにプログラムを作成できることはある程度確認できましたが、geminiにapiでメールの内容を送るため機密情報に留意する必要があり、業務で使用しているメールでの活用は難しいと思いました。コードの動作確認を行っていただく場合は、普段使用していないgmailなどでご確認いただければ幸いです。
実際のアプリケーションに組み込む場合を想定すると、クラウドサービスなどを利用して閉域網でgeminiをapiでホストするサーバを用意する方法などが考えられるかと思います。

次のステップとして、メールの返信が必要な場合にメール内容を提案し、確認後問題なければメールを送信する機能をlanggraphで作成したいと思います。