Amazon Connectで通話・テキスト化した内容をAmazon Bedrockで要約してみた

Amazon Connectで会話して文字起こしした内容を、今話題のBedrockで要約してみました!
2023.11.02

こんにちは、洲崎です。
Amazon Connectで会話した内容をAmazon Bedrockで要約してみました。

コンタクトセンターと文字起こし・要約

コンタクトセンターでは、日々多くの通話をオペレーターが対応しています。
通話内容の分析や後処理(対応履歴の入力作業など)で、通話の内容を確認する際、1件1件、通話を聞き起こすのは時間がかかります。
会話した内容の文字起こしはコンタクトセンターとかなり相性が良いため、導入や検証をしているコンタクトセンターは多いと思います。

一方、全ての音声をテキスト化しても、長い通話があると「どういった通話なのか?」を把握するのにも時間がかかります。
また、「こんにちは」といった挨拶や、「ええと〜」や「あー」等といった特に意味を設けない言葉(俗にいうフィラー)などもすべてテキスト化されます。
通話をテキスト化&要約までできると、コンタクトセンターではかなりの業務・分析の効率化につながります。

Amazon Connectは、Amazon Connect Contact Lensの機能で、会話した内容の文字起こしが可能です。
会話した内容の要約機能も備えていますが、2023/11/2時点で日本語が未対応です。
せっかくなので、Bedrockを活用して要約してみました。

構成

構成としては、下記の通りです。

流れとしては以下となります。

1.Amazon Connect Contact Lensの機能で、会話した内容をJSON形式でS3に出力します

  • Amazon Connectで会話した内容はAmazon Connect Contact Lensとコンタクトフローで有効化している場合、JSON形式で以下のS3パスに保存されます
    • s3://amazon-connect-xxx/Analysis/Voice/yyyy/MM/dd/xxx.json

2.Contact Lensで出力される内容は様々なパラメーターも記載されているので、会話した内容のみを抽出します

3.会話した内容を要約するためにBedrockと連係するLambdaを起動します

4.要約したテキストデータを別のS3バケットに保存します

前提

すべてのリソースはバージニアリージョンで作成・検証しています。(利用できるモデルが一番多いため)
BedrockのモデルはAnthropicのClaudeを利用します。
会話のサンプルデータとしては、以下のドキュメント(通話でのオリジナルの分析済みファイルの例)を日本語に置換して利用します。

やってみる

Bedrockの有効化

Amazon BedrockのClaudeが利用できることを確認します。

Claudeは利用するにあたってリクエスト申請が必要となるため、もしまだの方は申請をします。
申請方法については下記の記事を参照ください。

Amazon Connect Contact Lensの有効化

Amazon Connectで会話した内容をテキスト化するにあたって、Amazon Connect Contact Lens(旧:Contact Lens for Amazon Connect)を有効化します。

問い合わせフローでも「記録と分析の動作を設定」ブロックで、Contact Lensの音声分析を有効にします。

Lambda(会話内容の抽出)

Contact Lensで出力されるJSONは以下のドキュメントに例がありますが、文字起こしされる内容以外にも多くのパラメーターが記録されます。

