Como implantar um app de chat com a tecnologia do Gemini no Cloud Run

1. Introdução

Visão geral

Neste codelab, você vai aprender a criar um chatbot básico escrito em node usando a API Gemini da Vertex AI e a biblioteca de cliente da Vertex AI. Esse app usa um armazenamento de sessão do Express com suporte do Google Cloud Firestore.

O que você vai aprender

  • Como usar htmx, tailwindcss e express.js para criar um serviço do Cloud Run
  • Como usar as bibliotecas de cliente da Vertex AI para autenticar as APIs do Google
  • Como criar um chatbot para interagir com o modelo Gemini
  • Como implantar um serviço do Cloud Run sem um arquivo do Docker
  • Como usar um armazenamento de sessão do Express com suporte do Google Cloud Firestore

2. Configuração e requisitos

Pré-requisitos

Ativar o Cloud Shell

  1. No console do Cloud, clique em Ativar o Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se esta for a primeira vez que você inicia o Cloud Shell, uma tela intermediária vai aparecer descrevendo o que ele é. Se isso acontecer, clique em Continuar.

d95252b003979716.png

Leva apenas alguns instantes para provisionar e se conectar ao Cloud Shell.

7833d5e1c5d18f54.png

Essa máquina virtual é carregada com todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório pessoal persistente de 5 GB e é executada no Google Cloud, melhorando muito o desempenho da rede e a autenticação. Neste codelab, quase todo o trabalho pode ser feito com um navegador.

Depois de se conectar ao Cloud Shell, você vai ver que sua conta está autenticada e que o projeto está configurado com o ID do seu projeto.

  1. Execute o seguinte comando no Cloud Shell para confirmar se a conta está autenticada:
gcloud auth list

Resposta ao comando

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Execute o comando a seguir no Cloud Shell para confirmar se o comando gcloud sabe sobre seu projeto:
gcloud config list project

Resposta ao comando

[core]
project = <PROJECT_ID>

Se o projeto não estiver configurado, faça a configuração usando este comando:

gcloud config set project <PROJECT_ID>

Resposta ao comando

Updated property [core/project].

3. Ativar APIs e definir variáveis de ambiente

Ativar APIs

Antes de começar a usar este codelab, você precisa ativar várias APIs. Este codelab exige o uso das seguintes APIs. Para ativar essas APIs, execute o seguinte comando:

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

Configurar as variáveis de ambiente.

É possível definir variáveis de ambiente que serão usadas neste codelab.

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. Criar e configurar um projeto do Firebase

  1. No console do Firebase, clique em Adicionar projeto.
  2. Insira <YOUR_PROJECT_ID> para adicionar o Firebase a um dos seus projetos atuais do Google Cloud.
  3. Se solicitado, leia e aceite os Termos do Firebase.
  4. Clique em Continuar.
  5. Clique em Confirmar plano para confirmar o plano de faturamento do Firebase.
  6. É opcional ativar o Google Analytics para este codelab.
  7. Clique em Adicionar Firebase.
  8. Quando o projeto for criado, clique em Continuar.
  9. No menu Build, clique em Banco de dados do Firestore.
  10. Clique em Criar banco de dados.
  11. Escolha sua região no menu suspenso Localização e clique em Próxima.
  12. Use o Iniciar no modo de produção padrão e clique em Criar.

5. Crie uma conta de serviço

Essa conta de serviço será usada pelo Cloud Run para chamar a API Gemini da Vertex AI. Essa conta de serviço também terá permissões para ler e gravar no Firestore e ler secrets do Secret Manager.

Primeiro, crie a conta de serviço executando este comando:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --display-name="Cloud Run to access Vertex AI APIs"

Em segundo lugar, conceda o papel de usuário da Vertex AI à conta de serviço.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
  --role=roles/aiplatform.user

Agora, crie um secret no Secret Manager. O serviço do Cloud Run vai acessar esse secret como uma variável de ambiente, que é resolvida no momento da inicialização da instância. Saiba mais sobre secrets e o Cloud Run.

gcloud secrets create $SECRET_ID --replication-policy="automatic"
printf "keyboard-cat" | gcloud secrets versions add $SECRET_ID --data-file=-

Conceda à conta de serviço acesso ao secret da sessão do Express no Secret Manager.

gcloud secrets add-iam-policy-binding $SECRET_ID \
    --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
    --role='roles/secretmanager.secretAccessor'

Por fim, conceda à conta de serviço acesso de leitura e gravação ao Firestore.

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
  --role=roles/datastore.user

6. Criar o serviço do Cloud Run

Primeiro, crie um diretório para o código-fonte e acesse esse diretório.

mkdir chat-with-gemini && cd chat-with-gemini

Em seguida, crie um arquivo package.json com o seguinte conteúdo:

{
  "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"
  }
}

Em seguida, crie um arquivo de origem app.js com o conteúdo abaixo. Esse arquivo contém o ponto de entrada do serviço e a lógica principal do app.

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

Crie o arquivo tailwind.config.js para o tailwindCSS.

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

Crie o arquivo metadataService.js para receber o ID do projeto e a região do serviço do Cloud Run implantado. Esses valores serão usados para instanciar uma instância das bibliotecas de cliente da 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;
    }
};

