生成AI(GEMINI)を使用して、メールの要約

やりたいこと.

  • ゆるい規則性をもった文章を生成AI(GEMINI)で処理

  • サマリーをHTMLに記載.

コード

gemini__summary.py

import os, sys, re, glob, json5, tqdm
import extract_msg
import datetime
import google.generativeai as genai

# ========================================================= #
# ===  gemini__summary                                  === #
# ========================================================= #
def gemini__summary( path="msg/*.msg", outFile="dat/output.json", \
                     html_config="dat/html_config.json" ):

    # ------------------------------------------------- #
    # --- [1] search filename                       --- #
    # ------------------------------------------------- #
    filenames = glob.glob( path.lower() )
    stack     = {}

    # ------------------------------------------------- #
    # --- [2] summarize content                     --- #
    # ------------------------------------------------- #
    for ik, filename in enumerate( tqdm.tqdm( filenames, desc="Processing files" ) ):
        IDno     = "{:06}".format(ik+1)
        msg      = extract_msg.Message( filename )
        date     = ( msg.date ).strftime( '%Y/%m/%d' )
        subject  = msg.subject
        body     = msg.body
        prompt   = f"以下の転送メールについて、メール冒頭の「転送者の私見」とそれに続く「記事」を分けてください.私見は、[opinion]、記事は[content]のタグをつけて抜き出してください.私見は、冒頭の宛名や末尾の送信者の名前や署名、「お世話になります」などの挨拶が含まれる場合は取り除いて内容のみにしてください.また、記事は日本語3行で要約し、[summary]のタグをつけてください.3行の要約は、 '* ' から開始し、1行毎に改行してください.また、記事に放射性核種(At-211やAc225, 226Ra、単にCupperや銅、など)が言及される場合、放射性核種を[nuclide]のタグをつけて抜き出し・列挙してください.また、記事に会社名が含まれる場合、会社名を[company]のタグをつけて抜き出し・列挙してください.ただし、日立ハイテクや日立は除外してください.タグの記載順は、[opinion], [content], [summary], [nuclide], [company] の順としてください.:\n\n{body}"
        response = model.generate_content( prompt )
        texts    = response.text
        pattern1 = "\[opinion\](.*)\[content\]"
        pattern2 = "\[content\](.*)\[summary\]"
        pattern3 = "\[summary\](.*)\[nuclide\]"
        pattern4 = "\[nuclide\](.*)\[company\]"
        pattern5 = "\[company\](.*)"
        opinion  = ( ( re.search( pattern1, texts, flags=re.DOTALL ) ).group(1) ).strip()
        content  = ( ( re.search( pattern2, texts, flags=re.DOTALL ) ).group(1) ).strip()
        summary  = ( ( re.search( pattern3, texts, flags=re.DOTALL ) ).group(1) ).strip()
        nuclide  = ( ( re.search( pattern4, texts, flags=re.DOTALL ) ).group(1) ).strip()
        company  = ( ( re.search( pattern5, texts, flags=re.DOTALL ) ).group(1) ).strip()
        info     = { "No.":IDno, "filename":filename, "date":date, "subject":subject, \
                     "opinion":opinion, "content":content, "summary":summary, \
                     "nuclide":nuclide, "company":company }
        stack[ IDno ] = info

    # ------------------------------------------------- #
    # --- [3] sort and renumber                     --- #
    # ------------------------------------------------- #
    sorted_items = sorted( stack.items(), key=lambda x: datetime.datetime.strptime( x[1]['date'], '%Y/%m/%d') )
    stack_       = {}
    for ik, ( _, item ) in enumerate( sorted_items, 1 ):
        IDno            = "{:06}".format( ik )
        item["No."]     = IDno
        stack_[ IDno  ] = item
    stack = stack_
        
    # ------------------------------------------------- #
    # --- [4] save as json file                     --- #
    # ------------------------------------------------- #
    with open( outFile, "w" ) as f:
        json5.dump( stack, f, ensure_ascii=False )
    return( stack )
        
        
