Amazon Bedrock+Anthropic Claude 3 Sonnetで会話履歴を保持するSlackチャットボットを作成する

# はじめに

前回の記事｢Amazon Bedrock+Anthropic Claude 3 SonnetでSlackチャットボットを作成する｣では､会話履歴を保持しないシンプルなSlackチャットボットを作成しました｡

https://qiita.com/revsystem/items/dc5809f05ef2732f996e

本記事では､DynamoDBを使って会話履歴を保持するチャットボットを作成してみました｡会話履歴の呼び出しや更新にLangchainのChatMessageHistoryを使うことで､DynamoDBに会話履歴を保存する処理を簡単に実装できました｡

# デモ動画

![会話履歴を保持したチャットボットのデモ動画](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/e45fbcb1-1fe8-b729-b7a3-6b5686e4504c.gif)

## チャットボットとの会話履歴

このように､追加の質問に対し､チャットボットは会話履歴を踏まえた回答を生成しました｡

```text
tsuyshi: AIとAGIの違いをAI初学者に分かりやすく説明してください｡説明は端的に､200字程度にまとめてください｡

bedrock-slack-app: AIとAGIの主な違いは以下のようにまとめられます。
AI(人工知能)は、特定の狭い領域での問題解決を目的とした、人工的にプログラムされた知能システムのことです。AIは人間の知的作業を一部代替するものの、汎用的な知能は備えていません。
一方、AGI(人工般化知能)は、人間並みの汎用的な知性と理解力を持つ知能システムを指します。AGIは自己学習が可能で、様々な領域での問題解決に応用できる知能を有することが期待されています。しかし、AGIの実現にはまだ多くの課題があります。

tsuyoshi: それを実現するには､今後どのような技術やハードウェアが必要と考えられていますか?200字程度にまとめてください｡

bedrock-skack-app: AGIを実現するためには、以下のような技術やハードウェアの進歩が必要とされています。
・より強力な計算能力を持つハードウェア(量子コンピューターなど)
・深層学習などの機械学習技術の高度化
・膨大なデータから知識を獲得する技術
・言語理解や推論、問題解決などの高度な人工知能技術
・人間のような感情や意識を模倣するための新たな理論
・脳科学の進歩による人間の知能メカニズムの解明
・倫理的な課題への対応(AGIの安全性など)
これらの技術が統合され、人間の知能を模倣できるようになれば、AGIの実現に近づくと考えられています。

tsuyoshi: なるほど｡では､倫理的課題についてはどうでしょう? AGIの倫理的課題はどのようなものが考えられますか?200字程度にまとめてください｡

bedrock-slack-app: AGIには以下のような倫理的な課題が考えられます。
・AGIの目的や価値観を人間がコントロールできるか
・AGIが人間に危害を加えるリスクをどう回避するか
・AGIによる職場の自動化が雇用に与える影響
・AGIによるプライバシー侵害やセキュリティ脅威
・AGIの意思決定の透明性と説明責任をどう確保するか
・AGIが人間を超えた場合の人間への影響
・AGIの権利や人工物としての扱いをどうするか
AGIが人間社会に大きな影響を与えることは避けられません。そのため、AGIの開発段階から倫理的配慮を十分に行い、リスクを最小限に抑える必要があります。
```

# 構成図

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/4b362e66-852c-755f-6783-f68a4217db98.png)

# 参考情報

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html

https://gihyo.jp/book/2023/978-4-297-13839-4

https://qiita.com/cyberBOSE/items/b202a43498b678414e3a

https://qiita.com/yu-Matsu/items/a93cb86428c52ddfb06c

https://qiita.com/_akira19/items/90eba2787a7460f400b9


# 環境構築

- 以下のリソースを作成･削除･変更できる権限をもつAWSユーザーを利用すること
  - AWS IAM
  - AWS Lambda
  - AWS CloudFormation
  - AWS Secrets Manager
  - Amazon API Gateway
  - Amazon S3
  - Amazon CloudWatch Logs
  - Amazon Bedrock
  - Amazon DynamoDB
