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
という「定番の非同期パターン」にします。
全体アーキテクチャ
- Slack
/agent→ フロント Lambda(Function URL) - フロント Lambda は即レス(3秒以内)しつつ、ワーカー Lambda を 非同期 invoke
- ワーカー Lambda が Bedrock Agent を呼び出し、結果を Slack の
response_urlに POST - 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 最も利用の多かった日は?
期待する挙動:
- すぐに
Q:と「処理中」が表示される - 数秒後、そのメッセージが
Q/Aに置換されて回答が表示される
まとめ
- Slash コマンドは 3 秒制限があるので、Bedrock のような重い処理は 非同期構成が必須
- Function URL は body が Base64 のことがあるので decode が必要
- Lambda には
requestsがないのでurllib3を使うと手軽 -
response_urlを使うと「後追い返信」が簡単に実装できる
Discussion