Cómo usar Cloud Run con llamadas a funciones de Gemini

1. Introducción

Descripción general

En este codelab, verás cómo puedes darle a Gemini acceso a datos en tiempo real con una nueva función llamada Llamadas a funciones. Para simular datos en tiempo real, compilarás un extremo de servicio meteorológico que muestre el clima actual para 2 ubicaciones. Luego, compilarás una app de chat con tecnología de Gemini que use llamadas a función para recuperar el clima actual.

Usemos una imagen rápida para comprender las llamadas a funciones.

  • Las solicitudes de instrucciones sobre las ubicaciones del clima actuales en una ubicación determinada
  • Esta instrucción y el contrato de función para getWeather() se envían a Gemini.
  • Gemini le pregunta a la app del chat bot "getWeather(Seattle)". en su nombre
  • La app devuelve los resultados (40 grados Fahrenheit y lluvia)
  • Gemini le envía los resultados a la persona que llama

En resumen, Gemini no llama a la función. Como desarrollador, debes llamar a la función y enviar los resultados a Gemini.

Diagrama del flujo de llamadas a función

Qué aprenderás

  • Cómo funcionan las llamadas a funciones de Gemini
  • Cómo implementar una app de chatbot con tecnología de Gemini como servicio de Cloud Run

2. Configuración y requisitos

Requisitos previos

Activar Cloud Shell

  1. En la consola de Cloud, haz clic en Activar Cloud Shelld1264ca30785e435.png.

cb81e7c8e34bc8d.png

Si es la primera vez que inicias Cloud Shell, verás una pantalla intermedia que describe en qué consiste. Si apareció una pantalla intermedia, haz clic en Continuar.

d95252b003979716.png

El aprovisionamiento y la conexión a Cloud Shell solo tomará unos minutos.

7833d5e1c5d18f54.png

Esta máquina virtual está cargada con todas las herramientas de desarrollo necesarias. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que mejora considerablemente el rendimiento de la red y la autenticación. Gran parte de tu trabajo en este codelab, si no todo, se puede hacer con un navegador.

Una vez que te conectes a Cloud Shell, deberías ver que estás autenticado y que el proyecto está configurado con tu ID del proyecto.

  1. En Cloud Shell, ejecuta el siguiente comando para confirmar que tienes la autenticación:
gcloud auth list

Resultado del comando

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Ejecuta el siguiente comando en Cloud Shell para confirmar que el comando de gcloud conoce tu proyecto:
gcloud config list project

Resultado del comando

[core]
project = <PROJECT_ID>

De lo contrario, puedes configurarlo con el siguiente comando:

gcloud config set project <PROJECT_ID>

Resultado del comando

Updated property [core/project].

3. Configura variables de entorno y habilita las APIs

Configura variables de entorno

Puedes establecer variables de entorno que se usarán en este 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

Habilita las APIs

Antes de comenzar a usar este codelab, debes habilitar varias APIs. Este codelab requiere el uso de las siguientes APIs. Para habilitar esas APIs, ejecuta el siguiente comando:

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

4. Crea una cuenta de servicio para llamar a Vertex AI

Cloud Run usará esta cuenta de servicio para llamar a la API de Gemini en Vertex AI.

Primero, ejecuta este comando para crear la cuenta de servicio:

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

Segundo, otorga el rol de usuario de Vertex AI a la cuenta de servicio.

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

5. Crea el servicio de backend de Cloud Run

Primero, crea un directorio para el código fuente y desplázate a ese directorio con el comando cd.

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

Luego, crea un archivo package.json con el siguiente contenido:

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

A continuación, crea un archivo fuente app.js con el siguiente contenido. Este archivo contiene el punto de entrada del servicio y la lógica principal de la 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!");
});

Implementa el servicio meteorológico

Puedes usar este comando para implementar el servicio meteorológico.

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

Prueba el servicio meteorológico

Puedes verificar el clima de las 2 ubicaciones con 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 tiene 40 °F y lluvioso, y Nueva Orleans tiene 30 °F y siempre es húmeda.

6. Crea el servicio de frontend

Primero, ingresa al directorio frontend con el comando cd.

cd gemini-function-calling/frontend

Luego, crea un archivo package.json con el siguiente contenido:

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

A continuación, crea un archivo fuente app.js con el siguiente contenido. Este archivo contiene el punto de entrada del servicio y la lógica principal de la 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);
    });
}

Crea un archivo input.css para tailwindCSS.

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

Crea el archivo tailwind.config.js para tailwindCSS.

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

Crea el archivo metadataService.js para obtener el ID del proyecto y la región del servicio implementado de Cloud Run. Estos valores se usarán para crear una instancia de las bibliotecas cliente de 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;
    }
};

