🤖

Slackの /agent から Amazon Bedrock Agent を呼び出して質問に回答をもらおう

に公開

Slack の Slash コマンド(例:/agent)から Amazon Bedrock Agent を呼び出して、分析結果を Slack に返す仕組みを作りました。

ポイントは Slack の 3 秒制限operation_timeout)を回避するために、Lambda を フロント+ワーカーの2段構成にすることです。
この記事では、つまずきポイント(Base64 ボディ、requests がない、権限周り)も含めて、動くところまでまとめます。


できあがるもの

Slack のチャンネルでこう聞けるようになります。

/agent 利用の最も多かった日は?
  • すぐに「聞いています…」が返る(3秒以内)
  • 数秒後に Bedrock Agent の回答がチャンネルに投稿される

さらに、質問が流れて分からなくならないように Q/A 形式で置換することもできます(後述)。


なぜ2段構成が必要か(Slack の 3 秒制限)

Slash コマンドは 最初の HTTP レスポンスを 3 秒以内に返さないと Slack 側で operation_timeout になります(ここは変更できません)。

Bedrock Agent の応答が 5〜10 秒かかると、同期で返す方式では詰みます。

そこで、

  • フロント Lambda:3 秒以内に「処理中です」と返す
  • ワーカー Lambda:Bedrock Agent を呼び出して、response_url に結果を POST

という「定番の非同期パターン」にします。


全体アーキテクチャ

  1. Slack /agent → フロント Lambda(Function URL)
  2. フロント Lambda は即レス(3秒以内)しつつ、ワーカー Lambda を 非同期 invoke
  3. ワーカー Lambda が Bedrock Agent を呼び出し、結果を Slack の response_url に POST
  4. Slack に回答が表示される

事前準備

Bedrock Agent

  • Agent を作成して PREPARED まで持っていく
  • エイリアス(alias) を作り、エイリアスID を控える(例:QORZFCYSKZ

Lambda に設定するのは「名前」ではなく エイリアスID の方です。

Slack App(Slash Command)

Slack API で App を作成し、Slash Commands で /agent を作る

  • Request URL:フロント Lambda の Function URL(後述)
  • 例:https://xxxxx.lambda-url.ap-northeast-1.on.aws/

つまずきポイント1:Function URL の body が Base64

Lambda Function URL(payloadFormatVersion 2.0)だと、event["body"] が Base64 の場合があります。
event["isBase64Encoded"] を見て decode しましょう。


フロント Lambda(Slash Command 受け口)

役割

  • Slack からの request を受け取る
  • response_url を取り出す
  • ワーカー Lambda を InvocationType="Event" で非同期起動
  • Slack へ 即レス(3秒以内)

環境変数

  • WORKER_FUNCTION_NAME:ワーカー Lambda の関数名(例:divvy-analyst-worker

IAM(フロント Lambda 実行ロール)

  • AWSLambdaBasicExecutionRole(ログ)
  • lambda:InvokeFunction(ワーカーを呼ぶ)

Resource はワーカー Lambda の ARN を指定(後述)

フロント Lambda コード(Python)

import os
import json
import urllib.parse
import base64
import boto3

lambda_client = boto3.client("lambda")
WORKER_FUNCTION_NAME = os.environ["WORKER_FUNCTION_NAME"]

def lambda_handler(event, context):
    body = event.get("body", "") or ""
    if event.get("isBase64Encoded"):
        body = base64.b64decode(body).decode("utf-8")

    params = urllib.parse.parse_qs(body)

    # Slash command payload
    user_text = params.get("text", [""])[0]
    user_id = params.get("user_id", ["unknown_user"])[0]
    channel_id = params.get("channel_id", ["unknown_channel"])[0]
    command = params.get("command", [""])[0]
    response_url = params.get("response_url", [""])[0]

    # Slack URL check
    if params.get("ssl_check") == ["1"]:
        return {"statusCode": 200, "body": ""}

    if not user_text:
        msg = "例)`/agent 利用の最も多かった日は?` のように質問を入力してください 🚲"
        response_type = "ephemeral"
    else:
        payload = {
            "prompt": user_text,
            "session_id": f"{channel_id}-{user_id}",
            "response_url": response_url,
            "user_id": user_id,
            "channel_id": channel_id,
        }

        lambda_client.invoke(
            FunctionName=WORKER_FUNCTION_NAME,
            InvocationType="Event",
            Payload=json.dumps(payload).encode("utf-8"),
        )

        # 即レス(3秒以内)
        msg = f"🟦 *Q:* {user_text}\n⏳ Divvyエージェントに聞いています…数秒お待ちください"
        response_type = "in_channel"   # チャンネルに質問を残す

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "response_type": response_type,
            "text": msg,
        }),
    }

