Amazon Connect + GPT-4 Turboで、予約内容を復唱後、顧客の色々な返答を正しくヒアリングできるか検証 – Amazon Connect アドベントカレンダー 2023

2023.12.01

Amazon Connect アドベントカレンダー 2023、1日目の記事です!

クラスメソッドとギークフィードさん、スカイアーチHRソリューションズ さんの有志が募ってチャレンジしている企画になります。

(アドベントカレンダーのカレンダー一覧はこちら↓)

はじめに

Amazon Connect + GPT-4 Turbo JSONモードで、予約内容を復唱して確認後、顧客の色々な返答を正しくヒアリングできるか検証しました。

前回、Amazon Connect + GPT-4 Turbo JSONモードで、1回の発話から下記の5つの予約情報をヒアリングするチャットボットを構築しました。

  • 名前
  • 電話番号
  • 予約日
  • 予約時間
  • 人数

予約情報をヒアリング後、Connect側で予約内容を復唱するところまでを前回行いました。

今回、予約内容を復唱後、顧客の返答をヒアリングするチャットボットを構築しました。

例えば、予約情報を以下のように復唱し、確認します。

  • 「お名前は、クラスメソッド様、電話番号は09011111111、予約日時は、2023年11月28日の19時、ご予約の人数は4名様ですね。」

正しい場合、以下の通りに「はい、そうです」と伝えると、予約が完了します。

間違っている場合、下記のように顧客が修正箇所を伝えた後、再度復唱し、問題なければ予約が完了します。

ただし、何度も顧客が修正箇所を伝えるのは、ユーザー体験として悪いため、1回間違えたら、オペレーターに繋ぐようにします。Connectフローでは、ループ回数を簡易に設定できます。

また、下記のように間違っている、かつ、顧客が修正箇所を伝えなかった場合、オペレーターに繋ぎます。

つまり、返答は大きく分けて以下の3パターンあります。

  • 予約内容が正しい場合
    • 正しいと返答し、予約完了
  • 予約内容が異なる場合
    • 予約の訂正内容を返答した場合、再度復唱し、問題なければ予約完了
      • 異なる場合オペレーターに繋ぐ
    • 予約内容が異なるだけの返答した場合、オペレーターに繋ぐ

日本語には、「合っています」「問題ありません」「それでお願いします」など、予約が正しい場合の返答だけでも、色々な伝え方がありますので、上記の3つパターンで色々な伝え方で試して、正しく認識できるか検証します

前提

  • 2023年11月時点での検証内容です。今後のアップデートにより改善される可能性があります。恒久的な結果ではありません。
  • 今回は、いくつかのサンプルで検証を行っただけであり、他のサンプルでも同様の結果となるとは限りません。これらの結果は一例として参照ください。

構築

ConnectフローとLambdaのコードのみを載せます。他は、前回の記事と同じです。

Connectフロー

フローは下記の通りです。

フロー上部は、前回の記事のフローです。

フローの下部は一般的な条件分岐とループを利用しています。フローの詳細は、前回の記事をご参考ください。

コンタクトフローは、エクスポートしましたので、気になる方は下記をご確認ください。

