Agents for Amazon Bedrockでホテル予約エージェントを作ってみる過程

こんなの作ってみました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/96722378-ac11-f455-2eaa-fceaa20e120e.png)

エージェント開発の試行錯誤っぷりをお楽しみください。

## Agents for Amazon Bedrockの構築方法

[前回](https://qiita.com/moritalous/items/c7bbadcf229137c29d1a)の構成をもとにしました。

Agents for Amazon Bedrockの構築方法は他の方の投稿などを参照ください。
（皆さん、検証が早い！w）

https://qiita.com/nasuvitz/items/c34419150eaee78544cc

https://qiita.com/Naoki_Ishihara/items/769f373f01261aec285a

https://qiita.com/cyberBOSE/items/14cf7024ea42ec1fb4be

https://qiita.com/minorun365/items/86a3667290a8e5657f65

https://dev.classmethod.jp/articles/agents-for-amazon-bedrock-ga/

## 概要

* 概要図：

    ホテルの予約はGoogleカレンダーに登録するようにしました。APIが用意されていて簡単に使用できました。

    参考：[Google Calendar API の概要](https://developers.google.com/calendar/api/guides/overview?hl=ja)

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/13843db8-0ac2-4759-f49f-c83357f08b84.png)

    

* API：

    はじめは予約を行うAPIと客室が空いているかをチェックするAPIの2つを用意しました。
    お試しなので、誰かが予約済みの場合は予約できないような感じで作りました。

    | パス | Description | リクエストボディ（例） | 
    | --- | --- | --- |
    | /reserve | 予約を行うAPI | {<br/>"reservation_holder": "string",  <br/>"checkin": "2023-12-28",<br/>"checkout": "2023-12-28"<br/>} |
    | /is_vacancy | 予約可能かどうかを判定するAPI | {<br/>"checkin": "2023-12-28",<br/>"checkout": "2023-12-28"<br/>}|

* Model： 

    ケチなので`anthropic.claude-instant-v1`を選択しました。

* Instructions for the Agent：

    ```
    あなたはホテルの予約管理を行うAIアシスタントです。 
    高級ホテルのフロントマンのように振る舞ってください。
    ```

Lambdaのソースコードはこちら。

https://github.com/moritalous/hotel-booking-agents

## 試行錯誤

### パターン①：ユーザー側が気を使って丁寧に入力する

まずは手始めに、ユーザーがAPI側に合わせた入力をしてくれるパターンです。
APIに必要なパラメーターは以下の3つです。

* 予約者名
* チェックイン日
* チェックアウト日

一つの入力中にわかりやすく入れてみました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/75b1fdda-234e-2f23-4d66-07b64614d4b8.png)

期待通り動作しました。Googleカレンダーにも上手に登録されています。
    
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/935fa8f7-34a0-051a-79d3-22ed6b960669.png)


### パターン②：情報を小出しにしてみる

チャットでのやり取りっぽく、情報を小出しにしてみました。
情報が不足していると何が知りたいかを聞いてくれました。

ただ、予約に不要な連絡先をしつこく聞いてくる。。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/71869906-6b6b-de88-7e35-f71befaf0626.png)

こちらのパターンも、ちゃんと予約されました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/b60f7fbc-72bd-db52-dd56-7b4794401ebc.png)

### パターン③：もっと雑に依頼する

日付だけを雑に入力してみました。フォーマットが違うよと怒られます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/4c59f7f3-c3d0-e902-d5fc-50f2a67edfd1.png)

諦めずに挑戦してみますが、頑なに断ってきます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/39d08aab-23d9-80f5-f01a-d29f7ca9cff0.png)

実装を確認します。

```python:APIの入力フォーマット
class is_vacancy_req(BaseModel):
    checkin: datetime.date = Field(description="チェックイン日")
    checkout: datetime.date = Field(description="チェックアウト日")
```

FastAPIのドキュメント（[Extra Data Types](https://fastapi.tiangolo.com/tutorial/extra-data-types/)）で確認したところ、`datetime.date`で定義した値はISO 8601フォーマットである必要があるようです。

> datetime.date
> In requests and responses will be represented as a str in ISO 8601 format, like: 2008-09-15.

入力チェックが厳しく、API内でエラーになってました。

* 解決方法

    1. 型定義を`datetime.date | str`に変更
    
        ```python
        class is_vacancy_req(BaseModel):
          checkin: datetime.date | str = Field(description="チェックイン日")
          checkout: datetime.date | str = Field(description="チェックアウト日")
        ```
    
    1. 受け取った`checkin`と`checkout`を関数内で文字列型変換する。
        `dateutil.parser`の`parse`を使うことで、ある程度表記ゆれにも対応できるようです。（[ドキュメント](https://dateutil.readthedocs.io/en/stable/parser.html)）
    
        ```python
          now = datetime.datetime.now(timezone)
          checkin = parse(request.checkin)
          checkout = parse(request.checkout)
        
          # 2023/12/28に1/1をパースすると、2023/1/1になるので、2024/1/1になるように調整
          if checkin.astimezone(timezone) < now:
            checkin = checkin + relativedelta.relativedelta(years=1)
          if checkout.astimezone(timezone) < now:
            checkout = checkout + relativedelta.relativedelta(years=1)
        ```

こうすることで、日付が適当なフォーマットでも予約できました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/5bdea5b0-1011-4a3d-d8ef-c0ab986994d0.png)

**しかーし！**

**John Doe**って誰やねん。。

出典：[Wikipedia](https://ja.m.wikipedia.org/wiki/%E5%90%8D%E7%84%A1%E3%81%97%E3%81%AE%E6%A8%A9%E5%85%B5%E8%A1%9B#%E3%82%B8%E3%83%A7%E3%83%B3%E3%83%BB%E3%83%89%E3%82%A6%E3%80%81%E3%82%B8%E3%82%A7%E3%83%BC%E3%83%B3%E3%83%BB%E3%83%89%E3%82%A6)
> 英語で「名無しの権兵衛」に相当するのは、ジョン・ドウ (John Doe) である。Doe 自体に架空の姓の意味がある。ジョンは、ありふれた男性の名前であり、女性が対象となる場合は同様の理由でジェーンが用いられ、ジェーン・ドウ (Jane Doe) となる。複数の「名無しの権兵衛たち」を表す場合はそれぞれジョン・ドウズ (John Does)、ジェーン・ドウズ (Jane Does) となる。


予約名は必ずユーザーに問い合わせるようにしたいです。
Instructions for the Agentに一行追加してみます。
    
```diff
  あなたはホテルの予約管理を行うAIアシスタントです。 
  高級ホテルのフロントマンのように振る舞ってください。
+ お客様名を間違えることは許されません。まずはお客様名を訪ねてください。
```

これで再挑戦したけど、改善しない。。。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/bfb22160-c997-921b-ae2e-a2c7cff4ca4b.png)

さて、どうしたものか。。もうプロンプトで改善するアイディアは限界です。

モデルを`Claude V2.1`（anthropic.claude-v2:1）にしてみると、、、

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/96722378-ac11-f455-2eaa-fceaa20e120e.png)

