LINEBOT でClaude 3 に問い合わせできるようにしてみる (AmazonBedrock)

# 今回作成する成果物
- AmazonBedrock (Claude 3 Sonnet)にLINEBOTから質問をできるようにする

### 参考サイト
https://www.ryucom.co.jp/blog/aws/4713/

https://qiita.com/taku-0728/items/c80bcf65aba318ac6db0

https://developers.line.biz/ja/docs/messaging-api/receiving-messages/#verify-signature

## 構成図

![LINEBOT×Claude_アーキテクチャー図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/4c129bf7-e163-e78b-5317-0eae68c3bae1.jpeg)

## 前提
- AmazonBedrockでClaude 3 Sonnetが利用できるアカウントをすでに所有している
- AWSのリージョンは、バージニア北部(us-east-1)を利用する
- LINEアカウントを持っている状態

## 必要な工程
- LINEBOTの作成
- AWS側の準備(APIGateway、lambda作成)
- APIGatewayとLINEBOTの繋ぎこみ
- 動作確認

### LINEBOTの作成
LINE Developerに接続し、LINEBOTを作成していきます。

https://developers.line.biz/en/

右上の「Log in to Console」から進んでいきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/fff33ee9-99b3-50f6-af91-949622a5d737.png)

LINEアカウントでログインを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/3e96f692-afbf-de37-971a-9bd94f739642.png)

ログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/2a6d9f43-a2b3-fe84-6cd1-b8039499d68a.png)

ログイン後、ProvidersでCreateボタンを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/eb344a9c-e2de-8804-a57b-a2196ea6cdd8.png)

BOTのNameを入れ、Createを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/13d589ad-3291-f026-98fb-a8241027e81d.png)

Message API チャンネルを作っていきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/5f8e2648-511f-04e9-01d5-a2b7786ef6aa.png)

必要事項を入力します
Company or owner's country or region: Japan
Channel name:LINEBOTの名前 (※7日間変更できません)
Channel description:チャンネルの説明
Category: 適切なもの
Subcategory: 適切なもの
Email address: 自分のメールアドレス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/e03b1d37-f2ac-af1a-b2a4-73183eab9b95.png)

確認画面が出るので、OKで進める

登録が終わったら、
MessageAPIのタブの一番下にある、
Channel access token (long-lived)を発行し、メモしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/ad70c432-e22f-a47a-b11d-3071e0086bdb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/82b31b89-65c4-bb09-74b1-76a497c5ee35.png)

Auto-reply messagesも有効となっているため、無効化する
LINEBOTを友達追加した際に、送信するメッセージや、受信したメッセージへ自動応答する設定
初期値だと、
「メッセージありがとうございます！申し訳ありませんが、このアカウントでは個別のお問い合わせを受け付けておりません。次の配信までお待ちください」
と応答してしまうため、OFFとする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/1fe0a6b8-e6ea-9fa9-7159-2a3e502ade5b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/8109aae5-3d62-6a81-97ed-94a5f32586fb.png)



### lambdaの準備

ランタイム: Python 3.12
アーキテクチャ: arm64
IAMロールの権限: AmazonBedrockへの権限、CloudWatchLogsへの権限

```python
import os
import json
from linebot import LineBotApi
from linebot.models import TextSendMessage
import boto3
import datetime

# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
# チャネルアクセストークンを使用して、LineBotApiのインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

# Amazon BedRock Claude 3 Sonnetクライアントを作成
bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')

def lambda_handler(event, context):
    try:
        # LINEからメッセージを受信
        if event['events'][0]['type'] == 'message':
            # メッセージタイプがテキストの場合
            if event['events'][0]['message']['type'] == 'text':
                # リプライ用トークン
                replyToken = event['events'][0]['replyToken']
                # 受信メッセージ
                messageText = event['events'][0]['message']['text']

                model_id='anthropic.claude-3-sonnet-20240229-v1:0'
                max_tokens=1000

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

                # Amazon BedRock Claude 3 Sonnetに質問を送信
                response = bedrock_runtime.invoke_model(body=body, modelId=model_id)
                response_body = response['body'].read().decode('utf-8')
                
                # Amazon BedRockからの応答の中身のみを取り出す
                response_text = json.loads(response_body)['content'][0]['text']

                print("受信したメッセージ:", messageText)
                print("Amazon BedRockからの応答:", response_text)

                # メッセージを返信（Amazon BedRock Claude 3 Sonnetからの応答の中身を返す）
                LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=response_text))

    # エラーが起きた場合
    except Exception as e:
        print(f"エラー: {e}")
        return {'statusCode': 500, 'body': json.dumps(f'Exception occurred: {str(e)}')}

    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
```

