Amazon Bedrockで毎日小説が届くシステムを作ってみた

## はじめに
[Japan AWS Jr. Champions Advent Calendar](https://qiita.com/advent-calendar/2023/jr-champions) 10日目の記事です！

私は普段はネットワークやセキュリティサービスを中心にクラウドインフラ構築をしているのですが、AWS re:Invent 2023に参加して生成AIの勢いを肌で感じたので記事を書いてみました。

今回は、毎日新しい小説が届くシステムがあれば面白いと思いBedrockを活用して作ってみました！

## 実装内容
### 1. アーキテクチャ
シンプルなアーキテクチャです。
Bedrockで生成された小説をSNSでメール送信するLambda関数を、EventBridgeで毎日実行します。

![毎日小説が届くシステム.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/1859b3f0-f418-12b8-1f3c-0018e4563fb9.jpeg)


実装の手順を紹介していきます。
ちなみにすべてバージニア北部リージョン(us-east-1)にリソースを作成しています。

### 2. SNSトピックの作成
送信先のメールアドレスを登録したSNSトピックを作成しておきます。

![sns.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/f330727b-4e98-8179-f3ed-81ee81485816.png)


### 3. Bedrockのモデル有効化

Bedrockを利用するには、最初に利用したいモデルを有効化する必要があります。
今回はre:Invent 2023にてBedrockでの提供開始が発表されたClaude 2.1を利用したいと思います。
有効化する際に所属組織や利用目的を聞かれるので回答します。
数分待った後、画像のようにAccess grantedとなれば利用できるようになります。

![bedrock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/5b53bdb0-0613-ba82-4e70-2e5199139d89.png)

playgroundで試してみます。

![bedrock sandbox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/688971da-2785-eeec-fe91-972524161d69.png)


### 4. Lambda関数の作成

以下のパラメーターを指定してLambda関数を作成します。
- ランタイム： Python 3.11
- アーキテクチャ情報： x86_64
- 実行ロール： Bedrock APIを呼び出せるように以下の権限を持つIAMロールを指定します。
```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:*",
      "Resource": "*"
    }
  ]
}
```

また、Bedrock APIの応答には時間がかかるため、Lambdaのタイムアウトをデフォルトの3秒から1分に変更しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/e914a425-6994-c70f-535d-8ccb35dc81ce.png)


Bedrock APIを呼び出すコードを作成します。
各種パラメータは以下のドキュメントを参考に設定しました。

https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-claude.html

```python
import boto3
import json
import os

bedrock_runtime = boto3.client('bedrock-runtime')

def lambda_handler(event, context):
    # プロンプト
    prompt = '起承転結のある小説を書いてください。小説の本文以外は書かないでください'
    
    body = json.dumps({
        "prompt": "\n\nHuman:" + prompt + "\n\nAssistant:",
        "temperature": 0.5,
        "top_p": 1,
        "top_k": 250,
        "max_tokens_to_sample": 3000,
        "stop_sequences": ["\n\nHuman:"]
    })

    # Bedrock APIの呼び出し
    response = bedrock_runtime.invoke_model(
    	modelId = 'anthropic.claude-v2:1',
    	accept = 'application/json',
    	contentType = 'application/json',
        body = body
    )

    # レスポンスから回答本文を取り出す
    response_body = json.loads(nobel_response.get('body').read())
    output_nobel = response_body.get('completion')
    
    print(output_nobel)
```

実行したところエラーが発生しました。

![error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/681d996b-1670-36bb-73b9-fa1e34b6e00c.png)

Lambda標準のBoto3のバージョンが、Bedrockに対応していないことが原因のようです。
Boto3の最新バージョンをLambdaレイヤーに追加することでエラーは解消しました。

![lambda layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/91c60b24-27a9-6f55-7eb4-c42f2507cf0e.png)

エラーの解消にはこちらの記事を参考にさせていただきました。

https://dev.classmethod.jp/articles/lambda-boto3-layer/

続いて、先ほど作成しておいたSNSトピックを呼び出してBedrockにより出力された小説をメール送信するようにします。
SNSトピックのARNはLambda関数の環境変数に設定します。

![env.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/b4be74bb-a11f-0f95-149f-6dce68837b32.png)


```python
sns_client = boto3.client('sns')
topic_arn = os.environ['TOPIC_ARN']
    
params = {
    'TopicArn': topic_arn,
    'Subject': '短編小説メール' + today,
    'Message': output_nobel
    }
        
sns_client.publish(**params)
```

最後に、Lambda関数のトリガーとしてEventBridgeルールを作成します。
日本時間の朝7時に実行されるようにcronを設定します。

![eventbridge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/0c5d0975-ee0f-5ca5-4e23-04583e18a60d.png)


## 動作確認

Lambdaのコンソールからテスト実行をしたところメールが届きました！
プロンプトでの指示通り、起承転結がある構成になっています。

![nobel before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/d434facd-1f60-475f-cb47-58fed6b9034a.png)



少しコードに変更を加えてみます。
上記のコードではプロンプトの内容が固定であり毎日同じような小説が生成されてしまうため、実行する日付をテーマとした小説を生成するようにプロンプトを修正します。

```python
import datetime
from zoneinfo import ZoneInfo

dt_now = datetime.datetime.now(ZoneInfo("Asia/Tokyo"))
today = dt_now.strftime('%m月%d日')

# プロンプト
prompt = today + 'をテーマにした起承転結のある小説を書いてください。小説の本文以外は書かないでください'
```

メールの受信日をテーマにした小説が生成されるようになりました。


![nobel after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3567992/f04bfc8c-0962-bbe6-df15-5078537cd684.png)


## おわりに

生成AIを利用した開発の経験がなくても、思いついてから2~3時間程度でシステムを構築することができました！
やはりアイデアをすぐに形にできるところがクラウドの良さです。

今後は読者の好みを反映したり、前日の続きの話が受け取れるようにしたいと思います。
また、他のモデルも試してみたいと思います。
様々なモデルを利用して比較できる点がBedrockの魅力ですね。