Come utilizzare Cloud Run con la chiamata di funzione di Gemini

1. Introduzione

Panoramica

In questo codelab, scoprirai come concedere a Gemini l'accesso ai dati in tempo reale utilizzando una nuova funzionalità chiamata Chiamata di funzione. Per simulare dati in tempo reale, creerai un endpoint del servizio meteo che restituisce le condizioni meteorologiche attuali per due località. Quindi creerai un'app di chat, basata su Gemini, che utilizza le chiamate di funzione per recuperare le condizioni meteorologiche attuali.

Usiamo un breve grafico per comprendere la funzionalità chiamata di funzione.

  • Le richieste di prompt per le località meteo attuali in una determinata località
  • Questo prompt e la funzione contratto per getWeather() vengono inviati a Gemini
  • Gemini chiede all'app del bot di chat di chiamare "getWeather(Seattle)" per suo conto
  • L'app restituisce i risultati (40 gradi F e pioggia)
  • Gemini restituisce i risultati al chiamante

Ricapitolando, Gemini non chiama la funzione. In qualità di sviluppatore, devi richiamare la funzione e inviare i risultati a Gemini.

Diagramma del flusso di chiamata di funzione

Cosa imparerai a fare

  • Come funzionano le chiamate di funzione di Gemini
  • Eseguire il deployment di un'app di chatbot basata su Gemini come servizio Cloud Run

2. Configurazione e requisiti

Prerequisiti

Attiva Cloud Shell

  1. Dalla console Cloud, fai clic su Attiva Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Se è la prima volta che avvii Cloud Shell, ti verrà mostrata una schermata intermedia che descrive di cosa si tratta. Se ti è stata presentata una schermata intermedia, fai clic su Continua.

d95252b003979716.png

Il provisioning e la connessione a Cloud Shell dovrebbero richiedere solo qualche istante.

7833d5e1c5d18f54.png

Questa macchina virtuale viene caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni di rete e l'autenticazione. Gran parte, se non tutto, del lavoro in questo codelab può essere svolto con un browser.

Una volta stabilita la connessione a Cloud Shell, dovresti vedere che hai eseguito l'autenticazione e che il progetto è impostato sul tuo ID progetto.

  1. Esegui questo comando in Cloud Shell per verificare che l'account sia autenticato:
gcloud auth list

Output comando

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Esegui questo comando in Cloud Shell per confermare che il comando gcloud è a conoscenza del tuo progetto:
gcloud config list project

Output comando

[core]
project = <PROJECT_ID>

In caso contrario, puoi impostarlo con questo comando:

gcloud config set project <PROJECT_ID>

Output comando

Updated property [core/project].

3. Configura le variabili di ambiente e abilita le API

Configura le variabili di ambiente

Puoi impostare le variabili di ambiente che verranno utilizzate in questo 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

Abilita API

Prima di poter iniziare a utilizzare questo codelab, ci sono diverse API che dovrai abilitare. Questo codelab richiede l'utilizzo delle API seguenti. Puoi abilitare queste API eseguendo questo comando:

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

4. Crea un account di servizio per chiamare Vertex AI

Questo account di servizio verrà utilizzato da Cloud Run per chiamare l'API Gemini di Vertex AI.

Innanzitutto, crea l'account di servizio eseguendo questo comando:

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

Quindi, concedi il ruolo Utente Vertex AI all'account di servizio.

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

5. Crea il servizio Cloud Run di backend

Per prima cosa, crea una directory per il codice sorgente e accedi a quella directory.

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

Quindi, crea un file package.json con i seguenti contenuti:

{
    "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 questo punto, crea un file di origine app.js con i contenuti seguenti. Questo file contiene il punto di ingresso del servizio e la logica principale dell'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!");
});

Esegui il deployment del servizio meteo

Puoi utilizzare questo comando per eseguire il deployment del servizio meteo.

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

Prova il servizio meteo

Puoi verificare il meteo delle due località utilizzando 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 è di 40 gradi F e piovoso e New Orleans è di 39 gradi F e umido, sempre.

6. crea il servizio Frontend

Per prima cosa, vai alla directory del frontend.

cd gemini-function-calling/frontend

Quindi, crea un file package.json con i seguenti contenuti:

