はじめに
Vertex AI Agent Builder を使ったナレッジ検索AIチャットbot を LINE WORKS で作成します。
当記事では LINE WORKS との繋ぎ込み部分を中心に紹介していければと思います。LINE WORKS はビジネス版の LINE サービスで直感的な操作で扱えるチャットサービスとなっています!
システム構成図
- Agent Builder
- Cloud Run 関数
- Google Cloud Storage
- Secret Manager
- Cloud Scheduler
- LINE WORKS API
LINE WORKS API を利用するためにはアクセストークンが必要となるため Secret Manager で保持、Cloud Scheduler を使って定期更新を行います。
LINE WORKS の API と BOT を登録する
Developers ConsoleからAPIとBotの登録を行います。
APIの設定
API、ClientAppからアプリの新規追加を行います。
生成された Client ID と Client Secret は後で使うので保存しておきます。
OAuth Scope の管理から「bot」権限を選択します。
API の設定を保存して完了します。
Botの設定
Bot タブの登録から Bot 情報の登録を行います。
以下の項目を入力します。
- プロフィール画像(任意)
- Bot名
- 説明
- 管理者
Callback URL は後から設定します。
Bot、APIの基本設定は以上となります。
Agent Builder の用意
データストアの作成
Agent Builder からデータストアを作成します。今回はAIに生成してもらった架空の社内規定を Cloud Storage に保存して参照データとします。
Agent Builder アプリの作成
データストアの作成から Cloud Storage を選択してバケットを選択します。データストアが作成できたら Agent Builder アプリを作成します。
アプリの作成から検索を選択して、参照したいデータストアを選びます。
作成が完了したらプレビューで確認します。(反映には少し時間がかかります)
Vertex AI Studio でプロンプトを作成
Vertex AI Studio から新しいプロンプトを作成して、データストアのパスを入力します。
作成されたプロンプトからコードを取得します。
Cloud Run 関数で Bot アプリの作成
Tokenについて
LINE WORKS へ接続するためには Access Token が必要です。Access Token の期限は 24時間で切れてしまうため、Refresh Token を使用して再発行する必要があります。
- Access Token
- 有効期限: 24 時間
- Refresh Token
- 有効期限: 90 日
今回は Cloud Scheduler を使用して Token の更新を行います。
メイン処理関数
LINE WORKS からの応答と回答生成を行うメイン関数を作成します。回答生成部分は Vertex AI Studio から生成したコードを使用しています。
環境変数は LINE WORKS から取得した値を設定します。Token 情報はそれぞれの環境に合わせて管理してください。
参考ドキュメント
import functions_framework import base64 import hashlib import hmac import os import requests import logging import google.cloud.logging import google.auth import google.auth.transport.requests from flask import abort # Vertex AI Studio import vertexai from vertexai.generative_models import GenerativeModel, Tool import vertexai.preview.generative_models as generative_models BOT_SECRET = os.getenv("LINE_BOT_SECRET") PROJECT_NUMBER = os.getenv("PROJECT_NUMBER") PROJECT_ID = os.getenv("PROJECT_NUMBER") CLIENT_ID = os.getenv("CLIENT_ID") CLIENT_SECRET = os.getenv("CLIENT_SECRET") PRIVATE_KEY = os.getenv("PRIVATE_KEY") PUBLIC_KEY = os.getenv("PUBLIC_KEY") BOT_ID = os.getenv("BOT_ID") DATASTORE_ID = os.getenv("DATASTORE_ID") TOKEN = "" # Loggerの設定 logging.basicConfig( format="[%(asctime)s][%(levelname)s] %(message)s", level=logging.DEBUG ) logger = logging.getLogger() logging_client = google.cloud.logging.Client() logging_client.setup_logging() logger.setLevel(logging.DEBUG) @functions_framework.http def handle_request(request): # 署名検証 is_valid = validate_request(request) # Line Works からのrequestを取得 data = request.get_json() text = data["content"]["text"] # webリソースRAG output_text = generate(text) post_message(request, output_text) return "success", 200 # 署名検証 def validate_request(request): headers_signature = request.headers["X-WORKS-Signature"] body = request.data.decode("utf-8") hash = hmac.new( BOT_SECRET.encode("utf-8"), body.encode("utf-8"), hashlib.sha256 ).digest() signature = base64.b64encode(hash).decode("utf-8") if not hmac.compare_digest(signature, headers_signature): abort(403, "Invalid signature.") # Vertex AI Studio def generate(text): vertexai.init(project=PROJECT_ID, location="asia-northeast1") tools = [ Tool.from_retrieval( retrieval=generative_models.grounding.Retrieval( source=generative_models.grounding.VertexAISearch( datastore=f"projects/{PROJECT_ID}/locations/global/collections/default_collection/dataStores/{DATASTORE_ID}" ), disable_attribution=False, ) ), ] model = GenerativeModel( "gemini-1.5-flash-001", tools=tools, system_instruction=[ """提供された情報がない場合は「その質問には答えられません。」と返してください""" ], ) responses = model.generate_content( [f"""{text}"""], generation_config=generation_config, safety_settings=safety_settings, ) # 出力テキスト output_text = responses.candidates[0].content.parts[0].text return output_text generation_config = { "max_output_tokens": 8192, "temperature": 0, "top_p": 0.21, } safety_settings = { generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_ONLY_HIGH, generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_ONLY_HIGH, generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_ONLY_HIGH, generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_ONLY_HIGH, } # メッセージ送信 def post_message(request, output_text): data = request.get_json() userId = data["source"]["userId"] # APIの設定 api_url = f"https://www.worksapis.com/v1.0/bots/{BOT_ID}/users/{userId}/messages" headers = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"} # メッセージデータ data = {"content": {"type": "text", "text": output_text}} # リクエストを送信 response = requests.post(api_url, json=data, headers=headers) # レスポンスを確認 print(response.status_code)
Python で Token 発行を行うコード例
Access Token と Refresh Token の発行
import datetime import os import time import jwt import requests CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] PRIVATE_KEY = os.environ["PRIVATE_KEY"] BOT_ID = os.environ["BOT_ID"] SERVICE_ACCOUNT_ID = os.environ["SERVICE_ACCOUNT_ID"] # JWTを生成 payload = { "iss": CLIENT_ID, "sub": SERVICE_ACCOUNT_ID, "iat": int(time.time()), "exp": int(time.time()) + 3600, } encoded_jwt = jwt.encode(payload, PRIVATE_KEY, algorithm="RS256") api_url = "https://auth.worksmobile.com/oauth2/v2.0/token" headers = { "Content-Type": "application/x-www-form-urlencoded", } data = { "assertion": encoded_jwt, "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "scope": "bot", } # アクセストークンを発行 response = requests.post(api_url, data=data, headers=headers) if response.status_code == 200: data = response.json() refresh_token = data.get("refresh_token", None) access_token = data.get("access_token", None) if refresh_token: print("Access Token:", access_token) print("Refresh Token:", refresh_token) else: print("Access token not found in the response.") else: print(response.json()) refresh_token = False access_token = False
Refresh Token を使用して Access Token を再発行
import os import requests CLIENT_ID = os.environ["CLIENT_ID"] CLIENT_SECRET = os.environ["CLIENT_SECRET"] PRIVATE_KEY = os.environ["PRIVATE_KEY"] BOT_ID = os.environ["BOT_ID"] SERVICE_ACCOUNT_ID = os.environ["SERVICE_ACCOUNT_ID"] api_url = "https://auth.worksmobile.com/oauth2/v2.0/token" headers = { "Content-Type": "application/x-www-form-urlencoded", } # 保持してあるリフレッシュトークン refresh_token = "" data = { "refresh_token": refresh_token, "grant_type": "refresh_token", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, } # アクセストークンを発行 response = requests.post(api_url, data=data, headers=headers) if response.status_code == 200: data = response.json() access_token = data.get("access_token", None) if access_token: print("Access Token:", access_token) else: print("Access token not found in the response.") else: print("Failed to fetch data:", response.json()) access_token = False
LINE WORKS へ最終連携
Cloud Run 関数が作成できたら LINE WORKS の Developers Console で Callback URL を追加します。
Callback URL には Cloud Run 関数のURLを追加してください。
BOT の追加
Admin ページからBotの追加を行います。サービス設定 → Bot タブからBot追加を選択します。
Bot が正しく登録されている場合、追加ページで選択することができます。
追加後のデフォルト状態では非公開となってるため、必要に応じて公開設定の変更を行ってください。
公開する範囲をメンバー指定で制限することも可能です。
実際に BOT を利用する
新規作成 → 社内メンバーとトーク → Bot から選択します。
選択した Bot にメッセージを送信すると回答が返されます。
おわりに
LINE WORKS で Vertex AI を利用したナレッジ検索 BOT を作成することができました。
Slack などと比較するとスレッド管理が行えない分少し自由度は下がりますが、LINE WORKS でも十分にナレッジ検索ツールとして機能します!