Chatbot on Mattermost using Bedrock

# 0. はじまり
AWS Chatbotは2024年9月28日現在，Mattermostに対応していません。ちょっと残念です。
とはいえAWS Lambda以外はコーディングなしで，MattermostでもAmazon Bedrockを使用したChatbotが作れます。

# 1. 構成要素
- IAM Role
- Mattermost
    - region: ap-northeast-1
- Amazon Bedrock
    - region: us-east-1
- AWS Lambda
    - region: us-east-1
- API Gateway

Bedrockの使用リージョンは注意が必要です。北米でしか利用できないベースモデルがあるからです。

## 2.1 IAM Role
AWS Lambdaが使用するロールを作成しておきます。以下の許可ポリシーを設定します。
- AmazonBedrockFullAccess
    - Bedrockにアクセスする上で必要
- AWSLambdaBasicExecutionRole
    - CloudWatchログを使用する上で必要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/d2650674-7476-4475-79f9-eac7da15981d.png)

## 2.2 Setting up Amazon Bedrock
「Bedrock configurations / モデルアクセス」から，使用したいモデルに対してのリクエストを行います。
今回は「Claude 3 Sonnet」を使用しましたが，これでないといけない理由はありません。使いたいものを選択すれば良いです。
リクエストするには，「リクエスト可能」の文字を右クリックして，「モデルアクセスをリクエスト」します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/7ea2f0ce-e6bf-4d2d-391c-d1695d8acb66.png)

アクセスが付与されたら，「基盤モデル / ベースモデル」から，使用したいモデルを探し，「Model ID」を控えておきます。後ほど，Lambdaのコード中に埋め込みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/5b2b37e9-1480-b737-32d8-cc2ce982e15a.png)

## 2.3 Mattermost : Add user account for Chatbot
Chatbotは特定のアカウント上で動作します。ですので，Chatbotにしたいアカウントを作ります。普通にinviteして，新しいユーザを作るだけです。

## 2.4 Mattermost : Incoming Webhook
一旦Adminアカウントで作業します。

### 2.4.1 Chatbot用チャネル作成
まず，Chatbotを使用するチャネルを作ります。普通に新しくチャネルを作ればよいです。

### 2.4.2 ChatbotアカウントにAdmin権限を付与する
ChatbotのユーザでWebhookを作成するため，ChatbotアカウントにAdmin権限を与えます。これを行うことで，Chatbotアカウントで「Integrations」メニューを使用できます。
「System Console / Users」でユーザを表示し，Chatbotで発言させたいユーザの「System Admin」から「Manage Roles」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/474c4472-db3e-4e1a-eef9-710d6d3fbcd5.png)

「System Admin」を選択して「Save」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/9beb5f96-3033-48f8-ecba-b0048dda0eb4.png)

### 2.4.3 Create an Incoming Webhook
ChatbotアカウントでMattermostにアクセスします。

「Integrations / Incoming Webhook」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/d0034716-e66f-8d47-0ee6-8dff0291157f.png)

「Title」「Description」「Channel」「Lock to this channel」を適切に設定します。
「Save」を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/3bd91409-648b-12a8-3b73-cf8493b4b3bf.png)

ここで出てきたURLが「Incoming Webhook」のURLです。控えておきましょう。
「Done」を押したら，Incoming Webhookの取得作業は終わりです。
なお，このURLは「Incoming Webhooks」の画面で後ほど確認することもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/4a5dc38c-6a91-c6b7-78ba-5349f66aa31f.png)

## 2.5 AWS Lambda
「Lambda / 関数」から右上「関数の作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/403327cf-fd6b-ac58-dd7e-4078f83e7a8b.png)

「一から作成」
「関数名」は適切に。
「ランタイム」は「Python 3.12」にしておきます。
「アーキテクチャ」は「arm64」。
「実行ロール」は「既存のロールを使用する」。
「既存のロール」として以前に作成した「LambdaToBedrockAccessRole」を指定します。
「関数の作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/2a380d5f-de28-8f94-e47e-cd4064e14ff4.png)

関数のタイムアウト時間を1分にしておきましょう。Bedrockからの応答に10秒以上かかるからです。
「設定」タブを選択し，左の列から「一般設定」を選択。一般設定の右側の「編集」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/4d579e66-32f0-a325-f47d-d7c6a427fd8e.png)

タイムアウトを1分に変更して，「保存」。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/0e7dd0e9-2cb7-0c9a-775a-98e8351bb16e.png)

AWS Lambdaのコードは以下の通り。
- IMCOMING_WEBHOOK
- MODEL_ID
- SYSTEM_PROMPT

の3つは適切に置き換えてください。Incoming WebhookとModelIDは以前に控えたものを使用します。
SYSTEM_PROMPTは，このChatbotがどういう応答を返すのか，いわゆるプロンプトを記述します。日本語が解釈できるModelであれば，日本語で記述できます。

