Amazon Connectのフローでの離脱箇所と放棄呼をKinesis Data Streamsを用いて取得し、DynamoDBに保存してみた
はじめに
Amazon Connectのフローで離脱箇所や放棄呼をAmazon Kinesis Data Streams(以降、KDS)を用いて取得し、DynamoDBに保存する方法をまとめました。
利用用途は以下が挙げられます。
- IVRでの途中離脱箇所を知りたい
- オペレーターにつながる前に切られる放棄呼の有無を知りたい
Connectは、各通話ごとに問い合わせレコード(CTR)として通話記録を保存します。
Connectでは、KDSに問い合わせレコードを出力することができます。通常は問い合わせレコードは、どのフローで切断されたか情報はありませんが、フロー内で工夫すると取得ができます。工夫内容は後述します。
以下の構成図をもとに処理の流れを説明します。
- Connectのフロー内で、図の青丸箇所のいずれかで切断したとします
- 切断直後、問い合わせレコードがKDSにストリーミングされます
- KDSに保存されたことをトリガーにLambdaが起動し、問い合わせレコードから切断情報などを抽出し、DynamoDBに保存します
構築
以下の構築を解説します。
- DynamoDB
- KDS
- Lambda
- Connectフロー
DynamoDB
以下の設定で作成します
- テーブル名:
call-records
- パーティションキー:
contact_id
今回の切断箇所だけではなく、他の情報もDynamoDBに保存することを想定し、パーティションキーはContact IDにします。
Lambdaによって、DynamoDBには以下の属性を保存する想定です。
- コンタクトID
- 放棄呼の有無
- 発信者の通話時間
- 発信日時
- フロー離脱箇所
- 発信元電話番号
KDS
KDSは、プロビジョンドモード、シャード1で作成します。
Lambda
ランタイムPython3.12を使用して作成します。
- IAMポリシーは、以下を追加します。
- AmazonKinesisReadOnlyAccess
- AmazonDynamoDBFullAccess
import json import base64 import boto3 from datetime import datetime, timezone, timedelta JST = timezone(timedelta(hours=+9), 'JST') def decode_kinesis_record(kinesis_record): data_string = kinesis_record['data'] decoded_data = base64.b64decode(data_string) decoded_string = decoded_data.decode('utf-8') print('decode_kinesis_record:' + decoded_string) return json.loads(decoded_string) def extract_call_details(data_json): return { 'phone_number': data_json['CustomerEndpoint']['Address'], 'call_route': data_json['Attributes']['call_route'], 'start_time_utc': data_json['InitiationTimestamp'], 'end_time_utc': data_json['DisconnectTimestamp'], 'contact_id': data_json['ContactId'], 'call_completed': data_json['Attributes']['call_completed'] } def calculate_call_duration(start_time_utc, end_time_utc): start_time_utc = datetime.strptime(start_time_utc, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) end_time_utc = datetime.strptime(end_time_utc, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) return end_time_utc - start_time_utc def convert_to_jst(start_time_utc): start_time_utc = datetime.strptime(start_time_utc, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) start_time_jst = start_time_utc.astimezone(JST) return start_time_jst.strftime("%Y-%m-%dT%H:%M:%S%z") def save_call_record(call_details, call_duration, start_time_jst): dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('call-records') response = table.update_item( Key={ 'contact_id': call_details['contact_id'] }, UpdateExpression="SET phone_number = :phone, call_route = :route, start_time = :start, call_duration = :duration, call_completed = :completed", ExpressionAttributeValues={ ':phone': call_details['phone_number'], ':route': call_details['call_route'], ':start': start_time_jst, ':duration': str(call_duration), ':completed': call_details['call_completed'] }, ReturnValues="UPDATED_NEW" ) print('response:' + json.dumps(response, ensure_ascii=False)) def lambda_handler(event, context): print('event:' + json.dumps(event, ensure_ascii=False)) kinesis_record = event['Records'][0]['kinesis'] data_json = decode_kinesis_record(kinesis_record) call_details = extract_call_details(data_json) call_duration = calculate_call_duration(call_details['start_time_utc'], call_details['end_time_utc']) start_time_jst = convert_to_jst(call_details['start_time_utc']) save_call_record(call_details, call_duration, start_time_jst) return { 'statusCode': 200, }
DynamoDBに保存する属性は以下です
- コンタクトID:
contact_id
- 放棄呼の有無:
call_completed
- 発信者の通話時間:
call_duration
- 発信日時:
start_time
- フロー離脱箇所:
call_route
- 発信元電話番号:
phone_number
問い合わせレコードがKDSにストリーミングされると、Lambdaをトリガーさせるため、トリガーを以下の設定で行います
設定値については、以下が参考になります
Lambdaのevent
例として、KDSから受け取るLambdaのevent値を記載します。
{ "Records": [ { "kinesis": { "kinesisSchemaVersion": "1.0", "partitionKey": "9a5b35b2-3210-4873-8cd3-d83dd170204b", "sequenceNumber": "49651139538302143095151021919781625570957189547772870658", "data": "xxxxx", "approximateArrivalTimestamp": 1713397730.084 }, "eventSource": "aws:kinesis", "eventVersion": "1.0", "eventID": "shardId-000000000000:xxxxxx", "eventName": "aws:kinesis:record", "invokeIdentityArn": "arn:aws:iam::xxxxxxxxxxxx:role/service-role/cm-hirai-call-route-role-xxx", "awsRegion": "ap-northeast-1", "eventSourceARN": "arn:aws:kinesis:ap-northeast-1:xxxxxxxxxxxx:stream/cm-hirai-call-route" } ] }
上記のevent['Records'][0]['kinesis']['data']
は、base64でエンコードされているため、デコードすると、以下の値が取得できます。
{ "AWSAccountId": "xxxxxxxxxxx", "AWSContactTraceRecordFormatVersion": "2017-03-10", "Agent": null, "AgentConnectionAttempts": 0, "AnsweringMachineDetectionStatus": null, "Attributes": { "call_completed": "false", "call_route": "start" }, "Campaign": { "CampaignId": null }, "Channel": "VOICE", "ConnectedToSystemTimestamp": "2024-04-17T23:47:52Z", "ContactDetails": {}, "ContactId": "9c905123-6240-4484-b9d1-4e3e40660d8e", "CustomerEndpoint": { "Address": "+81xxxxxxxxx", "Type": "TELEPHONE_NUMBER" }, "CustomerVoiceActivity": null, "DisconnectReason": "CUSTOMER_DISCONNECT", "DisconnectTimestamp": "2024-04-17T23:47:56Z", "InitialContactId": null, "InitiationMethod": "INBOUND", "InitiationTimestamp": "2024-04-17T23:47:52Z", "InstanceARN": "arn:aws:connect:ap-northeast-1:xxxxxxxxxxx:instance/3ff2093d-af96-43fd-b038-3c07cdd7609c", "LastUpdateTimestamp": "2024-04-17T23:49:03Z", "MediaStreams": [ { "Type": "AUDIO" } ], "NextContactId": null, "PreviousContactId": null, "Queue": null, "Recording": null, "Recordings": null, "References": [], "ScheduledTimestamp": null, "SegmentAttributes": { "connect:Subtype": { "ValueString": "connect:Telephony" } }, "SystemEndpoint": { "Address": "+81xxxxxxxxx", "Type": "TELEPHONE_NUMBER" }, "Tags": { "aws:connect:instanceId": "3ff2093d-af96-43fd-b038-3c07cdd7609c", "aws:connect:systemEndpoint": "+81xxxxxxxxx" }, "TaskTemplateInfo": null, "TransferCompletedTimestamp": null, "TransferredToEndpoint": null, "VoiceIdResult": null }
上記からコンタクトIDやcall_route
、call_completed
などを取得し、DynamoDBに保存します。
Connect
Amazon Connect から問い合わせレコードをKDSにストリーミングする設定を行います。
コンソールから[データストリーミング]に遷移し、[Kinesis ストリーム]から[問い合わせ追跡レコード]で作成したKDSを設定するだけです。
Connect フロー
今回利用するConnect フローは以下の通りです。
フローは以下の流れです。
- 発信する
- アナウンスが流れるので、ユーザーがプッシュ式で「1」または「2」を入力します。
- 再度、アナウンスが流れるので、ユーザーがプッシュ式で「1」または「2」を入力します。
- 入力された内容がアナウンスされる
- 切断される
フロー (クリックすると展開します)
{ "Version": "2019-10-30", "StartAction": "83796d04-0550-4e7e-aa99-fce8b18c3f98", "Metadata": { "entryPointPosition": { "x": 241.6, "y": -47.2 }, "ActionMetadata": { "791eef75-931f-4116-8898-300cb47711db": { "position": { "x": 875.2, "y": 663.2 } }, "d6f52dbe-f77b-49ee-913e-11af21cf4f3b": { "position": { "x": 867.2, "y": 381.6 }, "dynamicParams": [] }, "0534b033-6e91-4b33-97f5-708c19b7aeec": { "position": { "x": 1324, "y": 138.4 } }, "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a": { "position": { "x": 2035.2, "y": 364 } }, "83796d04-0550-4e7e-aa99-fce8b18c3f98": { "position": { "x": 344, "y": -62.4 } }, "8ebcf8ff-9525-4b7e-b07e-b6699b340e2b": { "position": { "x": 859.2, "y": 200 }, "dynamicParams": [] }, "26cd035e-247c-4388-bff9-ba756df8235c": { "position": { "x": 1323.2, "y": 744.8 } }, "400c5522-9cb0-4054-93c1-1c813d715dee": { "position": { "x": 1084.8, "y": -184 }, "conditionMetadata": [ { "id": "f52c7bdd-2d72-468e-926f-e37f0fc37aab", "value": "1" }, { "id": "d6d3b356-0118-4876-97b7-4bd688c7f21b", "value": "2" } ] }, "a1972ceb-07b7-483e-b03f-2bd3dd6063fd": { "position": { "x": 1090.4, "y": 289.6 }, "conditionMetadata": [ { "id": "79e1cb23-1aa4-4841-8b71-b4723557d5f3", "value": "1" }, { "id": "d618d60a-c533-46dd-a605-c55459e451e1", "value": "2" } ] }, "0bb60df6-c203-413e-930c-0fe3eb2677ea": { "position": { "x": 349.6, "y": 110.4 }, "children": [ "c276029c-6058-4b01-91e5-159cf7c622f4" ], "overrideConsoleVoice": true, "fragments": { "SetContactData": "c276029c-6058-4b01-91e5-159cf7c622f4" }, "overrideLanguageAttribute": true }, "c276029c-6058-4b01-91e5-159cf7c622f4": { "position": { "x": 349.6, "y": 110.4 }, "dynamicParams": [] }, "1046bd11-9a7f-4746-a1eb-2ab571ceda91": { "position": { "x": 591.2, "y": 301.6 }, "conditionMetadata": [ { "id": "6b52ff60-18f7-4b45-81f3-02d69bb14d7f", "value": "1" }, { "id": "d3298e8f-e0bd-4aea-bad9-2ea2bc28c4cd", "value": "2" } ] }, "02b1970e-5964-40f3-8f02-a8a572528a8a": { "position": { "x": 349.6, "y": 289.6 }, "dynamicParams": [] }, "beb20743-c289-44c4-b15e-4296cd1524dc": { "position": { "x": 1323.2, "y": -48 }, "dynamicParams": [] }, "f8b767e9-6ce8-4d62-abe1-99ddb29a047b": { "position": { "x": 1823.2, "y": 292 } }, "2437cce9-a1c7-4b54-bd5e-48ee06780fcb": { "position": { "x": 1326.4, "y": -231.2 }, "dynamicParams": [] }, "9a65bea6-f010-4201-9b80-ba5fd949a730": { "position": { "x": 1322.4, "y": 343.2 }, "dynamicParams": [] }, "6ad4c36f-cde9-4895-9e2c-e3f0058300b0": { "position": { "x": 1324.8, "y": 553.6 }, "dynamicParams": [] }, "8cd3631c-4c50-486f-b1d0-0d19be958468": { "position": { "x": 1600.8, "y": 295.2 }, "dynamicParams": [] } }, "Annotations": [], "name": "cm-hirai-call-route", "description": "", "type": "contactFlow", "status": "published", "hash": {} }, "Actions": [ { "Parameters": { "Text": "有効な番号以外が入力されました。" }, "Identifier": "791eef75-931f-4116-8898-300cb47711db", "Type": "MessageParticipant", "Transitions": { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "Errors": [ { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "2" }, "TargetContact": "Current" }, "Identifier": "d6f52dbe-f77b-49ee-913e-11af21cf4f3b", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "Errors": [ { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Text": "有効な番号以外が入力されました。" }, "Identifier": "0534b033-6e91-4b33-97f5-708c19b7aeec", "Type": "MessageParticipant", "Transitions": { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "Errors": [ { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": {}, "Identifier": "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a", "Type": "DisconnectParticipant", "Transitions": {} }, { "Parameters": { "FlowLoggingBehavior": "Enabled" }, "Identifier": "83796d04-0550-4e7e-aa99-fce8b18c3f98", "Type": "UpdateFlowLoggingBehavior", "Transitions": { "NextAction": "0bb60df6-c203-413e-930c-0fe3eb2677ea" } }, { "Parameters": { "Attributes": { "call_route": "1" }, "TargetContact": "Current" }, "Identifier": "8ebcf8ff-9525-4b7e-b07e-b6699b340e2b", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "Errors": [ { "NextAction": "400c5522-9cb0-4054-93c1-1c813d715dee", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Text": "有効な番号以外が入力されました。" }, "Identifier": "26cd035e-247c-4388-bff9-ba756df8235c", "Type": "MessageParticipant", "Transitions": { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "Errors": [ { "NextAction": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "StoreInput": "False", "InputTimeLimitSeconds": "10", "Text": "1、もしくは、2、を押して下さい" }, "Identifier": "400c5522-9cb0-4054-93c1-1c813d715dee", "Type": "GetParticipantInput", "Transitions": { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "Conditions": [ { "NextAction": "2437cce9-a1c7-4b54-bd5e-48ee06780fcb", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } }, { "NextAction": "beb20743-c289-44c4-b15e-4296cd1524dc", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } } ], "Errors": [ { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "ErrorType": "InputTimeLimitExceeded" }, { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "ErrorType": "NoMatchingCondition" }, { "NextAction": "0534b033-6e91-4b33-97f5-708c19b7aeec", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "StoreInput": "False", "InputTimeLimitSeconds": "10", "Text": "1、もしくは、2、を押して下さい" }, "Identifier": "a1972ceb-07b7-483e-b03f-2bd3dd6063fd", "Type": "GetParticipantInput", "Transitions": { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "Conditions": [ { "NextAction": "9a65bea6-f010-4201-9b80-ba5fd949a730", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } }, { "NextAction": "6ad4c36f-cde9-4895-9e2c-e3f0058300b0", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } } ], "Errors": [ { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "ErrorType": "InputTimeLimitExceeded" }, { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "ErrorType": "NoMatchingCondition" }, { "NextAction": "26cd035e-247c-4388-bff9-ba756df8235c", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "TextToSpeechEngine": "Neural", "TextToSpeechStyle": "None", "TextToSpeechVoice": "Kazuha" }, "Identifier": "0bb60df6-c203-413e-930c-0fe3eb2677ea", "Type": "UpdateContactTextToSpeechVoice", "Transitions": { "NextAction": "c276029c-6058-4b01-91e5-159cf7c622f4" } }, { "Parameters": { "LanguageCode": "ja-JP" }, "Identifier": "c276029c-6058-4b01-91e5-159cf7c622f4", "Type": "UpdateContactData", "Transitions": { "NextAction": "02b1970e-5964-40f3-8f02-a8a572528a8a", "Errors": [ { "NextAction": "02b1970e-5964-40f3-8f02-a8a572528a8a", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "StoreInput": "False", "InputTimeLimitSeconds": "10", "Text": "1、もしくは、2、を押して下さい" }, "Identifier": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "Type": "GetParticipantInput", "Transitions": { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "Conditions": [ { "NextAction": "8ebcf8ff-9525-4b7e-b07e-b6699b340e2b", "Condition": { "Operator": "Equals", "Operands": [ "1" ] } }, { "NextAction": "d6f52dbe-f77b-49ee-913e-11af21cf4f3b", "Condition": { "Operator": "Equals", "Operands": [ "2" ] } } ], "Errors": [ { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "ErrorType": "InputTimeLimitExceeded" }, { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "ErrorType": "NoMatchingCondition" }, { "NextAction": "791eef75-931f-4116-8898-300cb47711db", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "start", "call_completed": "false" }, "TargetContact": "Current" }, "Identifier": "02b1970e-5964-40f3-8f02-a8a572528a8a", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "Errors": [ { "NextAction": "1046bd11-9a7f-4746-a1eb-2ab571ceda91", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "1-2" }, "TargetContact": "Current" }, "Identifier": "beb20743-c289-44c4-b15e-4296cd1524dc", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Text": "$.Attributes.call_route、が入力されました。終了します。" }, "Identifier": "f8b767e9-6ce8-4d62-abe1-99ddb29a047b", "Type": "MessageParticipant", "Transitions": { "NextAction": "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a", "Errors": [ { "NextAction": "d8f00f42-7a09-4ff9-b8d8-ef6d03ce172a", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "1-1" }, "TargetContact": "Current" }, "Identifier": "2437cce9-a1c7-4b54-bd5e-48ee06780fcb", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "2-1" }, "TargetContact": "Current" }, "Identifier": "9a65bea6-f010-4201-9b80-ba5fd949a730", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_route": "2-2" }, "TargetContact": "Current" }, "Identifier": "6ad4c36f-cde9-4895-9e2c-e3f0058300b0", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Errors": [ { "NextAction": "8cd3631c-4c50-486f-b1d0-0d19be958468", "ErrorType": "NoMatchingError" } ] } }, { "Parameters": { "Attributes": { "call_completed": "true" }, "TargetContact": "Current" }, "Identifier": "8cd3631c-4c50-486f-b1d0-0d19be958468", "Type": "UpdateContactAttributes", "Transitions": { "NextAction": "f8b767e9-6ce8-4d62-abe1-99ddb29a047b", "Errors": [ { "NextAction": "f8b767e9-6ce8-4d62-abe1-99ddb29a047b", "ErrorType": "NoMatchingError" } ] } } ] }
フローの離脱箇所
コンタクト属性は問い合わせレコードに保存されるため、分岐先情報をコンタクト属性として設定すると、フロー離脱箇所をDynamoDBに保存することができます。
[コンタクト属性の設定]ブロックでフローの分岐情報として、キー:call_route
、値(分岐先)を付与します。
今回は以下のように、キー:call_route
の値は、数字にしていますが、問い合わせの種別(例えば、カードの紛失や口座開設希望など)を設定するとよいでしょう。
例えば、1
を入力後、切断すると、切断箇所は1
になります。1
を押して次のフローで2
を押して切断すると、1-2
が切断箇所になります。
放棄呼
放棄呼の有無は、発信直後とフローの最後に[コンタクト属性の設定]ブロックとして、キー:call_completed
、値(true or false)を付与することで確認できます。
オペレーターにつながるまでに電話を切られることを、放棄呼と指しますが、今回は最後まで進めたかどうかでtrue
or false
と判定します。フローの最後にオペレーターに繋がるようにしてもよいです。
例えば、発信し、最初の音声が流れた瞬間に切断すると、放棄呼がありとなりコンタクト属性はfalse
となります。最後まで進めると、放棄呼なしであり、コンタクト属性はtrue
になります。
試してみる
それでは電話をかけて試してみます。
電話を終了してからDynamoDBに反映されるまでに、1分ほどかかります。
以下の結果となりました。
フローの離脱箇所はcall_route
で確認できます。例えば、call_route
が1
の場合は、最初の選択肢で1を選び、次の選択で切断されたということです。call_route
が2-1
の場合は、最初の選択で2を選び、次の選択で1を選んだ後に切断された場合です。
最後まで進めた場合(call_route
が1-2
や1-1
)、call_completed
がtrue
となり、放棄呼ではないということです。一方、発信直後に切断された場合は、call_completed
がfalse
で放棄呼があったことがわかります。
最後
Amazon Connectのフローで離脱箇所や放棄呼をKDSで取得できるようにし、DynamoDBに保存する方法をまとめました。
Amazon Connect フローのコンタクト属性に分岐先情報を設定することで、比較的容易に離脱箇所や放棄呼の情報を取得できます。参考になれば幸いです。