生成AIを活用した環境構築をやってみた　第3回：蓄積されたデータを基に旅行プランを生成する(画像ファイル編)

## はじめに
「生成AIを活用した環境構築をやってみた」の第3回記事です。
 
[第2回](https://qiita.com/mSatoCubesystem/items/0fe5a220e44bfda4516d)の記事では、S3にアップロードした画像のようをDynamoDBに登録することが出来ました。

今回の記事ではDynamoDBに登録された***画像の要素***を基にhtml形式で旅行プランを生成してもらう仕組みを作っていきます。
 
## 作業のゴール
今回は「***画像ファイルから生成された旅行プランの確認***」をゴールとします。

DynamoDBに登録された情報をもとに旅行プランを生成する仕組みを作成します。
写真をアップロードする都度、旅行プランを生成してしまうと似たようなプランが大量に生成されてしまう可能性がある為、今回は1日1回、プランを生成する仕組みとします。
作成したプランは最終的にはインターネット経由で参照できる仕組みとするため、html形式とします。
▼概要図
![gaiyou.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/650cee58-cf0d-4614-9528-b1dca0502743.png)

▼使用技術の説明
- ***Amazon S3***
Amazon Web Servicesが提供するオブジェクトストレージサービスです。
耐久性と可用性が高く、大量のデータを低コストで保存できます。データは複数のリージョンにレプリケートされ、セキュリティ設定やアクセス制御が可能です。
- ***DynamoDB*** 
AWSが提供するフルマネージドなNoSQLデータベースサービスです。スケーラブルで高パフォーマンスなデータアクセスが可能で、ストレージや性能の自動調整を行います。
前回作成した過去の旅行情報登録済のものを使いましょう。
- ***AWS Lambda***
コードをサーバーレスで実行するコンピューティングサービスです。
イベント駆動型の処理に最適であり、リクエストに基づき自動でスケーリングし、サーバーのプロビジョニングや管理が不要です。実行に対する課金により、コスト効率も追求できます。
- ***Amazon Bedrock***
機械学習モデルの迅速なデプロイと運用を支援するサービスです。
事前にトレーニングされたAIモデルを使用して、AIアプリケーションを効率的に構築できます。これにより、開発者は基盤構築よりもアプリケーションの改善に集中できます。
- ***EventBridge***
AWSのイベントバスサービスであり、各種AWSサービスやSaaSアプリケーションからのイベントを集約し、ルールに基づいて他のサービスへイベントを通知・配信することができます。
プランを生成を1日1回実行するスケジューラとして利用します。

## 作業の流れ
作業の流れとしては以下となります。
1. 手動で旅行プランを生成する
1. 自動で旅行プランを生成する

プランの作成には今回はBedrockの「Claude 3.5 Sonnet」を使用します。
※事前に基盤モデルのモデルアクセスをリクエストする必要がありますが、今回はアクセスが付与の環境のためリクエスト手順は割愛します。

## 各作業の説明
### 1 手動で旅行プランを生成する
Bedrockを使ってhtmlを生成するイメージをつかむ為、まずは手動で旅行プランを生成してみましょう。
 
#### 1.1 DynamoDBに登録されている情報の取得
DynamoDB ＞ 項目を探索 ＞ 「第2回で作成したテーブル名」へ遷移します。
テーブルの返された項目より、アクション ＞ 「結果をCSVにダウンロードする」を押下します。
この操作でDynamoDBに登録した情報をCSV形式で取得することが出来ました。
 
#### 1.2 旅行プランを生成するプロンプトの作成
Bedrock ＞ プレイグラウンド ＞ Chat / Text へ遷移します。
ModeをChatとし、「モデルを選択」
1.カテゴリ Anthropic ＞ 2.モデル Claude 3.5 Sonnet ＞ 3.推論 オンデマンド を指定して適用を押下します。
 
プロンプトを書く... に以下の情報を入力します。
 
```
あなたは神奈川県の情報に精通した旅行プランナーです。
大崎駅を出発地点とした旅行プランを、公共交通機関を使用する前提で考えてください。
旅行は土曜日の朝から日曜日の夜までの期間を考えます。
以下の情報は私が旅行した際に撮った写真の要素を分析したものです。1行目は各項目の項目名です。
この情報をもとに、私に推奨する旅行プランの生成してください。このプランは私のブログに掲載するため、html形式で出力する必要があります。
 
image	lable	confidence
image1.jpg	Mountain	99
image1.jpg	Nature	99
image1.jpg	Outdoors	99
image1.jpg	Water	99
image1.jpg	Wilderness	99
```
 
以下のような回答が返ってきました。PCにhtmlファイルとして保存しWEBブラウザで閲覧すると、最低限のWEBページにはできていそうですね。
 
``` html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>神奈川県自然満喫1泊2日旅行プラン</title> </head> <body> <h1>神奈川県自然満喫1泊2日旅行プラン</h1>
<h2>1日目（土曜日）</h2>
<ol>
<li>08:00 大崎駅出発</li>
<li>10:30 小田原駅着、小田原城見学</li>
<li>12:00 小田原駅から箱根登山鉄道で強羅へ</li>
<li>13:30 強羅公園で散策・昼食</li>
<li>15:00 箱根ロープウェイで大涌谷へ</li>
<li>16:30 大涌谷から芦ノ湖畔へバス移動</li>
<li>18:00 芦ノ湖畔のホテルにチェックイン</li>
<li>19:00 夕食は湖畔の景色を楽しみながらホテルで</li>
</ol>
 
<h2>2日目（日曜日）</h2>
<ol>
<li>06:30 早朝の芦ノ湖散策</li>
<li>08:00 ホテルで朝食</li>
<li>09:30 ホテル出発</li>
<li>10:00 箱根町港から海賊船で元箱根港へ</li>
<li>11:30 元箱根港から箱根神社へ徒歩移動、参拝</li>
<li>13:00 箱根関所跡で昼食・見学</li>
<li>15:00 箱根湯本駅へバス移動</li>
<li>16:30 箱根湯本駅から大崎駅へ向けて出発</li>
<li>19:00頃 大崎駅到着</li>
</ol>
 
<h3>旅行のポイント</h3>
<ul>
<li>箱根の山々、芦ノ湖、大涌谷など、雄大な自然を満喫できます。</li>
<li>ロープウェイや海賊船など、様々な乗り物で景色を楽しめます。</li>
<li>早朝の散策で静かな自然を体験できます。</li>
<li>歴史的な観光地（小田原城、箱根神社、箱根関所跡）も訪れます。</li>
</ul>
 
<p>このプランは、山、自然、アウトドア、水辺、原生林といった要素を含んでおり、箱根の豊かな自然を存分に楽しむことができます。公共交通機関を利用しながら、効率よく観光地を巡ることができるプランとなっています。天候や体力に応じて、適宜調整することをおすすめします。</p>
</body> </html>
```
 
 
### 2 自動で旅行プランを生成する
先ほどは手動でDynamoDBから情報を取得し、生成AIによりプランを生成しました。
毎回手動実行をすると手間がかかるので、これを自動で実行する仕組みを作成します。
 
### 2.1 Lambda関数の作成
- 関数名はvol3-image-lambdaとします
- ランタイムはPython 3.13とします
- 上記以外はデフォルトとします
- [関数の作成]をクリックします
![2-1-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/73a2a10d-4257-4bd2-9bcc-bcdab0b36294.png)

### 2.2 Lambda関数にポリシーを追加
- 以下3つのポリシーを割り当てます
![2-2-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/7c4771cc-69ba-4197-9874-1f2c4444abbe.png)

### 2.3 Lambda関数にPythonコードを記述
- 以下のコードを設定してDeployします
- コード内の「作成したDynamoDB名称」「バケット名」「保存するファイル名」「キー」については自身で作成したものを記載してください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/9d8ed9e8-e854-4feb-a229-3cd5bca69224.png)