- 使用するAWSリージョンは、us-east-1
- Slack Appを作成するためのアカウントや権限を持っている

## 開発環境構築

### 作業環境のOSバージョン

Windows 11上のWSLでUbuntu 23.04を動かしています。

```console
$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 23.04"
```

### Python環境

```console
$ python3 --version
Python 3.12.0
```

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

```
$ pip3 install --upgrade pip
$ pip3 --version
pip 24.0 from /home/xxx/.venv/lib/python3.12/site-packages/pip (python 3.12)
```

### AWS SAM CLIインストール

AWS上でサーバーレスアプリケーションを構築､実行するAWS SAMを使用します｡

[Installing the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) の手順に従い、AWS SAM CLIをインストールします。今回はx86_64環境でLinux OSを使用するため、x86_64 - command line installerの手順を実行します｡

```console
$ sam --version
SAM CLI, version 1.113.0
```

Slack Appの作成は､以前の記事を参考にしてください｡今回は､App Nameを`bedrock-slack-app`として作成しました｡

https://qiita.com/revsystem/items/93e9c744c63c5667a810#slack-app%E3%81%AE%E4%BD%9C%E6%88%90

### Slackシークレット情報をSecret Managerに登録

あらたにシークレットを作成し、ここまでの手順で作成した以下のシークレット情報を登録します。シークレット名は、`bedrock-slack-app`とします。このシークレット名は後述のtemplate.yaml内のSECRET_NAMEに定義しているため、異なるシークレット名を使用する場合はtemplate.yaml内のSECRET_NAMEも変更します。

|シークレットキー|値|
|-- |-- |
|SlackSigningSecret|SlackのSigning Secret|
|SlackBotToken|SlackのBot User OAuth Token|

![シークレット作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/4560082f-53fd-07ea-63e2-ed8ef005dfa2.png)

## アプリケーションの構築

ディレクトリ構造は以下のとおりです｡

```console
.
├── bedrock-slack-app
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
└── template.yaml
```

__init__.pyは空のファイルです｡
 bedrock-slack-app/requirements.txtは以下のとおりです｡boto3やrequestsも必要ですが､それらはLambdaレイヤーで追加するようtemplate.yamlに記述します｡

```console
slack-bolt
slack-sdk
langchain
```

#### template.yamlの構成

<details><summary>template.yaml (長いので折りたたんでいます｡クリックして展開)</summary>

```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Bedrock Slack App.

Resources:
  BedrockAssitantFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: bedrock-slack-app/
      Handler: app.lambda_handler
      Runtime: python3.12
      Role: !GetAtt LambdaRole.Arn
      Timeout: 300
      MemorySize: 512
      Architectures:
        - arm64
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref DynamoDBTable
      Environment:
        Variables:
          SECRET_NAME: 'bedrock-slack-app' # Name of the secret in Secrets Manager
          REGION_NAME: 'us-east-1' # Region of the secret in Secrets Manager
          DYNAMODB_TABLE_NAME: !Ref DynamoDBTable
      Events:
        Slack:
          Type: Api
          Properties:
            Method: POST
            Path: /slack/events
      Layers:
        # Layer for AWS Parameter Store and Secrets Manager
        # https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add
        - arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:11
        # Layer for boto3
        # https://github.com/keithrozario/Klayers?tab=readme-ov-file#list-of-arns
        - arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p312-arm64-boto3:3

  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: 'bedrock-slack-app-chat-history'
      AttributeDefinitions:
        - AttributeName: 'SessionId'
          AttributeType: 'S'
      KeySchema:
        - AttributeName: 'SessionId'
          KeyType: 'HASH'
      BillingMode: PAY_PER_REQUEST

  # IAM Role for lambda.
  LambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: bedrock-slack-app-lambda-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: allow-lambda-invocation
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                  - lambda:InvokeAsync
                Resource: "*"
        - PolicyName: SecretsManagerPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 'secretsmanager:GetSecretValue' # Required for Lambda to retrieve the secret
                Resource: "*"
        - PolicyName: allow-bedrock-access
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - bedrock:InvokeModel
                  - bedrock:InvokeModelWithResponseStream
                Resource: "*"
        - PolicyName: DynamoDBCrudPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                Resource: "*"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  BedrockLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${BedrockAssitantFunction}
      RetentionInDays: 14 # Optional. Default retention is 30 days.

Outputs:
  BedrockAssitantApi:
    Description: "The URL of Slack Event Subscriptions"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/slack/events"
  BedrockAssitantFunction:
    Description: "Bedrock Assistant Lambda Function ARN"
    Value: !GetAtt BedrockAssitantFunction.Arn
  BedrockAssitantFunctionIamRole:
    Description: "Implicit IAM Role created for Bedrock Assistant function"
    Value: !GetAtt LambdaRole.Arn

```

