Geminiを搭載したVertex AI SearchでGoogle Chatのチャットボットを作成してみた

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

G-gen の堂原です。Gemini を搭載した Vertex AI Search を用いて、Google Chat のチャットボットを作成してみましたので、紹介します。

はじめに

当記事では、Google Cloud(旧称 GCP)の Vertex AI Search を用いて、Google Chat のチャットボットを作成してみます。

Google Chat は Google Workspace に含まれるチャットツールです。REST API である Google Chat API を用いることで、Google Chat から利用するチャットボットを簡単に開発することが可能です。

Google Chat API と Vertex AI Search を組み合わせることで、データの検索や回答の要約をしてくれるチャットボットを開発できます。また、過去の質問と回答の履歴を取り込むことで、チャットボットの回答の精度を上げることができます。

チャットボットのサンプル

前提知識

Vertex AI Search は Google Cloud の生成 AI(Generative AI)関連サービスの1つで、Retrieval Augmented Generation(RAG)構成を簡単に構築できるサービスです。以下の記事をご参照ください。

blog.g-gen.co.jp

Vertex AI Search では、2024年1月のアップデートで、要約文の生成に Gemini Pro が利用できるようになりました。

Google Chat API

Google Chat API は、Google Chat に用意された REST API です。メッセージの投稿や、スペースの管理などをプログラムから行うことができます。Google Chat API を用いることで、Google Chat アプリ(チャットボット)を開発することができます。

Google Chat API を使うことで、対話型・非対話型のチャットボットが作成可能であり、メッセージ形式も柔軟にチューニングできます。

対話型のチャットボットを開発するには、Google Cloud コンソールで Google Chat API のセットアップを行う必要があります。その際、Google Chat から送られてきたメッセージを処理するためのプログラムを開発して、何らかのプラットフォームで稼働させる必要があります。Google Chat 自体には、プログラムを稼働させるプラットフォームは無いためです。以下は、実装方法の例です。

  • プログラムを Cloud Functions にデプロイする。トリガー URL をGoogle Chat API から指定する
  • プログラムを Google Apps Script で実装する。Apps Script プロジェクトのデプロイ ID を Google Chat API から指定する

なお Google Chat API を利用するには、Google Cloud プロジェクトが必須です。

構成図

当記事では、以下のような構成でチャットボットを開発しました。

  • Cloud Storage バケット内の PDF ファイルを検索対象とする Vertex AI Search データストア及びアプリを作成
  • Google Chat からメッセージを受け取り、Vertex AI Search に検索をかける Cloud Functions プログラムを開発
  • Cloud Functions のトリガー URL を指定するよう Google Chat API を設定

Google Workspace の公式ドキュメントにも、Cloud Functions を用いて Google Chat アプリを作成するチュートリアルがあり、上記の構成に似た設定を行っている箇所があるため、参考にしてください。

Vertex AI Search の設定

Vertex AI Search でデータストアとアプリ(App)を作成します。手順については、当記事では詳述しません、

今回のチャットボットでは、要約文を生成する必要があるため、アプリ(App)作成時に Advanced LLM features を有効化します。

Cloud Functions の設定

パラメータ

以下のようにパラメータを設定します。記載のないパラメータは、デフォルト値とします。

設定項目 小項目 設定値 補足
環境 第2世代
トリガー トリガーのタイプ HTTPS
認証 未承認の呼び出しを許可 Google Chat API は認証タイプ「承認が必要」に対応していないため、ソースコード内でのチェックが必要です
サービスアカウント 「ディスカバリー エンジン閲覧者」ロールを有しているものを指定
ランタイム環境変数 PROJECT_ID 今回使用している Google Cloud プロジェクトの ID
PROJECT_NUMBER 今回使用している Google Cloud プロジェクトのプロジェクト番号
DATA_STORE Vertex AI Search データストアの ID
CHAT_ISSUER リクエストが作成したチャットボットから来たものかを判別するために利用。値は「chat@system.gserviceaccount.com」で固定
PUBLIC_CERT_URL_PREFIX リクエストが作成したチャットボットから来たものかを判別するために利用。値は「https://www.googleapis.com/service_accounts/v1/metadata/x509/」で固定
ランタイム Python 3.11

ソースコード

requirements.txt

functions-framework==3.*
google-cloud-discoveryengine==0.11.6
oauth2client==4.1.3

main.py

ソースコード本文は、本記事の末尾に掲載します。以下に、重要なポイントを解説します。

import

