1. 简介
概览
在此 Codelab 中,您将了解如何使用 Vertex AI Gemini API 和 Vertex AI 客户端库创建一个以节点编写的基本聊天机器人。此应用使用由 Google Cloud Firestore 支持的快速会话存储。
学习内容
- 如何使用 htmx、tailwindcss 和 express.js 构建 Cloud Run 服务
- 如何使用 Vertex AI 客户端库向 Google API 进行身份验证
- 如何创建与 Gemini 模型互动的聊天机器人
- 如何在不使用 Docker 文件的情况下部署到 Cloud Run 服务
- 如何使用由 Google Cloud Firestore 支持的快速会话存储
2. 设置和要求
前提条件
- 您已登录 Cloud 控制台。
- 您之前已部署了 Cloud Run 服务。例如,您可以按照快速入门:部署 Web 服务开始操作。
激活 Cloud Shell
- 在 Cloud Console 中,点击激活 Cloud Shell。
如果这是您第一次启动 Cloud Shell,系统会显示一个中间屏幕,说明它是什么。如果您看到中间屏幕,请点击继续。
预配和连接到 Cloud Shell 只需花几分钟时间。
这个虚拟机装有所需的所有开发工具。它提供了一个持久的 5 GB 主目录,并在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。您在此 Codelab 中的大部分(即使不是全部)工作都可以通过浏览器完成。
在连接到 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
在开始使用此 Codelab 之前,您需要先启用多个 API。此 Codelab 需要使用以下 API。您可以通过运行以下命令来启用这些 API:
gcloud services enable run.googleapis.com \ cloudbuild.googleapis.com \ aiplatform.googleapis.com \ secretmanager.googleapis.com
设置环境变量
您可以设置要在整个 Codelab 中使用的环境变量。
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 结算方案。
- 您可以选择为此 Codelab 启用 Google Analytics。
- 点击添加 Firebase。
- 创建项目后,点击继续。
- 从构建菜单中,点击 Firestore 数据库。
- 点击创建数据库。
- 从位置下拉列表中选择您的区域,然后点击下一步。
- 使用默认的以生产模式开始,然后点击创建。
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 User 角色。
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/aiplatform.user
现在,在 Secret Manager 中创建 Secret。Cloud Run 服务会将此 Secret 作为环境变量进行访问,该环境变量在实例启动时进行解析。您可以详细了解 Secret 和 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. 在本地运行服务
首先,确保您位于此 Codelab 的根目录 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 User 和 Datastore User 角色);或者 2) 您可以模拟此 Codelab 中使用的服务账号进行登录。
选项 1) 将您的 ADC 凭据用于 ADC
如果您想使用自己的凭据,可以先运行 gcloud auth list
来验证您在 gcloud 中是如何进行身份验证的。接下来,您可能需要向自己的身份授予 Vertex AI User 角色。如果您的身份具有 Owner 角色,则表明您已拥有此 Vertex AI 用户角色。如果没有,您可以运行此命令来授予您的身份 Vertex AI 用户角色和 Datastore User 角色。
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 模拟服务账号
如果您想使用在此 Codelab 中创建的服务账号,您的用户账号需要具备 Service Account Token Creator 角色。您可以通过运行以下命令来获取此角色:
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. 恭喜!
恭喜您完成此 Codelab!
建议您查看 Cloud Run 和 Vertex AI Gemini API 文档。
所学内容
- 如何使用 htmx、tailwindcss 和 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
服务。此外,为避免意外调用 Gemini,建议您删除 vertex-ai-caller
服务账号或撤消 Vertex AI User 角色。
如果您选择删除整个项目,可以前往 https://console.cloud.google.com/cloud-resource-manager,选择您在第 2 步中创建的项目,然后选择“删除”。如果删除项目,则需要在 Cloud SDK 中更改项目。您可以通过运行 gcloud projects list
来查看所有可用项目的列表。