はじめに
Amazon Bedrocを試してみようと思い、以下の"作成してみた"をもとにSlack Appを作成してみました。
AWS Chalice を使って、Amazon Bedrock Slack Appを作成してみた
そして、Slack Appの作成やAWS Chaliceの利用が初めてだったこともあり、こちらの記事がとても参考になりました。
AWS Chalice を使って、Slack App (Slack Bolt for Python) を作成してみた
Slack Appが動作するようになるまでにこの2つの記事を行ったり来たりしたので、以下に内容を整理してみました。また、記事で紹介されているコードを実行するだけでは工夫がないので、IAMポリシーの制約や非同期処理を追加しました。
参考情報
前提条件
-
以下のリソースを作成・削除・変更できる権限をもつAWSユーザー
- AWS IAM
- AWS Lambda
- Amazon API Gateway
- Amazon S3
- AWS Chalice
- Amazon Bedrock
-
使用するリージョン: us-east-1
環境構築
AWS環境
AWSコンソールからAmazon Bedrockにアクセスします。
左側メニューのModel accessを開きます。いくつかある基盤モデルのなかからAL21 LabsのJurassic-2 Midを選択し、有効にするためのリクエストを送信します。送信後、数分程度でAccess statusがAccess grantedとなります。
参考記事のAWS 環境設定にあるように、aws configure
でデフォルトのリージョンやクレデンシャルを設定するか、~/.aws/config
や~/.aws/credentials
を編集します。
Python環境
$ python3 -m venv .venv
$ . .venv/bin/activate
$ python3 --version
Python 3.11.4
$ pip3 install --upgrade pip
$ pip3 --version
pip 23.0.1 from /home/xxx/.venv/lib/python3.11/site-packages/pip (python 3.11)
Slack Bolt for Pythonのclone
$ git clone https://github.com/slackapi/bolt-python.git
AWS Chaliceのサンプルがあるディレクトリに移動します。
$ cd bolt-python/examples/aws_chalice/
AWS Chaliseの環境設定
Lambda関数に標準で組み込まれているBoto3のバージョンがBedrock対応していないため、Lambda Layerに組み込みむモジュールにboto3 >= 1.28.57
を追加します。
slack_sdk
slack_bolt
boto3 >= 1.28.57
Slack Appの作成
https://api.slack.com/apps?new_app=1 を開き、Slack Appを作成します。
App Nameをbolt-python-chalice
とします。他の名前を設定する場合は、aws_chalice/.chalice/config.jsonとoauth_app.py内のアプリケーション名を変更します。
Basic Information画面のApp Credentialsに表示されているクレデンシャルを後述のconfig.jsonに設定します。
OAuth & Permissions画面のScopesにapp_mentions:read
とchat:write
のOAuthを追加します。
AWS Chaliseの設定
config.json
OAuth認証用のサンプルconfigをコピーします。
cp -p .chalice/config.json.oauth .chalice/config.json
.chalice/config.jsonを開き、Slack Appのクレデンシャル情報やスコープを定義します。
Slack のOAuth認証用にS3バケットをあらかじめ作成し、SLACK_INSTALLATION_S3_BUCKET_NAME
とSLACK_STATE_S3_BUCKET_NAME
にバケット名を定義します。
Lambda関数からBedrockやS3にアクセスするIAMポリシーを後述のpolicy-dev.jsonで追加するため、autogen_policy
はfalseとします。
{
"version": "2.0",
"app_name": "bolt-python-chalice",
"stages": {
"dev": {
"api_gateway_stage": "api",
"environment_variables": {
"SLACK_SIGNING_SECRET": "App CredentialsのSigning Secretの値",
"SLACK_CLIENT_ID": "App CredentialsのCLIENT IDの値",
"SLACK_CLIENT_SECRET": "App CredentialsのClient Secretの値",
"SLACK_SCOPES": "app_mentions:read,chat:write",
"SLACK_INSTALLATION_S3_BUCKET_NAME": "S3バケット名",
"SLACK_STATE_S3_BUCKET_NAME": "S3バケット名"
},
"autogen_policy": false,
"automatic_layer": true
}
}
}
ここでは、"dev"というステージを定義しました。"prod"などの名称で別のステージを定義することで環境を別々に管理できるようです。
IAMポリシーの設定
.chalice/policy-dev.jsonを新規作成します。
S3の権限は読み取り、書き込み、削除のみに制限しています。実際の運用では、config.jsonで定義したSLACK_INSTALLATION_S3_BUCKET_NAMEやSLACK_STATE_S3_BUCKET_NAMEのバケットのみ扱えるようResourceでも制限するほうが良いと思います。
Bedrockの権限は、指定されたBedrockモデルを呼び出しメンションで入力した内容を使用して推論を実行する権限のみに絞っています。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/ai21.j2-mid-v1"
}
]
}
サンプルアプリの修正
OAuth認証用のapp.pyをコピーします。
cp -p oauth_app.py app.py
App()の初期化部分を以下のように修正します。
bolt_app = App(
process_before_response=True,
oauth_flow=LambdaS3OAuthFlow(),
)
handle_app_mentions()をBedrockにクエリを渡し、スレッドに返信するように書き換えます。
import json
import re
import boto3
def handle_app_mentions(body, say, logger):
logger.info(body)
event = body["event"]
channel = event["channel"]
ts = event["ts"]
# Slackでメンションを受け取ったらメッセージからメンションを取り除く
mention_text = re.sub(r'<@.*?> ', "", event["text"])
# 応答を生成
# Bedrock Clientの作成
bedrock_client = boto3.client('bedrock-runtime')
titanInputText = f"{mention_text}\n"
bedrock_body = {
"prompt": titanInputText,
"maxTokens": 1024,
"temperature": 0.7,
"topP": 1,
}
body_bytes = json.dumps(bedrock_body).encode('utf-8')
response = bedrock_client.invoke_model(
accept="*/*",
body=body_bytes,
contentType="application/json",
modelId="ai21.j2-mid-v1",
)
resp_body = response["body"].read()
resp_body_json = json.loads(resp_body.decode('utf-8'))
logging.info(resp_body_json["completions"][0]["data"]["text"])
text_prompts = resp_body_json["completions"][0]["data"]["text"]
# スレッドに返信
say(f"{text_prompts}", thread_ts = ts)
クエリの内容によってはBedrockからの応答に時間がかるため、Slack APIがタイムアウトエラーを起こすこす場合があります。
Slack API サーバーから HTTP リクエストを受けるアプリケーションは、3 秒以内に HTTP レスポンスを返す必要があります。
3 秒以内に応答を返さなかった場合は「タイムアウトエラー」という扱いになります。Events API の場合は 3 回まで同じイベントの再送が発生します。
これにより、最初のリクエストを含めると合計で4回のイベントが発生します。つまり、1回のクエリに対してBedrockが4回も回答を送信します。これを避けるため、Lazyリスナーを使用すると良いようです。
def respond_to_slack_within_3_seconds(ack):
ack()
bolt_app.event("app_mention")(
ack=respond_to_slack_within_3_seconds,
lazy=[handle_app_mentions]
)
app.py全体
app.pyの全体はこのようになります。
import logging
import json
import re
import boto3
from chalice.app import Chalice, Response
from slack_bolt import App
from slack_bolt.adapter.aws_lambda.chalice_handler import ChaliceSlackRequestHandler
from slack_bolt.adapter.aws_lambda.lambda_s3_oauth_flow import LambdaS3OAuthFlow
# process_before_response must be True when running on FaaS
bolt_app = App(
process_before_response=True,
oauth_flow=LambdaS3OAuthFlow(),
)
#@bolt_app.event("app_mention")
def handle_app_mentions(body, say, logger):
logger.info(body)
event = body["event"]
channel = event["channel"]
ts = event["ts"]
# Slackでメンションを受け取ったらメッセージからメンションを取り除く
mention_text = re.sub(r'<@.*?> ', "", event["text"])
# 応答を生成
# Bedrock Clientの作成
bedrock_client = boto3.client('bedrock-runtime')
titanInputText = f"{mention_text}\n"
bedrock_body = {
"prompt": titanInputText,
"maxTokens": 1024,
"temperature": 0.7,
"topP": 1,
}
body_bytes = json.dumps(bedrock_body).encode('utf-8')
response = bedrock_client.invoke_model(
accept="*/*",
body=body_bytes,
contentType="application/json",
modelId="ai21.j2-mid-v1",
)
resp_body = response["body"].read()
resp_body_json = json.loads(resp_body.decode('utf-8'))
logging.info(resp_body_json["completions"][0]["data"]["text"])
text_prompts = resp_body_json["completions"][0]["data"]["text"]
# スレッドに返信
say(f"{text_prompts}", thread_ts = ts)
def respond_to_slack_within_3_seconds(ack):
ack()
bolt_app.event("app_mention")(
ack=respond_to_slack_within_3_seconds,
lazy=[handle_app_mentions]
)
ChaliceSlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
# Don't change this variable name "app"
app = Chalice(app_name="bolt-python-chalice")
slack_handler = ChaliceSlackRequestHandler(app=bolt_app, chalice=app)
@app.route(
"/slack/events",
methods=["POST"],
content_types=["application/x-www-form-urlencoded", "application/json"],
)
def events() -> Response:
return slack_handler.handle(app.current_request)
@app.route("/slack/install", methods=["GET"])
def install() -> Response:
return slack_handler.handle(app.current_request)
@app.route("/slack/oauth_redirect", methods=["GET"])
def oauth_redirect() -> Response:
return slack_handler.handle(app.current_request)
デプロイ
deploy.shを実行しデプロイします。Lambda関数に加え、Lambda LayerやAPI Gateway、IAMロール、IAMポリシーがまとめてデプロイされます。
$ ./deploy.sh
2回目以降は以下のコマンドでデプロイできます。
$ chalice deploy
デプロイが成功すると、以下のような情報がコンソールに出力されます。
Resources deployed:
- Lambda Layer ARN: arn:aws:lambda:us-east-1:123456789012:layer:bolt-python-chalice-dev-managed-layer:1
- Lambda ARN: arn:aws:lambda:us-east-1:123456789012:function:bolt-python-chalice-dev
- Rest API URL: https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/
このように、Lambda関数やLayer、API Gatewayが作成されました。
Slack Appの設定
Request URL,Redirect URLを設定
Event Subscriptions画面のEnable EventsをOnにし、Request URLにさきほど出力されたRest API URLの末尾にslack/events
を追加したURL
https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/slack/events
を入力します。
Verified✓ と表示されれば、正しいURLが入力されたことになります。
メンションイベントに応答するために、Subscribe to bot eventsにapp_mention
を追加します。
OAuth & Permissions画面のRedirect URLsにRest API URLの末尾にslack/oauth_redirect
を追加したURL https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/slack/oauth_redirect
を入力します。
Slack Workspaseにアプリをインストール
Rest API URLの末尾にslack/install
を追加したURL https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/slack/install
にアクセスします。インストールボタンが表示されるのでクリックしSlack Workspaseにアプリをインストールします。
もしくは、Install App画面のInstall to Workspace
をクリックしても同様の画面が表示されます。
インストールに成功すると、Thank you!画面が表示されます。Slackアプリをインストールしている場合はclick here
のリンク、Webブラウザを使用している場合は、this link
をクリックしてSlackを開きます。
動作確認
任意のチャンネルに@bolt-python-chalice
を招待し、メンションを送信します。 "こんにちは!"に対する返信が不自然ですが、生成AIについては自然な日本語でした。
今回はモデルにAL21 LabsのJurassic-2 Midを使用しました。日本語に対応しているAnthropicのClaudeだと違った応答になるかもしれません。
コードの作成を依頼してみました。これもきちんと答えてくれました。
ログは、CloudWatchロググループbolt-python-chalice-dev
で確認できます。
Chaliceが作成したリソース
作成したリソース
.chalis/deployed/dev.json にChaliceが作成したリソースの情報が記録されています。
{
"resources": [
{
"name": "managed-layer",
"resource_type": "lambda_layer",
"layer_version_arn": "arn:aws:lambda:us-east-1:123456789012:layer:bolt-python-chalice-dev-managed-layer:1"
},
{
"name": "api_handler_role",
"resource_type": "iam_role",
"role_arn": "arn:aws:iam::123456789012:role/bolt-python-chalice-dev-api_handler",
"role_name": "bolt-python-chalice-dev-api_handler"
},
{
"name": "api_handler",
"resource_type": "lambda_function",
"lambda_arn": "arn:aws:lambda:us-east-1:123456789012:function:bolt-python-chalice-dev"
},
{
"name": "rest_api",
"resource_type": "rest_api",
"rest_api_id": "xxxxxxxxxx",
"rest_api_url": "https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/"
}
],
"schema_version": "2.0",
"backend": "api"
}
作成したリソースの削除
デプロイコマンドでデプロイされたリソースは以下のコマンドで削除できます。
chalice delete
削除後に再度デプロイするとRest API URLが以前とは別のもになります。その場合、Slack Appの再設定が必要です。