Как развернуть чат-приложение на базе Gemini в Cloud Run

1. Введение

Обзор

В этой лаборатории кода вы увидите, как создать базового чат-бота, написанного на узле, с помощью API Vertex AI Gemini и клиентской библиотеки Vertex AI. Это приложение использует хранилище экспресс-сессий, поддерживаемое Google Cloud Firestore .

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

  • Как использовать htmx, Tailwindcss и express.js для создания службы Cloud Run
  • Как использовать клиентские библиотеки Vertex AI для аутентификации в API Google
  • Как создать чат-бота для взаимодействия с моделью Gemini
  • Как выполнить развертывание в облачной службе без файла Docker
  • Как использовать хранилище экспресс-сессий, поддерживаемое Google Cloud Firestore

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 и установите переменные среды.

Включить API

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

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

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

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

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.

  1. В консоли Firebase нажмите «Добавить проект» .
  2. Введите <YOUR_PROJECT_ID>, чтобы добавить Firebase в один из существующих проектов Google Cloud.
  3. При появлении запроса прочтите и примите условия Firebase .
  4. Нажмите Продолжить .
  5. Нажмите «Подтвердить план» , чтобы подтвердить тарифный план Firebase.
  6. Включать Google Analytics для этой лаборатории кода необязательно.
  7. Нажмите «Добавить Firebase» .
  8. Когда проект будет создан, нажмите «Продолжить» .
  9. В меню «Создать» выберите «База данных Firestore» .
  10. Нажмите Создать базу данных .
  11. Выберите свой регион в раскрывающемся списке «Местоположение» , затем нажмите «Далее» .
  12. Используйте режим запуска по умолчанию в рабочем режиме , затем нажмите «Создать» .

5. Создайте учетную запись службы.

Эта учетная запись службы будет использоваться Cloud Run для вызова API Vertex AI Gemini. Эта учетная запись службы также будет иметь разрешения на чтение и запись в Firestore, а также на чтение секретов из Secret Manager.

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

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

Теперь создайте секрет в Secret Manager. Служба Cloud Run будет получать доступ к этому секрету как к переменным среды, которые разрешаются во время запуска экземпляра. Вы можете узнать больше о секретах и ​​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.

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

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

Создайте файл 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 {
                // 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>`;

Наконец, создайте файл input.css для TailwindCSS.

@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. Запустите службу локально

Во-первых, убедитесь, что вы находитесь в корневом каталоге chat-with-gemini для вашей лаборатории кода.

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

Создайте секрет локального сеанса

Теперь создайте секрет локального сеанса для локальной разработки.

export SESSION_SECRET=local-secret

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

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

npm run dev

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

веб-предварительный просмотр - кнопка предварительного просмотра на порту 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 для хранения собранных контейнеров. Будет создан репозиторий с именем [cloud-run-source-deploy] в регионе [us-central1]». Нажмите «y», чтобы принять и продолжить.

9. Протестируйте сервис

После развертывания откройте URL-адрес службы в веб-браузере. Затем задайте Близнецам вопрос, например: «Я занимаюсь игрой на гитаре, но я также инженер-программист. Когда я вижу «C#», должен ли я думать о нем как о языке программирования или музыкальной ноте? Какой из них мне выбрать?»

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

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

Рекомендуем ознакомиться с документацией Cloud Run и Vertex AI Gemini APIs .

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

  • Как использовать htmx, Tailwindcss и express.js для создания службы Cloud Run
  • Как использовать клиентские библиотеки Vertex AI для аутентификации в API Google
  • Как создать чат-бота для взаимодействия с моделью Gemini
  • Как выполнить развертывание в облачной службе без файла Docker
  • Как использовать хранилище экспресс-сессий, поддерживаемое Google Cloud Firestore

11. Очистка

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

Чтобы удалить службу Cloud Run, перейдите в облачную консоль Cloud Run по адресу https://console.cloud.google.com/run и удалите службу chat-with-gemini . Вы также можете удалить учетную запись службы vertex-ai-caller или отозвать роль пользователя Vertex AI, чтобы избежать непреднамеренных вызовов Gemini.

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