【生成AI初心者大歓迎】1時間でRAG on AWSを作る！Slack AI チャットボット実践ハンズオン！

本資料は、社内で生成AIハンズオンを実施する際に作成したものです。
より多くの方にも参考にしていただけるよう、一般公開しています。

このハンズオンには、以下の3つの特徴があります：

1. AWS初心者やプログラミング未経験者でも取り組めるよう、ほぼGUIベースの操作のみで完結
2. 作ったもの社内でそのまま活用できるよう、AWS上だけでなくSlackとの連携まで実施
3. 多くの方が時間を取りやすいよう、所要時間を1時間に設定

↓ここからが本編です。

# 1. はじめに

## 1.1 このハンズオンの目的
ChatGPTの登場以降、生成AIは私たちの仕事や生活に大きな影響を与えています。 特にIT業界においては、もはや避けて通れないテーマとなっています。

このような状況を踏まえ、本ハンズオンでは以下を目指します

- 生成AIに関する知見を、実際に手を動かしながら身に付けていただき、お客様への提案や施策の企画などへ活用していただくこと
- ハンズオン内で作成したシステムを、そのまま実務でも活用していただき、業務効率化につなげていただくこと

## 1.2 生成AIの仕組みと特徴
まず最初に、生成AIがなぜ質問への回答や文章作成といった高度な言語タスクを実行できるのかを説明します。 

簡単に言うと、生成AIは人間の脳の仕組みをコンピューター上で再現しているため、人間のような言語処理が可能なのです。 

具体的には、私たちの脳の中にある神経細胞（ニューロン）のつながり（ネットワーク）を数式で表現した「ニューラルネットワーク」という仕組みを使っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/852b2164-2715-508a-72e4-a32844c524ce.png)
https://jitera.com/ja/insights/53089

生成AIは、このニューラルネットワークを使って膨大な量の文章（Wikipedia数千個分※）を読み込んで学習します。

この学習には、家庭用の高性能パソコン（ゲーミングPC）を数十万台分集めたような、とてつもない計算能力が必要です。 

このような大量の学習によって、生成AIは人間の言葉をよく理解できるようになり、質問に答えたり、文章をまとめたり、レポートを書いたり、プログラミングのコードを作ったりと、様々な言語の作業を正確にこなせるようになります。

:::note info
※Llama 3の情報を基に、Wikipedia英語版（約30億単語）とゲーミングPCでよく使われるRTX 4090の性能とを比較し推定
https://medium.com/axinc/llama3%E3%81%AE%E8%AB%96%E6%96%87%E3%82%92%E8%AA%AD%E3%82%80-fd232b658424
:::

そこで本ハンズオンでは、この優れた技術を実際の仕事で役立てるため、AWSの生成AIサービスとSlackを組み合わせた、実用的なチャットボットの作り方を学んでいきます。

# 2. AWSで生成AIを触ってみよう

まずは実際に生成AIを操作しながら、その特徴をより深く理解していきましょう。

そのために、AWSが提供する生成AI向けのマネージドサービス「Amazon Bedrock」を使用します。
Amazon Bedrockは、様々な生成AIモデルを簡単に利用できます。

## 2.1 環境準備

:::note warn
【重要：2024年11月21日現在】
オレゴンリージョン(us-west-2)でAmazon Bedrock Knowledge Basesを「ベクトルストアをクイック作成」オプションで作成すると、エラーが発生する事象が確認されています。そのため、本ハンズオンではバージニア北部リージョン(us-east-1)での実施を推奨します。以下の手順でもバージニア北部リージョンを選択してください。
:::

まず、AWSマネジメントコンソールにログインし、リージョンをバージニア北部(us-east-1)に変更します。
※画像はオレゴンリージョンになっていますが、バージニア北部(us-east-1)を選択してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/cefaef93-96c4-cf10-79bb-fd7c7c8b54ef.png)

その後、検索バーで「Bedrock」と入力し、「Amazon Bedrock」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/27c38181-170c-fd8d-5dee-ae6d9c02cd2f.png)

