Déployer une application de chat fournie par Gemini sur Cloud Run

1. Introduction

Présentation

Dans cet atelier de programmation, vous allez apprendre à créer un chatbot basique écrit dans un nœud à l'aide de l'API Gemini Vertex AI et de la bibliothèque cliente Vertex AI. Cette application utilise un magasin de sessions Express reposant sur Google Cloud Firestore.

Points abordés

  • Utiliser htmx, tailwindcss et express.js pour créer un service Cloud Run
  • Utiliser les bibliothèques clientes de Vertex AI pour s'authentifier auprès des API Google
  • Créer un chatbot pour interagir avec le modèle Gemini
  • Déployer sur un service Cloud Run sans fichier Docker
  • Utiliser un magasin de session Express sauvegardé par Google Cloud Firestore

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 vous explique de quoi il s'agit. Si un écran intermédiaire 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 dans Google Cloud, ce qui améliore considérablement les performances du réseau et l'authentification. Une grande partie, voire la totalité, de votre travail dans cet atelier de programmation peut être effectué dans un navigateur.

Une fois connecté à Cloud Shell, vous êtes 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. Activer les API et définir des variables d'environnement

Activer les API

Avant de 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 \
    secretmanager.googleapis.com

Configurer des variables d'environnement

Vous pouvez définir les 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>
SERVICE=chat-with-gemini
SERVICE_ACCOUNT="vertex-ai-caller"
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
SECRET_ID="SESSION_SECRET"

4. Créer et configurer un projet Firebase

  1. Dans la console Firebase, cliquez sur Ajouter un projet.
  2. Saisissez <YOUR_PROJECT_ID>. pour ajouter Firebase à l'un de vos projets Google Cloud existants
  3. Si vous y êtes invité, lisez et acceptez les Conditions d'utilisation de Firebase.
  4. Cliquez sur Continuer.
  5. Cliquez sur Confirmer le mode de facturation pour confirmer le mode de facturation Firebase.
  6. L'activation de Google Analytics pour cet atelier de programmation est facultative.
  7. Cliquez sur Ajouter Firebase :
  8. Une fois le projet créé, cliquez sur Continuer.
  9. Dans le menu Build (Compiler), cliquez sur Firestore Database (Base de données Firestore).
  10. Cliquez sur Créer une base de données.
  11. Choisissez votre région dans le menu déroulant Emplacement, puis cliquez sur Suivant.
  12. Utilisez l'option par défaut Start in production mode (Démarrer en mode production), puis cliquez sur Create (Créer).

5. Créer un compte de service

Ce compte de service permettra à Cloud Run d'appeler l'API Gemini Vertex AI. Ce compte de service sera également autorisé à lire et écrire dans Firestore, et à lire des secrets depuis Secret Manager.

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

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

À présent, créez un secret dans Secret Manager. Le service Cloud Run accédera à ce secret en tant que variable d'environnement, ce qui est résolu au moment du démarrage de l'instance. Obtenez plus d'informations sur les secrets et Cloud Run.

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

Accordez au compte de service l'accès au secret de session Express dans Secret Manager.

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

Enfin, accordez au compte de service un accès en lecture et en écriture à Firestore.

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

6. Créer le service Cloud Run

Tout d'abord, créez un répertoire pour le code source et utilisez la commande cd pour y accéder.

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

Ensuite, créez un fichier package.json avec le contenu suivant:

{
  "name": "chat-with-gemini",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "nodemon": "nodemon app.js",
    "cssdev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch",
    "tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css",
    "dev": "npm run tailwind && npm run nodemon"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@google-cloud/connect-firestore": "^3.0.0",
    "@google-cloud/firestore": "^7.5.0",
    "@google-cloud/vertexai": "^0.4.0",
    "axios": "^1.6.8",
    "express": "^4.18.2",
    "express-session": "^1.18.0",
    "express-ws": "^5.0.2",
    "htmx.org": "^1.9.10"
  },
  "devDependencies": {
    "nodemon": "^3.1.0",
    "tailwindcss": "^3.4.1"
  }
}

Ensuite, créez 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");

// cloud run retrieves secret at instance startup time
const secret = process.env.SESSION_SECRET;

const { Firestore } = require("@google-cloud/firestore");
const { FirestoreStore } = require("@google-cloud/connect-firestore");
var session = require("express-session");
app.set("trust proxy", 1); // trust first proxy
app.use(
    session({
        store: new FirestoreStore({
            dataset: new Firestore(),
            kind: "express-sessions"
        }),
        secret: secret,
        /* set secure to false for local dev session history testing */
        /* see more at https://expressjs.com/en/resources/middleware/session.html */
        cookie: { secure: true },
        resave: false,
        saveUninitialized: true
    })
);

