이 Codelab 정보
1. 소개
개요
이 Codelab에서는 함수 호출이라는 새로운 기능을 사용하여 Gemini에 실시간 데이터 액세스 권한을 부여하는 방법을 알아봅니다. 실시간 데이터를 시뮬레이션하려면 두 위치의 현재 날씨를 반환하는 날씨 서비스 엔드포인트를 빌드합니다. 그런 다음 함수 호출을 사용하여 현재 날씨를 가져오는 Gemini 기반의 채팅 앱을 빌드합니다.
간단한 시각 자료를 사용하여 함수 호출을 이해해 보겠습니다.
- 지정된 위치의 현재 날씨 위치에 대한 프롬프트 요청
- 이 프롬프트 + getWeather()의 함수 계약이 Gemini로 전송됩니다.
- Gemini가 채팅 봇 앱에서 'getWeather(Seattle)'를 호출하도록 요청합니다. 위임자
- 앱이 결과를 다시 보냅니다 (40디그레스 F 및 비).
- Gemini가 발신자에게 결과를 반환합니다.
요약하자면 Gemini는 함수를 호출하지 않습니다. 개발자는 함수를 호출하고 결과를 Gemini로 다시 전송해야 합니다.

학습할 내용
- Gemini 함수 호출 작동 방식
- Gemini 기반 챗봇 앱을 Cloud Run 서비스로 배포하는 방법
2. 설정 및 요구사항
기본 요건
- Cloud 콘솔에 로그인했습니다.
- 이전에 2세대 함수를 배포했습니다. 예를 들어 Cloud 함수 2세대 빠른 시작의 배포를 시작할 수 있습니다.
Cloud Shell 활성화
- Cloud Console에서 Cloud Shell 활성화
를 클릭합니다.

Cloud Shell을 처음 시작하는 경우에는 무엇이 있는지 설명하는 중간 화면이 표시됩니다. 중간 화면이 표시되면 계속을 클릭합니다.

Cloud Shell을 프로비저닝하고 연결하는 데 몇 분 정도만 걸립니다.

가상 머신에는 필요한 개발 도구가 모두 들어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 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 사용자 역할을 부여합니다.
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!");
});
날씨 서비스 배포
이 명령어를 사용하여 날씨 서비스를 배포할 수 있습니다.
gcloud run deploy $WEATHER_SERVICE \ --source . \ --region $REGION \ --allow-unauthenticated
날씨 서비스 테스트
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: []
};
배포된 Cloud Run 서비스의 프로젝트 ID와 리전을 가져오기 위한 metadataService.js 파일을 만듭니다. 이러한 값은 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 사용자 및 Datastore 사용자 역할이 모두 있는 경우) 2) 이 Codelab에서 사용된 서비스 계정을 가장하여 로그인할 수 있습니다.
옵션 1) ADC에 사용자 인증 정보 사용
사용자 인증 정보를 사용하려면 먼저 gcloud auth list를 실행하여 gcloud에서 인증된 방법을 확인합니다. 다음으로 ID에 Vertex AI 사용자 역할을 부여해야 할 수 있습니다. ID에 소유자 역할이 있다면 이 Vertex AI 사용자 역할이 이미 있는 것입니다. 그렇지 않은 경우 이 명령어를 실행하여 ID에 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용 서비스 계정 가장
이 Codelab에서 만든 서비스 계정을 사용하려면 사용자 계정에 서비스 계정 토큰 생성자 역할이 있어야 합니다. 다음 명령어를 실행하여 이 역할을 가져올 수 있습니다.
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
브라우저에서 프런트엔드의 서비스 URL을 엽니다. "시애틀 현재 날씨는 어때?"라고 물어보세요. 그러면 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 사용자 역할을 취소할 수 있습니다.
전체 프로젝트를 삭제하려면 https://console.cloud.google.com/cloud-resource-manager로 이동하여 2단계에서 만든 프로젝트를 선택한 후 삭제를 선택하면 됩니다. 프로젝트를 삭제하면 Cloud SDK에서 프로젝트를 변경해야 합니다. gcloud projects list를 실행하면 사용 가능한 모든 프로젝트의 목록을 볼 수 있습니다.