</details>

AWS SAM テンプレートファイル(template.yaml)に､作成するAWSリソースを定義します｡
Lambda関数用ロールやポリシー､Lambdaの環境変数などを記述します｡その他に､以下のレイヤーやリソースベースポリシーが含まれます｡

- Lambda関数からSecrets Managerにアクセスするための`AWS-Parameters-and-Secrets-Lambda-Extension`レイヤー
- Lambda関数内からimportするためのboto3をパッケージにしたレイヤー
- BedrockkからLambda関数を扱うためのリソースベースポリシー
- DynamoDBの操作を許可するポリシー
- DynamoDBテーブルの作成とSessionIdをプライマリキーに設定

DynamoDBテーブル名は`bedrock-slack-app-chat-history`としています｡そのほかに､テーブル操作を許可するポリシーを以下のようにtemplate.yamlに記述しています｡

```yaml
Resources:
  BedrockAssitantFunction:
    Type: AWS::Serverless::Function
    Properties:
(途中省略)
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref DynamoDBTable

(途中省略)
  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: 'bedrock-slack-app-chat-history'
      AttributeDefinitions:
        - AttributeName: 'SessionId'
          AttributeType: 'S'
      KeySchema:
        - AttributeName: 'SessionId'
          KeyType: 'HASH'
      BillingMode: PAY_PER_REQUEST

(途中省略)
  LambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      Policies:
(途中省略)
        - PolicyName: DynamoDBCrudPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                Resource: "*"
```

Lambdaレイヤーは以下のようにtemplate.yamlに記述しています｡

```yaml
      Layers:
        # Layer for AWS Parameter Store and Secrets Manager
        # https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add
        - arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:11
        # Layer for boto3
        # https://github.com/keithrozario/Klayers?tab=readme-ov-file#list-of-arns
        - arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p312-boto3:4
```

適用すると､Lambda関数のLayersに以下のように表示されます｡

![Lambdaレイヤー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/a55e7718-f8f9-7f98-e289-14e53704c994.png)

リソースベースポリシーは､以下のようにtemplate.yamlに記述しています｡

```yaml
  BacklogSearchFunction:
    Type: AWS::Serverless::Function
    Properties:

(途中省略)

  # Resouse based policy for lambda.
  PermissionForBacklogSearchToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt BacklogSearchFunction.Arn
      Action: lambda:InvokeFunction
      Principal: bedrock.amazonaws.com
```

適用すると､Lambda関数の設定のResource-based policy statementsに以下のように表示されます｡

![リソースベースのポリシー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/43f47c94-b549-32c5-f006-cce020ca1afe.png)

template.yaml内のEnvironmentにある`SECRET_NAME`と`REGION_NAME`には､それぞれ先ほど作成したSecrets Managerのシークレットの名前とリージョンを設定します｡

#### samconfig.tomlの構成

<details><summary>samconfig.toml (長いので折りたたんでいます｡クリックして展開)</summary>

