Gemini を使って Amazon Connect の要約機能を実装してみました

記事タイトルとURLをコピーする

忙しい方のための3行まとめ

  • Amazon Connect Contact Lens の要約機能は日本語未対応
  • Amazon Bedrock や他の AIサービスと連携すれば、日本語に対応した要約も実装可能
  • Gemini がだいぶ賢くなっている(気がする)

本題

はじめに

こんにちは、エアコンの温度調節が上手くいかず暑いと寒いをずっと繰り返しているサーバーワークスの松尾です。

Amazon Connect の通話を要約したい、というご要望をいただく機会が増えてきたので、ブログ化しました。

Amazon Connect Contact Lens には標準で要約機能が用意されているのですが、日本語には未対応です。

また、標準の要約機能では要約に使用するプロンプトの変更などもできないため、外部のAPIを利用した要約機能を試してみました。

AWSの生成AIサービスといえばAmazon Bedrockですが、どうせなら外部のAPIで試してみようと思い、今回はGoogle の Geminiを使用しています。

(もちろん、Amazon Bedrockでも同様の実装は可能です)

事前準備

必要なもの
  • AWSアカウント
    • Amazon Connect インスタンス構築済み
    • 電話番号取得済み
  • Google Cloud アカウント
    • Gemini API を使用します

Google Cloud の設定

Gemini APIの有効化

AWS といえば Bedrock ですが、どうせブログにするなら少しチャレンジしてみよう、ということで Google Cloud のAPIを使ってみます。

Gemini API と Vertex AI API どちらでも利用できそうでしたが、今回は手順の簡単な Gemini API のAPIキー認証で試しています。

APIキーの発行についてはここでは触れませんが、リンク先を参考に発行しました。

https://ai.google.dev/gemini-api/docs/api-key?hl=ja

AWSの設定

Parameter Storeの作成

APIキーはLambdaに直接持たせたくないので、Parameter Storeに保存します

Parameter Store の設定画面

要約機能(AWS Lambda)の実装

Contact Lens が出力した JSON の S3 URI を引数として受け取り、要約結果をJSONで返すLambda関数を作ってみます。タイムアウトは少し長めに設定しています。

  • ランタイム: Python 3.13
  • タイムアウト: 60秒

Lambda の設定画面

Lambda の実行Roleには標準の実行権限に加えて以下のアクセス権を付与します。

ssm:GetParameter は上記の画像の画面ではなく、作成後にIAMの設定画面から追加しています。

  • s3:GetObject
  • ssm:GetParameter

環境変数に先ほど作成した Parameter Store の名前を設定しておきます。

Lambdaの環境変数

コードはAmazon Q Developerと一緒に書いてみました。コピペで試していただけるよう、外部ライブラリの不要なコードにしています。

import json
import boto3
import os
import urllib.request
import re

# AWS クライアント初期化
s3_client = boto3.client("s3")
ssm_client = boto3.client("ssm")


def lambda_handler(event, context):
    """
    Lambda関数のメインハンドラー
    """
    try:
        # Parameter StoreからGemini APIキーを取得
        parameter_name = os.environ.get("GEMINI_API_KEY_PARAMETER_NAME")
        response = ssm_client.get_parameter(Name=parameter_name, WithDecryption=True)
        gemini_api_key = response["Parameter"]["Value"]

        # S3 URIから bucket と key を取得
        s3_uri = event["s3_uri"]
        match = re.match(r"s3://([^/]+)/(.+)", s3_uri)
        bucket_name, object_key = match.group(1), match.group(2)

        # S3からContact Lensファイルを取得
        response = s3_client.get_object(Bucket=bucket_name, Key=object_key)
        contact_lens_data = json.loads(response["Body"].read().decode("utf-8"))

        # Gemini APIで要約を生成
        summary = generate_summary_with_gemini(contact_lens_data, gemini_api_key)

        # 結果を返す
        return json.dumps({
            "status": "success",
            "s3_uri": s3_uri,
            "summary": summary
        }, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "status": "error",
            "message": str(e),
            "s3_uri": event.get("s3_uri", "unknown")
        }, ensure_ascii=False, indent=2)

