こんにちは!第一開発事業部の大瀧優杏です!

前回の記事では、AWS LambdaとAmazon Bedrockを組み合わせて、Slack内のスレッドを3行にまとめる「SummaryBot」の作成についてご紹介しました。
前回の記事

しかし、、、
1回のメンションに対して要約メッセージが2重・3重に重複して送信されてしまうという問題が発生してしまいました。😱
今回の記事では、この不具合の原因を考察し、実際に修正を行いSummaryBotを完成させました!

使用した技術スタック

  • 言語: Python 3.12
  • インフラ管理: AWS SAM
  • コンピューティング: AWS Lambda
  • API連携: Amazon API Gateway, Slack API (slack_sdk)
  • AI: Amazon Bedrock (Anthropic Claude 3 Haiku)

重複送信の原因

原因は下記になります。

  • Slack側の仕様として3秒以内に200レスポンスを要求
  • bedrockの処理が3秒を超過
  • 3秒以内にレスポンスを返却できない事態が発生
    →新たなリクエストが発生

じゃ今処理中です!みたいにスラック側に即200レスポンスを返却する応答用のlambdaを別に作ればいいのか!

と思い、実装しました😆

解決策

この問題を解決するため、Lambda関数を以下の2つに分割しました。

ReceiverFunction (受信専用): API Gateway経由でSlackからのリクエストを受け取る。すぐにProcessorFunctionを非同期実行(Event呼び出し)し、Slackに対して1秒以内にHTTP 200を返す。

ProcessorFunction (要約処理専用): バックグラウンドでBedrockを利用してスレッド履歴を要約し、完了後にSlackへ直接メッセージを送信する。

手順

Slack API側の処理は前回の記事で紹介したため割愛します。

AWS SAMテンプレートの作成
インフラをコード定義するため、プロジェクト直下に template.yaml を作成します。
変更点は、API Gatewayと2つのLambda関数、および相互の呼び出し権限を定義を追加した点になります。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  SlackEventsApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod

  ReceiverFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: lambda_function.receiver_handler
      Runtime: python3.12
      Timeout: 10
      MemorySize: 256
      Environment:
        Variables:
          PROCESSOR_FUNCTION_NAME: !Ref ProcessorFunction
      Events:
        SlackEvents:
          Type: Api
          Properties:
            RestApiId: !Ref SlackEventsApi
            Path: /slack/events
            Method: post
      Policies:
        # ProcessorFunctionを非同期で呼び出す権限
        - Statement:
            - Effect: Allow
              Action: "lambda:InvokeFunction"
              Resource: !GetAtt ProcessorFunction.Arn

  # Bedrockによる要約とSlackへの送信を非同期で行う処理専用Lambda
  ProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: lambda_function.processor_handler
      Runtime: python3.9
      Timeout: 120
      MemorySize: 512
      Policies:
        # Amazon Bedrockのモデルを呼び出す権限
        - Statement:
            - Effect: Allow
              Action: "bedrock:InvokeModel"
              Resource: "*"

Outputs:
  SlackEventsApiUrl:
    Description: Slack Events API Request URL
    Value: !Sub "https://${SlackEventsApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/slack/events"

Lambda関数の実装
1つのPythonファイル(lambda_function.py)内に、受信用と処理用の2つのハンドラーを定義します。

python
import os
import json
import boto3
from slack_sdk import WebClient

# 環境変数の設定とクライアントの初期化
SLACK_TOKEN = os.environ.get('SLACK_BOT_TOKEN') # デプロイ後に環境変数として設定
client = WebClient(token=SLACK_TOKEN) if SLACK_TOKEN else None
bedrock = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
lambda_client = boto3.client('lambda')

# -----------------------------------------------------------------------
# ReceiverFunction 用ハンドラー
# Slack Events API から呼ばれる。即座に 200 を返し、処理を非同期に委譲する。
# -----------------------------------------------------------------------
def receiver_handler(event, context):
    headers = event.get('headers', {})

    # Slackからのリトライリクエストを明示的に無視する
    if headers.get('x-slack-retry-num') or headers.get('X-Slack-Retry-Num'):
        return {'statusCode': 200, 'body': 'retry ignored'}

    body = json.loads(event.get('body', '{}'))

    # SlackのURL検証用(Event Subscriptionsの初回登録時に必要)
    if 'challenge' in body:
        return {'statusCode': 200, 'body': body['challenge']}

    # 無限ループ防止のため、ボット自身の発言の場合は処理を終了
    slack_event = body.get('event', {})
    if 'bot_id' in slack_event:
        return {'statusCode': 200, 'body': 'ignored bot message'}

    # ProcessorFunction を非同期で起動(InvocationType='Event')
    processor_function_name = os.environ['PROCESSOR_FUNCTION_NAME']
    lambda_client.invoke(
        FunctionName=processor_function_name,
        InvocationType='Event',
        Payload=json.dumps(body).encode('utf-8')
    )

    # 処理を待たずに即座に200 OKを返却
    return {'statusCode': 200, 'body': 'OK'}

# -----------------------------------------------------------------------
# ProcessorFunction 用ハンドラー
# ReceiverFunction から非同期で呼ばれ、Bedrock要約とSlack返信を行う。
# -----------------------------------------------------------------------
def processor_handler(event, context):
    slack_event = event['event']
    channel = slack_event['channel']
    thread_ts = slack_event.get('thread_ts') or slack_event['ts']

# 1. スレッドの履歴を取得
replies = client.conversations_replies(channel=channel, ts=thread_ts)
messages = [m['text'] for m in replies['messages'] if 'bot_id' not in m]
context_text = "\n".join(messages)

# 2. Bedrock (Claude 3 Haiku) で要約
prompt = f"以下のSlackスレッドを、重要点に絞って3行で要約してください。\n\n{context_text}"

native_request = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512,
"messages": [{"role": "user", "content": prompt}]
}

response = bedrock.invoke_model(
modelId="anthropic.claude-3-haiku-20240307-v1:0",
body=json.dumps(native_request)
)
result = json.loads(response['body'].read())
summary = result['content'][0]['text']

# 3. Slackの該当スレッドに返信
client.chat_postMessage(
channel=channel,
thread_ts=thread_ts,
text=f"要約しました!\n\n{summary}"
)

return {'statusCode': 200}

デプロイ完了後、以下の設定を行います。

環境変数の設定: AWSマネジメントコンソールから ProcessorFunction を開き、環境変数 SLACK_BOT_TOKEN に手順1で取得したトークンを設定します。

SlackのEvent Subscriptions設定
Slack APIの設定画面にて、Event Subscriptions を開き、API GatewayのURLを Request URL に貼り付けます。

Verified が確認できました👏🏻🥳

実際の使用レポ


正真正銘1件の通知になりました!!!!🙌🏻

今後の課題

現在はSlackトークンを環境変数に直接設定していますが、セキュリティ向上のため、AWS Systems Manager (SSM) パラメータストアへの移行を検討しています。

メリットとして下記が挙げられます。

  • セキュリティ面:トークンを暗号化して安全に管理できる。
  • コスト面 :Secrets Managerと比較して、標準パラメータであれば無料で利用できるため、コストを抑えられる。

今回挑戦してみたのですが、難しかったのでまた次の機会に取り入れていこうと思います^ ^
最後まで読んでいただきありがとうございました!