## 2.2 モデルアクセスの有効化
モデルアクセスの設定をするには、まず左側メニューから「モデルアクセス」を選択してください。
その後、モデルアクセスを初めて設定される方は「特定のモデルを有効にする」を、以前に他のモデルアクセスを設定したことがある方は「モデルアクセスを変更」をクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/d5279ce0-771d-dcf2-84f1-dd4d3cb94d2c.png)


「Claude 3.5 Haiku」と「Titan Text Embeddings V2」選択し、「次へ」→「送信」で有効化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/e1bc80ee-fb8f-6e3d-c928-060052f59816.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/9d364897-e074-5818-9fdd-2054585b72db.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/34f8526a-274d-529b-4140-bd7dc1644d76.png)

:::note warn
利用規約に同意することになりますのでご注意ください。
:::

:::note info
Claudeは、Anthropic社が開発した生成AIで、高性能な大規模言語モデル（LLM）です。
GPT-4oよりも多くのベンチマークで性能を上回っているのが特徴です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/8c7a3b59-80c1-a2d0-86cb-5cf356667008.png)
https://www.anthropic.com/news/3-5-models-and-computer-use
:::

## 2.3 モデルの動作確認

実際に有効化したClaude 3.5 haikuと対話してみましょう。
「プレイグラウンド」→「Chat/Text」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/c76874d7-cbe4-bc8a-4b93-7438f0a7997b.png)

Claude 3.5 haikuを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/e26070d1-c81a-c303-c5d5-55c99288fbc5.png)

これで質問できるようになりました。
以下のような質問をしてみましょう
```
Pythonでcsvからjsonに変換するスクリプトを書いてください
```
質問に対して回答してくれていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/a322b86c-18d3-a82e-ffbf-cfe684c62cfe.png)

次に、以下のような質問をしてみましょう
```
あなたはいつまでの情報を学習してますか？
また、HULFT Squareはどこの会社が提供しているサービスですか？
```
2022年1月までの情報しか学習していないため、回答が間違っていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/200543f7-1712-c373-7042-e1acbe8e0309.png)

:::note info
HULFT Squareは、2023年2月にリリースされた、株式会社セゾンテクノロジーが提供する、システム間のデータ連携を実現するクラウドサービス（iPaaS）です。
:::

このことから分かるように、LLMには事前学習していない、最新の情報や公開されていない情報については、回答できないことが分かります。

これを克服するために「RAG（Retrieval-Augmented Generation）」という手法が開発されています。
次章では、RAGを使ってLLMが答えられない情報も回答できるようにしていきます。

# 3. RAGの基礎と概要

## 3.1 RAGとは
RAGとは、LLMと外部の知識データベースを組み合わせて使用する手法です。

これにより、LLMが学習していない情報にも回答できるようになったり、誤った情報の生成（ハルシネーション）を減らすこともできます。

RAGの仕組みをより深く理解するために、まずは基本的な処理の流れを見ていきましょう。

## 3.2 RAGの処理フロー
RAGの基本的な処理の流れは以下のステップで構成されています：

①ドキュメントの前処理：必要な文書を検索可能な形式に加工し、データベースに保存します。
②質問：ユーザーから質問を受け取ります。
③検索：質問内容に関連する情報をデータベースから探し出します。
④取得：検索結果から必要な部分を取り出します。
⑤回答生成：抽出した情報をLLMに渡して、回答を生成します。

![図1：RAGの基本的な処理フロー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/a655618c-a903-1c5d-84c5-178de50ea7f1.png)

この仕組みにより、LLMは事前学習では得られなかった情報でも回答することができます。

それでは、この処理の技術的な詳細について、さらに掘り下げて説明していきましょう。

## 3.3 RAGの技術的な仕組み
#### ドキュメントの前処理
まず、ドキュメントの前処理では、長い文書を「チャンク」と呼ばれる小さな単位に分割します。

次に、各チャンクを「埋め込みモデル」というAIを使って数値データ（ベクトル）に変換し、ベクターDBに保存します。

#### 質問~回答生成処理
ユーザーからの質問も同様に数値データに変換され、ベクトル検索によって質問の意味に最も近いチャンクを見つけ出します。