2024年2月現在では、Vertex AI Search で Gemini Pro を指定するためには google.cloud.discoveryengine_v1alpha を用いる必要があります。

from google.cloud.discoveryengine_v1alpha import SearchServiceClient, SearchRequest

認証

先述のパラメータの通り、Google Chat API と Cloud Funcitons による実装では、Cloud Functions の認証タイプを「未承認の呼び出しを許可」にする必要があります。この状態では、Cloud Functions の呼び出し URL がわかれば、誰でも Cloud Functions を起動できてしまいます。

当記事の構成では、Google Chat からのリクエストに含まれている Authorization ヘッダーの情報を用いて、アクセス元が特定の Google Cloud プロジェクトにてセットアップされたチャットボットであることを確認します。これにより、チャットボット以外からの呼び出しや、他の Google Cloud プロジェクトで作成されたチャットボットからの呼び出しは拒否されます。また、後述する Google Chat API のセットアップを行うためには、Google Cloud プロジェクトに対して IAM 権限 chat.bots.update が必要です。

つまり、Cloud Functions からメッセージを受け取れるのは、開発者が意図したチャットボットに限定されます。そのチャットボットの設定変更も、IAM 権限を持っている者しかできません。

以上のことから、本構成は十分にセキュアであり、意図しない利用者からチャットボットが利用されることはない、ということができます。

詳細については以下の参考リンクを確認ください。

以下が、ソースコードの該当部分です。

    if req.method == "GET":
        return flask.make_response(flask.jsonify({"message": "Bad Request"}), 400)
    
    auth_header = req.headers.get("Authorization")
    # 「Authorization」ヘッダーが存在するか確認
    if not auth_header:
        print("Missing Authorization header")
        return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    
    # 「Authorization」ヘッダーが「Bearer」から始まるか確認
    if auth_header.split()[0] != "Bearer":
        print("Authorization header format is incorrect")
        return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    
    bearer_token = auth_header.split()[1]
    try:
        token = client.verify_id_token(
            bearer_token, PROJECT_NUMBER, cert_uri=PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER)
        if token['iss'] != CHAT_ISSUER:
            print("Invalid issuee")
            return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    except:
        print("Invalid token")
        return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)

Gemini Pro の指定

Vertex AI Search では、デフォルトでは基盤モデルとして PaLM 2 を利用します。Gemini Pro を使うには、Vertex AI Search へのリクエスト時にクラス ModelSpec で、モデルを明示的に指定する必要があります。パラメータ versionpreview にすることで Gemini Pro の指定が可能です。

    content_search_spec = SearchRequest.ContentSearchSpec(
        # スニペットを出力させない
        snippet_spec=SearchRequest.ContentSearchSpec().SnippetSpec(
            return_snippet=False
        ),
        # 要約文を出力させる
        summary_spec=SearchRequest.ContentSearchSpec().SummarySpec(
            summary_result_count=3,
            include_citations=False,
            
            # Gemini Proを用いるように指定
            model_spec=SearchRequest.ContentSearchSpec().SummarySpec().ModelSpec(
                version="preview"
            )
        )
    )
    
    # Vertex AI Searchにクエリを投げる
    response = discov_client.search(
        SearchRequest(
            serving_config=serving_config,
            query=text,
            page_size=3,
            content_search_spec=content_search_spec
        )
    )

Google Chat API の設定

Google Cloud プロジェクトで、Google Chat API を有効にします。その後、Google Chat API の「構成」タブにて以下の設定を行います。

設定項目 小項目 設定値 補足
インタラクティブ機能 1:1 のメッセージを受信する チェックをつけるとダイレクトメッセージでチャットボットが使えます
スペースとグループの会話に参加する チェックをつけるとスペースにチャットボットを導入できます
接続方法 「アプリの URL」を指定したうえで、Cloud Funcitons のトリガー URL を入力する
公開設定 「このチャットアプリを xxx (現在の Google アカウントの組織に依存) の特定のユーザーとグループが使用できるようにします」にチェックを付けて、チャットボットを使わせたいユーザまたはグループのメールアドレスを入力 より広い範囲で公開したい場合は Google Workspace Marketplace SDK を用いる必要があります

Google Chat API 設定画面サンプル

添付 : ソースコード

from typing import Any, Mapping
from google.cloud.discoveryengine_v1alpha import SearchServiceClient, SearchRequest
from google.protobuf.json_format import MessageToDict
from oauth2client import client
 
