0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS What's NewをBedrockで要約してSlackに投稿する【3部作 Part1】

0
Posted at

はじめに

本記事は、AWS What's NewをAIで要約・スライド化してCloudflareで公開するシステムの3部作のPart1です。

システム全体の概要は以下のサマリ記事をご覧ください。

このPart1では、AWS What's NewのRSSフィードをLambdaで定期取得し、Amazon Bedrockで要約してSlackに投稿するまでを解説します。また、Slackのボタンインタラクションを受け取ってBedrock AgentCoreを呼び出す部分も含みます。

RSS取得・Bedrock要約・Slack通知の基本的な構成は、AWS Samplesとして公開されている whats-new-summary-notifier を参考にしています。同プロジェクトはCDKでのデプロイに対応しており、SlackとMicrosoft Teams両方に対応しています。本記事ではスライド生成のためのBedrock AgentCore連携を追加した拡張構成を紹介します。

このPartで扱う範囲

[EventBridge (定期実行)]
        ↓
[Lambda: RSS取得 + Bedrock要約]
        ↓
[Slack: 要約メッセージ + ボタン投稿]
        ↓ (ボタンクリック)
[Slack Bolt: インタラクションハンドラ]
        ↓
[Bedrock AgentCore呼び出し] ← Part2へ続く

構成

AWS構成

  • Amazon EventBridge: 定期実行のスケジューリング
  • AWS Lambda: RSS取得・Bedrock要約・Slack投稿
  • Amazon Bedrock (Claude Haiku 4.5): What's Newの要約生成
  • Amazon DynamoDB: 処理済みエントリの管理(重複処理防止)
  • AWS Systems Manager Parameter Store: Slack Token等のシークレット管理

Slack構成

  • Slack App (Bolt): ボタンインタラクションのハンドリング
  • Block Kit: ボタン付きメッセージの構築

実装

1. EventBridgeの設定

Lambdaを定期実行するためにEventBridgeのスケジュールルールを作成します。
AWS What's NewのRSSフィードは頻繁に更新されるため、毎時間程度の実行が現実的です。今回は1時間ごとに実行するよう設定しています。

{
  "ScheduleExpression": "rate(1 hour)",
  "State": "ENABLED",
  "Targets": [
    {
      "Id": "WhatsnewSummaryLambda",
      "Arn": "<Lambda ARN>"
    }
  ]
}

2. Parameter Storeの準備

LambdaからSlackへの投稿に必要なTokenをParameter Storeに登録します。

パラメータ名 内容
/whatsnew/slack_bot_token Slack Bot Token (xoxb-...)
/whatsnew/slack_channel_id 投稿先チャンネルID
/whatsnew/slack_signing_secret Slack Signing Secret(Bolt用)

3. Lambda: RSS取得 + Bedrock要約 + Slack投稿

メインとなるLambda関数の実装です。

RSSフィードの取得

AWS What's NewのRSSフィードを取得し、DynamoDBで管理している処理済みエントリIDと照合することで新着エントリのみを抽出します。

whats-new-summary-notifier と同様に、DynamoDBでエントリIDを管理しています。時間ベースのフィルタと違い、Lambda実行タイミングのズレや再実行時の二重投稿を確実に防止できます。

import feedparser
import boto3

RSS_URL = "https://aws.amazon.com/about-aws/whats-new/recent/feed/"
TABLE_NAME = "whatsnew-processed-entries"

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(TABLE_NAME)

def is_processed(entry_id: str) -> bool:
    response = table.get_item(Key={"entry_id": entry_id})
    return "Item" in response

def mark_as_processed(entry_id: str):
    table.put_item(Item={"entry_id": entry_id})

def get_new_entries():
    feed = feedparser.parse(RSS_URL)

    new_entries = []
    for entry in feed.entries:
        if not is_processed(entry.id):
            new_entries.append(entry)

    return new_entries

Bedrockによる要約

取得したエントリのタイトルと概要をClaude Haiku 4.5に渡して要約します。

import json

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

def summarize(title: str, summary: str) -> str:
    prompt = f"""以下のAWS What's Newの内容を日本語で3〜5行に要約してください。
要約は箇条書きではなく、読みやすい文章でまとめてください。

タイトル: {title}
内容: {summary}
"""
    response = bedrock.invoke_model(
        modelId="us.anthropic.claude-haiku-4-5-20251001",
        body=json.dumps({
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 512,
            "messages": [{"role": "user", "content": prompt}],
        }),
    )
    result = json.loads(response["body"].read())
    return result["content"][0]["text"]

Slackへの投稿(ボタン付き)

要約した内容をBlock KitでSlackに投稿します。ボタンには後続処理で参照するために、エントリのURLをvalueとして持たせます。

from slack_sdk import WebClient

def post_to_slack(client: WebClient, channel: str, entry, summary: str):
    client.chat_postMessage(
        channel=channel,
        blocks=[
            {
                "type": "header",
                "text": {"type": "plain_text", "text": entry.title},
            },
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": summary},
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"<{entry.link}|AWS公式ページ>",
                },
            },
            {
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "スライドを作成する"},
                        "style": "primary",
                        "action_id": "create_slide",
                        "value": entry.link,
                    }
                ],
            },
        ],
    )

Lambdaハンドラ

import os
import boto3

ssm = boto3.client("ssm")

def get_param(name: str) -> str:
    return ssm.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"]