最後に、見つかったチャンクの情報をLLMに提供して回答を生成します。

![図2：RAGの技術的な処理フロー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/6d1e62e9-76b7-11da-02e8-a97ea84389db.png)

ここまでRAGの基本的な概念と仕組みについて説明してきました。

それでは実際に、AWS上でRAGを構築していきましょう。

# 4. Amazon Bedrock Knowledge Basesによる実装

## 4.1 システムアーキテクチャ
AWSの「Amazon Bedrock Knowledge Bases」というサービスは、RAGを構成する下記アーキテクチャを簡単に構築することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/d752ec59-2029-08aa-4b78-04639c965036.png)

システムの全体像が把握できたところで、実際の環境構築に進んでいきましょう。

## 4.2 環境構築
### 4.2.1 データの準備
#### RAGに読み込ませるデータのダウンロード
今回はRAGに読み込ませるドキュメントとしてHULFT SquareのHPを使用します。

下記URLにアクセスし
https://www.hulft.com/service/hulft-square
Ctrl+S でHTMLとして保存しておきます。

#### S3バケットの作成
AWSコンソールで「S3」を検索します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/c596b52d-fca6-ebe0-9591-71ff017a718f.png)

「バケットを作成」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/3371ad7d-beae-03b7-e230-5769ecc0a84f.png)

バケット名を設定し、その他デフォルトのままバケットを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/5a81aeaa-dee1-093f-acf4-2ec52e6ab16e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/c3271b05-f032-e857-f9a2-41e64ac513e8.png)

S3バケットが作成できたら、次はダウンロードしたファイルをアップロードしていきましょう。

#### ダウンロードしたファイルのアップロード
作成したS3バケットを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/22569101-a676-85ea-ba96-b90b2e95d407.png)

「アップロード」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/05e3f20f-7b66-594d-1a7a-fb2030484804.png)

「ファイルを追加」を選択し、先ほどダウンロードしたファイルを選択し、アップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/e03bc99e-d9bc-8ee0-4080-40d41d2454af.png)

これで読み込ませるデータの準備は完了しました。次は、このデータを活用するためのKnowledge Basesを作成していきましょう。

### 4.2.2 Knowledge Basesの構築
Amazon Bedrockコンソールに移動し、左メニューから「ナレッジベース」を選択し、「ナレッジベースを作成」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/dc8a6978-c8a7-c1f9-d7c9-93413bcf7ae3.png)

分かりやすいよう、ナレッジベース名に先ほどのS3バケットの名前を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/fc8380b5-28db-e400-30bd-b6583e5dd429.png)

その他はデフォルト設定で次へ進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/a6e19fda-a52c-61f9-3375-7b665ffb1e38.png)

「S3 URIの参照」を選択し、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/e5519a59-e381-b839-a0d1-81cd154874fe.png)

先ほど作成したS3バケットを選択し、次のページへ進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/6cfb7b2c-8c87-9bf9-e68a-2e0b26cd130e.png)

埋め込みモデルは「titan-text-embedding-v2」を選択し、ベクトルデータベースは「新しいベクトルストアをクイック作成」を選択（デフォルト）し、次へ進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/3b34e648-9249-70ae-16c6-3dd7205d8a2b.png)

最後に設定内容を確認し、作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/1add5621-a277-d6fa-67d3-8f2d077c1096.png)

作成されるまで数分待機します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/472decb4-b0a9-2222-ec61-13b796106584.png)

作成失敗した場合は、もう一度作成を試みます。

作成成功したら、Slackとの連携時に使用するためナレッジベースIDを保管しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/1de5bcc8-91fb-e054-b153-0c00c9ebae7a.png)

「データソース」→「同期」を選択し完了するまで待機します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/bee70c31-6734-ab72-ffbc-edf1ff9894c3.png)

これでAmazon Bedrock Knowledge Basesを作成し、データ同期まで完了しました。それでは、実際に作成したシステムの動作を確認していきましょう。