import functions_framework, flask, os
  
  
PROJECT_ID = os.environ.get("PROJECT_ID")
PROJECT_NUMBER = os.environ.get("PROJECT_NUMBER")
DATA_STORE = os.environ.get("DATA_STORE")
 
# Google Chatから送られてくるBearer Tokenの整合に使用
CHAT_ISSUER = os.environ.get("CHAT_ISSUER")
PUBLIC_CERT_URL_PREFIX = os.environ.get("PUBLIC_CERT_URL_PREFIX")
 
 
def search_document(text: str) -> Mapping[str, Any]:
    discov_client = SearchServiceClient()
    
    # Vertex AI Searchのアプリ等基本的な内容を設定
    serving_config = discov_client.serving_config_path(
        project=PROJECT_ID,
        location="global",
        data_store=DATA_STORE,
        serving_config="default_config"
    )
    
    # Vertex AI Searchの出力内容に関する設定
    content_search_spec = SearchRequest.ContentSearchSpec(
        # スニペットを出力させない
        snippet_spec=SearchRequest.ContentSearchSpec().SnippetSpec(
            return_snippet=False
        ),
        # 要約文を出力させる
        summary_spec=SearchRequest.ContentSearchSpec().SummarySpec(
            summary_result_count=3,
            include_citations=False,
            
            # Gemini Proを用いるように指定
            model_spec=SearchRequest.ContentSearchSpec().SummarySpec().ModelSpec(
                version="preview"
            )
        )
    )
    
    # Vertex AI Searchにクエリを投げる
    response = discov_client.search(
        SearchRequest(
            serving_config=serving_config,
            query=text,
            page_size=3,
            content_search_spec=content_search_spec
        )
    )
    
    # 要約文取得
    summary = response.summary.summary_text.replace("<b>", "").replace("</b>", "")
    
    # 関連ファイル取得
    references = []
    for r in response.results:
        r_dct = MessageToDict(r._pb)
        link = r_dct["document"]["derivedStructData"]["link"]
            
        references.append(link.split("/")[-1])
            
    result = {
        "summary": summary,
        "references": references
    }
  
    return result
 
 
# チャットボットが実際に出力するメッセージを作成する
def create_message(text: str) -> Mapping[str, Any]:
    result = search_document(text=text)
    reference_sentence = "<br>・".join(result["references"])
    
    # 返信文作成
    cards = {
        "cardsV2": [
            {
                "cardId": "searchResults",
                "card": {
                    "sections": [
                        {
                            "collapsible": False,
                            "widgets": [
                                {
                                    "textParagraph": {
                                        "text": "<b>回答文</b><br>" + result["summary"]
                                    }
                                },
                                {
                                    "divider": {}
                                },
                                {
                                    "textParagraph": {
                                        "text": "<b>関連ファイル</b><br>・" + reference_sentence
                                    }
                                }
                            ]
                        }
                    ]
                }
            }
        ]
    }
    
    return cards
 
 
# チャットボットからメッセージを受信
# → create_message() で作成した JSON を返す
@functions_framework.http
def get_chat(req: flask.Request):
    """
    正しいGoogle Chatから送られてきたリクエストかを確認
    """
    
    if req.method == "GET":
        return flask.make_response(flask.jsonify({"message": "Bad Request"}), 400)
    
    auth_header = req.headers.get("Authorization")
    # 「Authorization」ヘッダーが存在するか確認
    if not auth_header:
        print("Missing Authorization header")
        return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    
    # 「Authorization」ヘッダーが「Bearer」から始まるか確認
    if auth_header.split()[0] != "Bearer":
        print("Authorization header format is incorrect")
        return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    
    bearer_token = auth_header.split()[1]
    try:
        token = client.verify_id_token(
            bearer_token, PROJECT_NUMBER, cert_uri=PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER)
        if token['iss'] != CHAT_ISSUER:
            print("Invalid issuee")
            return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    except:
        print("Invalid token")
        return flask.make_response(flask.jsonify({"message": "Unauthorized"}), 401)
    
    """
    受け取ったメッセージでVertex AI Searchに検索をかける
    検索結果を整形し送信する
    """
    
    request_json = req.get_json(silent=True)
    
    text = request_json["message"]["text"]
    
    response = create_message(text=text)
  
    return response

堂原 竜希(記事一覧)

クラウドソリューション部データアナリティクス課。2023年4月より、G-genにジョイン。

Google Cloud Partner Top Engineer 2023, 2024に選出 (2024年はRookie of the yearにも選出)。休みの日はだいたいゲームをしているか、時々自転車で遠出をしています。