const expressWs = require("express-ws")(app);

app.use(express.static("public"));

// Vertex AI Section
const { VertexAI } = require("@google-cloud/vertexai");

// instance of Vertex model
let generativeModel;

// on startup
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
    console.log(`demo1: listening on port ${port}`);

    // get project and location from metadata service
    const metadataService = require("./metadataService.js");

    const project = await metadataService.getProjectId();
    const location = await metadataService.getRegion();

    // Vertex client library instance
    const vertex_ai = new VertexAI({
        project: project,
        location: location
    });

    // Instantiate models
    generativeModel = vertex_ai.getGenerativeModel({
        model: "gemini-1.0-pro-001"
    });
});

app.ws("/sendMessage", async function (ws, req) {
    if (!req.session.chathistory || req.session.chathistory.length == 0) {
        req.session.chathistory = [];
    }

    let chatWithModel = generativeModel.startChat({
        history: req.session.chathistory
    });

    ws.on("message", async function (message) {

        console.log("req.sessionID: ", req.sessionID);
        // get session id

        let questionToAsk = JSON.parse(message).message;
        console.log("WebSocket message: " + questionToAsk);

        ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div
                        id="questionToAsk"
                        class="text-black m-2 text-right border p-2 rounded-lg ml-24">
                        ${questionToAsk}
                    </div></div>`);

        // to simulate a natural pause in conversation
        await sleep(500);

        // get timestamp for div to replace
        const now = "fromGemini" + Date.now();

        ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div
                        id=${now}
                        class=" text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
                        ${spinnerSvg} 
                    </div></div>`);

        const results = await chatWithModel.sendMessage(questionToAsk);
        const answer =
            results.response.candidates[0].content.parts[0].text;

        ws.send(`<div
                        id=${now}
                        hx-swap-oob="true"
                        hx-swap="outerHTML"
                        class="text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
                        ${answer}
                    </div>`);

                    // save to current chat history
        let userHistory = {
            role: "user",
            parts: [{ text: questionToAsk }]
        };
        let modelHistory = {
            role: "model",
            parts: [{ text: answer }]
        };

        req.session.chathistory.push(userHistory);
        req.session.chathistory.push(modelHistory);

        // console.log(
        //     "newly saved chat history: ",
        //     util.inspect(req.session.chathistory, {
        //         showHidden: false,
        //         depth: null,
        //         colors: true
        //     })
        // );
        req.session.save();
    });

    ws.on("close", () => {
        console.log("WebSocket was closed");
    });
});

function sleep(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

// gracefully close the web sockets
process.on("SIGTERM", () => {
    server.close();
});

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 du projet et la région du service Cloud Run déployé. Ces valeurs serviront à 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 {
                // running on Clodu Run. Use project id from metadata service
                project = response.data;
            }
        } catch (ex) {
            // running locally on local terminal
            project = your_project_id;
        }

        return project;
    },

    getRegion: async () => {
        let region = "";
        try {
            // Fetch the token to make a GCF to GCF call
            const response = await axios.get(
                "http://metadata.google.internal/computeMetadata/v1/instance/region",
                {
                    headers: {
                        "Metadata-Flavor": "Google"
                    }
                }
            );

            if (response.data == "") {
                // running locally on Cloud Shell
                region = your_region;
            } else {
                // running on Clodu Run. Use region from metadata service
                let regionFull = response.data;
                const index = regionFull.lastIndexOf("/");
                region = regionFull.substring(index + 1);
            }
        } catch (ex) {
            // running locally on local terminal
            region = your_region;
        }
        return region;
    }
};