# ========================================================= #
# ===   Execution of Pragram                            === #
# ========================================================= #

if ( __name__=="__main__" ):

    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument( "--msgPath"      , help="path to the .msg ( msg/*.msg etc. )", \
                         default="msg/*.msg" )                ## optional
    parser.add_argument( "--outFile"      , help="output file name.", \
                         default="dat/output.json" )          ## optional
    parser.add_argument( "--html_config"  , help="output file name.", \
                         default="dat/output.json" )          ## optional
    parser.add_argument( "-n", "--no_gemini", help="no gemini api",\
                         default=False, action="store_true" ) ## flag
    args   = parser.parse_args()
    no_gemini = args.no_gemini
    msgPath   = args.msgPath
    outFile   = args.outFile
    
    # ------------------------------------------------- #
    # --- [1] gemini configuration                  --- #
    # ------------------------------------------------- #
    # Gemini APIの設定
    # export GEMINI_API_KEY="api key"
    my_api_key = os.environ.get( "GEMINI_API_KEY" )
    genai.configure( api_key=my_api_key )
    model      = genai.GenerativeModel()

    # ------------------------------------------------- #
    # --- [2] gemini summarizing                    --- #
    # ------------------------------------------------- #
    if ( not( no_gemini ) ):
        gemini__summary( msgPath, outFile=outFile )
    
    # ------------------------------------------------- #
    # --- [3] summarize as a html page              --- #
    # ------------------------------------------------- #
    import nkUtilities.generate__html as htm
    htm.generate__html( html_config=html_config, silent=True )

html_config.json

{

    "settings":{
	
	"title"      : "TAT news Library", 
	"cssFile"    : "dat/style.css",
	"paramsFile" : "dat/output.json",
	"htmlFile"   : "html/output.html",
	
    },

    "images": {

    },
    
}

style.css

/* 基本のページスタイル */
body {
    font-family: Arial, sans-serif;
    font-size: 20px; 
    margin: 20px;
}

h1 {
    color: #333;
    text-align: center;
}

/* ブール値のスタイル */
.bool-true, .bool-false {
    font-weight: bold;
}

.bool-true {
    color: green;
}

.bool-false {
    color: red;
}

/* 文字列のスタイル */
.string {
}

/* 文字列のスタイル */
.string.empty {
    font-style: italic;
    color: #999;
    display: block;
}

/* 数値のスタイル */
.int, .float {
    font-weight: bold;
}

.number {
    color: blue;
}

/* 配列のスタイル */
.table.array {
    border-collapse: collapse;
    width: 100%;
    font-size: 12px; 
}

.array-value {
    border: 1px solid #ddd;
    padding: 5px;
    text-align: center;
    font-size: 12px; 
}

/* オブジェクトのスタイル */
.table.object {
    border-collapse: collapse;
    width: 100%;
    font-size: 12px; 
}

.object-key {
    border: 1px solid #ddd;
    padding: 5px;
    font-weight: bold;
    text-align: right;
    background-color: #f0f0f0;
    font-size: 12px; 
}

.object-value {
    border: 1px solid #ddd;
    padding: 5px;
    text-align: left;
    font-size: 12px; 
}

/* 関数のスタイル */
.function {
    font-style: italic;
    color: #9900cc;
}

/* 未知のデータ型のスタイル */
.unknown {
    color: #ff0000;
}



/* 表のデザイン */

.table_design {
    border-collapse: collapse;
    width: 100%;
    max-width: 1500px;
    font-size: 12px; 
}
.table_design th, .table_design03 td {
    border-bottom: 2px solid #c1c7c6;
    padding: 1em;
}
.table_design th {
    border-bottom: 2px solid #4d9bc1;;
    font-weight: bold;
    text-align: left;
    width: 20%;
    min-width: 4em;
}