Как использовать Cloud Run с вызовом функций Gemini

1. Введение

Обзор

В этой лаборатории кода вы увидите, как предоставить Gemini доступ к данным в реальном времени с помощью новой функции под названием «Вызов функций» . Чтобы моделировать данные в реальном времени, вы создадите конечную точку службы погоды, которая будет возвращать текущую погоду для двух мест. Затем вы создадите приложение для чата на базе Gemini, которое использует вызов функций для получения текущей погоды.

Давайте воспользуемся быстрым визуальным представлением, чтобы понять вызов функций.

  • Оперативные запросы текущей погоды в заданном месте.
  • Это приглашение + контракт функции getWeather() отправляется в Gemini.
  • Gemini просит, чтобы приложение чат-бота вызывало «getWeather(Seattle)» от его имени.
  • Приложение отправляет результаты обратно (40 градусов по Фаренгейту и дождь).
  • Gemini отправляет результаты вызывающему абоненту

Напомним, что Gemini не вызывает функцию. Вы, как разработчик, должны вызвать функцию и отправить результаты обратно в Gemini.

Схема потока вызова функций

Что вы узнаете

  • Как работает вызов функций Gemini
  • Как развернуть приложение чат-бота на базе Gemini в качестве сервиса Cloud Run

2. Настройка и требования

Предварительные условия

Активировать Cloud Shell

  1. В Cloud Console нажмите «Активировать Cloud Shell». d1264ca30785e435.png .

cb81e7c8e34bc8d.png

Если вы запускаете Cloud Shell впервые, вы увидите промежуточный экран с описанием того, что это такое. Если вам был представлен промежуточный экран, нажмите «Продолжить» .

d95252b003979716.png

Подготовка и подключение к Cloud Shell займет всего несколько минут.

7833d5e1c5d18f54.png

Эта виртуальная машина загружена всеми необходимыми инструментами разработки. Он предлагает постоянный домашний каталог объемом 5 ГБ и работает в Google Cloud, что значительно повышает производительность сети и аутентификацию. Большую часть, если не всю, работу в этой лаборатории кода можно выполнить с помощью браузера.