def generate_summary_with_gemini(contact_lens_data, api_key):
    """
    Gemini APIを使用して会話の要約を生成
    """
    # Gemini API エンドポイント
    url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"

    # プロンプトを作成(元のJSONデータをそのまま含める)
    prompt = f"""以下はコールセンターの通話記録のJSONデータです。この会話を日本語で要約してください。

要約に含めるべき内容:
1. 通話の目的・理由
2. 主要な問題点や質問
3. 提供された解決策や回答
4. 通話の結果・結論
5. 必要に応じたフォローアップ事項

JSONデータ:
{json.dumps(contact_lens_data, ensure_ascii=False, indent=2)}

要約:"""

    # リクエストペイロード
    payload = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "temperature": 0.3,
            "maxOutputTokens": 1000,
        },
    }

    # HTTPリクエストを作成
    data = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    # APIを呼び出し
    with urllib.request.urlopen(req, timeout=30) as response:
        response_data = json.loads(response.read().decode("utf-8"))

    # レスポンスを解析
    return response_data["candidates"][0]["content"]["parts"][0]["text"].strip()
Amazon Connect Contact Lens 文字起こしの有効化

マネジメントコンソール上の Amazon Connect のインスタンス設定で、 分析ツールAmazon Connect Contact Lens を有効化します。

Contact Lens の有効化 -1

その後、Amazon Connect にログインし、コンタクトフローに 記録と分析の動作を設定 ブロックを追加して公開します。

Contact Lens の有効化 -2

Contact Lens の有効化 -3

通話後でも問題ないのですが、せっかちなのでリアルタイムで試してみました。

これで環境の準備は完了です。

試してみる

文字起こしデータを作成する

実際に電話をかけて、こんな会話をしてみました。(CV:全部わたし)

原稿はGeminiに作ってもらいました。

最初に作ってくれた原稿では田中さんが激昂していたので、少し柔らかくしてもらっています。

お客様(田中様): (やや丁寧すぎる口調で)もしもし、あの、大変恐縮なのですが、少しお尋ねしたいことがございまして。そちら様で、食料品の配送などを行っていらっしゃいますよね?

オペレーター(鈴木): はい、お電話ありがとうございます。さば屋の鈴木でございます。食料品の配送も承っております。恐れ入りますが、お客様のお名前をお伺いしてもよろしいでしょうか。

お客様(田中様): ああ、私ですか。ええ、田中と申します。田中と申します。本日は、ある件でお力を貸していただきたく、ご連絡差し上げました。

オペレーター(鈴木): 田中様でいらっしゃいますね。かしこまりました。本日はどのようなご用件でしょうか。

お客様(田中様): ええ、実はですね、先日と言いますか、本日の昼食のために、あるものを注文させていただいたのですが…。それが、どうにもこうにも、まだ私の手元に届いていないようなのです。まあ、急いでいるわけではないのですが、時間が経つにつれて、少々不安になりまして。

オペレーター(鈴木): 田中様、大変申し訳ございません。ご注文のお品物がまだお手元に届いていないとのこと、ご心配をおかけし、誠に恐縮でございます。差し支えなければ、ご注文いただいたお品物の詳細や、いつ頃ご注文されたかなど、お伺いしてもよろしいでしょうか。

お客様(田中様): ああ、そうですか。ええと、お品物というのは、いわゆる麺類でして、日本の伝統的な食事、つるつると喉越しの良い…そうです、そばでございますね。注文したのは、確かお昼を少し回った頃、時計が12時半あたりを指していたかと記憶しております。

オペレーター(鈴木): ありがとうございます。そばのご注文で、お昼の12時半ごろでいらっしゃいますね。ご注文番号がお分かりでしたら、より迅速にお調べできますが、いかがでしょうか?

お客様(田中様): ご注文番号でございますか。ええと、たしか、英数字の羅列で、XYZ…そう、XYZ12345だったと、うろ覚えですが、そんな感じだったかと思います。もし違いましたら、大変申し訳ございません。