Crea un archivo llamado 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>`;

Crea un directorio public nuevo.

mkdir public
cd public

Ahora, crea el archivo index.html para el frontend, que usará 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. Ejecuta el servicio de frontend de manera local

Primero, asegúrate de estar en el directorio frontend de tu codelab.

cd .. && pwd

Luego, ejecuta el siguiente comando para instalar las dependencias:

npm install

Usa ADC en la ejecución local

Si estás ejecutando en Cloud Shell, ya estás ejecutando en una máquina virtual de Google Compute Engine. Las credenciales predeterminadas de la aplicación usarán automáticamente tus credenciales asociadas con esta máquina virtual (como se muestra cuando se ejecuta gcloud auth list), por lo que no es necesario usar el comando gcloud auth application-default login. Puedes pasar a la sección Cómo ejecutar la app de manera local.

Sin embargo, si estás ejecutando en tu terminal local (es decir, no en Cloud Shell), deberás usar las credenciales predeterminadas de la aplicación para autenticarte en las APIs de Google. Puedes 1) acceder con tus credenciales (siempre que tengas roles de usuario de Vertex AI y usuario de Datastore) o 2) acceder usando la identidad de la cuenta de servicio que se usa en este codelab.

Opción 1) Usa tus credenciales para ADC

Si quieres usar tus credenciales, primero puedes ejecutar gcloud auth list para verificar cómo te autenticas en gcloud. A continuación, es posible que debas otorgar a tu identidad el rol de usuario de Vertex AI. Si tu identidad tiene el rol de propietario, ya tienes este rol de usuario de Vertex AI. De lo contrario, puedes ejecutar este comando para otorgar a tu identidad el rol de usuario de Vertex AI y el rol de usuario de 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

Luego, ejecuta el siguiente comando:

gcloud auth application-default login

Opción 2: Suplantación de identidad de una cuenta de servicio para ADC

Si deseas usar la cuenta de servicio que se creó en este codelab, tu cuenta de usuario deberá tener el rol de creador de tokens de cuentas de servicio. Para obtener este rol, ejecuta el siguiente comando:

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

Luego, ejecutarás el siguiente comando para usar ADC con la cuenta de servicio

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

Ejecuta la app de forma local

Por último, puedes iniciar la app ejecutando la siguiente secuencia de comandos. Esta secuencia de comandos de desarrollo también generará el archivo output.css desde tailwindCSS.

npm run dev

Para obtener una vista previa del sitio web, abre el botón Vista previa en la Web y selecciona Vista previa del puerto 8080.

Vista previa en la Web: vista previa en el botón del puerto 8080

8. Implementa y prueba el servicio de frontend

Primero, ejecuta este comando para iniciar la implementación y especifica la cuenta de servicio que se usará. Si no se especifica una cuenta de servicio, se usa la cuenta de servicio de procesamiento predeterminada.

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

Abre la URL de servicio del frontend en tu navegador. Haz una pregunta: "¿Cómo está el clima en Buenos Aires?". y Gemini debería responder: "Actualmente, está 40 grados y llueve". Si preguntas "¿Cómo está el clima en Boston?", Gemini responderá con el mensaje “No puedo cumplir con esta solicitud. La API del clima disponible no tiene datos para Boston".

9. ¡Felicitaciones!

¡Felicitaciones por completar el codelab!

Te recomendamos revisar la documentación de Cloud Run, las APIs de Vertex AI Gemini y las llamadas a funciones.

Temas abordados

  • Cómo funcionan las llamadas a funciones de Gemini
  • Cómo implementar una app de chatbot con tecnología de Gemini como servicio de Cloud Run

10. Limpia

Para evitar cargos involuntarios (por ejemplo, si este servicio de Cloud Run se invoca de forma involuntaria más veces que tu asignación mensual de invocación de Cloud Run en el nivel gratuito), puedes borrar el servicio de Cloud Run o borrar el proyecto que creaste en el paso 2.

Para borrar los servicios de Cloud Run, ve a la consola de Cloud Run en https://console.cloud.google.com/functions/ y borra los servicios $WEATHER_SERVICE y $FRONTEND que creaste en este codelab.

También puedes borrar la cuenta de servicio vertex-ai-caller o revocar el rol del usuario de Vertex AI para evitar llamadas involuntarias a Gemini.

Si decides borrar el proyecto completo, puedes ir a https://console.cloud.google.com/cloud-resource-manager, seleccionar el proyecto que creaste en el paso 2 y elegir Borrar. Si borras el proyecto, deberás cambiar los proyectos en tu SDK de Cloud. Para ver la lista de todos los proyectos disponibles, ejecuta gcloud projects list.