Crie um arquivo chamado 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>`;

Por fim, crie um arquivo input.css para o tailwindCSS.

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

Agora, crie um novo diretório public.

mkdir public
cd public

E, nesse diretório público, crie o arquivo index.html para o front-end, que vai usar o 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. Executar o serviço localmente

Primeiro, verifique se você está no diretório raiz chat-with-gemini do codelab.

cd .. && pwd

Em seguida, instale as dependências executando o seguinte comando:

npm install

Usar o ADC ao executar localmente

Se você estiver executando no Cloud Shell, já estará executando em uma máquina virtual do Google Compute Engine. Suas credenciais associadas a essa máquina virtual (conforme mostrado pela execução de gcloud auth list) serão usadas automaticamente pelas Application Default Credentials. Portanto, não é necessário usar o comando gcloud auth application-default login. Você pode pular para a seção Criar um secret de sessão local

No entanto, se você estiver executando no terminal local (ou seja, não no Cloud Shell), será necessário usar as Application Default Credentials para autenticar as APIs do Google. Você pode fazer login usando suas credenciais (desde que tenha os papéis de usuário da Vertex AI e usuário do Datastore) ou 2) fazer login representando a conta de serviço usada neste codelab.

Opção 1: usar suas credenciais para o ADC

Se você quiser usar suas credenciais, primeiro execute gcloud auth list para verificar como você está autenticado no gcloud. Em seguida, talvez seja necessário conceder à sua identidade o papel de usuário da Vertex AI. Se sua identidade tiver o papel de proprietário, você já terá esse papel de usuário da Vertex AI. Caso contrário, execute este comando para conceder à sua identidade o papel de usuário da Vertex AI e o papel de usuário do 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

Em seguida, execute o seguinte comando

gcloud auth application-default login

Opção 2: representar uma conta de serviço para o ADC

Se você quiser usar a conta de serviço criada neste codelab, sua conta de usuário precisará ter o papel de criador de token da conta de serviço. Para receber esse papel, execute o seguinte comando:

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member user:$USER \
  --role=roles/iam.serviceAccountTokenCreator

Em seguida, execute o seguinte comando para usar o ADC com a conta de serviço

gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS

Criar um secret de sessão local

Agora, crie um secret de sessão local para o desenvolvimento local.

export SESSION_SECRET=local-secret

Executar o aplicativo localmente

Por fim, inicie o app executando o seguinte script. Esse script também vai gerar o arquivo output.css do tailwindCSS.

npm run dev

Para visualizar o site, abra o botão "Visualização da Web" e selecione "Visualizar porta 8080".

Botão &quot;Visualizar na porta 8080&quot; da visualização na Web

8. Implante o serviço

Primeiro, execute este comando para iniciar a implantação e especificar a conta de serviço a ser usada. Se uma conta de serviço não for especificada, a conta de serviço padrão do Compute será usada.

gcloud run deploy $SERVICE \
 --service-account $SERVICE_ACCOUNT_ADDRESS \
 --source . \
  --region $REGION \
  --allow-unauthenticated \
  --set-secrets="SESSION_SECRET=$(echo $SECRET_ID):1"

Se você receber a mensagem "A implantação da origem requer um repositório do Docker do Artifact Registry para armazenar contêineres criados. Um repositório chamado [cloud-run-source-deploy] na região [us-central1] será criado.", pressione "y" para aceitar e continuar.

9. Testar o serviço

Depois de implantado, abra o URL do serviço no navegador da Web. Em seguida, faça uma pergunta ao Gemini, por exemplo, "Eu toco violão, mas também sou engenheiro de software. Quando vejo "C#", devo pensar nisso como uma linguagem de programação ou uma nota musical? Qual devo escolher?

10. Parabéns!

Parabéns por concluir o codelab.

Recomendamos que você revise a documentação das APIs Gemini do Cloud Run e da Vertex AI.

O que vimos

  • Como usar htmx, tailwindcss e express.js para criar um serviço do Cloud Run
  • Como usar as bibliotecas de cliente da Vertex AI para autenticar as APIs do Google
  • Como criar um chatbot para interagir com o modelo Gemini
  • Como implantar um serviço do Cloud Run sem um arquivo do Docker
  • Como usar um armazenamento de sessão do Express com suporte do Google Cloud Firestore

11. Liberar espaço

Para evitar cobranças acidentais (por exemplo, se os serviços do Cloud Run forem invocados mais vezes do que sua alocação mensal de invocações do Cloud Run na camada sem custo financeiro), exclua o Cloud Run ou o projeto criado na etapa 2.

Para excluir o serviço do Cloud Run, acesse o console do Cloud Run em https://console.cloud.google.com/run e exclua o serviço chat-with-gemini. Talvez você também queira excluir a conta de serviço vertex-ai-caller ou revogar o papel de usuário da Vertex AI para evitar chamadas acidentais ao Gemini.

Se você optar por excluir todo o projeto, acesse https://console.cloud.google.com/cloud-resource-manager, selecione o projeto criado na etapa 2 e escolha "Excluir". Se você excluir o projeto, será necessário mudar os projetos no SDK Cloud. Para conferir a lista de todos os projetos disponíveis, execute gcloud projects list.