オペレーター(鈴木): ありがとうございます。ご注文番号XYZ12345、12時半ごろのそばのご注文で承知いたしました。ただいま、状況を確認させていただきますので、恐れ入りますが、このまま少々お待ちいただけますでしょうか。

お客様(田中様): ええ、構いませんとも。お手数をおかけしてしまい、大変申し訳ないのですが、何分、食事が手元にないと、どうにも落ち着かないものでして。もしかしたら、どこかで手違いでもあったのではないかと、案じております。

オペレーター(鈴木): 大変ご迷惑をおかけしております。お待たせいたしました。ただいま確認が取れました。お客様のおそばは、現在、配達員がお客様のご自宅へ向かっております。システム上の表示では、あと5分ほどでご到着する見込みとなっております。

お客様(田中様): ああ、そうですか。それは何よりでございます。てっきり、途中で迷子にでもなってしまったのではないかと、少々案じておりました。これで、ようやく落ち着いて食事ができそうです。ご丁寧なご対応、誠にありがとうございます。

オペレーター(鈴木): とんでもございません。ご心配をおかけし、重ねてお詫び申し上げます。今後、このようなご不安をおかけすることがないよう、よりスムーズな情報連携に努めてまいります。また何かご不明な点がございましたら、お気軽にお申し付けください。本日はお問い合わせいただき、誠にありがとうございました。

無事文字起こしされました。

余談ですが、Contact Lensの文字起こしもずいぶん自然になりましたね。英数字も以前より綺麗になった印象です。

文字起こし結果(一部)

通話が終わり、通話後処理(ACW)の画面を閉じてしばらく待つと、S3に Contact Lens の処理結果が出力されます。

Contact Lens JSON

Contact Lens文字起こしの結果は以下のようになりました。

田中: もしもし、あの大変恐縮なのですが、少しお尋ねしたいことがございまして、そちら様で食料品の配送などを行っていらっしゃいますよね。
オペレーター: はい。お電話ありがとうございます。そば屋のスズキでございます。食料品の配送も承っております。恐れ入りますが、お客様のお名前をお伺いしてもよろしいでしょうか?
田中: ああ、私ですか?えー、タナカと申します。タナカと申します。本日はある件でお力を貸していただきたく、ご連絡差し上げました。
オペレーター: タナカ様でいらっしゃいますね。かしこまりました。本日はどのようなご用件でしょうか?
田中: え、実はですね、先日と言いますか、本日の昼食のためにあるものを注文させていただいたのですが、それがどうにもこうにもまだ私の手元に届いていないようなのです。まあ、急いでるわけではないのですが、時間が経つにつれて少々不安になりまして。
オペレーター: タナカ様、大変申し訳ございません。ご注文のお品物がまだお手元に届いていないとのこと、ご心配をおかけし、まとこ、誠に恐縮でございます。差し支えなければ、ご注文いただいたお品物の詳細や、いつ頃ご注文されたかなど、お伺いしてもよろしいでしょうか?
田中: そうですかお品物というのは、いわゆる麺類でして、日本の伝統的な食事、つるつると喉干しのいい、そうです、そばでございますね。注文したのは確かお昼を少し回った頃、時計が12時30分あたりをさしていたかと記憶しております。
オペレーター: ありがとうございます。そばのご注文で、お昼の12時30分頃でいらっしゃいますね。ご注文番号がお分かりでしたら、より迅速にお調べできますが、いかがでしょうか?
田中: ご注文番号でございますか?えっと、確か英数字の羅列でxy.z.そう。x.y.z.12345だったとうろ覚えですが、そんな感じだったかと思います。もし、違いましたら、大変申し訳ございません。
オペレーター: ありがとうございます。ご注文番号x.i.z.123c.五12時30分頃のそばのご注文で承知いたしました。ただいま状況を確認させていただきますので、恐れ入りますが、このまま少々お待ちいただけますでしょうか?
田中: ええ、構いませんとも、お手数をおかけしてしまい、大変申し訳ないのですが、内分食事が手元にないとドームにも落ち着かないものでして。もしかしたらどこかで手違いでもあったのではないかと案じております。
オペレーター: 大変ご迷惑をおかけしております。お待たせいたしました。ただいま確認が取れました。お客様のおそばは現在、配達員がお客様のご自宅へ向かっております。システム上の表示では、あと5分ほどでご到着する見込みとなっております。
田中: ああ、そうですか。それは何よりでございます。てっきり途中で迷子にでもなってしまったのではないかと少々案じておりました。これでようやく落ち着いて食事ができそうです。ご丁寧なご対応を誠にありがとうございます。
オペレーター: とんでもございません。ご心配をおかけし、重ねごり申し上げます。今後、このようなご不安をおかけすることがないよう、よりスムーズな情報連携に努めてまいります。また何かご不明な点がございましたら、お気軽にお申し付けください。本日はお問い合わせいただき、誠にありがとうございました。

