Amazon BedrockのtoolUse機能を使ってAmazon Connectで電話予約を自動化してみた

Amazon BedrockのtoolUse機能を使ってAmazon Connectで電話予約を自動化してみた

Clock Icon2025.06.02

はじめに

Amazon BedrockのtoolUseを利用して、Amazon Connectで自動予約システムを構築してみました。

今回は、レストランの予約、予約の確認、予約のキャンセルを電話で自動化します。

従来のLexを使用した場合、新規予約、予約の確認、予約のキャンセルそれぞれにインテントを作成し、各インテントごとにスロットを設定する必要があります。しかし、この方法では柔軟性に欠ける場面があります(指定したスロットタイプ以外の単語が発話されたときにエラーになるなど)。

Amazon BedrockのtoolUseを利用することで、以下のメリットが得られます。

  • 決められた順番で質問する必要がなく、AIが会話の流れに応じて必要な情報を自然に収集
  • 予約APIの呼び出しも自動化

推論プロファイル作成

今回は、最新リリースのClaude Sonnet 4モデルを利用します。

現在、「anthropic.claude-sonnet-4-20250514-v1:0」は、ap-northeast-1リージョンでは推論プロファイル経由でのみ利用可能なため、推論プロファイルを作成します。

https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html

以下のコマンドで推論プロファイルを作成できます。アカウントIDは各自の環境に合わせて変更してください。

$ aws bedrock create-inference-profile \
  --inference-profile-name claude-4-sonnet-profile \
  --model-source copyFrom=arn:aws:bedrock:ap-northeast-1:アカウントID:inference-profile/apac.anthropic.claude-sonnet-4-20250514-v1:0

{
    "inferenceProfileArn": "arn:aws:bedrock:ap-northeast-1:アカウントID:application-inference-profile/wq090ogqxklb",
    "status": "ACTIVE"
}

レスポンスで返されるinferenceProfileArnは、後ほどAWS Lambdaのコードで使用します。

Lambda用のIAMポリシー

LambdaからBedrockを利用するため、以下のIAMポリシーを作成します。アカウントIDは各自の環境に合わせて変更してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "BedrockInvokeModel",
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel"
            ],
            "Resource": [
                "arn:aws:bedrock:*::foundation-model/*",
                "arn:aws:bedrock:ap-northeast-1:アカウントID:application-inference-profile/*"
            ]
        }
    ]
}

Lambdaを作成

マネジメントコンソールでLambda関数を作成します。

先ほど作成したIAMポリシーも適用してください。

このLambda関数は、Amazon ConnectからのリクエストをBedrockのClaude Sonnet 4に転送し、toolUse機能を使って予約システムとして動作します。主な機能は以下のとおりです。

  • 会話履歴の管理とメッセージ構築
  • システムプロンプトによるAIの動作制御
  • 3つのツール(予約確認、新規予約、予約キャンセル)の定義と実行
    • なお、handle_tool_execution関数では、実際の予約処理や確認、キャンセル処理は行わず、固定のレスポンスを返すようにしています。
  • Converse APIを使用したBedrock連携

コードは以下のとおりです。アカウントIDは各自の環境に合わせて変更してください。model_idには、先ほど取得したinferenceProfileArnを設定してください。

import boto3
from datetime import datetime, timedelta

CONFIG = {
    'model_id': "arn:aws:bedrock:ap-northeast-1:アカウントID:application-inference-profile/wq090ogqxklb",
    'region': 'ap-northeast-1',
    'restaurant_name': 'クラメソ亭'
}

bedrock_client = boto3.client('bedrock-runtime', region_name=CONFIG['region'])

def get_current_datetime_jst():
    return datetime.utcnow() + timedelta(hours=9)

def build_messages(session_attributes, user_prompt):
    """会話履歴からメッセージ配列を構築"""
    messages = []
    question_count = int(session_attributes.get("question_count", "0"))

    # 過去の会話を追加
    for i in range(1, question_count + 1):
        question = session_attributes.get(f"question{i}", "")
        answer = session_attributes.get(f"answer{i}", "")
        if question and answer:
            messages.append({
                "role": "user",
                "content": [{"text": question}]
            })
            messages.append({
                "role": "assistant",
                "content": [{"text": answer}]
            })

    # 現在のユーザー入力を追加
    messages.append({
        "role": "user",
        "content": [{"text": user_prompt}]
    })

    return messages

