概要

2024年6月にAWSによって公開された生成AIでAWSアップデートを効率的にキャッチアップ!の記事内で、What’s New Summary Notifierという、AWSのニュース記事を要約してSlackに通知してくれる生成 AI アプリケーションのサンプル実装が公開されております。
そこで、本記事では弊社が運営するオウンドメディア「iret.media」の公開記事のレビューにこの仕組みを転用できないか、を検証してみました。

What’s New Summary Notifierの中身を見てみる

前段として、What’s New Summary Notifierの中身がどうなっているかを確認してみます。
インフラ構築には、AWS CDKが採用されています。
生成AIでAWSアップデートを効率的にキャッチアップ!から引用)

全体像はこんな感じです。

①Lambda関数(RSSNewsCrawler)が1時間おきに起動し、1時間ごとにWebサイトのRSSを取得(EventBridge)
②最新の記事(1週間以内に公開された記事)が、DynamoDBに登録される

下記のような形で、公開記事のurlをパーティションキーとして、notifier_name(RSS配信元のサイト)・ category(公開記事の種類)・pubtime(記事の公開時刻)・title(記事のタイトル)が登録されています。

notifier_nameについて
ここでは深くは触れませんが、複数のRSS配信サイトをソースとして通知をしたい場合に、配信元を識別するのに有効です。

③DynamoDBへのレコード登録をトリガーに、ニュースを取得して要約するLambda関数(NotifyNewEntry)が起動、Slack(またはTeams)に通知される。

検証スタート

今回の検証では、生成AIに記事の要約ではなく、記事の内容チェックをさせようということになっただけなので、生成AIに与えるプロンプト(指示)を変えるだけで、簡単にカスタマイズができそうです。

Slack通知用のWebhookの設定

Slackのワークフロービルダーにて、ワークフローを作成します。

以下の4つの変数を作成し、
ウェブリクエストのURLを控えておきます。
控えたURLは、こちらの手順を参考にAWS Systems Manager Parameter Storeに保存します。

続いて、ワークフロービルダー画面の右側にあるステップ>メッセージ から、チャンネルへメッセージを送信するを選択し、ステップを追加します。ステップ2の、スレッドでメッセージに返信するも同様の手順で追加します。
追加する際、それぞれ変数は下記のように挿入してください。

設定値の変更

What’s New Summary Notifierのリポジトリをcloneし、設定値の追加・修正を行います。

cdk.json

"context"キー配下の設定値を、一部変更します。
デフォルトでは、"modelId""modelId": "anthropic.claude-3-sonnet-20240229-v1:0"
となっており、Claude3 Sonnetが指定されています。今回は、より高い性能を持つと言われているClaude3.5 SonnetのmodelIdを指定してみます。(Claude3.5の性能に関する詳細はこちらを参照)
※Claude3.5利用にあたっては、モデルアクセスを行い、モデルが利用可能になっていることが前提です。

また、

"AwsSolutionsArchitectJapanese": {
    "outputLanguage": "Japanese. Each sentence must be output in polite and formal desu/masu style",
    "persona": "solutions architect in AWS"
},

の記載の下に、下記を追加します。

"IretEmployeeJapanese": {
    "outputLanguage": "日本語。各文は丁寧で正式なです/ます調で出力してください。",
    "persona": "アイレット株式会社(iret, Inc.)の社員"
}