```toml:samconfig.toml
# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1

[default]
[default.global.parameters]
stack_name = "bedrock-slack-app"

[default.build.parameters]
cached = true
parallel = true

[default.validate.parameters]
lint = true

[default.deploy.parameters]
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true
resolve_s3 = true
region = "us-east-1"

[default.package.parameters]
resolve_s3 = true

[default.sync.parameters]
watch = true

[default.local_start_api.parameters]
warm_containers = "EAGER"

[default.local_start_lambda.parameters]
warm_containers = "EAGER"

```
</details>

SAM CLIの実行設定ファイル(samconfig.toml)に､SAM CLIを実行する際の設定を定義します｡AWS SAMのチュートリアル: Hello World アプリケーションのデプロイを実行した際に作成されるsamconfig.tomlをもとにしています｡今回の例では､以下の点を変更しています｡

- `[default.global.parameters]`セクションのstack_nameを"sam-app"から"bedrock-slack-app"に変更
- `[default.deploy.parameters]`セクションにregion指定を追加
- `[default.deploy.parameters]`セクションのcapabilitiesを"CAPABILITY_IAM"から"CAPABILITY_NAMED_IAM"に変更

#### bedrock-slack-app/app.pyの構成

<details><summary>bedrock-slack-app/app.p (長いので折りたたんでいます｡クリックして展開)</summary>

