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

1. Введение

Обзор

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

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

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

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

Диаграмма потока вызова функций

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

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

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

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

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

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

cb81e7c8e34bc8d.png

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

d95252b003979716.png

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

7833d5e1c5d18f54.png

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

После подключения к 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.

Сначала создайте директорию для исходного кода и перейдите в неё с помощью команды `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. Создайте службу внешнего интерфейса.

Сначала перейдите в каталог frontend.

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 для внешнего интерфейса, который будет использовать 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&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

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

Если вы работаете в Cloud Shell, вы уже используете виртуальную машину Google Compute Engine. Ваши учетные данные, связанные с этой виртуальной машиной (как показано при выполнении команды gcloud auth list ), будут автоматически использоваться в Application Default Credentials, поэтому нет необходимости использовать команду 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-адрес сервиса для внешнего интерфейса в браузере. Задайте вопрос: «Какая сейчас погода в Сиэтле?», и Gemini должен ответить: «Сейчас 40 градусов и идет дождь». Если вы спросите: «Какая сейчас погода в Бостоне?», Gemini ответит: «Я не могу выполнить этот запрос. Доступный API погоды не содержит данных для Бостона».

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

Поздравляем с завершением практического занятия!

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

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

  • Как работает вызов функции 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 .