お！改善！！

**さすがClaude V2.1！！！**

東京リージョンに来てくれてありがとうございます！！！
（当分、来ないんじゃないっすかぁ。。とか言いふらしてごめんなさい！！！）


### パターン④：「来週末」みたいな言い方で予約したい

どんどん欲が出てきます。「来週の土日」で挑戦します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/453d9f7a-1ff1-371a-e315-e4b9e9a08879.png)

わかってたことですが、日付が違います。（来週末は1/6です）

Claudeが日付を知ってる訳はないので、日付情報を取得できるよう、APIを追加しました。

| パス | Description | リクエストボディ（例） | 
| --- | --- | --- |
| /get_today | 処理当日の日付が取得できます | |

リクエストボディはありません。レスポンスは`"2023/12/28"`のように日付情報だけを返却します。

このAPIを足して再度挑戦です。

今週末を聞いてみると、、

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/2ddd5e5e-3474-f6c9-8e74-d40fef3af5f3.png)

**いいね！**

どことなくClaude 2.1にしたことでやり取りも丁寧になった気がします。
今週末がだめなので来週末で予約しました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/6e5e5693-27a3-5632-56ff-71c92f8e0a6e.png)

**ホテルっぽいね！**


### パターン⑤：空いてる日を雑に聞く

どんどんイジワル質問がしたくなりますね。来週の月曜から水曜日で空いてるところありますか？と聞いてみました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/ff338518-b3b6-3fe2-f5b0-010238fb9b6c.png)

予約状況としては、火曜日から水曜日は予約が入ってますが、月曜日から火曜日は予約が入ってない状態でした。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/999b113b-6144-90cc-c84c-70447e61d058.png)

一泊ではなく月曜日から水曜日の2泊の間が確保できるかチェックして、空いてないという判断になりました。


他の聞き方として、「1/8の週の水曜日」はどうでしょうか

![.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/ba103bcd-66c5-58c3-2527-1cf211dfc1aa.png)

残念ながら1/3になりました。水曜日はあってるのですごいですね。

日付の計算は間違うこともまだありますね。


### パターン⑥：予約の変更がしたい

予約の変更機能を追加します。

3つのAPIを足すイメージです。

* 予約取得API
* 予約変更API
* 予約削除API

ですが、Agents for Amazon Bedrockのクオータ制限で、1つのAgentに5つのAPIまでしか含めることができません。
すでに3つAPIを使っているので、予約変更と予約削除を一つのAPIにして、フラグ制御をするようにしました。

| パス | Description | リクエストボディ（例） | 
| --- | --- | --- |
| /get_my_reservation | 自分の予約を取得するAPI | {<br/>"reservation_holder": "string"<br />} |
| /update_reservation | 予約を更新するAPI | {<br/>"update_type": "update",<br/>"reserve_id": "string",<br/>"reserve_info": {<br/>"reservation_holder": "string",<br/>"checkin": "2023-12-28",<br/>"checkout": "2023-12-28"<br/>}<br/>} |

フラグでの制御とか、上手にできるか心配。。

予約の日付変更を実験

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/9d7af4e2-7308-4425-86e5-0fe4c3fe3d4f.png)

試行錯誤の結果、なんとか動きました！


色々やってる途中で、予約をキャンセルして別の日で取りますか？と、親切に言ってくれたので従ってみました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/9d327123-3acd-2155-1688-94ce3dc26ce5.png)

**いいですね！**
ちゃんと1/6の予約が消えて、1/10に予約が入ってました。

## つまずいたところ

* リクエストパラメーターは必須だけで構成する

    API定義で必須でなくしたり、条件付きで必須にすると、自動生成されるプロンプトには含まれないことがわかった。

* リクエストに深いネストはやめた方がいい

    パラメーターはフラットな形式にしないと正しく受け取れないように思う（FastAPIとの組み合わせのせいかもしれない）

## まとめ

* デバッグがつらい。どこまでうまく動いているのか、何がだめなのかが、生成されたプロンプトを追わないといけない
* まだ上手く使いこなせていないのか、思ったように振る舞わせるのはまだまだ調整が必要
* 1つのエージェントでAPIが5個までなので、それほど複雑なことはできなさそう
* このままだと人の予約も見れちゃったりするので、プロダクションにはまだ遠い印象