また、肝心の文字起こしの部分も"Transcript""Content"を見にいく必要があります。

    "Transcript": [
        {
            "BeginOffsetMillis": 0,
            "Content": "こんにちは",
            "EndOffsetMillis": 90,
            "Id": "the ID of the turn",
            "ParticipantId": "AGENT",
            "Sentiment": "NEUTRAL",
            "LoudnessScore": [
                79.27
            ]
        },
        {
            "BeginOffsetMillis": 160,
            "Content": "こんにちは。私の名前はピーターです、助けてください。",
            "EndOffsetMillis": 4640,
            "Id": "the ID of the turn",
            "ParticipantId": "CUSTOMER",
            "Sentiment": "NEUTRAL",
            "LoudnessScore": [
                66.56,
                40.06,
                85.27,
                82.22,
                77.66
            ],
            "Redaction": {
                "RedactedTimestamps": [
                    {
                        "BeginOffsetMillis": 3290,
                        "EndOffsetMillis": 3620
                    }
                ]
            }
        }

データの前処理として、"Content"の内容のみを抽出するLambda関数を作成します。
サンプルコードの内容は以下のとおりです。

import json
import boto3

def lambda_handler(event, context):
    s3 = boto3.client('s3')
    
    # S3バケットのオブジェクトを取得
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']
    
    # JSON取得
    response = s3.get_object(Bucket=bucket, Key=key)
    data = response['Body'].read().decode('utf-8')
    data = json.loads(data)
    
    # "Transcript"の中にある"Content"を抽出
    transcript_content = []
    for item in data['Transcript']:
        content = item['Content']
        transcript_content.append(content)
    output_str = '\n'.join(transcript_content)
    
    # 出力キーを作成(".txt"に変換)
    output_key = key.replace('.json', '.txt') 
    
    # S3バケットに出力
    output_bucket = 'your-output-bucket-name'  # 出力するバケット名を記載
    s3.put_object(Bucket=output_bucket, Key=output_key, Body=output_str)

上記のコードを利用することで、JSONの中身から会話した内容のみを抽出できます。

加工前(クリックすると開きます)
{
    "Version": "1.1.0",    
    "AccountId": "your AWS account ID",
    "Channel": "VOICE",
    "ContentMetadata": {
        "Output": "Raw" 
    },
    "JobStatus": "COMPLETED",
    "LanguageCode": "en-US",
    "Participants": [
        {
            "ParticipantId": "e9b36a6d-12aa-4c21-9745-1881648ecfc8",
            "ParticipantRole": "CUSTOMER"
        },
        {
            "ParticipantId": "2b2288b4-ff6e-4996-8d8e-260fd5a8ac02",
            "ParticipantRole": "SYSTEM"
        },
        {
            "ParticipantId": "f36a545d-67b2-4fd4-89fb-896136b609a7",
            "ParticipantRole": "AGENT"
        }
    ],
    "Categories": {
        "MatchedCategories": [],
        "MatchedDetails": {}
    },
    "ConversationCharacteristics": {
        "TotalConversationDurationMillis": 32110,
        "Sentiment": {
            "OverallSentiment": {
                "AGENT": 0,
                "CUSTOMER": 3.1
            },
            "SentimentByPeriod": {
                "QUARTER": {
                    "AGENT": [
                        {
                            "BeginOffsetMillis": 0,
                            "EndOffsetMillis": 7427,
                            "Score": 0
                        },
                        {
                            "BeginOffsetMillis": 7427,
                            "EndOffsetMillis": 14855,
                            "Score": -5
                        },
                        {
                            "BeginOffsetMillis": 14855,
                            "EndOffsetMillis": 22282,
                            "Score": 0
                        },
                        {
                            "BeginOffsetMillis": 22282,
                            "EndOffsetMillis": 29710,
                            "Score": 5
                        }
                    ],
                    "CUSTOMER": [
                        {
                            "BeginOffsetMillis": 0,
                            "EndOffsetMillis": 8027,
                            "Score": -2.5
                        },
                        {
                            "BeginOffsetMillis": 8027,
                            "EndOffsetMillis": 16055,
                            "Score": 5
                        },
                        {
                            "BeginOffsetMillis": 16055,
                            "EndOffsetMillis": 24082,
                            "Score": 5
                        },
                        {
                            "BeginOffsetMillis": 24082,
                            "EndOffsetMillis": 32110,
                            "Score": 5
                        }
                    ]
                }
            }
        },
        "Interruptions": {
            "InterruptionsByInterrupter": {},
            "TotalCount": 0,
            "TotalTimeMillis": 0
        },
        "NonTalkTime": {
            "TotalTimeMillis": 0,
            "Instances": []
        },
        "TalkSpeed": {
            "DetailsByParticipant": {
                "AGENT": {
                    "AverageWordsPerMinute": 239
                },
                "CUSTOMER": {
                    "AverageWordsPerMinute": 163
                }
            }
        },
        "TalkTime": {
            "TotalTimeMillis": 28698,
            "DetailsByParticipant": {
                "AGENT": {
                    "TotalTimeMillis": 15079
                },
                "CUSTOMER": {
                    "TotalTimeMillis": 13619
                }
            }
        }
    },
    "CustomModels": [], 
    "Transcript": [
        {
            "BeginOffsetMillis": 0,
            "Content": "こんにちは",
            "EndOffsetMillis": 90,
            "Id": "the ID of the turn",
            "ParticipantId": "AGENT",
            "Sentiment": "NEUTRAL",
            "LoudnessScore": [
                79.27
            ]
        },
        {
            "BeginOffsetMillis": 160,
            "Content": "こんにちは。私の名前はピーターです、助けてください。",
            "EndOffsetMillis": 4640,
            "Id": "the ID of the turn",
            "ParticipantId": "CUSTOMER",
            "Sentiment": "NEUTRAL",
            "LoudnessScore": [
                66.56,
                40.06,
                85.27,
                82.22,
                77.66
            ],
            "Redaction": {
                "RedactedTimestamps": [
                    {
                        "BeginOffsetMillis": 3290,
                        "EndOffsetMillis": 3620
                    }
                ]
            }
        },
        {
            "BeginOffsetMillis": 4640,
            "Content": "こんにちは。ピーター、どうしました?",
            "EndOffsetMillis": 6610,
            "Id": "the ID of the turn",
            "ParticipantId": "AGENT",
            "Sentiment": "NEUTRAL",
            "LoudnessScore": [
                70.23,
                73.05,
                71.8
            ],
            "Redaction": {
                "RedactedTimestamps": [
                    {
                        "BeginOffsetMillis": 5100,
                        "EndOffsetMillis": 5450
                    }
                ]
            }
        },
        {
            "BeginOffsetMillis": 7370,
            "Content": "プランのサブスクリプションをキャンセルしたいです",
            "EndOffsetMillis": 11190,
            "Id": "the ID of the turn",
            "ParticipantId": "CUSTOMER",
            "Sentiment": "NEGATIVE",
            "LoudnessScore": [
                77.18,
                79.59,
                85.23,
                81.08,
                73.99
            ],
            "IssuesDetected": [
                {
                    "CharacterOffsets": {
                        "BeginOffsetChar": 0,
                        "EndOffsetChar": 55
                    },
                    "Text": "プランのサブスクリプションをキャンセルしたいです"
                }
            ]
        },
        {
            "BeginOffsetMillis": 11220,
            "Content": "それは悲しいです。今なら特別に20%の割引を提供しますよ。",
            "EndOffsetMillis": 15210,
            "Id": "the ID of the turn",
            "ParticipantId": "AGENT",
            "Sentiment": "NEGATIVE",
            "LoudnessScore": [
                75.92,
                75.79,
                80.31,
                80.44,
                76.31
            ]
        },
        {
            "BeginOffsetMillis": 15840,
            "Content": "それはいいですね。ありがとうございます",
            "EndOffsetMillis": 18120,
            "Id": "the ID of the turn",
            "ParticipantId": "CUSTOMER",
            "Sentiment": "POSITIVE",
            "LoudnessScore": [
                73.77,
                79.17,
                77.97,
                79.29
            ]
        },
        {
            "BeginOffsetMillis": 18310,
            "Content": "OK、あなたのアカウントに割引を適用しました.",
            "EndOffsetMillis": 21820,
            "Id": "the ID of the turn",
            "ParticipantId": "AGENT",
            "Sentiment": "NEUTRAL",
            "LoudnessScore": [
                83.88,
                86.75,
                86.97,
                86.11
            ],
            "OutcomesDetected": [
                {
                    "CharacterOffsets": {
                        "BeginOffsetChar": 9,
                        "EndOffsetChar": 77
                    },
                    "Text": "OK、あなたのアカウントに割引を適用しました"
                }
            ]
        },
        {
            "BeginOffsetMillis": 22610,
            "Content": "素晴らしい。どうもありがとう。",
            "EndOffsetMillis": 24140,
            "Id": "the ID of the turn",
            "ParticipantId": "CUSTOMER",
            "Sentiment": "POSITIVE",
            "LoudnessScore": [
                79.11,
                81.7,
                78.15
            ]
        },
        {
            "BeginOffsetMillis": 24120,
            "Content": "とんでもないです。今日中に詳細もお送りしますね",
            "EndOffsetMillis": 29710,
            "Id": "the ID of the turn",
            "ParticipantId": "AGENT",
            "Sentiment": "POSITIVE",
            "LoudnessScore": [
                87.07,
                83.96,
                76.38,
                88.38,
                87.69,
                76.6
            ],
            "ActionItemsDetected": [
                {
                    "CharacterOffsets": {
                        "BeginOffsetChar": 12,
                        "EndOffsetChar": 102
                    },
                    "Text": "とんでもないです。今日中に詳細もお送りしますね"
                }
            ]
        },
        {
            "BeginOffsetMillis": 30580,
            "Content": "ありがとう。お客様。素敵な夜をお過ごしください。",
            "EndOffsetMillis": 32110,
            "Id": "the ID of the turn",
            "ParticipantId": "CUSTOMER",
            "Sentiment": "POSITIVE",
            "LoudnessScore": [
                81.42,
                82.29,
                73.29
            ]
        }
    ]    
    }
}