蕎麦屋の名前を「さば屋」にしたのがこだわりポイントだったのですが、私の滑舌では限界がありました。

要約Lambdaを実行する

出力されたJSONファイルのS3パスをパラメータとしてLambdaを実行します。

Lambdaの実行画面(Lambda関数のテストタブで実行)

成功すると以下のようにJSONで要約結果が出力されます。

Lamdaのテスト実行結果

整形するとこんな結果でした。必要な情報を端的にまとめてくれており、読みやすい印象です。

### コールセンター通話記録の要約

**1. 通話の目的・理由:**

顧客のタナカ氏が、昼食に注文したそばがまだ届かないため、配送状況を確認するために電話をかけた。

**2. 主要な問題点や質問:**

* 注文したそばが届いていない。
* 配達状況、特に配達員がどこにいるのか、いつ頃到着するのかを知りたい。

**3. 提供された解決策や回答:**

オペレーターは、タナカ氏の注文情報(そば、12時30分頃の注文、注文番号の一部)を確認し、配達状況を調査した。その結果、配達員がタナカ氏の自宅へ向かっており、5分ほどで到着する見込みであることを伝えた。

**4. 通話の結果・結論:**

タナカ氏は、そばが配達中であることを知り、安心した。オペレーターは、顧客に迷惑をかけたことを謝罪し、今後の改善を約束した。通話は、顧客がオペレーターの対応に感謝し、オペレーターが改めて謝罪と感謝を述べる形で終了した。

**5. 必要に応じたフォローアップ事項:**

特になし。配達員が5分後に到着する見込みであるため、顧客へのフォローアップは不要と判断される。

ちなみに

同じJSONを Bedrock (Nova Pro) でも要約してみました。(こちらはマネジメントコンソールから試しました)

# 通話要約

## 通話の目的・理由
顧客(タナカ様)は、本日の昼食用に注文したそばがまだ届いておらず、その件について問い合わせを行いました。

## 主要な問題点や質問
- タナカ様が注文したそばがまだ届いていない。
- 注文したそばの詳細と注文時間(12時30分頃)。
- 注文番号(xy.z.12345)の確認。

## 提供された解決策や回答
- オペレーターが注文状況を確認し、そばが現在配達員によって配達中であること、あと5分ほどで到着する見込みであることを伝えました。

## 通話の結果・結論
- タナカ様のそばは配達中であり、間もなく到着することが確認されました。
- タナカ様は安心し、落ち着いて食事ができるようになった。

## 必要に応じたフォローアップ事項
- オペレーターは今後このような不安を起こさないよう、よりスムーズな情報連携に努めることを確認しました。
- 顧客が今後何か不明な点があれば、お気軽に連絡するよう促しました。

同じプロンプトだと少し冗長な印象ですが、こちらも内容は十分に理解できます。

AWS内で完結でき、実行速度、コスト的にもメリットがあるため、選択肢としては悪くないと思います。

まとめ

というわけで、外部APIと連携することで、日本語の通話履歴も任意のLLMで要約することが可能です。

実運用にあたっては要約をどのタイミングで実行するか、などもう少し考慮事項がありますが、この記事が誰かのお役に立てば幸いです。

さいごに

暑い日が続きますが、皆様お身体に気をつけてお過ごしください。