После подключения к Cloud Shell вы увидите, что вы прошли аутентификацию и что для проекта установлен идентификатор вашего проекта.

  1. Выполните следующую команду в 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`
  1. Выполните следующую команду в Cloud Shell, чтобы убедиться, что команда gcloud знает о вашем проекте:
gcloud config list project

Вывод команды

[core]
project = <PROJECT_ID>

Если это не так, вы можете установить его с помощью этой команды:

gcloud config set project <PROJECT_ID>

Вывод команды

Updated property [core/project].

3. Настройте переменные среды и включите API.

Настройка переменных среды

Вы можете установить переменные среды, которые будут использоваться в этой лаборатории кода.

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

Прежде чем вы сможете начать использовать эту кодовую лабораторию, вам необходимо включить несколько API. Для этой лаборатории кода требуется использование следующих API. Вы можете включить эти API, выполнив следующую команду:

gcloud services enable run.googleapis.com \
    cloudbuild.googleapis.com \
    aiplatform.googleapis.com

4. Создайте сервисную учетную запись для вызова Vertex AI.

Эта учетная запись службы будет использоваться Cloud Run для вызова API Vertex AI Gemini.

Сначала создайте учетную запись службы, выполнив следующую команду:

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.

Сначала создайте каталог для исходного кода и перейдите в этот каталог.

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 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);
    });
}

Создайте файл input.css для TailwindCSS.

@tailwind base;
@tailwind components;
@tailwind utilities;

Создайте файл tailwind.config.js для TailwindCSS.

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ["./**/*.{html,js}"],
    theme: {
        extend: {}
    },
    plugins: []
};

Создайте файл metadataService.js , чтобы получить идентификатор проекта и регион для развернутой службы Cloud Run. Эти значения будут использоваться для создания экземпляра клиентских библиотек 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

Теперь создайте файл 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 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&apos;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. Запустите службу Frontend локально.

Сначала убедитесь, что вы находитесь в каталоге frontend вашей лаборатории кода.

cd .. && pwd

Затем установите зависимости, выполнив следующую команду:

npm install

Использование ADC при локальном запуске

Если вы используете Cloud Shell, вы уже работаете на виртуальной машине Google Compute Engine. Ваши учетные данные, связанные с этой виртуальной машиной (как показано при запуске gcloud auth list ), будут автоматически использоваться учетными данными приложения по умолчанию, поэтому нет необходимости использовать команду gcloud auth application-default login . Вы можете перейти к разделу «Запустить приложение локально».

Однако если вы работаете на локальном терминале (т. е. не в Cloud Shell), вам потребуется использовать учетные данные приложения по умолчанию для аутентификации в API Google. Вы можете либо 1) войти в систему, используя свои учетные данные (при условии, что у вас есть роли пользователя Vertex AI и пользователя хранилища данных), либо 2) вы можете войти в систему, выдав себя за учетную запись службы, используемую в этой кодовой лаборатории.

Вариант 1) Использование ваших учетных данных для ADC

Если вы хотите использовать свои учетные данные, вы можете сначала запустить gcloud auth list чтобы проверить, как вы прошли аутентификацию в gcloud. Далее вам может потребоваться предоставить себе роль пользователя Vertex AI. Если у вас есть роль владельца, у вас уже есть эта роль пользователя Vertex AI. Если нет, вы можете запустить эту команду, чтобы предоставить вам роль пользователя Vertex AI и роль пользователя хранилища данных.

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

Запустите приложение локально

Наконец, вы можете запустить приложение, запустив следующий скрипт. Этот сценарий разработки также сгенерирует файл output.css из TailwindCSS.

npm run dev

Вы можете просмотреть веб-сайт, открыв кнопку «Просмотр в Интернете» и выбрав «Просмотр порта 8080».

веб-предварительный просмотр - кнопка предварительного просмотра на порту 8080

8. Разверните и протестируйте службу Frontend.

Сначала запустите эту команду, чтобы начать развертывание, и укажите используемую учетную запись службы. Если учетная запись службы не указана, используется учетная запись службы вычислений по умолчанию.

gcloud run deploy $FRONTEND \
  --service-account $SERVICE_ACCOUNT_ADDRESS \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Откройте URL-адрес службы внешнего интерфейса в браузере. Задайте вопрос «Какая сейчас погода в Сиэтле?» и Близнецы должны ответить: «Сейчас 40 градусов и идет дождь». Если вы спросите: «Какая сейчас погода в Бостоне?», Gemini ответит: «Я не могу выполнить этот запрос. Доступный API погоды не имеет данных для Бостона».

9. Поздравляем!

Поздравляем с завершением работы над кодом!

Рекомендуем просмотреть документацию Cloud Run , Vertex AI Gemini API и вызов функций .

Что мы рассмотрели

  • Как работает вызов функций Gemini
  • Как развернуть приложение чат-бота на базе Gemini в качестве сервиса Cloud Run

10. Очистка

Чтобы избежать непреднамеренных расходов (например, если эта услуга Cloud Run случайно вызывается больше раз, чем ежемесячно выделено количество вызовов Cloud Run на уровне бесплатного пользования ), вы можете либо удалить службу Cloud Run, либо удалить проект, созданный на шаге 2.

Чтобы удалить службы Cloud Run, перейдите в облачную консоль Cloud Run по адресу https://console.cloud.google.com/functions/ и удалите службы $WEATHER_SERVICE и $FRONTEND, созданные вами в этой лаборатории кода.

Вы также можете удалить учетную запись службы vertex-ai-caller или отозвать роль пользователя Vertex AI, чтобы избежать непреднамеренных вызовов Gemini.

Если вы решите удалить весь проект, вы можете перейти на https://console.cloud.google.com/cloud-resource-manager , выбрать проект, созданный на шаге 2, и нажать «Удалить». Если вы удалите проект, вам придется изменить проекты в Cloud SDK. Вы можете просмотреть список всех доступных проектов, запустив gcloud projects list .