## 4.3 動作検証
「ナレッジベースをテスト」から「モデルを選択」を選択し、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/02cb9fd3-2133-d756-890d-d560cf2f621d.png)

モデルは先ほどと同様Claude 3.5 Haikuを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/b1558024-f119-7cb4-0992-a6e88f1820ca.png)

それでは、以下の質問を試してみましょう：
```
HULFT Squareはどこの会社が提供しているサービスですか？
```

先ほどは間違っていた質問でも、今回は回答が得られていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/67c5fe4d-9cab-85bb-c175-6bd707df0acc.png)


このように、Amazon Bedrock Knowledge Basesを用いることで、簡単にRAGを実装でき、LLMが学習していない情報からでも、正確な回答が得られるようになりました。

# 5. Slackとの連携

これまでAmazon Bedrock Knowledge Basesを使ってRAGシステムを構築し、テストコンソールで動作確認を行いました。

本章では、このシステムを実際の業務で活用できるよう、Slackと連携させる実装を行います。

## 5.1 システムアーキテクチャ

Slackと連携したシステムの全体像は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/24d91cb7-205e-8c9e-6643-f6078884554e.png)

ユーザーがSlackでボットにメンションを送ると、そのメッセージはAmazon API Gatewayを経由してAWS Lambdaに届きます。

Lambda関数は受け取ったメッセージを元に、先ほど作成したKnowledge Basesに問い合わせを行い、その回答をSlackに返信します。

## 5.2 Slack Appの作成

まず、Slackにチャットボットをインストールするための準備として、Slack Appを作成します。

Slack APIにアクセスし、

https://api.slack.com/apps

「Create New App」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/5a355e58-4c1d-24da-a73a-09cf3c9266f5.png)

「From Scratch」を選択し、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/47284bd9-e8c5-d60b-737f-b889357e4c7e.png)

App Nameを設定し、導入したいSlackワークスペースを選択し、「Create App」を選択し作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/b4e81732-8832-05e1-0c67-f170ce4ff2c7.png)

次に「OAuth & Permissions」セクションに移動し、「Bot Token Scopes」で以下の権限を追加し、
  ```
  - app_mentions:read
  - chat:write
  - channels:read
  ```
  
「Install to Workspace」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/59333186-c352-b460-dd2d-c0e7b79503d2.png)

確認画面で「許可する」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/40de0b49-7fa6-c3a6-cb7e-38308189796a.png)

OAuth TokensのBot User OAuth Tokenと、Basic informationにあるSigning Secretは後で使用するので保管してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/b8e21340-195b-1988-2234-c1b151caeba9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/e6b428eb-0d60-0358-e350-f48e66a5f73b.png)

## 5.3 AWS側の環境構築
AWSコンソールで「Lambda」と検索し、クリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/f9db1b2d-f1a0-f2a1-4883-09aba25a5948.png)

そして「関数の作成」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/0c6d4ab8-d711-5617-c7ed-d9a3e74b31f3.png)

「設計図の使用」を選択し、設計図名は「Handle Slack slash commands」、関数名は任意に設定します。
実行ロールは「基本的な Lambda アクセス権限で新しいロールを作成」のままとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/127d3cae-1e48-d2c9-2afc-9c0a89f0a87c.png)

API Gatewayトリガーでは、インテントは「新規APIを作成」、APIタイプは「HTTP API」、セキュリティは「オープン」に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/d29277c8-4f47-bb0e-c90a-2fb72f409de0.png)

環境変数には以下を設定してください。

| 環境変数名 | 値 | 説明 |
|------------|----|----|
| `KNOWLEDGE_BASE_ID` | 作成したナレッジベースのID | 4.2で作成したKnowledge Baseから取得する動的な値 |
| `MODEL_ARN` | arn:aws:bedrock:[リージョン]:[アカウントID]:inference-profile/us.anthropic.claude-3-5-haiku-20241022-v1:0 | リージョンとアカウントIDを入力　※オレゴンリージョン(us-west-2)もしくはバージニア北部リージョン(us-east-1)のみ |
| `SLACK_BOT_TOKEN` | xoxb-で始まる文字列 | 5.2で作成したSlack AppのBot User OAuth Tokenから取得する動的な値 |
| `SLACK_SIGNING_SECRET` | 英数字の文字列 | 5.2で作成したSlack AppのSigning Secretから取得する動的な値 |

