1週間でツアーガイドさんを雇う

彼の誕生日に旅行へ行くことになったがプレゼントを用意していなかったので、
旅行を楽しんでもらうためにガイドさんを作ることにしました。

初めてのBedrock（生成AI）の活用に不安があったものの、
複雑な手順はなく簡単に構築できました。

## やりたいこと
- 日時をトリガーにメッセージ送信
　→EventBridge
- 特定の位置情報をトリガーにメッセージ送信
　→API Gateway、iOSショートカット機能（オートメーション）

# 構築

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/65e36820-e98e-4042-b2c5-5b1370817391.png)

## ①内部処理作成（Lambda、Bedrock）
### 〇Lambda
EventBridgeやAPI Gateway(iOSショートカット)で設定した**JSON**を
Lambdaで受け取り、
**状況に適したプロンプト（AIへの指示内容）をBedrockへ**送ります。

<details><summary>Lambdaソース（Python）</summary>

```rust:Python
import json
import boto3

sns = boto3.client('sns')
bedrock = boto3.client(service_name='bedrock-runtime', region_name='リージョン名') # 東京リージョンの場合「'ap-northeast-1'」

def lambda_handler(event, context):
    print("Navi_start")

    # Bedrockへの指示内容

    # 'body' キーがある場合（API Gateway(LambdaProxy統合)）
    if 'body' in event and event['body'] is not None:
        try:
            # body は文字列なので、JSON にパースする
            jsonbody = json.loads(event['body'])
        except json.JSONDecodeError as e:
            return {
                "statusCode": 400,
                "body": json.dumps({"error": f"Invalid JSON in body: {str(e)}"})
            }
    else:
        # 'body' キーがない場合（Lambdaテスト,EventBridge）
        jsonbody = event

    situation = (jsonbody['situation'])

    # 京都駅に到着したとき
    if situation == 'arrival':
        prompt = 'あなたは京都で働く観光ガイドです。観光客に向けて歓迎のメッセージと、京都の歴史や見どころについて簡潔に伝えてください。'
    # 宇治に到着したとき
    elif situation == 'uji':
        prompt = 'あなたは観光ガイドです。京都の宇治の歴史や見どころについて教えてください。'
    # 祇園に到着したとき
    elif situation == 'gion':
        prompt = 'あなたは観光ガイドです。京都の祇園の歴史や見どころについて教えてください。'
    # 京都を離れるとき
    elif situation == 'deperture':
        prompt = 'あなたは京都で働く観光ガイドです。私は本日京都観光を終えました。メッセージを伝えてください。'
    elif situation == '':
        prompt = ''
    print(prompt)

    body = json.dumps({
                "inputText": prompt,
                "textGenerationConfig": {
                    "maxTokenCount": 512,
                    "stopSequences": [],
                    "temperature": 0.5,
                    "topP": 0.9
                }
    })


    # Bedrockの設定
    # モデルはTitan Text G1 - Expressを使用
    modelId = 'amazon.titan-text-express-v1'
    accept = 'application/json'
    contentType = 'application/json'

    response = bedrock.invoke_model(
        body = body,
        modelId = modelId,
        accept = accept,
        contentType = contentType
    )


    # メッセージ送信
    
    # APIレスポンスからBODYを取り出す
    response_body = json.loads(response['body'].read())

    # BODYから応答のテキストを取り出す
    print (f"Full response from Bedrock: {response_body}")
    
    outputText = response_body.get('results')[0].get('outputText').replace("\nBot:", "")
    print (outputText)
    
    # Amazon SNSでSMS送信
    try:
        response = sns.publish(
            TopicArn = '先ほど作成したSNSトピックのARN',
            Message = outputText
        )
        print("SMS sent successfully:", response)
    except Exception as e:
        print("Error sending SMS:", e)


    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'success!'
        })
    }

```
modelIdはカタログに書いてある「Model ID」をそのまま使用します。

</details>

### 〇Bedrock
LambdaからBedrockを呼び出します。
- LambdaのIAMロールに、Bedrockの実行権限ポリシーを付与する
「IAM」＞「ロール」からLambdaに付与されているポリシーを選択し、「ポリシーを追加」します。
2つのアクションを許可したポリシーを付与します。

```rust:bedrock呼び出しポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowBedrockInvoke",
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": "*"
        }
    ]
}
```
- サービスの「Bedrock」＞「モデルカタログ」から、使用するモデルのアクセスを付与する

- （モデルは、「Bedrock」＞「モデルカタログ」内の「Open in playground」でお試し可能）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/f8ee3939-0b3a-45c7-944c-d8e7fa68feb9.png)


<details><summary>困ったエラー①　モデルアクセス</summary>

Bedrock呼び出し時、「IDが異なっていてモデルにアクセスできないよ」というエラーにハマりました。

```rust:表示されたエラー
[ERROR] AccessDeniedException: An error occurred (AccessDeniedException) when calling the InvokeModel operation: You don't have access to the model with the specified model ID.
```

わたしの場合はこれらが原因でした。
・モデルIDの綴りを間違えていた
・カタログに記載のモデルID（「:0」等も含む）を正確に記載していない
・Bedrock側でモデルアクセスをリクエストしていない
・リージョン名のタイプミス

</details>


<details><summary>困ったエラー②　JSON</summary>

トリガーに設定しようとしていたEventBridge（Lambdaのテスト）とAPI GatewayからLambdaを実行した時、それぞれの場合でTypeErrorを起こしていました。

