Comment utiliser Cloud Run avec l'appel de fonction Gemini

1. Introduction

Présentation

Dans cet atelier de programmation, vous allez découvrir comment donner accès à Gemini à des données en temps réel à l'aide d'une nouvelle fonctionnalité appelée Appel de fonctions. Pour simuler des données en temps réel, vous allez créer un point de terminaison de service météo qui renvoie la météo actuelle pour deux lieux. Vous allez ensuite créer une application de chat optimisée par Gemini qui utilise l'appel de fonction pour récupérer la météo actuelle.

Utilisons un visuel rapide pour comprendre les appels de fonction.

  • La requête demande les conditions météo actuelles dans un lieu donné.
  • Ce prompt et le contrat de fonction pour getWeather() sont envoyés à Gemini.
  • Gemini demande à l'application de chatbot d'appeler "getWeather(Seattle)" en son nom.
  • L'application renvoie les résultats (4 °C et pluie).
  • Gemini renvoie les résultats à l'appelant.

Pour récapituler, Gemini n'appelle pas la fonction. En tant que développeur, vous devez appeler la fonction et renvoyer les résultats à Gemini.

Diagramme du flux d'appel de fonction

Points abordés

  • Fonctionnement de l'appel de fonction Gemini
  • Déployer une application de chatbot optimisée par Gemini en tant que service Cloud Run

2. Préparation

Prérequis

Activer Cloud Shell

  1. Dans Cloud Console, cliquez sur Activer Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Si vous démarrez Cloud Shell pour la première fois, un écran intermédiaire s'affiche pour vous expliquer de quoi il s'agit. Si cet écran s'est affiché, cliquez sur Continuer.

d95252b003979716.png

Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.

7833d5e1c5d18f54.png

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier de programmation dans un navigateur.

Une fois connecté à Cloud Shell, vous êtes en principe authentifié, et le projet est défini sur votre ID de projet.

  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :
gcloud auth list

Résultat de la commande

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que la commande gcloud connaît votre projet :
gcloud config list project

Résultat de la commande

[core]
project = <PROJECT_ID>

Si vous obtenez un résultat différent, exécutez cette commande :

gcloud config set project <PROJECT_ID>

Résultat de la commande

Updated property [core/project].

3. Configurer des variables d'environnement et activer des API

Configurer des variables d'environnement

Vous pouvez définir des variables d'environnement qui seront utilisées tout au long de cet atelier de programmation.

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

Activer les API

Avant de pouvoir commencer à utiliser cet atelier de programmation, vous devez activer plusieurs API. Cet atelier de programmation nécessite l'utilisation des API suivantes. Vous pouvez activer ces API en exécutant la commande suivante :

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

4. Créer un compte de service pour appeler Vertex AI

Ce compte de service sera utilisé par Cloud Run pour appeler l'API Gemini Vertex AI.

Commencez par créer le compte de service en exécutant cette commande :

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

Ensuite, attribuez le rôle Utilisateur Vertex AI au compte de service.

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

5. Créer le service Cloud Run de backend

Commencez par créer un répertoire pour le code source et accédez-y.

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

Créez ensuite un fichier package.json avec le contenu suivant :

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

Créez ensuite un fichier source app.js avec le contenu ci-dessous. Ce fichier contient le point d'entrée du service et la logique principale de l'application.

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

Déployer le service météo

Vous pouvez utiliser cette commande pour déployer le service météo.

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

Tester le service météo

Vous pouvez vérifier la météo pour les deux lieux à l'aide de 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, il fait 4 °C et il pleut, tandis qu'à La Nouvelle-Orléans, il fait 37 °C et il fait toujours humide.

6. Créer le service de frontend

Commencez par accéder au répertoire frontend.

cd gemini-function-calling/frontend

Créez ensuite un fichier package.json avec le contenu suivant :

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

Créez ensuite un fichier source app.js avec le contenu ci-dessous. Ce fichier contient le point d'entrée du service et la logique principale de l'application.

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

Créez un fichier input.css pour tailwindCSS.

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

Créez le fichier tailwind.config.js pour tailwindCSS.

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

Créez le fichier metadataService.js pour obtenir l'ID de projet et la région du service Cloud Run déployé. Ces valeurs seront utilisées pour instancier une instance des bibliothèques clientes 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;
    }
};