:::note info
#### リージョンとアカウントIDの確認方法
1. リージョン: マネジメントコンソール右上のリージョン選択から確認（例：バージニア北部の場合は「us-east-1」）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/465e9873-3051-f34e-c709-d327862a2252.png)

2. アカウントID: マネジメントコンソール右上の権限名/ユーザ名をクリックした後に表示されるハイフンなしの12桁の数字

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/bedb95a5-0683-ab69-5f59-5387fde79f50.png)

:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/5fa5c542-9b2f-7678-b34e-f791247f0b76.png)

「関数の作成」を選択します。

Lambda関数が正常に作成されることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/3617140e-ecb2-70ca-20fd-caccf9fa6d61.png)

そして、Lambda関数のコードに、下記コードを全文コピペし、「Deploy」を選択します。

```python
import json
import os
import urllib.request
import urllib.parse
import boto3
import logging
import hashlib
import hmac
import time

# ロギングの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 環境変数
SLACK_BOT_TOKEN = os.environ['SLACK_BOT_TOKEN']
SLACK_SIGNING_SECRET = os.environ['SLACK_SIGNING_SECRET']
KNOWLEDGE_BASE_ID = os.environ['KNOWLEDGE_BASE_ID']
MODEL_ARN = os.environ['MODEL_ARN']

# Bedrockクライアントの初期化
bedrock_agent_runtime = boto3.client('bedrock-agent-runtime')

def verify_slack_request(headers, body):
    """
    Slackリクエストの検証を行う関数
    """
    # タイムスタンプの取得と検証
    timestamp = headers.get('x-slack-request-timestamp', '')
    if not timestamp:
        logger.warning("No timestamp in request")
        return False

    # 現在時刻との差分を確認（5分以上経過したリクエストは拒否）
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        logger.warning("Request is too old")
        return False

    # 署名の検証
    slack_signature = headers.get('x-slack-signature', '')
    if not slack_signature:
        logger.warning("No signature in request")
        return False

    # 署名の計算と比較
    sig_basestring = f'v0:{timestamp}:{body}'
    calculated_signature = 'v0=' + hmac.new(
        SLACK_SIGNING_SECRET.encode(),
        sig_basestring.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(calculated_signature, slack_signature):
        logger.warning("Invalid signature")
        return False

    return True

def query_knowledge_base(query_text):
    """
    Knowledge Baseに問い合わせを行う関数
    """
    try:
        response = bedrock_agent_runtime.retrieve_and_generate(
            input={
                'text': query_text
            },
            retrieveAndGenerateConfiguration={
                'type': 'KNOWLEDGE_BASE',
                'knowledgeBaseConfiguration': {
                    'knowledgeBaseId': KNOWLEDGE_BASE_ID,
                    'modelArn': MODEL_ARN,
                    'retrievalConfiguration': {
                        'vectorSearchConfiguration': {
                            'numberOfResults': 5
                        }
                    }
                }
            }
        )
        return response['output']['text']
    except Exception as e:
        logger.error(f"Error querying knowledge base: {str(e)}")
        return "申し訳ありません。問い合わせ中にエラーが発生しました。"

def send_slack_message(channel_id, message, thread_ts=None):
    """
    Slackにメッセージを送信する関数
    """
    headers = {
        'Authorization': f'Bearer {SLACK_BOT_TOKEN}',
        'Content-Type': 'application/json'
    }
    payload = {
        'channel': channel_id,
        'text': message
    }
    if thread_ts:
        payload['thread_ts'] = thread_ts
    
    try:
        data = json.dumps(payload).encode('utf-8')
        req = urllib.request.Request(
            'https://slack.com/api/chat.postMessage',
            data=data,
            headers=headers,
            method='POST'
        )
        with urllib.request.urlopen(req) as response:
            return json.loads(response.read().decode('utf-8'))
    except Exception as e:
        logger.error(f"Error sending message: {str(e)}")
        return {'ok': False, 'error': str(e)}

def lambda_handler(event, context):
    try:
        # リクエストの詳細をログに記録
        logger.info(f"Received event: {json.dumps(event)}")
        
        # ヘッダーの準備（大文字小文字の違いに対応）
        headers = {k.lower(): v for k, v in event.get('headers', {}).items()}
        body = event.get('body', '')

        # 重複リクエストのチェック
        retry_num = headers.get('x-slack-retry-num')
        if retry_num:
            logger.info(f"Detected x-slack-retry-num: {retry_num}. Exiting to avoid processing a retry from Slack.")
            return {
                "statusCode": 200,
                "body": json.dumps({"message": "Request identified as a retry, thus ignored."})
            }

        # リクエストの検証
        if not verify_slack_request(headers, body):
            return {
                'statusCode': 401,
                'body': json.dumps({'error': 'Invalid request signature'})
            }

        if not body:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json'},
                'body': json.dumps({'error': 'No body provided'})
            }

        # Base64エンコードされている場合はデコード
        if event.get('isBase64Encoded', False):
            import base64
            body = base64.b64decode(body).decode('utf-8')
        
        slack_event = json.loads(body)

        # Slack URLの検証チャレンジ
        if 'challenge' in slack_event:
            return {
                'statusCode': 200,
                'headers': {'Content-Type': 'text/plain'},
                'body': slack_event['challenge']
            }

        # メンションイベントの処理
        if 'event' in slack_event:
            event_data = slack_event['event']
            if event_data.get('type') == 'app_mention':
                channel_id = event_data['channel']
                thread_ts = event_data.get('thread_ts', event_data['ts'])
                
                # メッセージテキストから不要な部分を削除
                message_text = event_data['text']
                # メンションを除去（<@USERID>形式）
                message_text = ' '.join(word for word in message_text.split() if not word.startswith('<@'))
                
                # 処理中メッセージを送信
                processing_message = "お問い合わせを処理中です。少々お待ちください..."
                send_slack_message(channel_id, processing_message, thread_ts)
                
                # Knowledge Baseに問い合わせ
                response_text = query_knowledge_base(message_text)
                
                # 最終的な回答を送信
                response = send_slack_message(channel_id, response_text, thread_ts)
                
                if not response.get('ok', False):
                    raise Exception(f"Failed to send message: {response.get('error', 'Unknown error')}")

        return {
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'message': 'Success'})
        }

    except Exception as e:
        logger.error(f"Error: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'error': str(e)})
        }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/d90e4ee3-4f96-3f2c-5f86-db8710fd8953.png)

続いて、タイムアウト時間を延長させるために、「設定」から「編集」を選択し、タイムアウトを30秒に変更し、保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/ce022265-6b41-f9a2-5e6f-ef284682730a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/3820dd88-11b6-98b1-0214-4aa398a2a84b.png)

 そして、Lambda関数にKnowledge Basesへの権限を与えるために、「アクセス権限」から「ロール名」を選択し、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/1bfc5570-f50b-14af-9dae-96de7b74d291.png)

許可ポリシーの許可を追加からポリシーをアタッチを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/bf388b5b-ddb3-e81a-851e-c12ead6070a0.png)

ポリシーの中から「AmazonBedrockFullAccess」を選択して許可を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/885dcd9d-3989-4ef8-62a5-255409073bfd.png)

そして、トリガーからAPI GatewayのAPI エンドポイントをコピーして保管しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/6b2744e0-4a8b-04b3-3ed9-824130705005.png)

### 5.3 Slack AppとLambdaの連携

Slack AppのEvent SubscriptionsのRequest URLに、先ほどコピーしたAPI Gatewayのエンドポイントを貼り付けます。

そして、「Subscribe to bot events」で「app_mention」を追加し、「Save Change」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/41b96734-ca6a-804d-a765-c53cddccb84a.png)

Slackチャンネルへのチャットボットを追加するために、使用したいSlackチャンネルを開き、
チャンネル右上の詳細から「アプリを追加する」を選択、作成したアプリを検索して追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/f41ef1b0-8109-6059-2f8b-66bff38411b6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/4ceb3e22-b07e-5fc9-b85e-3aff48465437.png)

:::note info
ワークスペースからSlackのアプリが見つからない場合、下記の記事を参考にして追加します。
https://qiita.com/lighlighlighlighlight/items/74b0b4940d49caff6bb8
:::

## 5.5 動作検証

Slackチャンネルでボットにメンションを送信して動作を確認します。

`@[ボット名] HULFT Squareはどこの会社が提供しているサービスですか？`と入力すると、Knowledge Basesと連携して適切な回答が返ってきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/f958b554-9baa-b06b-068f-0b2139d9a3fd.png)

これで、SlackからRAGシステムを利用できる環境が整いました。

チーム内で簡単に情報を共有・検索できるチャットボットとして活用することができます。

# 6. 本番運用に向けて

ここまでSlackチャットボットの実装方法を学んできましたが、実際の本番環境での運用に向けては、いくつかの考慮すべき点があります。

よって、本章ではシステムを本番環境で活用する際の注意点と、より良い実装のためのアプローチについて説明します。

## 6.1 運用コストについて

本システムの運用には、主に以下のAWSサービスの利用料金が発生します：

- Amazon Bedrock
- Amazon OpenSearch Serverless
- AWS Lambda
- Amazon API Gateway
- Amazon S3

これらを合計すると、最低月額約200ドル程度の費用が見込まれます。

特にOpenSearch Serverlessの利用料金が大きな割合を占めることに注意が必要です。

## 6.2 セキュリティ対策

現在の実装では、API Gatewayのエンドポイントがオープンな状態となっており、DDoSに対して脆弱です。

よって、本番環境では、API GatewayのオーソライザーにSlack Appの認証を追加したり、HTTP APIからREST APIへの変更しWAFの統合したりなどを検討する必要があります。

## 6.3 精度向上のためのポイント

実際に運用を始めると、期待した回答が得られないケースが発生することがあります。
その主な原因の一つが、チャンク分割の問題です。

Knowledge Basesのテスト画面では、検索時に取得されるチャンクを確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/6dadf0c5-dc6d-4e96-796f-dbc4f4b263c2.png)

上記のように、デフォルトのチャンク長（300トークン）では文章が断片的になってしまうことがあります。
この問題に対する解決策として、Knowledge Basesのチャンク戦略の設定を変更したり、元データを綺麗に分割しておくなどが考えられます。

## 6.4 代替実装方法の紹介

今回はLambda関数を使用した実装を行いましたが、より簡単な実装方法も存在します。

### AWS Chatbotによる実装
AWS Chatbotを使用することで、コードを書かずにSlackやTeamsとAmazon Bedrockを連携させることができます。ただし、AWSアカウントにつき1つしか作成できないため、今回のハンズオンでは扱いませんでした。

詳細は以下の記事を参照してください

https://qiita.com/moritalous/items/b63d976c2c40af1c39e5

### Web UIによる実装
SlackではなくWeb UIでチャットボットを実装したい場合は、AWS公式の生成AIサンプルアプリケーション「Generative AI Use Cases JP（GenU）」の活用をお勧めします。

このアプリケーションを使うことで、nowledge Basesを活用したRAGシステムの迅速な構築が可能で、
ReactやAWS CDKを使用しており、カスタマイズが容易です。

詳細は以下のリポジトリを参照してください

https://github.com/aws-samples/generative-ai-use-cases-jp

# 7. クリーンアップ手順

ハンズオン終了後は、不要な課金を防ぎ、使用しないリソースによるセキュリティリスクを排除するため、以下のリソースを必ず削除してください

- Lambda関数
- API Gateway
- LambdaのCloudWatch Logs
- Knowledge Bases
- OpenSearch Serverless ※最も高額なリソースのため、特に注意
- データソース用のS3バケット
- Knowledge basesのIAMロール
- Slack App

以上で本ハンズオンは終了となります。ご清聴ありがとうございました。