Créer un fichier intitulé 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>`;

Enfin, créez un fichier input.css pour tailwindCSS.

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

Maintenant, créez un répertoire public.

mkdir public
cd public

Dans ce répertoire public, créez le fichier index.html du frontal, 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 1</title>
    </head>
    <body>
        <div id="herewego" text-center>
            <!-- <div id="replaceme2" hx-swap-oob="true">Hello world</div> -->
            <div
                class="container mx-auto mt-8 text-center max-w-screen-lg"
            >
                <div
                    class="overflow-y-scroll bg-white p-2 border h-[500px] space-y-4 rounded-lg m-auto"
                >
                    <div id="toupdate"></div>
                </div>
                <form
                    hx-trigger="submit, keyup[keyCode==13] from:body"
                    hx-ext="ws"
                    ws-connect="/sendMessage"
                    ws-send=""
                    hx-on="htmx:wsAfterSend: document.getElementById('message').value = ''"
                >
                    <div class="mb-6 mt-6 flex gap-4">
                        <textarea
                            rows="2"
                            type="text"
                            id="message"
                            name="message"
                            class="block grow rounded-lg border p-6 resize-none"
                            required
                        >
Is C# a programming language or a musical note?</textarea
                        >
                        <button
                            type="submit"
                            class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium"
                        >
                            Send
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </body>
</html>

7. Exécuter le service en local

Tout d'abord, vérifiez que vous vous trouvez bien dans le répertoire racine chat-with-gemini de votre atelier de programmation.

cd .. && pwd

Ensuite, installez les dépendances en exécutant la commande suivante:

npm install

Utiliser ADC en cas d'exécution locale

Si vous exécutez Cloud Shell, vous exécutez déjà le projet sur une machine virtuelle Google Compute Engine. Vos identifiants associés à cette machine virtuelle (comme indiqué en exécutant gcloud auth list) seront automatiquement utilisés par les identifiants par défaut de l'application. Il n'est donc pas nécessaire d'exécuter la commande gcloud auth application-default login. Vous pouvez passer à la section Créer un secret de session locale.

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

Option 1 : Utiliser vos identifiants pour l'ADC

Si vous souhaitez utiliser vos identifiants, vous pouvez d'abord exécuter gcloud auth list pour vérifier votre authentification dans gcloud. Ensuite, vous devrez peut-être attribuer à votre identité le rôle "Utilisateur Vertex AI". Si votre identité dispose du rôle Propriétaire, vous disposez déjà de ce rôle utilisateur Vertex AI. Si ce n'est pas le cas, vous pouvez exécuter cette commande pour attribuer à votre identité les rôles d'utilisateur Vertex AI et 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 du 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, vous exécuterez la commande suivante pour utiliser ADC avec le compte de service

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

Créer un secret de session locale

À présent, créez un secret de session locale pour le développement local.

export SESSION_SECRET=local-secret

Exécuter l'application en local

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

npm run dev

Pour prévisualiser le site Web, ouvrez le bouton "Aperçu sur le Web", puis sélectionnez le port d'aperçu 8080

Aperçu sur le Web : Prévisualiser sur le bouton du port 8080

8. Déployer le service

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

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

Si le message "Le déploiement depuis la source nécessite un dépôt Docker Artifact Registry pour stocker les conteneurs créés : Un dépôt nommé [cloud-run-source-deploy], situé dans la région [us-central1], sera créé.", appuyez sur "y". pour accepter et continuer.

9. Tester le service

Une fois le service déployé, ouvrez l'URL du service dans votre navigateur Web. Ensuite, posez une question à Gemini, par exemple : "Je joue de la guitare, mais je suis aussi ingénieur logiciel. Quand je vois "C#", dois-je le considérer comme un langage de programmation ou comme une note de musique ? Lequel dois-je choisir ?"

10. Félicitations !

Félicitations ! Vous avez terminé cet atelier de programmation.

Nous vous recommandons de consulter la documentation sur les API Cloud Run et Vertex AI Gemini.

Points abordés

  • Utiliser htmx, tailwindcss et express.js pour créer un service Cloud Run
  • Utiliser les bibliothèques clientes de Vertex AI pour s'authentifier auprès des API Google
  • Créer un chatbot pour interagir avec le modèle Gemini
  • Déployer sur un service Cloud Run sans fichier Docker
  • Utiliser un magasin de session Express sauvegardé par Google Cloud Firestore

11. Effectuer un nettoyage

Pour éviter des frais accidentels (par exemple, si les services Cloud Run sont invoqués plus de fois que l'allocation mensuelle des appels Cloud Run dans le niveau sans frais), vous pouvez supprimer Cloud Run ou le projet créé à l'étape 2.

Pour supprimer le service Cloud Run, accédez à la console Cloud de Cloud Run à l'adresse https://console.cloud.google.com/run et supprimez le service chat-with-gemini. 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 cliquer sur "Supprimer". Si vous supprimez le projet, vous devrez le modifier dans Cloud SDK. Vous pouvez afficher la liste de tous les projets disponibles en exécutant gcloud projects list.