コンタクトフロー(クリックすると展開します)
{
  "Version": "2019-10-30",
  "StartAction": "8630447c-510e-47c9-b2aa-b2f3aa2452f9",
  "Metadata": {
    "entryPointPosition": { "x": -17.6, "y": 52 },
    "ActionMetadata": {
      "6b6a5984-8096-4946-987b-54537088a266": {
        "position": { "x": 264, "y": 28.8 },
        "children": ["b7292ab9-a159-4c0f-ad14-7c05ebcb0f7c"],
        "overrideConsoleVoice": true,
        "fragments": {
          "SetContactData": "b7292ab9-a159-4c0f-ad14-7c05ebcb0f7c"
        },
        "overrideLanguageAttribute": true
      },
      "b7292ab9-a159-4c0f-ad14-7c05ebcb0f7c": {
        "position": { "x": 264, "y": 28.8 },
        "dynamicParams": []
      },
      "8630447c-510e-47c9-b2aa-b2f3aa2452f9": {
        "position": { "x": 60, "y": 38.4 }
      },
      "ace21dc3-9b85-41e2-a0eb-32aaea2c0085": {
        "position": { "x": 461.6, "y": 8.8 }
      },
      "17b3d6aa-cb41-4601-b8fd-47a19c08dce2": {
        "position": { "x": 658.4, "y": 0 },
        "parameters": { "PromptId": { "displayName": "Beep.wav" } },
        "promptName": "Beep.wav"
      },
      "2e6627fa-f5fc-4f6c-b018-72d6f3624689": {
        "position": { "x": 1336.8, "y": 741.6 }
      },
      "5bf35837-3aad-4374-91f1-9ca0c27c5afc": {
        "position": { "x": 40, "y": 452.8 }
      },
      "69b3180b-f876-488f-998f-723d37f28153": {
        "position": { "x": 44.8, "y": 217.6 },
        "toCustomer": false,
        "fromCustomer": true
      },
      "d1aa2cd0-2c59-4321-a479-c83670e51601": {
        "position": { "x": 477.6, "y": 209.6 }
      },
      "6fd071f1-63e5-4bdf-bc61-afa46555ff47": {
        "position": { "x": 257.6, "y": 209.6 },
        "parameters": {
          "LexV2Bot": {
            "AliasArn": {
              "displayName": "TestBotAlias",
              "useLexBotDropdown": true,
              "lexV2BotName": "cm-hirai-one-voice"
            }
          }
        },
        "dynamicMetadata": {
          "x-amz-lex:audio:max-length-ms:*:*": false,
          "x-amz-lex:audio:start-timeout-ms:*:*": false,
          "x-amz-lex:audio:end-timeout-ms:*:*": false
        },
        "useLexBotDropdown": true,
        "lexV2BotName": "cm-hirai-one-voice",
        "lexV2BotAliasName": "TestBotAlias",
        "conditionMetadata": [
          {
            "id": "061bc93c-643d-427d-86a8-265f29e759cf",
            "operator": {
              "name": "Equals",
              "value": "Equals",
              "shortDisplay": "="
            },
            "value": "OneVoice"
          }
        ]
      },
      "c8417ac2-833e-4db8-b217-7c3a5f43f5e9": {
        "position": { "x": 259.2, "y": 992 },
        "parameters": { "PromptId": { "displayName": "Beep.wav" } },
        "promptName": "Beep.wav"
      },
      "49007031-591c-41e6-a446-ad9ac799e081": {
        "position": { "x": 256.8, "y": 451.2 },
        "parameters": {
          "LambdaFunctionARN": { "displayName": "cm-hirai-chatgpt" }
        },
        "dynamicMetadata": {}
      },
      "a9643ec3-3551-4413-9a6d-5fc1ff04ddaf": {
        "position": { "x": 484, "y": 451.2 },
        "conditions": [],
        "conditionMetadata": [
          {
            "id": "afc04313-dc62-4ef5-a577-e6088c87b13b",
            "operator": {
              "name": "Equals",
              "value": "Equals",
              "shortDisplay": "="
            },
            "value": "success"
          }
        ]
      },
      "f57d84b5-a511-4214-a271-8761733a8553": {
        "position": { "x": 699.2, "y": 456 },
        "parameters": {
          "Attributes": {
            "name": { "useDynamic": true },
            "phone_number": { "useDynamic": true },
            "date": { "useDynamic": true },
            "time": { "useDynamic": true },
            "number": { "useDynamic": true },
            "phone_number_convert": { "useDynamic": true },
            "date_convert": { "useDynamic": true },
            "time_convert": { "useDynamic": true }
          }
        },
        "dynamicParams": [
          "name",
          "phone_number",
          "date",
          "time",
          "number",
          "phone_number_convert",
          "date_convert",
          "time_convert"
        ]
      },
      "09619494-6c89-4083-bc40-0fcce47cdc62": {
        "position": { "x": 480, "y": 1411.2 }
      },
      "22315fde-b791-49d1-b3a1-61097d48785d": {
        "position": { "x": 54.4, "y": 1233.6 }
      },
      "bda850db-26be-4d44-8d0c-e43cb7eab32e": {
        "position": { "x": 273.6, "y": 1230.4 },
        "parameters": {
          "LambdaFunctionARN": { "displayName": "cm-hirai-chatgpt-2" }
        },
        "dynamicMetadata": {}
      },
      "b9cf4d35-ed55-4562-a1a6-43df0ddae1d0": {
        "position": { "x": 932, "y": 992.8 }
      },
      "9b3b4113-1f90-491d-8d21-c13c95cb5cd1": {
        "position": { "x": 472.8, "y": 998.4 },
        "toCustomer": false,
        "fromCustomer": true
      },
      "bfcaa372-8f96-4790-adc9-0aa23a43a6a5": {
        "position": { "x": 712.8, "y": 974.4 },
        "parameters": {
          "LexV2Bot": {
            "AliasArn": {
              "displayName": "TestBotAlias",
              "useLexBotDropdown": true,
              "lexV2BotName": "cm-hirai-one-voice"
            }
          }
        },
        "dynamicMetadata": {
          "x-amz-lex:audio:max-length-ms:*:*": false,
          "x-amz-lex:audio:start-timeout-ms:*:*": false,
          "x-amz-lex:audio:end-timeout-ms:*:*": false
        },
        "useLexBotDropdown": true,
        "lexV2BotName": "cm-hirai-one-voice",
        "lexV2BotAliasName": "TestBotAlias",
        "conditionMetadata": [
          {
            "id": "0d0615d0-c832-40ec-a7e4-ae549b6c92bd",
            "operator": {
              "name": "Equals",
              "value": "Equals",
              "shortDisplay": "="
            },
            "value": "OneVoice"
          }
        ]
      },
      "e934ef26-facb-438a-9972-204e9e9ebbc8": {
        "position": { "x": 939.2, "y": 1473.6 }
      },
      "55f7b24b-208a-43d8-b182-061fd5c050f8": {
        "position": { "x": 480, "y": 1216.8 },
        "conditions": [],
        "conditionMetadata": [
          {
            "id": "7d784be4-339a-4bb7-90f7-cc258badf914",
            "operator": {
              "name": "Equals",
              "value": "Equals",
              "shortDisplay": "="
            },
            "value": "true"
          }
        ]
      },
      "6e6efbde-31e5-4e13-a5ba-9187dfece4b4": {
        "position": { "x": 707.2, "y": 1212.8 }
      },
      "1174e064-aaab-4ac1-9b3c-5a928a2977d0": {
        "position": { "x": 51.2, "y": 988 }
      },
      "ea1420e6-dce0-482e-96bd-c509bd3e1425": {
        "position": { "x": 896, "y": 453.6 }
      },
      "ddc0145a-ac98-445b-bb5a-b1aad3dc1441": {
        "position": { "x": 1124.8, "y": 671.2 }
      },
      "a65b7aed-cf7d-4a4c-b468-42350f2ae79d": {
        "position": { "x": 1112.8, "y": 455.2 }
      }
    },
    "Annotations": [],
    "name": "demo",
    "description": "",
    "type": "contactFlow",
    "status": "published",
    "hash": {}
  },
  "Actions": [
    {
      "Parameters": {
        "TextToSpeechEngine": "Neural",
        "TextToSpeechStyle": "None",
        "TextToSpeechVoice": "Kazuha"
      },
      "Identifier": "6b6a5984-8096-4946-987b-54537088a266",
      "Type": "UpdateContactTextToSpeechVoice",
      "Transitions": { "NextAction": "b7292ab9-a159-4c0f-ad14-7c05ebcb0f7c" }
    },
    {
      "Parameters": { "LanguageCode": "ja-JP" },
      "Identifier": "b7292ab9-a159-4c0f-ad14-7c05ebcb0f7c",
      "Type": "UpdateContactData",
      "Transitions": {
        "NextAction": "ace21dc3-9b85-41e2-a0eb-32aaea2c0085",
        "Errors": [
          {
            "NextAction": "ace21dc3-9b85-41e2-a0eb-32aaea2c0085",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": { "FlowLoggingBehavior": "Enabled" },
      "Identifier": "8630447c-510e-47c9-b2aa-b2f3aa2452f9",
      "Type": "UpdateFlowLoggingBehavior",
      "Transitions": { "NextAction": "6b6a5984-8096-4946-987b-54537088a266" }
    },
    {
      "Parameters": {
        "Text": "予約に必要な、お名前、電話番号、予約の日付とお時間、人数をお伝え下さい。"
      },
      "Identifier": "ace21dc3-9b85-41e2-a0eb-32aaea2c0085",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "17b3d6aa-cb41-4601-b8fd-47a19c08dce2",
        "Errors": [
          {
            "NextAction": "17b3d6aa-cb41-4601-b8fd-47a19c08dce2",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "PromptId": "arn:aws:connect:ap-northeast-1:アカウントID:instance/3ff2093d-af96-43fd-b038-3c07cdd7609c/prompt/54bb3277-0484-45eb-bdc7-2e0b1af31b5c"
      },
      "Identifier": "17b3d6aa-cb41-4601-b8fd-47a19c08dce2",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "69b3180b-f876-488f-998f-723d37f28153",
        "Errors": [
          {
            "NextAction": "69b3180b-f876-488f-998f-723d37f28153",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {},
      "Identifier": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
      "Type": "DisconnectParticipant",
      "Transitions": {}
    },
    {
      "Parameters": { "Text": "メッセージをお預かりしました。" },
      "Identifier": "5bf35837-3aad-4374-91f1-9ca0c27c5afc",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "49007031-591c-41e6-a446-ad9ac799e081",
        "Errors": [
          {
            "NextAction": "49007031-591c-41e6-a446-ad9ac799e081",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "MediaStreamingState": "Enabled",
        "MediaStreamType": "Audio",
        "Participants": [
          { "ParticipantType": "Customer", "MediaDirections": ["From"] }
        ]
      },
      "Identifier": "69b3180b-f876-488f-998f-723d37f28153",
      "Type": "UpdateContactMediaStreamingBehavior",
      "Transitions": {
        "NextAction": "6fd071f1-63e5-4bdf-bc61-afa46555ff47",
        "Errors": [
          {
            "NextAction": "6fd071f1-63e5-4bdf-bc61-afa46555ff47",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "MediaStreamingState": "Disabled",
        "Participants": [
          { "ParticipantType": "Customer", "MediaDirections": ["To", "From"] }
        ],
        "MediaStreamType": "Audio"
      },
      "Identifier": "d1aa2cd0-2c59-4321-a479-c83670e51601",
      "Type": "UpdateContactMediaStreamingBehavior",
      "Transitions": {
        "NextAction": "5bf35837-3aad-4374-91f1-9ca0c27c5afc",
        "Errors": [
          {
            "NextAction": "5bf35837-3aad-4374-91f1-9ca0c27c5afc",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": ",",
        "LexV2Bot": {
          "AliasArn": "arn:aws:lex:ap-northeast-1:アカウントID:bot-alias/TM0D9GHJ7K/TSTALIASID"
        },
        "LexSessionAttributes": {
          "x-amz-lex:audio:max-length-ms:*:*": "13000",
          "x-amz-lex:audio:start-timeout-ms:*:*": "1000",
          "x-amz-lex:audio:end-timeout-ms:*:*": "1000"
        }
      },
      "Identifier": "6fd071f1-63e5-4bdf-bc61-afa46555ff47",
      "Type": "ConnectParticipantWithLexBot",
      "Transitions": {
        "NextAction": "d1aa2cd0-2c59-4321-a479-c83670e51601",
        "Conditions": [
          {
            "NextAction": "d1aa2cd0-2c59-4321-a479-c83670e51601",
            "Condition": { "Operator": "Equals", "Operands": ["OneVoice"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "d1aa2cd0-2c59-4321-a479-c83670e51601",
            "ErrorType": "NoMatchingCondition"
          },
          {
            "NextAction": "d1aa2cd0-2c59-4321-a479-c83670e51601",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "PromptId": "arn:aws:connect:ap-northeast-1:アカウントID:instance/3ff2093d-af96-43fd-b038-3c07cdd7609c/prompt/54bb3277-0484-45eb-bdc7-2e0b1af31b5c"
      },
      "Identifier": "c8417ac2-833e-4db8-b217-7c3a5f43f5e9",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "9b3b4113-1f90-491d-8d21-c13c95cb5cd1",
        "Errors": [
          {
            "NextAction": "9b3b4113-1f90-491d-8d21-c13c95cb5cd1",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "LambdaFunctionARN": "arn:aws:lambda:ap-northeast-1:アカウントID:function:cm-hirai-chatgpt",
        "InvocationTimeLimitSeconds": "8",
        "ResponseValidation": { "ResponseType": "JSON" }
      },
      "Identifier": "49007031-591c-41e6-a446-ad9ac799e081",
      "Type": "InvokeLambdaFunction",
      "Transitions": {
        "NextAction": "a9643ec3-3551-4413-9a6d-5fc1ff04ddaf",
        "Errors": [
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": { "ComparisonValue": "$.External.result" },
      "Identifier": "a9643ec3-3551-4413-9a6d-5fc1ff04ddaf",
      "Type": "Compare",
      "Transitions": {
        "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
        "Conditions": [
          {
            "NextAction": "f57d84b5-a511-4214-a271-8761733a8553",
            "Condition": { "Operator": "Equals", "Operands": ["success"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "ErrorType": "NoMatchingCondition"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Attributes": {
          "name": "$.External.name",
          "phone_number": "$.External.phone_number",
          "date": "$.External.date",
          "time": "$.External.time",
          "number": "$.External.number",
          "phone_number_convert": "$.External.phone_number_convert",
          "date_convert": "$.External.date_convert",
          "time_convert": "$.External.time_convert"
        },
        "TargetContact": "Current"
      },
      "Identifier": "f57d84b5-a511-4214-a271-8761733a8553",
      "Type": "UpdateContactAttributes",
      "Transitions": {
        "NextAction": "ea1420e6-dce0-482e-96bd-c509bd3e1425",
        "Errors": [
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": { "Text": "担当者に変わります。\n" },
      "Identifier": "09619494-6c89-4083-bc40-0fcce47cdc62",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "e934ef26-facb-438a-9972-204e9e9ebbc8",
        "Errors": [
          {
            "NextAction": "e934ef26-facb-438a-9972-204e9e9ebbc8",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": { "Text": "メッセージをお預かりしました。" },
      "Identifier": "22315fde-b791-49d1-b3a1-61097d48785d",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "bda850db-26be-4d44-8d0c-e43cb7eab32e",
        "Errors": [
          {
            "NextAction": "bda850db-26be-4d44-8d0c-e43cb7eab32e",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "LambdaFunctionARN": "arn:aws:lambda:ap-northeast-1:アカウントID:function:cm-hirai-chatgpt-2",
        "InvocationTimeLimitSeconds": "8",
        "ResponseValidation": { "ResponseType": "JSON" }
      },
      "Identifier": "bda850db-26be-4d44-8d0c-e43cb7eab32e",
      "Type": "InvokeLambdaFunction",
      "Transitions": {
        "NextAction": "55f7b24b-208a-43d8-b182-061fd5c050f8",
        "Errors": [
          {
            "NextAction": "09619494-6c89-4083-bc40-0fcce47cdc62",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "MediaStreamingState": "Disabled",
        "Participants": [
          { "ParticipantType": "Customer", "MediaDirections": ["To", "From"] }
        ],
        "MediaStreamType": "Audio"
      },
      "Identifier": "b9cf4d35-ed55-4562-a1a6-43df0ddae1d0",
      "Type": "UpdateContactMediaStreamingBehavior",
      "Transitions": {
        "NextAction": "22315fde-b791-49d1-b3a1-61097d48785d",
        "Errors": [
          {
            "NextAction": "22315fde-b791-49d1-b3a1-61097d48785d",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "MediaStreamingState": "Enabled",
        "MediaStreamType": "Audio",
        "Participants": [
          { "ParticipantType": "Customer", "MediaDirections": ["From"] }
        ]
      },
      "Identifier": "9b3b4113-1f90-491d-8d21-c13c95cb5cd1",
      "Type": "UpdateContactMediaStreamingBehavior",
      "Transitions": {
        "NextAction": "bfcaa372-8f96-4790-adc9-0aa23a43a6a5",
        "Errors": [
          {
            "NextAction": "bfcaa372-8f96-4790-adc9-0aa23a43a6a5",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": ",",
        "LexV2Bot": {
          "AliasArn": "arn:aws:lex:ap-northeast-1:アカウントID:bot-alias/TM0D9GHJ7K/TSTALIASID"
        },
        "LexSessionAttributes": {
          "x-amz-lex:audio:max-length-ms:*:*": "13000",
          "x-amz-lex:audio:start-timeout-ms:*:*": "1000",
          "x-amz-lex:audio:end-timeout-ms:*:*": "1000"
        }
      },
      "Identifier": "bfcaa372-8f96-4790-adc9-0aa23a43a6a5",
      "Type": "ConnectParticipantWithLexBot",
      "Transitions": {
        "NextAction": "b9cf4d35-ed55-4562-a1a6-43df0ddae1d0",
        "Conditions": [
          {
            "NextAction": "b9cf4d35-ed55-4562-a1a6-43df0ddae1d0",
            "Condition": { "Operator": "Equals", "Operands": ["OneVoice"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "b9cf4d35-ed55-4562-a1a6-43df0ddae1d0",
            "ErrorType": "NoMatchingCondition"
          },
          {
            "NextAction": "b9cf4d35-ed55-4562-a1a6-43df0ddae1d0",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {},
      "Identifier": "e934ef26-facb-438a-9972-204e9e9ebbc8",
      "Type": "DisconnectParticipant",
      "Transitions": {}
    },
    {
      "Parameters": { "ComparisonValue": "$.External.confirmed" },
      "Identifier": "55f7b24b-208a-43d8-b182-061fd5c050f8",
      "Type": "Compare",
      "Transitions": {
        "NextAction": "a9643ec3-3551-4413-9a6d-5fc1ff04ddaf",
        "Conditions": [
          {
            "NextAction": "6e6efbde-31e5-4e13-a5ba-9187dfece4b4",
            "Condition": { "Operator": "Equals", "Operands": ["true"] }
          }
        ],
        "Errors": [
          {
            "NextAction": "a9643ec3-3551-4413-9a6d-5fc1ff04ddaf",
            "ErrorType": "NoMatchingCondition"
          }
        ]
      }
    },
    {
      "Parameters": { "Text": "予約が完了しました。電話を切ります。" },
      "Identifier": "6e6efbde-31e5-4e13-a5ba-9187dfece4b4",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "e934ef26-facb-438a-9972-204e9e9ebbc8",
        "Errors": [
          {
            "NextAction": "e934ef26-facb-438a-9972-204e9e9ebbc8",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "よろしければ、はい、と、\n異なる場合、修正内容をお伝えください。"
      },
      "Identifier": "1174e064-aaab-4ac1-9b3c-5a928a2977d0",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "c8417ac2-833e-4db8-b217-7c3a5f43f5e9",
        "Errors": [
          {
            "NextAction": "c8417ac2-833e-4db8-b217-7c3a5f43f5e9",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": { "LoopCount": "2" },
      "Identifier": "ea1420e6-dce0-482e-96bd-c509bd3e1425",
      "Type": "Loop",
      "Transitions": {
        "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
        "Conditions": [
          {
            "NextAction": "a65b7aed-cf7d-4a4c-b468-42350f2ae79d",
            "Condition": {
              "Operator": "Equals",
              "Operands": ["ContinueLooping"]
            }
          },
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "Condition": { "Operator": "Equals", "Operands": ["DoneLooping"] }
          }
        ]
      }
    },
    {
      "Parameters": { "Text": "担当者に変わります。\n" },
      "Identifier": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
        "Errors": [
          {
            "NextAction": "2e6627fa-f5fc-4f6c-b018-72d6f3624689",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    },
    {
      "Parameters": {
        "Text": "お名前は、$.Attributes.name 、様、\nお電話番号は、$.Attributes.phone_number_convert 、\n予約日時は、$.Attributes.date_convert 、の\n、$.Attributes.time_convert 、\nご予約の人数は、$.Attributes.number 、めいですね。\n"
      },
      "Identifier": "a65b7aed-cf7d-4a4c-b468-42350f2ae79d",
      "Type": "MessageParticipant",
      "Transitions": {
        "NextAction": "1174e064-aaab-4ac1-9b3c-5a928a2977d0",
        "Errors": [
          {
            "NextAction": "ddc0145a-ac98-445b-bb5a-b1aad3dc1441",
            "ErrorType": "NoMatchingError"
          }
        ]
      }
    }
  ]
}

「予約完了です」と伝える「プロンプトの再生」ブロックがありますが、今回は実際には予約処理までは行いません。

また、オペレーターへの転送も実際には、キューを転送するブロックを作成しますが、今回は省略します。「プロンプトの再生」ブロックで「担当者に変わります」としています。

Lambda

Lambdaの設定については、前回の記事通りです。コードのみ異なります。

コードは下記の通りです。

from decimal import Decimal
from datetime import datetime, timedelta, timezone
from ebmlite import loadSchema
from enum import Enum
from botocore.config import Config
import boto3, os, struct, json, openai

openai.api_key = os.environ["API_Key"]

JST = timezone(timedelta(hours=+9))
date = lambda: datetime.now(JST).strftime('%Y%m%d')
time = lambda: datetime.now(JST).strftime('%H%M')
current_date = date()
current_time = time()

def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

class Mkv(Enum):
    SEGMENT = 0x18538067
    CLUSTER = 0x1F43B675
    SIMPLEBLOCK = 0xA3

class Ebml(Enum):
    EBML = 0x1A45DFA3

class KVSParser:
    def __init__(self, media_content):
        self.__stream = media_content["Payload"]
        self.__schema = loadSchema("matroska.xml")
        self.__buffer = bytearray()

    @property
    def fragments(self):
        return [fragment for chunk in self.__stream if (fragment := self.__parse(chunk))]

    def __parse(self, chunk):
        self.__buffer.extend(chunk)
        header_elements = [e for e in self.__schema.loads(self.__buffer) if e.id == Ebml.EBML.value]
        if header_elements:
            fragment_dom = self.__schema.loads(self.__buffer[:header_elements[0].offset])
            self.__buffer = self.__buffer[header_elements[0].offset:]
            return fragment_dom

def get_simple_blocks(media_content):
    parser = KVSParser(media_content)
    return [b.value for document in parser.fragments for b in 
            next(filter(lambda c: c.id == Mkv.CLUSTER.value, 
                        next(filter(lambda s: s.id == Mkv.SEGMENT.value, document)))) 
            if b.id == Mkv.SIMPLEBLOCK.value]

def create_audio_sample(simple_blocks, margin=4):
    position = 0
    total_length = sum(len(block) - margin for block in simple_blocks)
    combined_samples = bytearray(total_length)
    for block in simple_blocks:
        temp = block[margin:]
        combined_samples[position:position+len(temp)] = temp
        position += len(temp)
    return combined_samples

def convert_bytearray_to_wav(samples):
    length = len(samples)
    channel = 1
    bit_par_sample = 16
    format_code = 1
    sample_rate = 8000
    header_size = 44
    wav = bytearray(header_size + length)
    
    wav[0:4] = b"RIFF"
    wav[4:8] = struct.pack("<I", 36 + length)
    wav[8:12] = b"WAVE"
    wav[12:16] = b"fmt "
    wav[16:20] = struct.pack("<I", 16)
    wav[20:22] = struct.pack("<H", format_code)
    wav[22:24] = struct.pack("<H", channel)
    wav[24:28] = struct.pack("<I", sample_rate)
    wav[28:32] = struct.pack("<I", sample_rate * channel * bit_par_sample // 8)
    wav[32:34] = struct.pack("<H", channel * bit_par_sample // 8)
    wav[34:36] = struct.pack("<H", bit_par_sample)
    wav[36:40] = b"data"
    wav[40:44] = struct.pack("<I", length)
    
    wav[44:] = samples
    return wav

def create_kvs_client():
    region_name = "ap-northeast-1"
    return boto3.client("kinesisvideo", region_name=region_name)

def create_archive_media_client(ep):
    region_name = "ap-northeast-1"
    return boto3.client("kinesis-video-archived-media", endpoint_url=ep, config=Config(region_name=region_name))

def get_media_data(arn, start_timestamp, end_timestamp):
    kvs_client = create_kvs_client()

    list_frags_ep = kvs_client.get_data_endpoint(StreamARN=arn, APIName="LIST_FRAGMENTS")["DataEndpoint"]
    list_frags_client = create_archive_media_client(list_frags_ep)

    fragment_list = list_frags_client.list_fragments(
        StreamARN = arn, 
        FragmentSelector = {
            "FragmentSelectorType": "PRODUCER_TIMESTAMP",
            "TimestampRange": {"StartTimestamp": datetime.fromtimestamp(start_timestamp), "EndTimestamp": datetime.fromtimestamp(end_timestamp)}
        }
    )

    sorted_fragments = sorted(fragment_list["Fragments"], key = lambda fragment: fragment["ProducerTimestamp"])
    fragment_number_array = [fragment["FragmentNumber"] for fragment in sorted_fragments]
    print("Received fragment_number_array:" + json.dumps(fragment_number_array,default=decimal_to_int, ensure_ascii=False))


    get_media_ep = kvs_client.get_data_endpoint(StreamARN=arn, APIName="GET_MEDIA_FOR_FRAGMENT_LIST")["DataEndpoint"]
    get_media_client = create_archive_media_client(get_media_ep)

    media = get_media_client.get_media_for_fragment_list(StreamARN = arn, Fragments = fragment_number_array)
    return media

def save_audio_to_tmp(wav_audio, filename="output.wav"):
    with open(f"/tmp/{filename}", "wb") as out_file:
        out_file.write(wav_audio)

def transcribe_audio_file(file_path):
    with open(file_path, "rb") as audio_file:
        return openai.Audio.transcribe("whisper-1", audio_file)

def extract_json_format(input_text):
    input_text = f"""
    ## 役割
    あなたは、お客さんから聞いた予約情報を最後に繰り返して確認した際、お客さんの返答から、はい、もしくは、いいえを判断し、JSON形式で出力するシステムです。
    ## ルール
    - 予約情報は、「名前(name)」「電話番号(phone_number)」「日付(date)」「時間(time)」「人数(number)」の5つを確認しました
        - 「名前(name)」
            - 名前は、ひらがな、に変換ください。
        - 「電話番号(phone_number)」
            - 電話番号にハイフン(-)は必要ないので、削除してください。数字のみを出力ください。
        - 「予約の日付(date)」
            - 「予約の日付(day)」は、8桁の数字での日付形式(例"20231118")に変換してください。
            - 今日の日付は、{current_date}、です。「予約の日付(date)」は、未来の値です。
                - 例 3日と言われたら、今月の3日のことです。もし今月の3日が過ぎていれば、翌月の3日です。
                - 例 5月と言われたら、今年の5月のことです。今年の5月が過ぎていれば、翌年の5月です。
                - 今日の予約も可能です。
            - 月曜日が週の最初で週末は土日を指します。
        - 「予約時間(time)」
            - 予約可能日時は、9:00 ~ 20:00です。
            - 5時と言われたら、0500ではなく、1700のことです。
            - 今の時間は、{current_time}、です。「予約の時間(time)」は、未来の値です。
            - 今から予約も可能です。
        ## 例
        {{
        "name": "やまだいちろう",
        "phone_number": "09011111111",
        "date": "20231120",
        "time": "1430",
        "number": "2",
        }}
        - 「人数(number)」
    - お客さんからの返答から、予約情報が正しかったか、もしくは誤っていたか判定します
        - キー名は、"confirmed"
        - 正しい場合、値は"true"、誤っている場合、値は"false"です。
    - 分からない場合、confirmedの値は、nullにしてください。
    - JSON形式以外は出力しないでください。
    ## 例:正しい("true")場合のJSON。
        - 正しい場合、confirmedのみ出力ください。
    {{
      "confirmed": "true"
    }}
    ## 例:名前が誤っており("false")、たなかに訂正する場合のJSON
        - 誤っている場合、confirmedと誤っているキーのみ出力ください。
    {{
      "confirmed": "false",
      "name": たなか
    }}
    ## 例:日付が誤っているが("false")、訂正後の値が分からない場合のJSON
        - 誤っている場合、confirmedと誤っているキーのみ出力ください。訂正後の値が分からない場合、nullにしてください。
    {{
      "confirmed": "false",
      "date": null
    }}
    ## 繰り返し確認に対するお客さんからの返答
    {input_text}
    """

    response = openai.ChatCompletion.create(
        model="gpt-4-1106-preview",
        messages=[
            {"role": "user", "content": input_text}
        ],
        response_format= { "type":"json_object" },
        temperature=0,
    )
    response_content = response["choices"][0]["message"]["content"]
    response_content = json.loads(response_content)
    print("Received extract_json_format:" + json.dumps(response_content, default=decimal_to_int, ensure_ascii=False))
    return response_content

def convert_reservation_info(reservation_info):
    # 電話番号をカンマ区切りに変換
    if reservation_info.get('phone_number'):
        reservation_info['phone_number_convert'] = ','.join(list(reservation_info['phone_number']))

    # 日付を年月日形式に変換
    if reservation_info.get('date'):
        date = reservation_info['date']
        year = date[:4]
        month = int(date[4:6])
        day = int(date[6:]) 
        reservation_info['date_convert'] = f"{year}年{month}月{day}日"

    # 時間を時分形式に変換
    if reservation_info.get('time'):
        time = reservation_info['time']
        hour = int(time[:2])  
        minute = int(time[2:])
        if minute == 0:
            reservation_info['time_convert'] = f"{hour}時"
        else:
            reservation_info['time_convert'] = f"{hour}時{minute}分"

    print("Received convert_reservation_info:" + json.dumps(reservation_info, default=decimal_to_int, ensure_ascii=False))
    return reservation_info

def check_and_add_result(reservation_info):
    if all(reservation_info.values()):
        reservation_info["result"] = "success"
    else:
        reservation_info["result"] = "failure"

    return reservation_info

def update_json(data, update_data):
    data.update(update_data)
    print("update_json: " + json.dumps(data, ensure_ascii=False))
    return data

def lambda_handler(event, context):
    print("Received event:" + json.dumps(event,default=decimal_to_int, ensure_ascii=False))
    reservation_data = event["Details"]["ContactData"]["Attributes"]
    media_streams = event["Details"]["ContactData"]["MediaStreams"]["Customer"]["Audio"]
    stream_arn = media_streams["StreamARN"]
    start_time = float(media_streams["StartTimestamp"]) / 1000
    end_time = float(media_streams["StopTimestamp"]) / 1000
    combined_samples = create_audio_sample(
        get_simple_blocks(get_media_data(stream_arn, start_time, end_time)))

    wav_audio = convert_bytearray_to_wav(combined_samples)
    save_audio_to_tmp(wav_audio)

    transcript = transcribe_audio_file("/tmp/output.wav")
    transcript = str(transcript["text"])
    print("Transcript result:" + transcript)

    reservation_info = extract_json_format(transcript)
    reservation_info = check_and_add_result(reservation_info)
    reservation_info = convert_reservation_info(reservation_info)

    updated_reservation_info = update_json(reservation_data, reservation_info)

    return updated_reservation_info

コードの解説は、前回の記事をご参考ください。

Lambdaで使用したプロンプトは下記の通りです。

def extract_json_format(input_text):
    input_text = f"""
    ## 役割
    あなたは、お客さんから聞いた予約情報を最後に繰り返して確認した際、お客さんの返答から、はい、もしくは、いいえを判断し、JSON形式で出力するシステムです。
    ## ルール
    - 予約情報は、「名前(name)」「電話番号(phone_number)」「日付(date)」「時間(time)」「人数(number)」の5つを確認しました
        - 「名前(name)」
            - 名前は、ひらがな、に変換ください。
        - 「電話番号(phone_number)」
            - 電話番号にハイフン(-)は必要ないので、削除してください。数字のみを出力ください。
        - 「予約の日付(date)」
            - 「予約の日付(day)」は、8桁の数字での日付形式(例"20231118")に変換してください。
            - 今日の日付は、{current_date}、です。「予約の日付(date)」は、未来の値です。
                - 例 3日と言われたら、今月の3日のことです。もし今月の3日が過ぎていれば、翌月の3日です。
                - 例 5月と言われたら、今年の5月のことです。今年の5月が過ぎていれば、翌年の5月です。
                - 今日の予約も可能です。
            - 月曜日が週の最初で週末は土日を指します。
        - 「予約時間(time)」
            - 予約可能日時は、9:00 ~ 20:00です。
            - 5時と言われたら、0500ではなく、1700のことです。
            - 今の時間は、{current_time}、です。「予約の時間(time)」は、未来の値です。
            - 今から予約も可能です。
        ## 例
        {{
        "name": "やまだいちろう",
        "phone_number": "09011111111",
        "date": "20231120",
        "time": "1430",
        "number": "2",
        }}
        - 「人数(number)」
    - お客さんからの返答から、予約情報が正しかったか、もしくは誤っていたか判定します
        - キー名は、"confirmed"
        - 正しい場合、値は"true"、誤っている場合、値は"false"です。
    - 分からない場合、confirmedの値は、nullにしてください。
    - JSON形式以外は出力しないでください。
    ## 例:正しい("true")場合のJSON。
        - 正しい場合、confirmedのみ出力ください。
    {{
      "confirmed": "true"
    }}
    ## 例:名前が誤っており("false")、たなかに訂正する場合のJSON
        - 誤っている場合、confirmedと誤っているキーのみ出力ください。
    {{
      "confirmed": "false",
      "name": たなか
    }}
    ## 例:日付が誤っているが("false")、訂正後の値が分からない場合のJSON
        - 誤っている場合、confirmedと誤っているキーのみ出力ください。訂正後の値が分からない場合、nullにしてください。
    {{
      "confirmed": "false",
      "date": null
    }}
    ## 繰り返し確認に対するお客さんからの返答
    {input_text}
    """

Lambdaは、Connectのフロー内で呼ばれ、例えば以下の内容を返します。

{
    "date": "20231205",
    "number": "5",
    "name": "くらすめそっど",
    "phone_number": "0901234",
    "time": "1900",
    "phone_number_convert": "0,9,0,1,2,3,4",
    "date_convert": "2023年12月5日",
    "time_convert": "19時",
    "confirmed": "true",
    "result": "success"
}

試してみた

今回の主題である予約内容を復唱後、色々な返答パターンを正しく認識できるか検証しました。

正しい返答

予約内容が正しい場合、顧客の返答を色々なパターンで試しました。

正しい場合、Lambdaでは下記の通りに返します。

{
// 省略
    "confirmed": "true",
// 省略
}
発話内容 GPT-4 JSONモードの
判断
正否
はい true
問題ありません "confirmed": "true"
大丈夫です "confirmed": "true"
その通りです "confirmed": "true"
それでお願いします "confirmed": "true"
合ってるよ "confirmed": "true"
間違いありません "confirmed": "true"
その通り、明日の5時です。 "confirmed": "true"
はい、山田です。時間と人数も合っています。 "confirmed": "true"
正しいです "confirmed": "true"
オーケーです "confirmed": "true"
名前も合っています "confirmed": "true"
確認ありがとう "confirmed": "null"
ありがとう "confirmed": "null"

色々な返答もGPT-4では、正しく認識してくれますね。

訂正の返答

予約内容が異なる場合、色々な返答を検証しました。

返答は大きく分けて以下の2パターンあります。

  • 予約内容が異なるだけの返答
  • 予約の訂正内容を返答

予約内容が異なるだけの返答

予約内容が異なる場合、発話によって、下記のようにLambdaが返します。

{
// 省略
    "confirmed": "false",
// 省略
}
発話内容 GPT-4 JSONモード
出力の一部を抜粋
正否
いいえ {
"confirmed": "false",
}
違います {
"confirmed": "false",
}
名前以外が違います {
"confirmed": "false",
"phone_number": null,
"date": null,
"time": null,
"number": null
}
正しくありません。 {
"confirmed": "false",
}
名前は合っていますが、電話番号が違います {
"confirmed": "false",
"phone_number": null
}
名前をもう一度確認していただけますか? {
"confirmed": "false",
"name": null
}
申し訳ありませんが、その内容は誤っています。 {
"confirmed": "false",
}
名前と電話番号が違います {
"confirmed": "false",
"name": null,
"phone_number": null
}
時間が違います {
"confirmed": "false",
"time": null
}

正しく、認識してくれています。この場合は、オペレーターに繋ぎます。

「時間が違います」と発話すると、時間をnullにして、他の値はそのまま下記のようにLambdaが返してくれます。

{
    "date": "20231205",
    "number": "5",
    "name": "くらすめそっど",
    "phone_number": "0901234",
    "time": "null",
    "phone_number_convert": "0,9,0,1,2,3,4",
    "date_convert": "2023年12月5日",
    "time_convert": "null",
    "confirmed": "true",
    "result": "success"
}

予約の訂正内容を返答

発話内容 GPT-4 JSONモード
出力の一部を抜粋
正否
人数は2人でした {
"confirmed": "false",
"number": "2"
}
明日ではなく明後日です {
"confirmed": "false",
"date": "20231129"
}
名前は山田ではなく田中です {
"confirmed": "false",
"name": "たなか"
}
5時ではなく7時です {
"confirmed": "false",
"time": "1900"
}

GPT-4は正しく認識してJSON形式で返してくれますね。この場合、訂正内容で予約内容の確認を復唱します。

「5時ではなく7時です」と発話すると、timetime_convertを17時に変更し、Lambdaが返してくれます。

{
    "date": "20231205",
    "number": "5",
    "name": "くらすめそっど",
    "phone_number": "0901234",
    "time": "1700",
    "phone_number_convert": "0,9,0,1,2,3,4",
    "date_convert": "2023年12月5日",
    "time_convert": "17時00分",
    "confirmed": "true",
    "result": "success"
}

Lexを利用した場合

今回は、OpenAI社の文字起こしサービスであるWhisperを利用したかったため、Lexは採用しませんでした。

Lexの場合、文字起こしはAmaozn Transcribeで文字起こしされます。

以前、2つのサービスを比較すると、精度のみの観点ですと、Whisperのほうがよい結果となりました。

今回、Connectのフローは、下記の通りです。

かなりフローが複雑だと感じますが、Lexを採用した場合、下記のフローで実現できます。

かなりシンプルではないでしょうか。

Whisperを採用する場合とLexを採用する場合で、フローの構築や運用負荷、文字起こしの精度などいくつかの検討すべき観点がありますので、取捨選択で決めるとよいと考えます。

最後に

今回は、Amazon Connect + GPT-4 Turboで、予約内容を復唱後、顧客の色々な返答を正しくヒアリングできるか検証しました。

検証結果としては、色々な返答に正しく認識してくれることがわかりました。

Lexを使えば、Connectフローはよりシンプルになりますが、OpenAI社の文字起こしサービスであるWhisperは使えません。

また、日本語対応の生成AIでは、Amazon BedrockのClaudeがありますが、必ずJSONで返してくれるGPT-4のJSONモードのような機能はまだないため、今回は採用しませんでした。

ただし、生成AIの分野が発展が早いので、今後も色々なサービスを組み合わせて検証していきたいと思います。