1. 簡介
總覽
在本程式碼研究室中,您將瞭解如何使用 Vertex AI Gemini API 和 Vertex AI 用戶端程式庫,建立以節點編寫的基本聊天機器人。這個應用程式使用 Google Cloud Firestore 支援的快速工作階段存放區。
課程內容
- 如何使用 htmx、tailwindcs 和 express.js 建構 Cloud Run 服務
- 如何使用 Vertex AI 用戶端程式庫向 Google API 進行驗證
- 如何建立與 Gemini 模型互動的聊天機器人
- 如何在沒有 Docker 檔案的情況下部署至 Cloud Run 服務
- 如何使用 Google Cloud Firestore 支援的快速工作階段儲存庫
2. 設定和需求
必要條件
- 您已登入 Cloud 控制台。
- 先前已部署 Cloud Run 服務。舉例來說,您可以按照從原始碼部署網路服務的快速入門導覽課程著手。
啟用 Cloud Shell
- 在 Cloud 控制台中,按一下「啟用 Cloud Shell」圖示
。
如果您是第一次啟動 Cloud Shell,系統會顯示中繼畫面,說明這項服務的內容。如果系統顯示中繼畫面,請按一下「繼續」。
佈建並連線至 Cloud Shell 只需幾分鐘的時間。
這個虛擬機器已載入所有必要的開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,大幅提高網路效能和驗證能力。在本程式碼研究室中,您的大部分作業都可透過瀏覽器完成。
連線至 Cloud Shell 後,您應會發現自己通過驗證,且專案已設為您的專案 ID。
- 在 Cloud Shell 中執行下列指令,確認您已通過驗證:
gcloud auth list
指令輸出
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com> To set the active account, run: $ gcloud config set account `ACCOUNT`
- 在 Cloud Shell 中執行下列指令,確認 gcloud 指令知道您的專案:
gcloud config list project
指令輸出
[core] project = <PROJECT_ID>
如果尚未設定,請使用下列指令進行設定:
gcloud config set project <PROJECT_ID>
指令輸出
Updated property [core/project].
3. 啟用 API 並設定環境變數
啟用 API
開始使用本程式碼研究室之前,您必須先啟用多個 API。本程式碼研究室需要使用下列 API。您可以執行下列指令來啟用這些 API:
gcloud services enable run.googleapis.com \ cloudbuild.googleapis.com \ aiplatform.googleapis.com \ secretmanager.googleapis.com
設定環境變數
您可以設定將在本程式碼研究室中使用的環境變數。
PROJECT_ID=<YOUR_PROJECT_ID> REGION=<YOUR_REGION, e.g. us-central1> SERVICE=chat-with-gemini SERVICE_ACCOUNT="vertex-ai-caller" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com SECRET_ID="SESSION_SECRET"
4. 建立及設定 Firebase 專案
- 在 Firebase 控制台,按一下「新增專案」。
- 輸入 <YOUR_PROJECT_ID>,將 Firebase 新增至現有的其中一項 Google Cloud 專案
- 如果出現提示訊息,請詳閱並接受 Firebase 條款。
- 按一下「繼續」。
- 按一下「確認方案」即可確認訂閱 Firebase 計費方案。
- 您可以選擇在本程式碼研究室中啟用 Google Analytics。
- 按一下「新增 Firebase」。
- 專案建立完成後,按一下「Continue」。
- 在「建構」選單中,按一下「Firestore 資料庫」。
- 按一下 [Create database] (建立資料庫)。
- 從「位置」下拉式選單中選擇您的區域,然後點選「下一步」。
- 使用預設的「在正式環境中啟動」,然後點選「建立」。
5. 建立服務帳戶
Cloud Run 會使用這個服務帳戶呼叫 Vertex AI Gemini API。這個服務帳戶也有權讀取、寫入 Firestore,以及讀取 Secret Manager 的密鑰。
首先,請執行下列指令來建立服務帳戶:
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run to access Vertex AI APIs"
接著,將「Vertex AI 使用者」角色授予服務帳戶。
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/aiplatform.user
接著在 Secret Manager 中建立密鑰。Cloud Run 服務會以環境變數的形式存取這組密鑰,並在執行個體啟動時完成解析。進一步瞭解密鑰和 Cloud Run。
gcloud secrets create $SECRET_ID --replication-policy="automatic" printf "keyboard-cat" | gcloud secrets versions add $SECRET_ID --data-file=-
並將 Secret Manager 中快速工作階段密鑰的存取權授予服務帳戶。
gcloud secrets add-iam-policy-binding $SECRET_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role='roles/secretmanager.secretAccessor'
最後,授予服務帳戶 Firestore 的讀取及寫入權限。
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/datastore.user
6. 建立 Cloud Run 服務
首先,建立原始碼的目錄,並以 cd 指向該目錄。
mkdir chat-with-gemini && cd chat-with-gemini
接著,建立含有以下內容的 package.json
檔案:
{ "name": "chat-with-gemini", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "start": "node app.js", "nodemon": "nodemon app.js", "cssdev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch", "tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css", "dev": "npm run tailwind && npm run nodemon" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@google-cloud/connect-firestore": "^3.0.0", "@google-cloud/firestore": "^7.5.0", "@google-cloud/vertexai": "^0.4.0", "axios": "^1.6.8", "express": "^4.18.2", "express-session": "^1.18.0", "express-ws": "^5.0.2", "htmx.org": "^1.9.10" }, "devDependencies": { "nodemon": "^3.1.0", "tailwindcss": "^3.4.1" } }
接著,建立含有以下內容的 app.js
來源檔案。此檔案包含服務的進入點,且包含應用程式的主要邏輯。
const express = require("express"); const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); const path = require("path"); const fs = require("fs"); const util = require("util"); const { spinnerSvg } = require("./spinnerSvg.js"); // cloud run retrieves secret at instance startup time const secret = process.env.SESSION_SECRET; const { Firestore } = require("@google-cloud/firestore"); const { FirestoreStore } = require("@google-cloud/connect-firestore"); var session = require("express-session"); app.set("trust proxy", 1); // trust first proxy app.use( session({ store: new FirestoreStore({ dataset: new Firestore(), kind: "express-sessions" }), secret: secret, /* set secure to false for local dev session history testing */ /* see more at https://expressjs.com/en/resources/middleware/session.html */ cookie: { secure: true }, resave: false, saveUninitialized: true }) ); const expressWs = require("express-ws")(app); app.use(express.static("public")); // Vertex AI Section const { VertexAI } = require("@google-cloud/vertexai"); // instance of Vertex model let generativeModel; // on startup const port = parseInt(process.env.PORT) || 8080; app.listen(port, async () => { console.log(`demo1: listening on port ${port}`); // get project and location from metadata service const metadataService = require("./metadataService.js"); const project = await metadataService.getProjectId(); const location = await metadataService.getRegion(); // Vertex client library instance const vertex_ai = new VertexAI({ project: project, location: location }); // Instantiate models generativeModel = vertex_ai.getGenerativeModel({ model: "gemini-1.0-pro-001" }); }); app.ws("/sendMessage", async function (ws, req) { if (!req.session.chathistory || req.session.chathistory.length == 0) { req.session.chathistory = []; } let chatWithModel = generativeModel.startChat({ history: req.session.chathistory }); ws.on("message", async function (message) { console.log("req.sessionID: ", req.sessionID); // get session id let questionToAsk = JSON.parse(message).message; console.log("WebSocket message: " + questionToAsk); ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div id="questionToAsk" class="text-black m-2 text-right border p-2 rounded-lg ml-24"> ${questionToAsk} </div></div>`); // to simulate a natural pause in conversation await sleep(500); // get timestamp for div to replace const now = "fromGemini" + Date.now(); ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div id=${now} class=" text-blue-400 m-2 text-left border p-2 rounded-lg mr-24"> ${spinnerSvg} </div></div>`); const results = await chatWithModel.sendMessage(questionToAsk); const answer = results.response.candidates[0].content.parts[0].text; ws.send(`<div id=${now} hx-swap-oob="true" hx-swap="outerHTML" class="text-blue-400 m-2 text-left border p-2 rounded-lg mr-24"> ${answer} </div>`); // save to current chat history let userHistory = { role: "user", parts: [{ text: questionToAsk }] }; let modelHistory = { role: "model", parts: [{ text: answer }] }; req.session.chathistory.push(userHistory); req.session.chathistory.push(modelHistory); // console.log( // "newly saved chat history: ", // util.inspect(req.session.chathistory, { // showHidden: false, // depth: null, // colors: true // }) // ); req.session.save(); }); ws.on("close", () => { console.log("WebSocket was closed"); }); }); function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } // gracefully close the web sockets process.on("SIGTERM", () => { server.close(); });
建立 tailwindCSS 的 tailwind.config.js
檔案。
/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./**/*.{html,js}"], theme: { extend: {} }, plugins: [] };
建立 metadataService.js
檔案,取得已部署 Cloud Run 服務的專案 ID 和區域。這些值將用於對 Vertex AI 用戶端程式庫的執行個體執行個體化。
const your_project_id = "YOUR_PROJECT_ID"; const your_region = "YOUR_REGION"; const axios = require("axios"); module.exports = { getProjectId: async () => { let project = ""; try { // Fetch the token to make a GCF to GCF call const response = await axios.get( "http://metadata.google.internal/computeMetadata/v1/project/project-id", { headers: { "Metadata-Flavor": "Google" } } ); if (response.data == "") { // running locally on Cloud Shell project = your_project_id; } else { // running on Clodu Run. Use project id from metadata service project = response.data; } } catch (ex) { // running locally on local terminal project = your_project_id; } return project; }, getRegion: async () => { let region = ""; try { // Fetch the token to make a GCF to GCF call const response = await axios.get( "http://metadata.google.internal/computeMetadata/v1/instance/region", { headers: { "Metadata-Flavor": "Google" } } ); if (response.data == "") { // running locally on Cloud Shell region = your_region; } else { // running on Clodu Run. Use region from metadata service let regionFull = response.data; const index = regionFull.lastIndexOf("/"); region = regionFull.substring(index + 1); } } catch (ex) { // running locally on local terminal region = your_region; } return region; } };
建立名為 spinnerSvg.js
的檔案
module.exports.spinnerSvg = `<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" ></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" ></path></svg>`;
最後,為 tailwindCSS 建立 input.css
檔案。
@tailwind base; @tailwind components; @tailwind utilities;
現在,建立新的 public
目錄。
mkdir public cd public
在該公開目錄中,為前端建立 index.html
檔案,用來使用 htmx。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous" ></script> <link href="./output.css" rel="stylesheet" /> <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script> <title>Demo 1</title> </head> <body> <div id="herewego" text-center> <!-- <div id="replaceme2" hx-swap-oob="true">Hello world</div> --> <div class="container mx-auto mt-8 text-center max-w-screen-lg" > <div class="overflow-y-scroll bg-white p-2 border h-[500px] space-y-4 rounded-lg m-auto" > <div id="toupdate"></div> </div> <form hx-trigger="submit, keyup[keyCode==13] from:body" hx-ext="ws" ws-connect="/sendMessage" ws-send="" hx-on="htmx:wsAfterSend: document.getElementById('message').value = ''" > <div class="mb-6 mt-6 flex gap-4"> <textarea rows="2" type="text" id="message" name="message" class="block grow rounded-lg border p-6 resize-none" required > Is C# a programming language or a musical note?</textarea > <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium" > Send </button> </div> </form> </div> </div> </body> </html>
7. 在本機執行服務
首先,請確認您位於程式碼研究室的根目錄 chat-with-gemini
。
cd .. && pwd
接著執行下列指令,安裝依附元件:
npm install
在本機執行時使用 ADC
如果您是在 Cloud Shell 中執行,代表已經在 Google Compute Engine 虛擬機器上執行。應用程式預設憑證會自動使用與這個虛擬機器相關聯的憑證 (如執行 gcloud auth list
後所示),因此您不必使用 gcloud auth application-default login
指令。您可以跳到「建立本機工作階段密鑰」一節
不過,如果您是在本機終端機 (而非 Cloud Shell 中) 執行應用程式,則必須使用應用程式預設憑證向 Google API 進行驗證。您可以 1) 以憑證登入 (前提是您同時擁有 Vertex AI 使用者和 Datastore 使用者角色);或是 2) 您可以模擬在這個程式碼研究室中使用的服務帳戶,藉此登入。
做法 1) 針對 ADC 使用憑證
如要使用憑證,可以先執行 gcloud auth list
以驗證您在 gcloud 中的驗證方式。接下來,您可能需要向身分授予「Vertex AI 使用者」角色。如果您的身分具備「擁有者」角色,則代表具備這個 Vertex AI 使用者角色。如果沒有,您可以執行以下指令,授予身分識別 Vertex AI 使用者角色和 Datastore 使用者角色。
USER=<YOUR_PRINCIPAL_EMAIL> gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/aiplatform.user gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/datastore.user
接著執行下列指令
gcloud auth application-default login
選項 2) 模擬 ADC 的服務帳戶
如要使用在本程式碼研究室中建立的服務帳戶,您的使用者帳戶必須具備服務帳戶權杖建立者角色。如要取得這個角色,請執行下列指令:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/iam.serviceAccountTokenCreator
接著執行下列指令,以透過服務帳戶使用 ADC
gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS
建立本機工作階段密鑰
現在,請為本機開發作業建立本機工作階段密鑰。
export SESSION_SECRET=local-secret
在本機執行應用程式
最後,您可以執行以下指令來啟動應用程式。這個指令碼也會產生 tailwindCSS 的 output.css 檔案。
npm run dev
您可以預覽網站,方法是開啟「網頁預覽」按鈕,然後選取「預覽通訊埠 8080」
8. 部署服務
首先,請執行下列指令來啟動部署作業,並指定要使用的服務帳戶。如未指定服務帳戶,系統會使用預設的運算服務帳戶。
gcloud run deploy $SERVICE \ --service-account $SERVICE_ACCOUNT_ADDRESS \ --source . \ --region $REGION \ --allow-unauthenticated \ --set-secrets="SESSION_SECRET=$(echo $SECRET_ID):1"
如果系統顯示「透過來源部署時,必須有 Artifact Registry Docker 存放區來儲存建構的容器」訊息,系統會在區域 [us-central1] 中建立名為 [cloud-run-source-deploy] 的存放區。"請按一下「y」接受並繼續
9. 測試服務
部署完成後,請在網路瀏覽器中開啟服務網址。然後向 Gemini 提問,例如:「我練習了吉他,但也是軟體工程師。看到「C#」時,我應該將其視為程式設計語言或音符?我該選擇哪一個?」
10. 恭喜!
恭喜您完成本程式碼研究室!
建議您查看說明文件 Cloud Run 和 Vertex AI Gemini API。
涵蓋內容
- 如何使用 htmx、tailwindcs 和 express.js 建構 Cloud Run 服務
- 如何使用 Vertex AI 用戶端程式庫向 Google API 進行驗證
- 如何建立與 Gemini 模型互動的聊天機器人
- 如何在沒有 Docker 檔案的情況下部署至 Cloud Run 服務
- 如何使用 Google Cloud Firestore 支援的快速工作階段儲存庫
11. 清除所用資源
為避免產生意外費用 (舉例來說,如果 Cloud Run 服務意外叫用次數超過免費方案的每月 Cloud Run 叫用分配數量),您可以刪除 Cloud Run 或刪除步驟 2 中建立的專案。
如要刪除 Cloud Run 服務,請前往 https://console.cloud.google.com/run 的 Cloud Run Cloud 控制台,然後刪除 chat-with-gemini
服務。建議您刪除 vertex-ai-caller
服務帳戶或撤銷 Vertex AI 使用者角色,以免不慎呼叫 Gemini。
如果選擇刪除整個專案,您可以前往 https://console.cloud.google.com/cloud-resource-manager,選取您在步驟 2 建立的專案,然後選擇「刪除」。如果刪除專案,您必須變更 Cloud SDK 中的專案。您可以執行 gcloud projects list
來查看可用專案的清單。