```python
import json
import urllib.request
import ssl
import boto3
import base64

# Incoming Webhook
WEBHOOK_URL = "INCOMING_WEBHOOK"

def lambda_handler(event, context):
    bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')

    model_id = 'MODEL_ID'
    system_prompt = "SYSTEM_PROMPT"
    max_tokens = 1000

    body = event['body']
    message_text = base64.b64decode(body).decode()

    user_message = {"role": "user", "content": message_text}
    messages = [user_message]
    
    body = json.dumps(
        {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": max_tokens,
            "system": system_prompt,
            "messages": messages
        }  
    )  

    response = bedrock_runtime.invoke_model(body=body, modelId=model_id)
    response_body = json.loads(response.get('body').read())

    req_headers = {
        "Content-Type": "application/json",
    }

    req_json = {
        "text":json.dumps(response_body['content'][0]['text']).encode().decode('unicode-escape'),
    }

    req = urllib.request.Request(WEBHOOK_URL, json.dumps(req_json).encode(), req_headers)
    urllib.request.urlopen(req)
```
コードソースにコードを貼り付けたら，「Deploy」です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/77dd6e48-ebf3-008c-2cf0-374173790edc.png)

ここでテストをしたい場合は，
```
    body = event['body']
    message_text = base64.b64decode(body).decode()
```
を
```
    # body = event['body']
    message_text = "私はJavaの初心者です。何から始めればいいですか?"
```
といった形に書き換えて，「Deploy」したあと「Test」します。[^1]
Testを押すとテストイベントの設定を求められますが，テスト用に修正した都合上，eventを使用していないので，イベント名だけ入力すれば実行できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/52f8949d-6fdc-52c0-dfc9-ca77cd1836ee.png)

message_textに対するBedrockの回答が，Mattermostのchannelに出力されれば，ここまでは動作しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/74d90ac2-fbc0-1f10-2ce0-f1ab125a17a4.png)

テストした後は，修正した2行を戻して，「Deploy」し直しておきます。

## 2.6 API Gateway
API GatewayでAPIを作成します。この作成結果をOutgoing Webhookの指定に使用します。
「APIを作成」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/5898db94-52b9-6c99-c66e-1eca5dfa098e.png)

APIタイプは「HTTP API」にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/9e83642e-1059-9556-bb3c-bbd473a9dd67.png)

「統合」を押して，「Lambda」を選択。
作成したLambda関数を指定します。
「API名」は適切に設定しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/a820ea95-0aa5-bb03-9889-4fab6f63c456.png)

「メソッド」はPOSTにします。ここでリソースパスを指定した場合は，後ほどCallback URLを指定するときにリソースパスをURLに追加することになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/db3ccdca-250d-c459-945a-b284bc5619e2.png)

「ステージ名」は$defaultのままで「次へ」。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/aae7efd6-5939-de1b-ee26-e328476bce8e.png)

確認して作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/5c026d48-ee05-9227-233e-b95b8f38c9db.png)

表示されているルートの「POST」を選択して，「統合」の「設定」を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/d3796331-1c7d-7e35-d137-21f8f0221dd7.png)

作成したLambda関数が設定されていることを確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/cb08ce69-138b-e9b6-d16f-fea3ff3ea40c.png)

左側ペインから，「Deploy / Stages」を選択し，APIを選択すると，「ステージの詳細」に「URLを呼び出す」という項目があります。このURLを控えておきます。Mattermost側でOutgoing Webhookのパラメータ(Callback URLs)として使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/2a7c726f-0418-fd72-550d-6c6b02cce061.png)


## 2.7 Mattermost : Outgoing Webhook
Chatbotのアカウントに戻ります。
「Outgoing Webhook」の「Add Outgoing Webhook」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/bb288a70-a256-dfbc-bea3-07115522e273.png)

必要な項目を設定していきます。
「Channel」に加えて，「Trigger Words」も設定しておくといいでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/9d2039ae-a710-418c-0c27-d08934110c04.png)
「Callback URLs」に，API Gatewayから取得したURLを貼り付けます。このとき，パスつきのURLでAPIを設定している場合は，そのパスをURLの後ろに追記しておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/ed316c4c-8e15-814b-18d4-9061ab17dc62.png)

「Save」を押すと，「Setup Successful」と表示されます。「Done」を押せば終了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/4c9498a5-f450-79eb-af1e-e8fb10750b37.png)

## 2.8 最終動作確認
Chatbot以外のMattermostアカウントで，Chatbotを有効にしたchannelに入り，Trigger Wordsの後ろに質問を書きます。
10数秒して，結果が返ってくれば完成。
お疲れ様でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059/dadbc734-178b-477e-24ad-61d6784359f8.png)

# まとめ
Amazon Bedrockを使用して，Mattermost上で動作するChatbotを作成しました。
ここまでできれば，あとはAmazon Bedrockの機能を使ってさらに高度な応用に挑戦することができます。

# 参考
- [LambdaでBedrockのClaude 3を呼び出してみた](https://qiita.com/nabata/items/5bcc3beb76182f626040)
 - [Mattermostの「褒めbot」をLambdaとPythonで作ってみた](https://qiita.com/t15/items/5cae2fc9916363858f6c)
- [API GatewayとLambdaで送られてくるeventを確認する](https://wiki.kazuma.tokyo/api-gateway%E3%81%A8lambda%E3%81%A7%E9%80%81%E3%82%89%E3%82%8C%E3%81%A6%E3%81%8F%E3%82%8Bevent%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B/#google_vignette)
- [AWS トレーニングを活用して、ノーコード実装の生成 AI チャットボットを設計する](https://aws.amazon.com/jp/builders-flash/202409/create-nocode-gen-ai-chat/)

[^1]: 「イベントJSONにbody書けばコード修正しなくてもいいのでは?」という疑問が沸きますが，Mattermostが送信するリクエストのbodyはBase64encodingされているので，AWS LambdaのイベントJSONに分かりやすく書けません。要調査です。