"Amazon Bedrock Slack Appを作成してみた"の記事を読んでSlack Appを作成してみた

# はじめに

Amazon Bedrocを試してみようと思い､以下の"作成してみた"をもとにSlack Appを作成してみました｡

[AWS Chalice を使って、Amazon Bedrock Slack Appを作成してみた](https://dev.classmethod.jp/articles/aws-chalice-bedrock-slack-app/)

そして､Slack Appの作成やAWS Chaliceの利用が初めてだったこともあり､こちらの記事がとても参考になりました｡

[AWS Chalice を使って、Slack App (Slack Bolt for Python) を作成してみた](https://dev.classmethod.jp/articles/aws-chalice-slack-app-bolt-for-python/)

Slack Appが動作するようになるまでにこの2つの記事を行ったり来たりしたので､以下に内容を整理してみました｡また､記事で紹介されているコードを実行するだけでは工夫がないので､IAMポリシーの制約や非同期処理を追加しました｡

# 参考情報

https://github.com/slackapi/bolt-python/tree/main

https://dev.classmethod.jp/articles/aws-chalice-bedrock-slack-app/

https://dev.classmethod.jp/articles/aws-chalice-slack-app-bolt-for-python/

https://dev.classmethod.jp/articles/python-boto3-amazon-bedrock-get-started/

https://dev.classmethod.jp/articles/slack-resend-matome/

https://dev.classmethod.jp/articles/bolt-lambda/

https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html

https://slack.dev/bolt-python/ja-jp/concepts#lazy-listeners

https://slack.dev/bolt-python/ja-jp/tutorial/getting-started#create-an-app

https://qiita.com/seratch/items/6d142a9128c6831a6718

https://qiita.com/hirai-11/items/e6c8b12dab5728d36684

# 前提条件

* 以下のリソースを作成･削除･変更できる権限をもつAWSユーザー
    * AWS IAM
    * AWS Lambda
    * Amazon API Gateway
    * Amazon S3
    * AWS Chalice
    * Amazon Bedrock

* 使用するリージョン: us-east-1

# 環境構築

## AWS環境

AWSコンソールからAmazon Bedrockにアクセスします｡
左側メニューの[Model access](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess)を開きます｡いくつかある基盤モデルのなかからAL21 LabsのJurassic-2 Midを選択し､有効にするためのリクエストを送信します｡送信後､数分程度でAccess statusがAccess grantedとなります｡

参考記事の[AWS 環境設定](https://dev.classmethod.jp/articles/aws-chalice-bedrock-slack-app/#:~:text=east%2D1%23/modelaccess-,AWS%20%E7%92%B0%E5%A2%83%E8%A8%AD%E5%AE%9A,-Chalice%E3%81%A7%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)にあるように､`aws configure`でデフォルトのリージョンやクレデンシャルを設定するか､`~/.aws/config`や`~/.aws/credentials`を編集します｡

## Python環境

```console
$ python3 -m venv .venv
$ . .venv/bin/activate
```

```console
$ python3 --version
Python 3.11.4
```

```console
$ 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

```console
$ git clone https://github.com/slackapi/bolt-python.git
```

AWS Chaliceのサンプルがあるディレクトリに移動します｡

```console
$ cd bolt-python/examples/aws_chalice/
```

## AWS Chaliseの環境設定

Lambda関数に標準で組み込まれているBoto3のバージョンがBedrock対応していないため､Lambda Layerに組み込みむモジュールに`boto3 >= 1.28.57`を追加します｡

```text:requirements.txt
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に設定します｡

![スクリーンショット 2023-10-15 221539.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/14c726cf-99ae-4b64-3827-3609bce019a1.png)

OAuth & Permissions画面のScopesに`app_mentions:read`と`chat:write`のOAuthを追加します｡

![スクリーンショット 2023-10-15 221637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/314afd1e-33b4-3e89-e265-253936c4886f.png)

## AWS Chaliseの設定

### config.json

OAuth認証用のサンプルconfigをコピーします｡

```console
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とします｡

```json:.chalice/config.json
{
  "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モデルを呼び出しメンションで入力した内容を使用して推論を実行する権限のみに絞っています｡

```json:.chalice/policy-dev.json
{
    "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をコピーします｡

```console
cp -p oauth_app.py app.py
```

App()の初期化部分を以下のように修正します｡

```python:app.py(抜粋)
bolt_app = App(
    process_before_response=True,
    oauth_flow=LambdaS3OAuthFlow(),
)
```

handle_app_mentions()をBedrockにクエリを渡し､スレッドに返信するように書き換えます｡

```python:app.py(抜粋)
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がタイムアウトエラーを起こすこす場合があります｡

https://qiita.com/seratch/items/6d142a9128c6831a6718#slack-%E3%81%8B%E3%82%89%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%82%92%E5%8F%97%E3%81%91%E3%81%9F%E3%82%89-3-%E7%A7%92%E4%BB%A5%E5%86%85%E3%81%AB%E5%BF%9C%E7%AD%94%E3%81%99%E3%82%8B

> Slack API サーバーから HTTP リクエストを受けるアプリケーションは、3 秒以内に HTTP レスポンスを返す必要があります｡

> 3 秒以内に応答を返さなかった場合は「タイムアウトエラー」という扱いになります。Events API の場合は 3 回まで同じイベントの再送が発生します。

これにより､最初のリクエストを含めると合計で4回のイベントが発生します。つまり､1回のクエリに対してBedrockが4回も回答を送信します｡これを避けるため､Lazyリスナーを使用すると良いようです｡

```python:app.py(抜粋)
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の全体はこのようになります｡

```python: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ポリシーがまとめてデプロイされます｡

```console
$ ./deploy.sh
```

2回目以降は以下のコマンドでデプロイできます｡

```console
$ chalice deploy
```

デプロイが成功すると､以下のような情報がコンソールに出力されます｡

```console
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が作成されました。
![スクリーンショット 2023-10-18 234521.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/c3fef44d-da84-2129-6e6a-0b28013257da.png)

## 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が入力されたことになります｡

![スクリーンショット 2023-10-15 221804.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/8083209d-f9ed-4d3b-fa25-5dedb7d80312.png)

メンションイベントに応答するために、Subscribe to bot eventsに`app_mention`を追加します。

![スクリーンショット 2023-10-15 221837.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/dc4c0043-f373-18ea-b474-334caf02af76.png)


OAuth & Permissions画面のRedirect URLsにRest API URLの末尾に`slack/oauth_redirect`を追加したURL `https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/slack/oauth_redirect`を入力します｡

![スクリーンショット 2023-10-15 221900.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/43da2297-cea1-81f7-62de-3d72a5966adc.png)


### Slack Workspaseにアプリをインストール

Rest API URLの末尾に`slack/install`を追加したURL `https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/api/slack/install`にアクセスします｡インストールボタンが表示されるのでクリックしSlack Workspaseにアプリをインストールします｡

![スクリーンショット 2023-10-15 213015.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/8c9271da-950b-fa1d-6932-c640a90827e5.png)

![スクリーンショット 2023-10-15 213411.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/362c9315-0f3d-e188-81b7-9a0076440455.png)


もしくは､Install App画面の`Install to Workspace`をクリックしても同様の画面が表示されます｡

![スクリーンショット 2023-10-15 214046.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/dd296ce9-cf69-e98f-0ae0-cbdf6eab3afe.png)


インストールに成功すると､Thank you!画面が表示されます｡Slackアプリをインストールしている場合は`click here`のリンク､Webブラウザを使用している場合は､`this link`をクリックしてSlackを開きます｡

![スクリーンショット 2023-10-15 214923.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/c5694334-7452-c3cf-2cc6-8f4dd1482f45.png)


# 動作確認

任意のチャンネルに`@bolt-python-chalice`を招待し､メンションを送信します｡ "こんにちは!"に対する返信が不自然ですが､生成AIについては自然な日本語でした｡
今回はモデルにAL21 LabsのJurassic-2 Midを使用しました｡日本語に対応しているAnthropicのClaudeだと違った応答になるかもしれません｡

![スクリーンショット 2023-10-16 003916.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/cec070c8-c9e9-7855-e719-d9657ecab17b.png)

コードの作成を依頼してみました。これもきちんと答えてくれました。

![スクリーンショット 2023-10-18 231744.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/c230e051-3c1d-2b8d-3c0a-aa9929274056.png)



ログは､CloudWatchロググループ`bolt-python-chalice-dev`で確認できます｡

# Chaliceが作成したリソース

## 作成したリソース

.chalis/deployed/dev.json にChaliceが作成したリソースの情報が記録されています｡

```json:.chalis/deployed/dev.json
{
  "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"
}
```

## 作成したリソースの削除

デプロイコマンドでデプロイされたリソースは以下のコマンドで削除できます｡

```console
chalice delete
```

削除後に再度デプロイするとRest API URLが以前とは別のもになります｡その場合､Slack Appの再設定が必要です｡