{
  "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 questo punto, crea un file di origine app.js con i contenuti seguenti. Questo file contiene il punto di ingresso del servizio e la logica principale dell'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 file input.css per tailwindCSS.

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

Crea il file tailwind.config.js per tailwindCSS.

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

Crea il file metadataService.js per ottenere l'ID progetto e la regione per il servizio Cloud Run di cui è stato eseguito il deployment. Questi valori verranno utilizzati per creare un'istanza delle librerie client di 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 file denominato 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 una nuova directory public.

mkdir public
cd public

Ora crea il file index.html per il front-end, che utilizzerà 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. Esegui il servizio Frontend in locale

Innanzitutto, assicurati di trovarti nella directory frontend per il tuo codelab.

cd .. && pwd

Quindi, installa le dipendenze eseguendo questo comando:

npm install

Utilizzo di ADC durante l'esecuzione in locale

Se l'esecuzione è in Cloud Shell, significa che è già in esecuzione su una macchina virtuale Google Compute Engine. Le tue credenziali associate a questa macchina virtuale (come mostrato dall'esecuzione di gcloud auth list) verranno utilizzate automaticamente dalle credenziali predefinite dell'applicazione, quindi non è necessario usare il comando gcloud auth application-default login. Puoi andare direttamente alla sezione Eseguire l'app in locale.

Se invece è in esecuzione sul terminale locale (ovvero non in Cloud Shell), dovrai utilizzare le Credenziali predefinite dell'applicazione per autenticarti alle API di Google. Puoi 1) accedere utilizzando le tue credenziali (a condizione che tu disponga di entrambi i ruoli Utente Vertex AI e Utente Datastore) o 2) accedere impersonando l'account di servizio utilizzato in questo codelab.

Opzione 1) Utilizzare le tue credenziali per ADC

Se vuoi utilizzare le tue credenziali, puoi prima eseguire gcloud auth list per verificare il modo in cui hai eseguito l'autenticazione in gcloud. Successivamente, potresti dover concedere alla tua identità il ruolo Vertex AI User. Se la tua identità ha il ruolo Proprietario, hai già questo ruolo utente Vertex AI. In caso contrario, puoi eseguire questo comando per concedere alla tua identità il ruolo utente Vertex AI e il ruolo Utente 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

Poi esegui questo comando

gcloud auth application-default login

Opzione 2) Furto d'identità di un account di servizio per ADC

Se vuoi utilizzare l'account di servizio creato in questo codelab, il tuo account utente dovrà avere il ruolo Creatore token account di servizio. Puoi ottenere questo ruolo eseguendo questo comando:

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

Successivamente, eseguirai questo comando per utilizzare ADC con l'account di servizio

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

Esegui l'app localmente

Infine, puoi avviare l'app eseguendo lo script riportato di seguito. Questo script dev genera anche il file output.css da tailwindCSS.

npm run dev

Puoi visualizzare l'anteprima del sito web aprendo il pulsante Anteprima web e selezionando la porta di anteprima 8080

anteprima web: pulsante Anteprima sulla porta 8080

8. esegui il deployment e il test del servizio Frontend

Innanzitutto, esegui questo comando per avviare il deployment e specifica l'account di servizio da utilizzare. Se non viene specificato un account di servizio, viene utilizzato l'account di servizio Compute predefinito.

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

Apri l'URL del servizio per il frontend nel browser. Fai la domanda "Che tempo fa a Seattle?" e Gemini dovrebbe rispondere "Al momento c'è 40 gradi e piove". Se chiedi "Che tempo fa a Roma?", Gemini risponderà con "Non posso soddisfare questa richiesta. L'API meteo disponibile non contiene dati per Boston."

9. Complimenti!

Complimenti per aver completato il codelab.

Ti consigliamo di consultare la documentazione Cloud Run, le API Gemini di Vertex AI e le chiamate di funzione.

Argomenti trattati

  • Come funzionano le chiamate di funzione di Gemini
  • Eseguire il deployment di un'app di chatbot basata su Gemini come servizio Cloud Run

10. Esegui la pulizia

Per evitare addebiti involontari, ad esempio se questo servizio Cloud Run viene richiamato più volte rispetto all'allocazione mensile del richiamo di Cloud Run nel livello senza costi, puoi eliminare il servizio Cloud Run o eliminare il progetto che hai creato nel passaggio 2.

Per eliminare i servizi Cloud Run, vai alla console Cloud di Cloud Run all'indirizzo https://console.cloud.google.com/functions/ ed elimina i servizi $WEAther_SERVICE e $FRONTEND che hai creato in questo codelab.

Potresti anche voler eliminare l'account di servizio vertex-ai-caller o revocare il ruolo Vertex AI User per evitare chiamate involontarie a Gemini.

Se scegli di eliminare l'intero progetto, puoi andare all'indirizzo https://console.cloud.google.com/cloud-resource-manager, selezionare il progetto che hai creato nel passaggio 2 e scegliere Elimina. Se elimini il progetto, dovrai modificarli in Cloud SDK. Puoi visualizzare l'elenco di tutti i progetti disponibili eseguendo gcloud projects list.