```python:bedrock-slack-app/app.py
import ast
import logging
import os
import re
import time
from typing import Any

import boto3
from botocore.exceptions import ClientError
from langchain.callbacks.base import BaseCallbackHandler
from langchain.schema import LLMResult
from langchain_community.chat_message_histories import DynamoDBChatMessageHistory
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

CHAT_UPDATE_INTERVAL_SEC = 1

SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(
    format="%(asctime)s [%(levelname)s] %(message)s",
    level=logging.DEBUG
)

logger = logging.getLogger(__name__)


class SecretsManager:
    """
    Class to retrieve secrets from Secrets Manager

    Attributes:
        secret_name (str): The name of the secret
        region_name (str): The name of the region
        client (boto3.client): The client for Secrets Manager
    """

    def __init__(self, secret_name, region_name):
        self.secret_name = secret_name
        self.region_name = region_name
        self.client = boto3.client(
            service_name='secretsmanager',
            region_name=region_name
        )

    def get_secret(self, key):
        """
        Retrieves the value of a secret based on the provided key.

        Args:
            key (str): The key of the secret to retrieve.

        Returns:
            str: The value of the secret.

        Raises:
            ClientError: If there is an error retrieving the secret.
        """
        try:
            get_secret_value_response = self.client.get_secret_value(
                SecretId=self.secret_name
            )
        except ClientError as e:
            raise e

        secret_data = get_secret_value_response['SecretString']
        secret = ast.literal_eval(secret_data)

        return secret[key]


secrets_manager = SecretsManager(
    secret_name=os.environ.get("SECRET_NAME"),
    region_name=os.environ.get("REGION_NAME")
)

app = App(
    signing_secret=secrets_manager.get_secret("SlackSigningSecret"),
    token=secrets_manager.get_secret("SlackBotToken"),
    process_before_response=True,
)


class SlackStreamingCallbackHandler(BaseCallbackHandler):
    """
    A callback handler for handling events during Slack streaming.

    Attributes:
        last_send_time (float): The timestamp of the last message sent.
        message (str): The accumulated message to be sent.

    Args:
        channel (str): The Slack channel to send messages to.
        ts (str): The timestamp of the message to be updated.
    """

    last_send_time = time.time()
    message = ""

    def __init__(self, userid, channel, ts):
        self.userid = userid
        self.channel = channel
        self.ts = ts
        self.interval = CHAT_UPDATE_INTERVAL_SEC
        self.update_count = 0

    def on_llm_new_token(self, token: str, **kwargs) -> None:
        """
        Event handler for a new token received.

        Args:
            token (str): The new token received.
            **kwargs: Additional keyword arguments.
        """

        self.message += token

        now = time.time()
        if now - self.last_send_time > self.interval:

            # mention_message = f"<@{self.userid}> {self.message}"
            # message_blocks = create_message_blocks(mention_message)

            app.client.chat_update(
                channel=self.channel,
                ts=self.ts,
                text=f"<@{self.userid}> {self.message}",
                # blocks=message_blocks
            )
            self.last_send_time = now
            self.update_count += 1

            if self.update_count / 10 > self.interval:
                self.interval = self.interval * 2

    def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
        """
        Event handler for the end of Slack streaming.

        Args:
            response (LLMResult): The result of the Slack streaming.
            **kwargs: Additional keyword arguments.

        Returns:
            Any: The result of the event handling.
        """

        mention_message = f"<@{self.userid}> {self.message}"
        message_blocks = create_message_blocks(mention_message)

        app.client.chat_update(
            channel=self.channel,
            ts=self.ts,
            text=self.message,
            blocks=message_blocks
        )


def create_message_blocks(text):
    """
    Creates the message blocks for updating the Slack message.

    Args:
        text (str): The updated text for the Slack message.

    Returns:
        list: The message blocks for updating the Slack message.
    """

    message_context = "Claude 3 Sonnetで生成される情報は不正確な場合があります。"
    message_blocks = [
        {"type": "section", "text": {"type": "mrkdwn", "text": text}},
        {"type": "divider"},
        {"type": "context", "elements": [{"type": "mrkdwn", "text": message_context}]},
    ]
    return message_blocks


def handle_app_mentions(event, say):
    """
    Handle app mentions in Slack.

    Args:
        event (dict): The event data containing information about the mention.
        say (function): The function used to send a message in Slack.

    Returns:
        None
    """

    channel = event["channel"]
    thread_ts = event["ts"]
    input_text = re.sub("<@.*>", "", event["text"])
    userid = event["user"]

    id_ts = event["ts"]
    if "thread_ts" in event:
        id_ts = event["thread_ts"]

    result = say("\n\nお待ちください...", thread_ts=thread_ts)
    ts = result["ts"]

    message_history = DynamoDBChatMessageHistory(
            table_name="bedrock-slack-app-chat-history",
            session_id=id_ts,
            ttl=3600
        )
    system_message = """
あなたはチャットでユーザを支援するAIアシスタントです｡

* Let's think step by step
* Take a deep breath
* 回答のルールが<rules></rules>にあります｡ ルールを理解してください｡ルールは必ず守ってください｡例外はありません｡
* 「〜って何？」「〜とは？」「〜を説明して」というような概要を聞く質問については、「〜の概要」と読み替えてください｡

<rules>
 * 日本語で質問された場合、必ず日本語で回答してください｡
 * 質問に具体性がなく回答できない場合は、質問の仕方をアドバイスしてください｡､
</rules>
"""

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system_message),
            MessagesPlaceholder(variable_name="messages"),
            MessagesPlaceholder(variable_name="human_message")
        ]
    )

    callback = SlackStreamingCallbackHandler(userid=userid, channel=channel, ts=ts)

    llm = BedrockChat(
        model_id="anthropic.claude-3-sonnet-20240229-v1:0",
        streaming=True,
        callbacks=[callback],
        model_kwargs={
            "max_tokens": 1000,
            "temperature": 0.85,
            "top_p": 0.999
        },
        region_name="us-east-1"
    )

    # chainの定義
    chain = prompt | llm

    content = []
    content_text = {"type": "text", "text": input_text}
    content.append(content_text)
    result = chain.invoke(
            {
                "messages": message_history.messages,
                "human_message": [HumanMessage(content=content)]
            }
        )

    message_history.add_user_message(input_text)
    message_history.add_ai_message(result.content)


def respond_to_slack_within_3_seconds(ack):
    """
    Responds to a Slack message within 3 seconds.

    Parameters:
    - ack: A function to acknowledge the Slack message.

    Returns:
    None
    """
    ack()


app.event("app_mention")(
    ack=respond_to_slack_within_3_seconds,
    lazy=[handle_app_mentions]
)


def lambda_handler(event, context):
    """
    Lambda function handler for processing Slack events.

    Args:
        event (dict): The event data passed to the Lambda function.
        context (object): The runtime information of the Lambda function.

    Returns:
        dict: The response data to be returned by the Lambda function.
    """
    print(event)

    retry_counts = event.get("multiValueHeaders", {}).get("X-Slack-Retry-Num", [0])

    if retry_counts[0] != 0:
        logging.info("Skip slack retrying(%s).", retry_counts)
        return {}

    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)

```
</details>