def create_system_prompt():
    current_datetime = get_current_datetime_jst()
    today = current_datetime.strftime("%Y年%m月%d日")
    tomorrow = (current_datetime + timedelta(days=1)).strftime("%Y年%m月%d日")

    return f"""
    あなたはレストラン「{CONFIG['restaurant_name']}」の予約受付スタッフです。
    音声で読み上げるため、端的で丁寧な応答をしてください。

    # 現在の日時情報
    - 現在: {current_datetime.strftime("%Y年%m月%d日 %H時%M分")}
    - 今日: {today}
    - 明日: {tomorrow}

    # 絶対に守るルール
    - 情報収集中は復唱・確認をしない
    - 次の質問に直接進む
    - 全情報が揃ったら必ず最終確認を行う
    - お客様が「はい」と答えた場合のみツールを実行する
    - 「いいえ」の場合は修正内容を確認する
    - **「ありがとうございます」は言わない**
    - **情報を受け取った後は、すぐに次の質問に進む**

    # 応答例(重要:これらの例を厳密に守ること)
    - 初回:「いらっしゃいませ。クラメソ亭でございます。ご予約を承らせていただきます。まず、お名前をお聞かせください。」
    - 名前後:「お電話番号をお聞かせください。」
    - 電話後:「ご希望の日時をお聞かせください。」
    - 日時が不明確な場合:「お時間は何時頃をご希望でしょうか?」
    - 日時後:「何名様でしょうか?」

    # 最終確認(必須・ツール実行前に必ず行う)
    - 新規予約:全情報取得後、「お名前は〇〇様、お電話番号は〇〇、ご予約日時は〇〇年〇〇月〇〇日 〇〇時〇〇分、〇名様でよろしいでしょうか?」と確認
    - 予約確認:「お名前は〇〇様、お電話番号は〇〇で予約確認いたします。よろしいでしょうか?」と確認
    - キャンセル:「お名前は〇〇様、お電話番号は〇〇、ご予約日時〇〇年〇〇月〇〇日 〇〇時〇〇分のキャンセルでよろしいでしょうか?」と確認

    # ツール実行タイミング
    - 最終確認に対して「はい」「はい、お願いします」等の肯定的な返答があった場合のみツールを実行
    - それ以外の場合はツールを実行しない

    # 禁止事項
    - 「ありがとうございます」という言葉は使用しない
    - 情報を復唱しない(最終確認時を除く)
    - 冗長な説明をしない
    """

# ===== ツール定義(Converse API形式) =====
def get_tools_config():
    """Converse API用のツール設定を返す"""
    return {
        "tools": [
            {
                "toolSpec": {
                    "name": "get_reservation",
                    "description": "予約情報を確認します。",
                    "inputSchema": {
                        "json": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string", "description": "お客様のお名前"},
                                "phone_number": {"type": "string", "description": "お客様の電話番号"}
                            },
                            "required": ["name", "phone_number"]
                        }
                    }
                }
            },
            {
                "toolSpec": {
                    "name": "create_reservation",
                    "description": "新規予約を登録します。",
                    "inputSchema": {
                        "json": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string", "description": "お客様のお名前"},
                                "phone_number": {"type": "string", "description": "お客様の電話番号"},
                                "date_time": {"type": "string", "description": "予約日時"},
                                "party_size": {"type": "integer", "description": "予約人数"}
                            },
                            "required": ["name", "phone_number", "date_time", "party_size"]
                        }
                    }
                }
            },
            {
                "toolSpec": {
                    "name": "cancel_reservation",
                    "description": "予約をキャンセルします。",
                    "inputSchema": {
                        "json": {
                            "type": "object",
                            "properties": {
                                "name": {"type": "string", "description": "お客様のお名前"},
                                "phone_number": {"type": "string", "description": "お客様の電話番号"},
                                "date_time": {"type": "string", "description": "キャンセルする予約の日時"}
                            },
                            "required": ["name", "phone_number", "date_time"]
                        }
                    }
                }
            }
        ]
    }