なお、ここで指定する "outputLanguage"はレビューを出力させる言語、"persona"は回答させるAIモデルに指定するペルソナです。
サンプルの実装では、英語で記載がされていましたが、今回の検証では日本語で指定をしてみました。
最終的に"context"キー以下の設定値はこのようになっています。

    "context": {
        "modelRegion": "us-east-1",
        "modelId": "anthropic.claude-3-5-sonnet-20240620-v1:0",
        "summarizers": {
            "AwsSolutionsArchitectEnglish": {
                "outputLanguage": "English.",
                "persona": "solutions architect in AWS"
            },
            "AwsSolutionsArchitectJapanese": {
                "outputLanguage": "Japanese. Each sentence must be output in polite and formal desu/masu style",
                "persona": "solutions architect in AWS"
            },
            "IretEmployeeJapanese": {
                "outputLanguage": "日本語。各文は丁寧で正式なです/ます調で出力してください。",
                "persona": "アイレット株式会社(iret, Inc.)の社員"
            }
        },

さらに、
"notifiers"キー以下の部分を、下記に変更します。
"summarizerName"の値を先ほど追加したIretEmployeeJapaneseにし、(名前はなんでも可)、
"rssUrl"の部分に、弊社のRSSを指定します。

"notifiers": {
    "IretMediaBlogs": {
        "destination": "slack",
        "summarizerName": "IretEmployeeJapanese",
        "webhookUrlParameterName": "/WhatsNew/URL",
                "rssUrl": {
                    "IretMedia": "https://iret.media/feed/"
        }
    }
},

index.py(whats-new-summary/notify-to-app/index.py)

summarize_blog関数内の、
変数prompt_dataの中身 を、以下のように書き換えます。

prompt_data = f"""
<input>{blog_body}</input>
<persona>あなたはプロフェッショナルな{persona}です。</persona>
<instruction>以下の観点について、<input></input>タグ内の記事をレビューしてください:
- 文法的なエラー、タイポ、および明確さ。
- 適切な見出しやサブ見出しの有無。
- 必要に応じて、図表やスクリーンショットが使用されているか。
- 機能しないコードの回避。ライブラリのバージョンや実行環境の詳細が指定されているか。
- 記事のタイトルと内容の一貫性。
- サービスや製品名が正式名称で記載されているか。
- クレデンシャルが公開されていないか。
- 極端に短いセクションや内容が乏しい部分がないか。
- ライセンス要件と著作権への配慮:
    - すべての第三者コンテンツ(画像やコードスニペットなど)がライセンスに従って使用されているか確認する。
    - 必要に応じて、適切な帰属が提供されているか確認する。
    - 記事が著作権や知的財産権を侵害していないか確認する。

修正点と提案を箇条書きで提供してください。重大な問題が見つからない場合は、記事がよく書かれていることを示すフィードバックを提供するか、さらに改善が可能な部分を提案してください。すべてのフィードバックが明確で実行可能であることを確認してください。</instruction>
<outputLanguage>{language}</outputLanguage>
<summaryRule>記事で特定された主なエラーの種類について要約してください。出力形式は<outputFormat></outputFormat>タグで定義されています。</summaryRule>
<outputFormat><thinking>(エラーや提案の箇条書き、または問題が見つからない場合のフィードバック)</thinking><summary>(最終的な要約)</summary></outputFormat>
"""

デプロイ

※デプロイ前に下記コマンドを叩きます。

npm ci
cdk bootstrap(デプロイを実施するリージョンでbootstrap済みであれば実施不要

cdk deployコマンドを叩いてデプロイします。成功すれば下記のような表示が出るかと思います。

maeno1:whats-new-summary-notifier maeno$ cdk deploy --profile XXXXX
・・・・・・(長いので割愛)
・・・・・・
・・・・・・
↓
↓
成功した場合

 ✅  WhatsNewSummaryNotifierStack

✨  Deployment time: 33.86s

Stack ARN:
arn:aws:cloudformation:<region>:<acount id>:stack/WhatsNewSummaryNotifierStack/XXXXXXXXX

✨  Total time: 51.96s

検証結果

Slackに、iret.mediaの投稿レビューがされていることを確認できました🙌
プロンプトで指定した観点に沿って、記事をレビューすることができているようです。
必要に応じて、プロンプト内の、タグ内で指定しているレビュー観点を調整すれば、AIによるFBの内容は調整できそうですね。


記事レビューの内容

番外編:他にもできそうなこと

まだ試すことはできていないですが、下記のようなこともできそうです。

RSS取得間隔の調整

デフォルトでは、1時間に1回実行されるようになっていますが(マネジメントコンソールの画像参照)、
EventBridgeの設定をいじる事で、RSSの取得間隔を変更することができるようです。

//whats-new-summary-notifier-stack.ts(一部抜粋)
//ここでcron式に指定する値を設定

    for (const notifierName in notifiers) {
      const notifier = notifiers[notifierName];
      // const cron is a cronOption defined in a notifier. if it is not defined, set default schedule (every hour)
      const schedule: CronOptions = notifier['schedule'] || {
        minute: '0',
        hour: '*',
        day: '*',
        month: '*',
        year: '*',
      };

レビュー対象記事の範囲の調節

デフォルトでは、過去7日以内に公開された記事がDynamoDBに書き込まれるようになっています。

具体的には、whats-new-summary/rss-crawler/index.pyの、recently_published関数で実装されています。

# whats-new-summary/rss-crawler/index.py
def recently_published(pubdate):
    """Check if the publication date is recent

    Args:
        pubdate (str): The publication date and time
    """

    elapsed_time = datetime.datetime.now() - str2datetime(pubdate)
    print(elapsed_time)
    if elapsed_time.days > 7:
        return False

    return True

if文における日数の指定を任意の数字に変えることで、書き込まれる対象の記事(通知対象の記事)
の範囲を広げたり、狭めたりすることができます。

# whats-new-summary/rss-crawler/index.py
    if elapsed_time.days > 7: #この数字を変える
        return False

以下は、RSSを取得し、DynamoDBにデータを登録する関数の全体のコードです。

# whats-new-summary/rss-crawler/index.py
def handler(event, context):

    notifier_name, notifier = event.values()

    rss_urls = notifier["rssUrl"]
    for rss_name, rss_url in rss_urls.items():
        rss_result = feedparser.parse(rss_url)
        print(json.dumps(rss_result))
        print("RSS updated " + rss_result["feed"]["updated"])
        if not recently_published(rss_result["feed"]["updated"]):
            # Do not process RSS feeds that have not been updated for a certain period of time.
            # If you want to retrieve from the past, change this number of days and re-import.
            print("Skip RSS " + rss_name)
            continue
        add_blog(rss_name, rss_result["entries"], notifier_name)

If you want to retrieve from the past, change this number of days and re-import.というコメントにもあるように、過去の記事も取得できるようにするためには、数字を変えて、データを再インポート(データの洗い替え?)を行う必要があるようです。

まとめ/所感

今回は、「iret.mediaの記事を、AIにレビューを行わせて、Slackに通知する」という検証を行いました。
これまでの業務でAWS CDKは使ったことがありませんでしたが、カスタマイズ自体は簡単に行うことができました。アイデア次第で、色々なカスタマイズができそうですね。
また、今回の検証を通して、OSSをただ試して使うだけでなく、「どう自分の中/自社の業務効率化に活用できるかを考える」視点も大事にしていきたいな、と感じました。

参照資料