bedrock-slack-backlog-rag-app/app.pyはメンションされたメッセージを取得し､Amazon Bedrockを使用して生成したテキストをストリーミングで返すアプリケーションです｡また､DynamoDBChatMessageHistoryを使用して､会話履歴をDynamoDBに保存します｡


## ビルドとデプロイ

ビルドとデプロイ､Slack Appの設定は以前の記事を参考にしてください｡

https://qiita.com/revsystem/items/93e9c744c63c5667a810#%E3%83%93%E3%83%AB%E3%83%89

## 動作確認

任意のチャンネンルに､`rock-slack-app` (Slack Appの作成時に設定した名前)を招待し､メンション形式で依頼をポストします｡動作しない場合のログやBedrockが返すレスポンスは CloudWatch Logs に出力されているログが参考になります｡
デモ動画の会話履歴は､DynamoDBのテーブルに以下のように保存されています｡DynamoDBへの履歴保存や取り出しを自前で実装するとかなり手間がかかりそうですが､LangchainのChatMessageHistoryを使うことで簡単に実装できます｡

```json:DynamoDBのテーブルに保存された会話履歴
{
  "SessionId": {
    "S": "1713112012.742099"
  },
  "expireAt": {
    "N": "1713115654"
  },
  "History": {
    "L": [
      {
        "M": {
          "data": {
            "M": {
              "additional_kwargs": {
                "M": {}
              },
              "content": {
                "S": " AIとAGIの違いをAI初学者に分かりやすく説明してください｡説明は端的に､200字程度にまとめてください｡"
              },
              "example": {
                "BOOL": false
              },
              "id": {
                "NULL": true
              },
              "name": {
                "NULL": true
              },
              "response_metadata": {
                "M": {}
              },
              "type": {
                "S": "human"
              }
            }
          },
          "type": {
            "S": "human"
          }
        }
      },
      {
        "M": {
          "data": {
            "M": {
              "additional_kwargs": {
                "M": {}
              },
              "content": {
                "S": "AIとAGIの主な違いは以下のようにまとめられます。\n\nAI(人工知能)は、特定の狭い領域での問題解決を目的とした、人工的にプログラムされた知能システムのことです。AIは人間の知的作業を一部代替するものの、汎用的な知能は備えていません。\n\n一方、AGI(人工般化知能)は、人間並みの汎用的な知性と理解力を持つ知能システムを指します。AGIは自己学習が可能で、様々な領域での問題解決に応用できる知能を有することが期待されています。しかし、AGIの実現にはまだ多くの課題があります。"
              },
              "example": {
                "BOOL": false
              },
              "id": {
                "NULL": true
              },
              "name": {
                "NULL": true
              },
              "response_metadata": {
                "M": {}
              },
              "type": {
                "S": "ai"
              }
            }
          },
          "type": {
            "S": "ai"
          }
        }
      },
      {
        "M": {
          "data": {
            "M": {
              "additional_kwargs": {
                "M": {}
              },
              "content": {
                "S": " それを実現するには､今後どのような技術やハードウェアが必要と考えられていますか?200字程度にまとめてください｡"
              },
              "example": {
                "BOOL": false
              },
              "id": {
                "NULL": true
              },
              "name": {
                "NULL": true
              },
              "response_metadata": {
                "M": {}
              },
              "type": {
                "S": "human"
              }
            }
          },
          "type": {
            "S": "human"
          }
        }
      },
      {
        "M": {
          "data": {
            "M": {
              "additional_kwargs": {
                "M": {}
              },
              "content": {
                "S": "AGIを実現するためには、以下のような技術やハードウェアの進歩が必要とされています。\n\n・より強力な計算能力を持つハードウェア(量子コンピューターなど)\n・深層学習などの機械学習技術の高度化\n・膨大なデータから知識を獲得する技術\n・言語理解や推論、問題解決などの高度な人工知能技術\n・人間のような感情や意識を模倣するための新たな理論\n・脳科学の進歩による人間の知能メカニズムの解明\n・倫理的な課題への対応(AGIの安全性など)\n\nこれらの技術が統合され、人間の知能を模倣できるようになれば、AGIの実現に近づくと考えられています。"
              },
              "example": {
                "BOOL": false
              },
              "id": {
                "NULL": true
              },
              "name": {
                "NULL": true
              },
              "response_metadata": {
                "M": {}
              },
              "type": {
                "S": "ai"
              }
            }
          },
          "type": {
            "S": "ai"
          }
        }
      },
      {
        "M": {
          "data": {
            "M": {
              "additional_kwargs": {
                "M": {}
              },
              "content": {
                "S": " なるほど｡では､倫理的課題についてはどうでしょう? AGIの倫理的課題はどのようなものが考えられますか?200字程度にまとめてください｡"
              },
              "example": {
                "BOOL": false
              },
              "id": {
                "NULL": true
              },
              "name": {
                "NULL": true
              },
              "response_metadata": {
                "M": {}
              },
              "type": {
                "S": "human"
              }
            }
          },
          "type": {
            "S": "human"
          }
        }
      },
      {
        "M": {
          "data": {
            "M": {
              "additional_kwargs": {
                "M": {}
              },
              "content": {
                "S": "AGIには以下のような倫理的な課題が考えられます。\n\n・AGIの目的や価値観を人間がコントロールできるか\n・AGIが人間に危害を加えるリスクをどう回避するか\n・AGIによる職場の自動化が雇用に与える影響\n・AGIによるプライバシー侵害やセキュリティ脅威\n・AGIの意思決定の透明性と説明責任をどう確保するか\n・AGIが人間を超えた場合の人間への影響\n・AGIの権利や人工物としての扱いをどうするか\n\nAGIが人間社会に大きな影響を与えることは避けられません。そのため、AGIの開発段階から倫理的配慮を十分に行い、リスクを最小限に抑える必要があります。"
              },
              "example": {
                "BOOL": false
              },
              "id": {
                "NULL": true
              },
              "name": {
                "NULL": true
              },
              "response_metadata": {
                "M": {}
              },
              "type": {
                "S": "ai"
              }
            }
          },
          "type": {
            "S": "ai"
          }
        }
      }
    ]
  }
}
```


`if now - self.last_send_time > self.interval:`の箇所で以下のようにコメントアウトしています｡ストリーミングで回答を生成している最中も[Section block](https://api.slack.com/reference/block-kit/blocks#section)を使って出力するする場合はこのコメントアウトを外してください｡

```python
            # mention_message = f"<@{self.userid}> {self.message}"
            # message_blocks = create_message_blocks(mention_message)

            app.client.chat_update(
                channel=self.channel,
                ts=self.ts,
                text=f"<@{self.userid}> {self.message}",
                # blocks=message_blocks
            )
```

# まとめ

前回作成したSlackチャットボットをベースに､DynamoDBを使って会話履歴を保持するSlackチャットボットを作成しました｡LangchainのChatMessageHistoryを使うことで､DynamoDBに会話履歴を保存する処理を簡単に実装できました｡
system_messageの内容を変更することで､チャットボットのキャラクターをカスタマイズしたり､口調を調整することもできると思います｡
もしくは､チャットボットへのメンションで`ラップのリズムに乗せて説明してください｡`といったリクエストを送ると､チャットボットがそれに答えてくれます｡(クオリティの善し悪しはさておき)

![チャットボットとの会話](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/71a9f0fe-96e2-ca6d-7723-9507b3e6de6c.png)