``` python
import boto3
import json
from datetime import datetime
import tempfile


def lambda_handler(event, context):
    # DynamoDBからデータ取得
    dynamodb = boto3.resource('dynamodb')
    # 作成したDynamoDB名称を指定する
    table = dynamodb.Table('作成したDynamoDB名称')
    response = table.scan()

    # 取得したデータからプロンプトを作成
    data = response['Items']
    prompt = create_prompt(data)
    
    # 作成したプロンプトをBedrockに投げ込む
    bedrock_response = send_to_bedrock(prompt)

    # S3クライアントを作成
    s3_client = boto3.client('s3')

    # バケット名を指定
    bucket_name = "バケット名"
    # 保存するファイル名を指定 
    file_name = "保存するファイル名"
    Key = 'キー'

    # S3にファイルをアップロード
    with tempfile.NamedTemporaryFile() as tf:
        with open(tf.name, 'w+') as f:
            f.write(bedrock_response)
        s3_client.upload_fileobj(tf, bucket_name, Key, ExtraArgs={"ContentType": "text/html"})
    
    return {
        'statusCode': 200
    }

def create_prompt(data):
    # プロンプト作成ロジック
    labels = ','.join(item['lable'] for item in data)
    prompt = (
        "以下は旅行に行った際の写真の情報です。"
        + labels +
        "この情報を基におすすめの関東の旅行先をURL付きで3つ教えてください。"
        "また、大崎駅から日帰りでの旅程を検討してください。"
        "おすすめのランチ情報も載せてください。"
        "返答内容を、文字コードをUTF-8形式のhtmlタグでのみ返してください。"
    )
    return prompt

# 作成したプロンプトをBedrockに投げ込む
def send_to_bedrock(prompt):

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

    # パラメータの設定
    max_tokens = 5000
    system_prompt = "必ず日本語で答えてください"
    user_message = {"role": "user", "content": prompt}
    messages = [user_message]
    model_id = 'anthropic.claude-3-5-sonnet-20240620-v1:0'
    
    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": max_tokens,
        "system": system_prompt,
        "messages": messages,
        "temperature": 0.7,
        "top_p": 0.9,
    })
    
    response = bedrock_runtime.invoke_model(
        modelId=model_id,
        contentType='application/json',
        accept='application/json',
        body=body
    )

    response_body = json.loads(response.get('body').read())
    return response_body['content'][0]['text']

```
### 2.4 テスト実行
- lambda関数のテストを実行します
 ![2-4-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/66fd5950-4a67-42d3-af82-0b78e3577594.png)