LINEBOTSDKのLayerも必要なので、
ローカル環境 または、Cloud9等で、layer用のZIPを作成する
> pip install line-bot-sdk -t python

Layer設定が完了したら、lambdaの設定タブで以下を変更する
一般設定 タイムアウト:10分
環境変数 LINE_CHANNEL_ACCESS_TOKENにChannel access token(メモしたやつ)を入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/e4982aca-f29f-9d9f-1bc6-a3f3f74bf6ad.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/62927c1d-bbfc-8f39-c367-b3db659d6d34.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/0b30e242-7884-8278-0106-ef53a0ec89d9.png)


一旦、ここでlambdaの動作チェックを行います。
テストイベントに以下のjsonを入れ、実行します。
500エラーは発生しますが、ひとまずBedrockから回答が来れば、Lambdaの設定は問題ありません。

```json
{
  "events": [
    {
      "type": "message",
      "message": {
        "type": "text",
        "text": "テストが見えていますか？"
      },
      "replyToken": "123456789",
      "mode": "active"
    }
  ]
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/2ff99655-f9c7-9902-5a8a-3d90e07c945d.png)

### APIGateway作成
APIGatewayを以下の要領で作成します。
プロトコル: REST
エンドポイント: Regional

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/6ee7285b-5944-8876-97d7-601991a39d61.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/7806144f-7f92-82fe-4871-a03f2f0cbd44.png)

メソッドの作成でPOSTを設定します。
その際、外部からたくさんのリクエストが来ると困るため、LINEからの通信のみ許可します。
HTTPリクエストヘッダーの設定で、x-line-signatureを追加し必須とします。
x-line-signatureが付与されていない通信が大量に来たとしても、APIGatewayで拒否できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/75c8dd45-615b-8003-2896-0054f100eded.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/f5bf67d3-73d9-3792-2076-18e25164fa0f.png)

画面が戻ったら、APIをデプロイを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/c60900df-d69e-6943-50ca-708f1d0c4aff.png)

初回デプロイなので、ステージがありません。
今回はprodとします。
※実際はなんでもOKです。のちほど出てくるWEBhookのURLになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/4beb4ca5-6712-17e2-1ca5-bc6894072fa2.png)

ステージの画面に戻ったら、URLをメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/107d948d-1607-3d4c-7734-7fbddaf6704e.png)

ここまで来たら、AWS側の設定は完了です。
LINE developerに戻り、APIGatewayとLINEBOTの連携設定を行います。

### LINEBOTとAPIGateway連携

Webhook settingsにメモしたAPIGatewayのURLを入力する。
入力後、Use webhookをONにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/8765ce56-e872-91a4-36f4-943e66803df2.png)

これで設定完了
あとは、QRコードを読み取って、Claudeとやり取りができるか確認します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/a6bfb456-031b-5efa-7919-ba42007a30ec.png)

### 動作確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/8a777705-340b-ae27-b192-fbc74e505313.png)
いろいろ間違ってますが、ひとまず応答してくれました

### IaC

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  NameTag:
    Type: String
    Description: 'Common resource name'
    Default: 'LineBOT'
  LINEBOTChannelAccessToken:
    Type: String
    Description: 'ChannelAccessToken for LINEBOT'
    NoEcho: true

Resources:
  linebotFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${NameTag}-lambda'
      Runtime: python3.12
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import os
          import json
          from linebot import LineBotApi
          from linebot.models import TextSendMessage
          import boto3
          import datetime

          # 環境変数からLINE Botのチャネルアクセストークンを取得
          LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
          # チャネルアクセストークンを使用して、LineBotApiのインスタンスを作成
          LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

          # Amazon BedRock Claude 3 Sonnetクライアントを作成
          bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')

          def lambda_handler(event, context):
              try:
                  # LINEからメッセージを受信
                  if event['events'][0]['type'] == 'message':
                      # メッセージタイプがテキストの場合
                      if event['events'][0]['message']['type'] == 'text':
                          # リプライ用トークン
                          replyToken = event['events'][0]['replyToken']
                          # 受信メッセージ
                          messageText = event['events'][0]['message']['text']

                          model_id='anthropic.claude-3-sonnet-20240229-v1:0'
                          max_tokens=1000

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

                          # Amazon BedRock Claude 3 Sonnetに質問を送信
                          response = bedrock_runtime.invoke_model(body=body, modelId=model_id)
                          response_body = response['body'].read().decode('utf-8')

                          # Amazon BedRockからの応答の中身のみを取り出す
                          response_text = json.loads(response_body)['content'][0]['text']

                          print("受信したメッセージ:", messageText)
                          print("Amazon BedRockからの応答:", response_text)

                          # メッセージを返信（Amazon BedRock Claude 3 Sonnetからの応答の中身を返す）
                          LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=response_text))

              # エラーが起きた場合
              except Exception as e:
                  print(f"エラー: {e}")
                  return {'statusCode': 500, 'body': json.dumps(f'Exception occurred: {str(e)}')}

              return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
      MemorySize: 512
      Timeout: 600
      Architectures: 
        - arm64
      Environment:
        Variables:
          LINE_CHANNEL_ACCESS_TOKEN: !Ref LINEBOTChannelAccessToken
      Tags:
        - Key: Name
          Value: !Ref NameTag

  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt linebotFunction.Arn
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/
    DependsOn:
      - ApiGateway

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: AmazonBedrock
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: bedrock:InvokeModel
                Resource: !Sub 'arn:aws:bedrock:${AWS::Region}::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0'
      Tags:
        - Key: Name
          Value: !Ref NameTag

  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub '${NameTag}-APIGateway'
      EndpointConfiguration:
        Types:
          - REGIONAL
      Tags:
        - Key: Name
          Value: !Ref NameTag
    DependsOn:
      - linebotFunction

  ApiGatewayRootMethod:
    Type: AWS::ApiGateway::Method
    DependsOn:
      - LambdaPermission
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !GetAtt ApiGateway.RootResourceId
      HttpMethod: POST
      AuthorizationType: NONE
      RequestParameters:
        method.request.header.x-line-signature: true
      MethodResponses:
        - StatusCode: '200'
          ResponseModels:
            application/json: Empty
      Integration:
        Type: AWS
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${linebotFunction.Arn}/invocations'
        PassthroughBehavior: WHEN_NO_MATCH
        ContentHandling: CONVERT_TO_TEXT
        TimeoutInMillis: 29000
        CacheNamespace: !GetAtt ApiGateway.RootResourceId
        IntegrationResponses:
          - StatusCode: '200'

  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - ApiGatewayRootMethod
    Properties:
      RestApiId: !Ref ApiGateway

  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    DependsOn:
      - ApiGatewayDeployment
    Properties:
      RestApiId: !Ref ApiGateway
      DeploymentId: !Ref ApiGatewayDeployment
      StageName: prod
      Tags:
        - Key: Name
          Value: !Ref NameTag
```

なお、上記のymlに、 **途中で作成した、LINEBOTSDKのlayer設定** が必要です。
パラメータ LINEBOTChannelAccessTokenには、LINEDeveloper内で取得した、
Channel access token (long-lived)の内容を入力してください。

## 最後に
LINEBOT×AWS×Claude 3 Sonnetで、LINEBOTを作ってみました。
LINE経由で、問い合わせできるのは、楽だなと思って作ってみましたが、誰かの役に立てばうれしいです。

なお、Amazon Bedrock (Claude 3 Sonnet Edition)だと、モデル提供ベンダーやAWSに問い合わせの履歴を見られることもないですが、
LINEは、トークのやり取りについて、情報収集を行っているため、機微な情報をやり取りする場合は、注意が必要かもしれません。

実際に、BOTを作るタイミングで、以下の同意画面が出ています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747609/8e33b93b-e2ec-8ec4-b251-2f63a9c848b4.png)
利用用途に応じて、適切にご利用ください！

ではまた！