Créez un fichier appelé 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>`;

Créez un répertoire public.

mkdir public
cd public

Créez maintenant le fichier index.html pour le frontend, qui utilisera 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. Exécuter le service de frontend en local

Tout d'abord, assurez-vous de vous trouver dans le répertoire frontend de votre atelier de programmation.

cd .. && pwd

Installez ensuite les dépendances en exécutant la commande suivante :

npm install

Utiliser les identifiants par défaut de l'application lors de l'exécution en local

Si vous exécutez Cloud Shell, vous êtes déjà sur une machine virtuelle Google Compute Engine. Vos identifiants associés à cette machine virtuelle (tels qu'ils sont affichés lors de l'exécution de gcloud auth list) seront automatiquement utilisés par les identifiants par défaut de l'application. Il n'est donc pas nécessaire d'utiliser la commande gcloud auth application-default login. Vous pouvez passer directement à la section Exécuter l'application en local.

Toutefois, si vous exécutez l'exemple sur votre terminal local (c'est-à-dire pas dans Cloud Shell), vous devrez utiliser les identifiants par défaut de l'application pour vous authentifier auprès des API Google. Vous pouvez soit 1) vous connecter à l'aide de vos identifiants (à condition de disposer des rôles Utilisateur Vertex AI et Utilisateur Datastore), soit 2) vous connecter en empruntant l'identité du compte de service utilisé dans cet atelier de programmation.

Option 1 : Utiliser vos identifiants pour ADC

Si vous souhaitez utiliser vos identifiants, vous pouvez d'abord exécuter gcloud auth list pour vérifier votre méthode d'authentification dans gcloud. Ensuite, vous devrez peut-être attribuer le rôle Utilisateur Vertex AI à votre identité. Si votre identité dispose du rôle Propriétaire, vous disposez déjà de ce rôle utilisateur Vertex AI. Sinon, vous pouvez exécuter cette commande pour attribuer à votre identité le rôle d'utilisateur Vertex AI et le rôle d'utilisateur 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

Exécutez ensuite la commande suivante :

gcloud auth application-default login

Option 2 : Emprunter l'identité d'un compte de service pour ADC

Si vous souhaitez utiliser le compte de service créé dans cet atelier de programmation, votre compte utilisateur doit disposer du rôle Créateur de jetons de compte de service. Vous pouvez obtenir ce rôle en exécutant la commande suivante :

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

Ensuite, exécutez la commande suivante pour utiliser ADC avec le compte de service.

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

Exécuter l'application en local

Enfin, vous pouvez démarrer l'application en exécutant le script suivant. Ce script de développement générera également le fichier output.css à partir de tailwindCSS.

npm run dev

Vous pouvez prévisualiser le site Web en ouvrant le bouton "Aperçu sur le Web" et en sélectionnant "Prévisualiser le port 8080".

Bouton &quot;Aperçu sur le Web&quot; > &quot;Prévisualiser sur le port 8080&quot;

8. Déployer et tester le service de frontend

Commencez par exécuter cette commande pour lancer le déploiement et spécifier le compte de service à utiliser. Si aucun compte de service n'est spécifié, le compte de service de calcul par défaut est utilisé.

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

Ouvrez l'URL du service pour le frontend dans votre navigateur. Posez la question "Quel temps fait-il actuellement à Seattle ?". Gemini devrait répondre "Il fait actuellement 4 degrés et il pleut." Si vous demandez "Quel temps fait-il actuellement à Boston ?", Gemini répondra par "Je ne peux pas traiter cette demande. L'API météo disponible ne contient pas de données pour Boston."

9. Félicitations !

Bravo ! Vous avez terminé cet atelier de programmation.

Nous vous recommandons de consulter la documentation Cloud Run, les API Gemini Vertex AI et l'appel de fonction.

Points abordés

  • Fonctionnement de l'appel de fonction Gemini
  • Déployer une application de chatbot optimisée par Gemini en tant que service Cloud Run

10. Effectuer un nettoyage

Pour éviter des frais involontaires (par exemple, si ce service Cloud Run est invoqué par inadvertance plus de fois que votre quota mensuel d'invocations Cloud Run dans le niveau sans frais), vous pouvez supprimer le service Cloud Run ou le projet que vous avez créé à l'étape 2.

Pour supprimer les services Cloud Run, accédez à la console Cloud Run à l'adresse https://console.cloud.google.com/functions/, puis supprimez les services $WEATHER_SERVICE et $FRONTEND que vous avez créés dans cet atelier de programmation.

Vous pouvez également supprimer le compte de service vertex-ai-caller ou révoquer le rôle Utilisateur Vertex AI pour éviter tout appel involontaire à Gemini.

Si vous choisissez de supprimer l'intégralité du projet, vous pouvez accéder à https://console.cloud.google.com/cloud-resource-manager, sélectionner le projet que vous avez créé à l'étape 2, puis choisir "Supprimer". Si vous supprimez le projet, vous devrez changer de projet dans votre SDK Cloud. Vous pouvez afficher la liste de tous les projets disponibles en exécutant gcloud projects list.