加工後

こんにちは
こんにちは。私の名前はピーターです、助けてください。
こんにちは。ピーター、どうしました?
プランのサブスクリプションをキャンセルしたいです
それは悲しいです。今なら特別に20%の割引を提供しますよ。
それはいいですね。ありがとうございます
OK、あなたのアカウントに割引を適用しました.
素晴らしい。どうもありがとう。
とんでもないです。今日中に詳細もお送りしますね
ありがとう。お客様。素敵な夜をお過ごしください。

Lambda(Bedrockと連係)

会話した内容を抽出できたら、Bedrockと連係してテキストの内容を要約します。
2023/11/2時点では、Lambda関数に標準で組み込まれているBoto3のバージョンが、Bedrockに対応しておりません。
下の記事を参考に、使うLambdaに対して「Lambdaレイヤー」を設定しておきます。

Bedrockと連係するLambdaのサンプルコードの内容は以下のとおりです。

import boto3
import json
import os

region = os.environ.get('AWS_REGION')
bedrock_runtime_client = boto3.client('bedrock-runtime', region_name=region)
s3_client = boto3.client('s3')

 
def get_bedrock_response(input_text):
    prompt = f'\n\nHuman: テキストの中にある内容を要約して、要約した内容のみを出力してください。{input_text}\n\nAssistant:'

    # modelId = 'anthropic.claude-V2'
    modelId = 'anthropic.claude-v2' 
    accept = 'application/json'
    contentType = 'application/json'

    body = json.dumps({
        "prompt": prompt,
        "max_tokens_to_sample": 1000
    })

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

    response_body = json.loads(response.get('body').read())

    return response_body.get('completion')
    
