1. 简介
概览
在此 Codelab 中,您将了解如何使用名为函数调用的新功能授予 Gemini 访问实时数据的权限。为了模拟实时数据,您将构建一个天气服务端点,用于返回 2 个地点的当前天气信息。然后,您将构建一个由 Gemini 提供支持的聊天应用,该应用使用函数调用来检索当前天气。
我们来快速直观地了解函数调用。
- 该提示会请求获取指定地点的当前天气地点
- 系统会将此提示 + getWeather() 的函数协定发送给 Gemini
- Gemini 会要求聊天机器人应用调用“getWeather(西雅图)”代表其
- 应用发回结果(40 度出站流量,有雨)
- Gemini 会将结果发回给来电者
简而言之,Gemini 不调用 Functions 函数,作为开发者,您必须调用该函数并将结果发回 Gemini。
学习内容
- Gemini 函数调用的运作方式
- 如何将由 Gemini 提供支持的聊天机器人应用作为 Cloud Run 服务进行部署
2. 设置和要求
前提条件
- 您已登录 Cloud 控制台。
- 您之前已部署了第 2 代函数。例如,您可以按照《Cloud Functions(第 2 代)快速入门》中的说明开始部署。
激活 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
设置环境变量
您可以设置要在整个 Codelab 中使用的环境变量。
PROJECT_ID=<YOUR_PROJECT_ID> REGION=<YOUR_REGION, e.g. us-central1> WEATHER_SERVICE=weatherservice FRONTEND=frontend SERVICE_ACCOUNT="vertex-ai-caller" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
启用 API
在开始使用此 Codelab 之前,您需要先启用多个 API。此 Codelab 需要使用以下 API。您可以通过运行以下命令来启用这些 API:
gcloud services enable run.googleapis.com \ cloudbuild.googleapis.com \ aiplatform.googleapis.com
4. 创建服务账号以调用 Vertex AI
Cloud Run 将使用此服务账号调用 Vertex AI Gemini API。
首先,通过运行以下命令创建服务账号:
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
5. 创建后端 Cloud Run 服务
首先,为源代码创建一个目录,然后通过 cd 命令进入该目录。
mkdir -p gemini-function-calling/weatherservice gemini-function-calling/frontend && cd gemini-function-calling/weatherservice
然后,创建一个包含以下内容的 package.json
文件:
{ "name": "weatherservice", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.18.3" } }
接下来,创建一个包含以下内容的 app.js
源文件。此文件包含服务的入口点以及应用的主要逻辑。
const express = require("express"); const app = express(); app.get("/getweather", (req, res) => { const location = req.query.location; let temp, conditions; if (location == "New Orleans") { temp = 99; conditions = "hot and humid"; } else if (location == "Seattle") { temp = 40; conditions = "rainy and overcast"; } else { res.status(400).send("there is no data for the requested location"); } res.json({ weather: temp, location: location, conditions: conditions }); }); const port = parseInt(process.env.PORT) || 8080; app.listen(port, () => { console.log(`weather service: listening on port ${port}`); }); app.get("/", (req, res) => { res.send("welcome to hard-coded weather!"); });
部署 Weather Service
您可以使用此命令部署天气服务。
gcloud run deploy $WEATHER_SERVICE \ --source . \ --region $REGION \ --allow-unauthenticated
测试 Weather Service
您可以使用 curl 验证这两个位置的天气情况:
WEATHER_SERVICE_URL=$(gcloud run services describe $WEATHER_SERVICE \ --platform managed \ --region=$REGION \ --format='value(status.url)') curl $WEATHER_SERVICE_URL/getweather?location=Seattle curl $WEATHER_SERVICE_URL/getweather?location\=New%20Orleans
西雅图的气温为 40 华氏度,有雨;新奥尔良市始终湿润,气温为 99 度。
6. 创建前端服务
首先,使用 cd 命令进入前端目录。
cd gemini-function-calling/frontend
然后,创建一个包含以下内容的 package.json
文件:
{ "name": "demo1", "version": "1.0.0", "description": "", "main": "index.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/vertexai": "^0.4.0", "axios": "^1.6.7", "express": "^4.18.2", "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"); const expressWs = require("express-ws")(app); app.use(express.static("public")); const { VertexAI, FunctionDeclarationSchemaType } = require("@google-cloud/vertexai"); // get project and location from metadata service const metadataService = require("./metadataService.js"); // instance of Gemini model let generativeModel; // 1: define the function const functionDeclarations = [ { function_declarations: [ { name: "getweather", description: "get weather for a given location", parameters: { type: FunctionDeclarationSchemaType.OBJECT, properties: { location: { type: FunctionDeclarationSchemaType.STRING }, degrees: { type: FunctionDeclarationSchemaType.NUMBER, "description": "current temperature in fahrenheit" }, conditions: { type: FunctionDeclarationSchemaType.STRING, "description": "how the weather feels subjectively" } }, required: ["location"] } } ] } ]; // on instance startup const port = parseInt(process.env.PORT) || 8080; app.listen(port, async () => { console.log(`demo1: listening on port ${port}`); 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" }); }); const axios = require("axios"); const baseUrl = "https://weatherservice-k6msmyp47q-uc.a.run.app"; app.ws("/sendMessage", async function (ws, req) { // this chat history will be pinned to the current // Cloud Run instance. Consider using Firestore & // Firebase anonymous auth instead. // start ephemeral chat session with Gemini const chatWithModel = generativeModel.startChat({ tools: functionDeclarations }); ws.on("message", async function (message) { 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); // Function calling demo let response1 = await results.response; let data = response1.candidates[0].content.parts[0]; let methodToCall = data.functionCall; if (methodToCall === undefined) { console.log("Gemini says: ", data.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"> ${data.text} </div>`); // bail out - Gemini doesn't want to return a function return; } // otherwise Gemini wants to call a function console.log( "Gemini wants to call: " + methodToCall.name + " with args: " + util.inspect(methodToCall.args, { showHidden: false, depth: null, colors: true }) ); // make the external call let jsonReturned; try { const responseFunctionCalling = await axios.get( baseUrl + "/" + methodToCall.name, { params: { location: methodToCall.args.location } } ); jsonReturned = responseFunctionCalling.data; } catch (ex) { // in case an invalid location was provided jsonReturned = ex.response.data; } console.log("jsonReturned: ", jsonReturned); // tell the model what function we just called const functionResponseParts = [ { functionResponse: { name: methodToCall.name, response: { name: methodToCall.name, content: { jsonReturned } } } } ]; // // Send a follow up message with a FunctionResponse const result2 = await chatWithModel.sendMessage( functionResponseParts ); // This should include a text response from the model using the response content // provided above const response2 = await result2.response; let answer = response2.candidates[0].content.parts[0].text; console.log("answer: ", answer); 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>`); }); ws.on("close", () => { console.log("WebSocket was closed"); }); }); function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }
为 tailwindCSS 创建 input.css
文件。
@tailwind base; @tailwind components; @tailwind utilities;
为 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 { // 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 { // 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>`;
创建一个新的 public
目录。
mkdir public cd public
现在,为前端创建将使用 htmx 的 index.html
文件。
<!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 2</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 > What's is the current weather in Seattle?</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 的 frontend
目录中。
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
在本地运行应用
最后,您可以通过运行以下脚本来启动应用。该开发脚本还会通过 tailwindCSS 生成 output.css 文件。
npm run dev
您可以预览网站,方法是打开“网页预览”按钮,然后选择“预览端口 8080”
8. 部署并测试前端服务
首先,运行此命令,开始部署并指定要使用的服务账号。如果未指定服务账号,则使用默认计算服务账号。
gcloud run deploy $FRONTEND \ --service-account $SERVICE_ACCOUNT_ADDRESS \ --source . \ --region $REGION \ --allow-unauthenticated
在浏览器中打开前端的服务网址。提出问题:“西雅图目前的天气如何?”Gemini 应该会回答“目前的气温为 40 度,有雨”。如果您问“波士顿现在的天气如何?”,Gemini 会回答“我无法完成此请求,可用的天气 API 没有波士顿的数据。"
9. 恭喜!
恭喜您完成此 Codelab!
建议您查看 Cloud Run、Vertex AI Gemini API 和函数调用文档。
所学内容
- Gemini 函数调用的运作方式
- 如何将由 Gemini 提供支持的聊天机器人应用作为 Cloud Run 服务进行部署
10. 清理
为避免产生意外费用(例如,如果此 Cloud Run 服务被意外调用的次数超过免费层级中的每月 Cloud Run 调用次数),您可以删除该 Cloud Run 服务或删除您在第 2 步中创建的项目。
如需删除 Cloud Run 服务,请前往 Cloud Run Cloud 控制台(网址为 https://console.cloud.google.com/functions/),并删除您在此 Codelab 中创建的 $WEATHER_SERVICE 和 $FRONTEND 服务。
此外,为避免意外调用 Gemini,建议您删除 vertex-ai-caller
服务账号或撤消 Vertex AI User 角色。
如果您选择删除整个项目,可以前往 https://console.cloud.google.com/cloud-resource-manager,选择您在第 2 步中创建的项目,然后选择“删除”。如果删除项目,则需要在 Cloud SDK 中更改项目。您可以通过运行 gcloud projects list
来查看所有可用项目的列表。