```rust:EventBridgeから受け取り、Lambdaのテスト実行時
[ERROR] TypeError: can only concatenate str (not "NoneType") to str
```

```rust:API Gatewayから受け取り
[ERROR] TypeError: the JSON object must be str, bytes or bytearray, not NoneType
```
原因は、トリガーに設定しようとしていたEventBridge（Lambdaのテスト）とAPI Gatewayで、JSONの格納方法が異なっていたからでした。

なので、今回は受け取ったJSONの格納型によって処理を分けました。

```rust:Lambda(Python)
    # 'body' キーがある場合（API Gateway(LambdaProxy統合)）
    if 'body' in event and event['body'] is not None:
        try:
            # body は文字列なので、JSON にパースする
            jsonbody = json.loads(event['body'])
        except json.JSONDecodeError as e:
            return {
                "statusCode": 400,
                "body": json.dumps({"error": f"Invalid JSON in body: {str(e)}"})
            }
    else:
        # 'body' キーがない場合（Lambdaテスト,EventBridge）
        jsonbody = event
```

</details>

## ②出力先作成（Amazon SNS）
用意したメッセージはAmazon SNSを使用し、SMSで送信します。

**②-1．トピック作成**
タイプは「スタンダード」（FIFOの必要はない為）。
名前は任意（例では「TripGuide_guide」）です。
表示名は**メッセージの送信者**となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/f3ad25e4-8821-4f19-9a03-c13f6aebbb66.png)
↓　実際に届くメッセージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/d98095bf-95c6-45d4-ad4d-205245d15544.png)

**②-2．サブスクリプション作成**
今回は自身の電話番号宛にメッセージを送信します。
エンドポイント（電話番号）は、あらかじめサンドボックスに設定した電話番号を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/e851c16b-eef0-4d18-8576-084ab07e70b0.png)

## ③トリガー作成（EventBridge、API Gateway,iOSショートカット機能）
### 〇EventBridge
スケジュール設定などを行います。
ターゲットは、先ほど作成したLambdaを選択します。
ペイロード、アクセス許可は以下のように設定しました。
その他はデフォルト値など任意で設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/2e009bd5-acf7-47a7-a02e-74fbb25cfbbd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/b390e09a-afc3-4d28-902c-36238c57de39.png)


### 〇API Gateway
**③-1．新しいAPI作成**
「APIを作成」＞「REST API」を構築　を選択後、以下の設定画面となります。
「新しいAPI」を選択します。
API名は任意です。
APIエンドポイントタイプは、「リージョン」にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/d1d12488-0031-4ab8-a1c4-d45c76a6ec4a.png)


**③-2．「メソッド作成」**
「API」＞「リソース」にて「メソッドを作成」します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/c9f6a114-4752-4e3d-801a-5d201565b41d.png)

API Gatewayで受け取った情報はLambdaに渡したいので、
統合タイプ「Lambda関数」、
「Lambdaプロキシ統合」を設定します。

Lambda関数は、先ほど作成したLambdaを選択します。

その他の設定は任意です。
IAMロール（アクセス許可）はAPI Gateway作成後に自動で付けてくれるようです。
 
 <details><summary>（オプション）API Gatewayのログを出力したい</summary>
まず、サービスの「IAM」＞「ロール」にて「ロールを作成」します。

ユースケースで「API Gateway」を選択します。
自動でCloudWatch Logsの権限ポリシー（AmazonAPIGatewayPushToCloudWatchLogs）を設定してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/8a7be737-12e2-4eb6-a1e4-7effd837afc8.png)


「設定」＞ログ記録の「編集」で、作成したロールのARNを指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/90750f38-b4d3-4780-a87d-f9447b4ed23b.png)

出力したいログの種類は、「API」＞「ステージ」の「ログとトレース」にて、ステージごとに設定が可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/138770d9-e42a-4ace-a676-dc2da1e708fd.png)

</details>


 <details><summary> （オプション）HTTPリクエストヘッダーを設定したい</summary>
API GatewayにてHTTPリクエストヘッダーを設定できます。
設定した場合、リクエスト送信時（iOSショートカット）にも同じヘッダーを設定する必要があります。（下記に記載あり）

</details>

### 〇iOS ショートカット機能（オートメーション）
「ショートカット」アプリでAPI Gatewayへのリクエスト（JSON）を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/e76d98a5-86e1-469f-bfd4-00f322fa0a3d.png)

**アクション①：URL**
API Gatewayの「API」＞「ステージ」に記載されているURLを設定します。

**アクション②：URLの内容を取得**
変数に「URL」を設定し、詳細も記載する。
 ・方法：POST
 ・ヘッダ：なし
 （API GatewayのHTTPリクエストヘッダーを設定した場合は、ここで記載する）
 ・本文を要求：JSON　
 （例：{ situation: Uji } 宇治駅に到着した時メッセージを送信する設定）


このショートカットを、オートメーションに追加します。
・オートメーションを作成「＋」　＞「到着」で場所（宇治駅）を選択する。
・先ほど作成したショートカットを指定する。

 ## 完成！実行してみる
 登録した電話番号宛にメッセージが届きました！
 ↓　リクエストが{ situation: arrive }の場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3747814/4fd9b4fc-1cbc-4018-ab52-67014b65cac0.png)

一通りの構築をやってみてデータの流れが分かるようになり、良いお勉強になりました。
AWSがなんとなくだけわかるという方に、アプリ構築おすすめです。
今後このガイドさんを使い、Bedrockの拡張やセキュリティ面にも触れてみる予定です！