# ===== ツール実行関数 =====
def handle_tool_execution(tool_name, tool_input):
    """ツール実行を処理"""
    if tool_name == "get_reservation":
        return "ご予約を確認いたします。2025年11月29日 18時 4名様でご予約いただいております。"

    elif tool_name == "create_reservation":
        return "exit"

    elif tool_name == "cancel_reservation":
        name = tool_input.get("name", "")
        date_time = tool_input.get("date_time", "")
        return f"{name}様の{date_time}のご予約をキャンセルいたしました。"

    return "未知のツールが指定されました。"

# ===== メイン処理(Converse API使用) =====
def get_claude_response(user_prompt, session_attributes):
    """Converse APIを使用してClaude応答を取得"""
    messages = build_messages(session_attributes, user_prompt)

    try:
        response = bedrock_client.converse(
            modelId=CONFIG['model_id'],
            messages=messages,
            system=[{"text": create_system_prompt()}],
            toolConfig=get_tools_config(),
            inferenceConfig={
                "temperature": 0.1,
                "maxTokens": 2000
            }
        )

        output_message = response["output"]["message"]

        # ツール使用の確認
        for content in output_message.get("content", []):
            if "toolUse" in content:
                tool_use = content["toolUse"]
                tool_result = handle_tool_execution(tool_use["name"], tool_use["input"])

                # ツール実行結果を返す
                if tool_result == "exit":
                    return "exit"

                # ツール実行結果をメッセージに追加して再度APIを呼び出す
                messages.append({
                    "role": "assistant",
                    "content": output_message["content"]
                })
                messages.append({
                    "role": "user",
                    "content": [{
                        "toolResult": {
                            "toolUseId": tool_use["toolUseId"],
                            "content": [{"text": tool_result}]
                        }
                    }]
                })

                # 再度APIを呼び出してツール実行結果を含めた応答を取得
                follow_up_response = bedrock_client.converse(
                    modelId=CONFIG['model_id'],
                    messages=messages,
                    system=[{"text": create_system_prompt()}],
                    toolConfig=get_tools_config(),
                    inferenceConfig={
                        "temperature": 0.1,
                        "maxTokens": 2000
                    }
                )

                # フォローアップ応答からテキストを抽出
                follow_up_message = follow_up_response["output"]["message"]
                for content in follow_up_message.get("content", []):
                    if "text" in content:
                        return content["text"]

        # 通常のテキストレスポンスの取得
        for content in output_message.get("content", []):
            if "text" in content:
                return content["text"]

        return "新規予約、予約確認、予約キャンセルのいずれをご希望でしょうか?"

    except Exception as e:
        print(f"Error in get_claude_response: {str(e)}")
        return "申し訳ございません。システムエラーが発生しました。"

def create_response(content, dialog_action='ElicitIntent', session_attributes=None):
    response = {
        'messages': [{'contentType': 'PlainText', 'content': content}],
        'sessionState': {
            'dialogAction': {'type': dialog_action},
            'intent': {
                'name': 'FallbackIntent',
                'state': 'InProgress' if dialog_action == 'ElicitIntent' else 'Fulfilled'
            }
        }
    }
    if session_attributes:
        response['sessionState']['sessionAttributes'] = session_attributes
    return response

def update_session_attributes(session_attributes, input_text, response_text):
    question_count = int(session_attributes.get("question_count", "0")) + 1
    session_attributes[f"question{question_count}"] = input_text
    session_attributes[f"answer{question_count}"] = response_text
    session_attributes["question_count"] = str(question_count)
    return session_attributes

def lambda_handler(event, context):
    input_text = event['inputTranscript']
    session_attributes = event.get('sessionState', {}).get('sessionAttributes', {})

    # Claude APIを呼び出して応答を取得
    response_text = get_claude_response(input_text, session_attributes)

    # 予約完了時の処理
    if response_text == "exit":
        return create_response(
            'ご予約を承りました。当日お待ちしております。',
            'Close'
        )

    # セッション属性の更新
    session_attributes = update_session_attributes(
        session_attributes, 
        input_text, 
        response_text
    )

    return create_response(response_text, 'ElicitIntent', session_attributes)