def handler(event, context):
    token = get_param("/whatsnew/slack_bot_token")
    channel = get_param("/whatsnew/slack_channel_id")
    client = WebClient(token=token)

    entries = get_new_entries()
    for entry in entries:
        summary = summarize(entry.title, entry.summary)
        post_to_slack(client, channel, entry, summary)
        mark_as_processed(entry.id)  # 投稿後にDynamoDBへ記録

4. Slack Bolt: ボタンインタラクションのハンドリング

Slackのボタンが押されたときのイベントを受け取るため、Slack Boltを使ったアプリを用意します。今回はLambda上でBoltを動かしています。

from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

signing_secret = get_param("/whatsnew/slack_signing_secret")
token = get_param("/whatsnew/slack_bot_token")

app = App(token=token, signing_secret=signing_secret, process_before_response=True)

@app.action("create_slide")
def handle_create_slide(ack, body, say):
    ack()
    url = body["actions"][0]["value"]
    title = body["message"]["blocks"][0]["text"]["text"]
    channel = body["channel"]["id"]
    thread_ts = body["message"]["ts"]

    say(f"スライドを生成中です... :hourglass_flowing_sand:\nタイトル: {title}")

    # Bedrock Agentを呼び出す(Part2で詳解)
    invoke_agent_runtime(title=title, url=url, channel=channel, thread_ts=thread_ts)

def lambda_handler(event, context):
    handler = SlackRequestHandler(app=app)
    return handler.handle(event, context)

process_before_response=True はLambdaのタイムアウト制約に対応するための設定です。Slackは3秒以内にレスポンスを要求するため、ack() を先に返してから非同期で処理を進めます。

5. Bedrock AgentCoreの呼び出し

ボタンのインタラクションを受けたら、スライド生成を担うBedrock AgentCoreを呼び出します。詳細はPart2で解説しますが、呼び出し部分のみここに示します。

import boto3

bedrock_agentcore = boto3.client('bedrock-agentcore', region_name=AWS_REGION)

def invoke_agent_runtime(title: str, url: str, channel: str, thread_ts: str):
    payload_dict = {
            'articleUrl': url,
            'channel': channel,
            'thread_ts': thread_ts
        }
    payload = json.dumps(payload_dict).encode('utf-8')
    response = bedrock_agentcore.invoke_agent_runtime(
        agentRuntimeArn="<AgentCore ARN>",
        payload=payload,
        sessionId=title[:30]  # セッションIDに記事タイトルの先頭を使用
    )
    # レスポンスのストリーム処理
    for event in response["completion"]:
        if "chunk" in event:
            print(event["chunk"]["bytes"].decode())

Slackアプリの設定

Slackアプリ側で以下の設定が必要です。

Interactivity & Shortcuts

ボタンのインタラクションを受け取るために、Request URLにLambdaのFunction URL(またはAPI Gateway URL)を設定します。

https://<your-lambda-url>/slack/events

OAuth & Permissions

Botに以下のスコープを付与します。

スコープ 用途
chat:write メッセージ投稿
chat:write.public パブリックチャンネルへの投稿
channels:history パブリックチャンネルのスレッド取得(Part2のAgentが使用)

ポイントと注意点

Slackの3秒ルール

Slackはボタンを押してから3秒以内にHTTP 200レスポンスを要求します。Bedrock AgentCoreの呼び出しはそれ以上かかるため、ack() を先に返してから非同期で処理を進める設計にしています。

Lambdaで非同期処理を行う場合は、Boltの process_before_response=True オプションを有効にした上で、別のLambdaを非同期invoke(InvocationType="Event")するか、SQSを介す方法が安定します。

RSSの重複取得防止

DynamoDBのテーブルに処理済みエントリのIDを記録することで、Lambda実行タイミングのズレや再実行時の二重投稿を防いでいます。テーブル設計はシンプルで、パーティションキーに entry_id(RSSエントリのID)のみを持たせています。

属性名 内容
entry_id String (PK) RSSエントリのID

古いエントリを自動削除したい場合は、TTL属性を追加して保持期間を設定することもできます。

おわりに

Part1ではEventBridge → Lambda → Bedrock → Slackという一本のパイプラインを構築しました。このパートを振り返って、特に重要だった設計判断を整理します。

イベント駆動で処理を疎結合に保つ
RSSフィード取得・要約・Slack投稿をすべて1つのLambdaで完結させています。EventBridgeのスケジュール実行が唯一の起点で、それ以外に常時稼働するプロセスはありません。Lambda+Bedrock従量課金のみのコスト構造で、記事が来ない日はほぼゼロ費用です。

DynamoDBのPK一意制約でRSS重複取得を防ぐ
「処理済みか否か」の判定を専用テーブルで管理することで、Lambdaの再実行や実行タイミングのズレによる二重投稿を防いでいます。複雑なロジックはなく、エントリIDをPKに書き込むだけの最小設計です。

Slackの3秒ルールをack()の先行返却で突破
Bedrock AgentCoreの呼び出しは数十秒~数分かかることがあります。ボタンインタラクション受信後すぐにack()でSlackに200を返し、処理は非同期で進める構成が必須でした。process_before_response=TrueとLambdaの非同期invokeを組み合わせることでこれを実現しています。

ボタンがPart2への橋渡し
Slackに投稿されたボタンを押したとき、AgentCoreに{articleUrl, channel, thread_ts}を渡して呼び出すのがこのパートの終着点です。ここから先はPart2で解説します。

次のPart2では、このBedrock AgentCoreがTavilyで情報を補完しながらMarpスライドを生成する部分を解説します。

サイトの方にもアクセスいただいて、気になるポイントがあればコメントいただけると嬉しいです!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?