ワーカー Lambda(Bedrock 呼び出し+Slack 返信)

役割

  • Bedrock Agent を呼び出して回答を得る
  • Slack の response_url に POST して結果を投稿

環境変数

  • AGENT_ID:Bedrock Agent の ID(例:B9BHV56DXW
  • AGENT_ALIAS_ID:エイリアスID(例:QORZFCYSKZ

IAM(ワーカー Lambda 実行ロール)

  • AWSLambdaBasicExecutionRole
  • bedrock:InvokeAgent(Bedrock Agent 呼び出し)

つまずきポイント2:requests がない

Lambda には requests が標準で入っていないので、Layer を使うか、urllib3 を使うのが簡単です(この記事では urllib3)。

ワーカー Lambda コード(Python / requests不要)

import os
import json
import boto3
import urllib3
from botocore.exceptions import ClientError

http = urllib3.PoolManager()
bedrock = boto3.client("bedrock-agent-runtime")

AGENT_ID = os.environ["AGENT_ID"]
AGENT_ALIAS_ID = os.environ["AGENT_ALIAS_ID"]

def call_bedrock_agent(prompt: str, session_id: str) -> str:
    try:
        response = bedrock.invoke_agent(
            agentId=AGENT_ID,
            agentAliasId=AGENT_ALIAS_ID,
            sessionId=session_id,
            inputText=prompt,
        )

        completion = ""
        stream = response.get("completion")
        if stream:
            for event in stream:
                if "chunk" in event:
                    completion += event["chunk"]["bytes"].decode("utf-8")

        return completion or "(Bedrock Agent から返答がありませんでした)"

    except ClientError as e:
        code = e.response.get("Error", {}).get("Code", "Unknown")
        return f"Bedrock Agent 呼び出しでエラーが発生しました: {code}"
    except Exception as e:
        return f"予期せぬエラーが発生しました: {e}"

def lambda_handler(event, context):
    prompt = event["prompt"]
    session_id = event["session_id"]
    response_url = event["response_url"]

    answer = call_bedrock_agent(prompt, session_id)

    # 最初の「質問+処理中」メッセージを、Q/A 形式で置換する
    slack_body = {
        "replace_original": True,
        "response_type": "in_channel",
        "text": f"🟦 *Q:* {prompt}\n🟩 *A:* {answer}",
    }

    encoded = json.dumps(slack_body).encode("utf-8")

    r = http.request(
        "POST",
        response_url,
        body=encoded,
        headers={"Content-Type": "application/json"},
    )

    print("Slack POST status:", r.status)
    return {"statusCode": 200}

IAM ポリシー例

フロント Lambda ロール:ワーカーを invoke

ワーカー Lambda の ARN を指定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:<アカウントID>:function:divvy-analyst-worker"
    }
  ]
}

※ ARN は Lambda の関数詳細画面の Function ARN からコピペできます。

ワーカー Lambda ロール:Bedrock Agent invoke

最小ならこう(まずは Resource: "*" でも動作確認はできます)。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:InvokeAgent",
      "Resource": "*"
    }
  ]
}

動作確認

Slack で:

/agent 最も利用の多かった日は?

期待する挙動:

  1. すぐに Q: と「処理中」が表示される
  2. 数秒後、そのメッセージが Q/A に置換されて回答が表示される

まとめ

  • Slash コマンドは 3 秒制限があるので、Bedrock のような重い処理は 非同期構成が必須
  • Function URL は body が Base64 のことがあるので decode が必要
  • Lambda には requests がないので urllib3 を使うと手軽
  • response_url を使うと「後追い返信」が簡単に実装できる

Discussion