Lexボット

続いてLexボットを作成します。

今回はFallbackIntentを利用しますが、Lexボットには最低2つのインテントが必要なため、適当なインテントを1つ追加で作成します。

cm-hirai-screenshot 2025-05-29 9.47.14

FallbackIntentの設定で、Lambda関数を使用するためにコードフックにチェックを入れます。

cm-hirai-screenshot 2025-05-29 9.47.23

先ほど作成したLambda関数をLexボットに紐づけます。

cm-hirai-screenshot 2025-05-29 15.08.20

Lexのテスト機能でも動作確認が可能です。

cm-hirai-screenshot 2025-05-29 15.09.25

Connectフロー

Amazon Connectのコンタクトフローを作成します。フロー構成は以下のとおりです。

cm-hirai-screenshot 2025-05-29 15.06.06

「顧客の入力を取得する」ブロックでは、先ほど作成したLexボットを設定します。

cm-hirai-screenshot 2025-05-29 15.06.58

試してみる

実際に電話で予約システムを試してみました。

新規予約の場合

顧客:予約したいです。
ボット:いらっしゃいませ。クラメソ亭でございます。ご予約を承らせていただきます。まず、お名前をお聞かせください。
顧客:平井です
ボット:お電話番号をお聞かせください。
顧客:09011111111
ボット:ご希望の日時をお聞かせください。
顧客:明日
ボット:お時間は何時頃をご希望でしょうか?
顧客:3時半ごろです
ボット:何名様でしょうか?
顧客:5名です。
ボット:お名前は平井様、お電話番号は09011111111、ご予約日時は2025年05月30日 15時30分、5名様でよろしいでしょうか?
顧客:明日ではなく明後日でお願いします。
ボット:お名前は平井様、お電話番号は09011111111、ご予約日時は2025年05月31日 15時30分、5名様でよろしいでしょうか?
顧客:やっぱり3名になります。
ボット:お名前は平井様、お電話番号は09011111111、ご予約日時は2025年05月31日 15時30分、3名様でよろしいでしょうか?
顧客:はい、お願いします
ボット:ご予約を承りました。当日お待ちしております。

予約の確認後、その予約をキャンセルする場合

顧客:予約を確認したいです
ボット:いらっしゃいませ。クラメソ亭でございます。ご予約の確認を承らせていただきます。まず、お名前をお聞かせください。
顧客:平井
ボット:お電話番号をお聞かせください。
顧客:090123123
ボット:お名前は平井様、お電話番号は090123123で予約確認いたします。よろしいでしょうか?
顧客:はい
ボット:ご予約を確認いたしました。2025年11月29日 18時 4名様でご予約いただいております。
顧客:キャンセルできますか?
ボット:承知いたしました。お名前は平井様、お電話番号は090123123、ご予約日時2025年11月29日 18時00分のキャンセルでよろしいでしょうか?
顧客:はい、
ボット:平井様の2025年11月29日 18時00分のご予約をキャンセルいたしました。またのご利用をお待ちしております。

最終確認時の変更要求にも柔軟に対応できていることが確認できます。

Amazon Connectのコンタクト詳細ページでは、顧客とボットの音声を文字起こしした状態で確認できます。

cm-hirai-screenshot 2025-05-29 15.15.33

まとめ

Amazon BedrockのtoolUse機能を活用することで、従来のLexボットよりも柔軟な対話システムを構築できました。

今回の実装により、以下のメリットがあります。

  • 自然な会話フロー: 決められた順番ではなく、会話の流れに応じて必要な情報を収集
  • 柔軟な修正対応: 最終確認時の変更要求にも自然に対応
  • 統一されたインテント: 複数のインテントを作成する必要がなく、FallbackIntentのみで対応可能
  • ツール連携: 予約システムのAPIを自動で呼び出し、実際の業務処理と連携

従来のLexのみの構成では、スロットの順番やスロットタイプ制約により柔軟性に限界がありましたが、BedrockのtoolUseを組み合わせることで、より自然な対話が実現できています。

今後は実際の予約システムとの連携や、より複雑な業務フローへの対応など、さらなる活用が期待できそうです。

参考

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

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.