- 想定通り実行されました。指定したS3バケットの中には生成されたhtmlファイルが格納されていますね
 ![2-4-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/c8f09e2c-92d0-496d-8d8d-c51d964f2705.png)

### 2.5 lambda関数の自動実行設定
- Lambda > 関数 > 作成したLambda関数 > 設定 > トリガーへ遷移します
- 関数の概要より「トリガーを追加」を押下します
- トリガーの設定よりEventBridgeを選択し、新規のルールを作成を行います
ルール名：createplan
ルールタイプ：スケジュール式　cron(0 * * * ? *)
![2-5-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/22b5fdd4-048a-4c8e-9ef1-7eee353602b8.png)

### 2.6 lambda関数の自動実行確認
- 今回は動作確認のため、1時間に1回実行する設定としました
- 1時間後にS3を確認すると、htmlファイルが更新されているはずです

### 2.7 旅行プランの確認
- 格納されたhtmlファイルを選択します
- [開く]をクリックします
![2-7-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/836b5806-4ffd-4778-b48c-492fd1ffa94a.png)

- 正常に動作すれば以下のようになります
![2-7-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4029590/e7785a26-28a4-4dbc-8be3-678d4480b66c.png)

## おわりに
画像ファイル1つから旅行プランの考案およびwebページ用のhtmlを自動生成することができましたね。
1枚の画像からここまで簡単にwebページが作れることに驚きと感動を覚えました。
次は音声ファイルからwebページ用のhtmlを作成できるか挑戦してみようと思います。