Como usar o Cloud Run com a chamada de função do Gemini

1. Introdução

Visão geral

Neste codelab, você vai aprender a dar acesso do Gemini a dados em tempo real usando um novo recurso chamado chamada de função. Para simular dados em tempo real, você vai criar um endpoint de serviço de clima que retorna o clima atual de dois locais. Em seguida, você vai criar um app de chat com tecnologia do Gemini que usa a chamada de função para recuperar o clima atual.

Vamos usar um visual rápido para entender a chamada de função.

  • O comando solicita locais de clima atual em um determinado local.
  • Esse comando e o contrato de função para getWeather() são enviados ao Gemini.
  • O Gemini pede que o app de bot de chat chame "getWeather(Seattle)" em nome dele.
  • O app envia os resultados (40 graus F e chuvoso).
  • O Gemini envia os resultados de volta ao autor da chamada.

Em resumo, o Gemini não chama a função. Você, como desenvolvedor, precisa chamar a função e enviar os resultados de volta ao Gemini.

Diagrama do fluxo de chamada de função

O que você vai aprender

  • Como funciona a chamada de função do Gemini
  • Como implantar um app de chatbot com tecnologia do Gemini como um serviço do Cloud Run

2. Configuração e requisitos

Pré-requisitos

  • Você fez login no console do Cloud.
  • Você já implantou uma função de 2ª geração. Por exemplo, siga o guia de início rápido para implantar uma função do Cloud de 2ª geração para começar.

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 será exibida descrevendo o que ele é. Se você recebeu uma tela intermediária, 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 seguinte comando no Cloud Shell para confirmar se o comando gcloud conhece 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. Configurar variáveis de ambiente e ativar APIs

Configurar as variáveis de ambiente

Você pode definir variáveis de ambiente que serão usadas neste codelab.

PROJECT_ID=<YOUR_PROJECT_ID>
REGION=<YOUR_REGION, e.g. us-central1>
WEATHER_SERVICE=weatherservice
FRONTEND=frontend
SERVICE_ACCOUNT="vertex-ai-caller"
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

Ativar APIs

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

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

4. Criar uma conta de serviço para chamar a Vertex AI

Essa conta de serviço será usada pelo Cloud Run para chamar a API Vertex AI Gemini.

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

5. Criar o serviço de back-end do Cloud Run

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

mkdir -p gemini-function-calling/weatherservice gemini-function-calling/frontend && cd gemini-function-calling/weatherservice

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

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

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.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!");
});

Implantar o serviço de clima

Você pode usar esse comando para implantar o serviço de clima.

gcloud run deploy $WEATHER_SERVICE \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Testar o serviço de clima

Você pode verificar o clima dos dois locais usando o 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

Seattle tem 40 graus F e chuvoso, e Nova Orleans tem 99 graus F e úmido, sempre.

6. Criar o serviço de front-end

Primeiro, acesse o diretório de front-end.

cd gemini-function-calling/frontend

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

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

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

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

Crie um arquivo input.css para o tailwindCSS.

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

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 {
                // 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;
    }
};

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>`;

Crie um novo diretório public.

mkdir public
cd public

Agora 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 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. Executar o serviço de front-end localmente

Primeiro, verifique se você está no diretório frontend 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 Executar o app localmente.

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 nas 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 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. Você pode receber esse papel executando 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

Executar o app localmente

Por fim, você pode iniciar o app executando o seguinte script. Esse script de desenvolvimento também vai gerar o arquivo output.css do tailwindCSS.

npm run dev

Você pode visualizar o site abrindo o botão "Visualização da Web" e selecionando "Visualizar porta 8080".

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

8. Implantar e testar o serviço de front-end

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 $FRONTEND \
  --service-account $SERVICE_ACCOUNT_ADDRESS \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Abra o URL do serviço para o front-end no navegador. Faça uma pergunta "Qual é o clima atual em Seattle?" e o Gemini vai responder "Atualmente está 40 graus e chuvoso". Se você perguntar "Qual é o clima atual em Boston?" o Gemini vai responder "Não é possível atender a essa solicitação. A API de clima disponível não tem dados para Boston".

9. Parabéns!

Parabéns por concluir o codelab.

Recomendamos revisar a documentação do Cloud Run, das APIs Vertex AI Gemini e da chamada de função.

O que vimos

  • Como funciona a chamada de função do Gemini
  • Como implantar um app de chatbot com tecnologia do Gemini como um serviço do Cloud Run

10. Liberar espaço

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

Para excluir os serviços do Cloud Run, acesse o console do Cloud Run em https://console.cloud.google.com/functions/ e exclua os serviços $WEATHER_SERVICE e $FRONTEND criados neste codelab.

Você também pode 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. Você pode conferir a lista de todos os projetos disponíveis executando gcloud projects list.