def lambda_handler(event, context):
    # # S3バケットのオブジェクトを取得
    input_bucket_name = event['Records'][0]['s3']['bucket']['name']
    object_key = event['Records'][0]['s3']['object']['key']

    s3_object = s3_client.get_object(Bucket=input_bucket_name, Key=object_key)
    file_content = s3_object['Body'].read().decode('utf-8')
    
    response = get_bedrock_response(file_content)

    # 出力するバケットを指定
    output_bucket_name = 'your-output-bucket-name' 
    output_file_name = f"summary_{object_key}"
    
    s3_client.put_object(
        Bucket=output_bucket_name,
        Key=output_file_name,
        Body=response
    )
    
    return {
        'statusCode': 200,
        'body': json.dumps('complete')
    }

別のS3バケットに出力された内容を見ると、要約されていました!

要約前

こんにちは
こんにちは。私の名前はピーターです、助けてください。
こんにちは。ピーター、どうしました?
プランのサブスクリプションをキャンセルしたいです
それは悲しいです。今なら特別に20%の割引を提供しますよ。
それはいいですね。ありがとうございます
OK、あなたのアカウントに割引を適用しました.
素晴らしい。どうもありがとう。
とんでもないです。今日中に詳細もお送りしますね
ありがとう。お客様。素敵な夜をお過ごしください。

要約後(Bedrockが出力した内容です)

要約:
ピーターはプランのサブスクリプションをキャンセルしたいと言っています。 
20%の割引を提供すると言われました。
ピーターは割引を受け入れました。  
アカウントに割引が適用されました。
ピーターは感謝しています。

最後に

Amazon Connectで会話して文字起こしされた内容をBedrockで要約してみました。
AWSでここまで完結できるのが凄く嬉しいです。
あとはオペレーター、顧客別で要約ができるかなども別で試してみます。

コンタクトセンターと生成系AIは凄く相性がいいので、今後もいろいろと試していきます。

ではまた!コンサルティング部の洲崎でした。