Cara Men-deploy aplikasi percakapan yang didukung Gemini di Cloud Run

1. Pengantar

Ringkasan

Dalam codelab ini, Anda akan melihat cara membuat bot chat dasar yang ditulis dalam node menggunakan Vertex AI Gemini API dan library klien Vertex AI. Aplikasi ini menggunakan penyimpanan sesi ekspres yang didukung oleh Google Cloud Firestore.

Yang akan Anda pelajari

  • Cara menggunakan htmx, tailwindcss, dan express.js untuk membangun layanan Cloud Run
  • Cara menggunakan library klien Vertex AI untuk mengautentikasi ke Google API
  • Cara membuat chatbot untuk berinteraksi dengan model Gemini
  • Cara men-deploy ke layanan Cloud Run tanpa file Docker
  • Cara menggunakan penyimpanan sesi ekspres yang didukung oleh Google Cloud Firestore

2. Penyiapan dan Persyaratan

Prasyarat

Mengaktifkan Cloud Shell

  1. Dari Cloud Console, klik Aktifkan Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Jika ini pertama kalinya Anda memulai Cloud Shell, Anda akan melihat layar perantara yang menjelaskan apa itu Cloud Shell. Jika Anda melihat layar perantara, klik Lanjutkan.

d95252b003979716.png

Perlu waktu beberapa saat untuk penyediaan dan terhubung ke Cloud Shell.

7833d5e1c5d18f54.pngS

Mesin virtual ini dimuat dengan semua alat pengembangan yang diperlukan. Layanan ini menawarkan direktori beranda tetap sebesar 5 GB dan beroperasi di Google Cloud, sehingga sangat meningkatkan performa dan autentikasi jaringan. Sebagian besar pekerjaan Anda dalam codelab ini dapat dilakukan dengan browser.

Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda telah diautentikasi dan project sudah ditetapkan ke project ID Anda.

  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa Anda telah diautentikasi:
gcloud auth list

Output perintah

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa perintah gcloud mengetahui project Anda:
gcloud config list project

Output perintah

[core]
project = <PROJECT_ID>

Jika tidak, Anda dapat menyetelnya dengan perintah ini:

gcloud config set project <PROJECT_ID>

Output perintah

Updated property [core/project].

3. Mengaktifkan API dan Menetapkan Variabel Lingkungan

Mengaktifkan API

Sebelum Anda dapat mulai menggunakan codelab ini, ada beberapa API yang perlu Anda aktifkan. Codelab ini memerlukan penggunaan API berikut. Anda dapat mengaktifkan API tersebut dengan menjalankan perintah berikut:

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

Menyiapkan variabel lingkungan

Anda dapat menetapkan variabel lingkungan yang akan digunakan di seluruh codelab ini.

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. Membuat dan mengonfigurasi project Firebase

  1. Di Firebase console, klik Add project.
  2. Masukkan <YOUR_PROJECT_ID> untuk menambahkan Firebase ke salah satu project Google Cloud Anda yang sudah ada
  3. Jika diminta, tinjau dan setujui persyaratan Firebase.
  4. Klik Lanjutkan.
  5. Klik Confirm Plan untuk mengonfirmasi paket penagihan Firebase.
  6. Mengaktifkan Google Analytics di codelab ini bersifat opsional.
  7. Klik Add Firebase.
  8. Setelah project dibuat, klik Continue.
  9. Dari menu Build, klik Firestore database.
  10. Klik Buat database.
  11. Pilih wilayah dari drop-down Lokasi, lalu klik Berikutnya.
  12. Gunakan Mulai dalam mode produksi default, lalu klik Buat.

5. Membuat akun layanan

Akun layanan ini akan digunakan oleh Cloud Run untuk memanggil Vertex AI Gemini API. Akun layanan ini juga akan memiliki izin untuk membaca dan menulis ke Firestore serta membaca secret dari Secret Manager.

Pertama, buat akun layanan dengan menjalankan perintah ini:

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

Kedua, berikan peran Vertex AI User ke akun layanan.

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

Sekarang, buat secret di Secret Manager. Layanan Cloud Run akan mengakses rahasia ini sebagai variabel lingkungan, yang diselesaikan pada waktu startup instance. Anda dapat mempelajari rahasia dan Cloud Run lebih lanjut.

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

Berikan akses ke rahasia sesi ekspres ke akun layanan di Secret Manager.

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

Terakhir, beri akun layanan akses baca dan tulis ke Firestore.

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

6. Membuat layanan Cloud Run

Pertama, buat direktori untuk kode sumber dan {i>cd<i} ke direktori tersebut.

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

Lalu, buat file package.json dengan konten berikut:

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

Selanjutnya, buat file sumber app.js dengan konten di bawah ini. File ini berisi titik entri untuk layanan dan berisi logika utama untuk aplikasi.

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

Buat file tailwind.config.js untuk tailwindCSS.

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

Buat file metadataService.js untuk mendapatkan project dan region untuk layanan Cloud Run yang di-deploy. Nilai ini akan digunakan untuk membuat instance library klien 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;
    }
};

Buat file dengan nama 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>`;

Terakhir, buat file input.css untuk tailwindCSS.

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

Sekarang, buat direktori public baru.

mkdir public
cd public

Dan dalam direktori publik tersebut, buat file index.html untuk frontend, yang akan menggunakan 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. Menjalankan layanan secara lokal

Pertama, pastikan Anda berada di direktori utama chat-with-gemini untuk codelab Anda.

cd .. && pwd

Selanjutnya, instal dependensi dengan menjalankan perintah berikut:

npm install

Menggunakan ADC saat berjalan secara lokal

Jika menjalankan Cloud Shell, berarti Anda sudah menjalankannya di virtual machine Google Compute Engine. Kredensial Anda yang terkait dengan virtual machine ini (seperti yang ditunjukkan dengan menjalankan gcloud auth list) akan otomatis digunakan oleh Application Default Credentials, sehingga tidak perlu menggunakan perintah gcloud auth application-default login. Anda dapat langsung ke bagian Membuat rahasia sesi lokal

Namun, jika menjalankan di terminal lokal (bukan di Cloud Shell), Anda harus menggunakan Kredensial Default Aplikasi untuk melakukan autentikasi ke Google API. Anda dapat 1) login menggunakan kredensial (asalkan Anda memiliki peran Vertex AI User dan Datastore User) atau 2) Anda dapat login dengan meniru akun layanan yang digunakan dalam codelab ini.

Opsi 1) Menggunakan kredensial Anda untuk ADC

Jika ingin menggunakan kredensial, Anda dapat menjalankan gcloud auth list terlebih dahulu untuk memverifikasi cara autentikasi Anda di gcloud. Selanjutnya, Anda mungkin perlu memberikan peran Vertex AI User ke identitas Anda. Jika identitas Anda memiliki peran Pemilik, berarti Anda sudah memiliki peran pengguna Vertex AI ini. Jika tidak, Anda dapat menjalankan perintah ini untuk memberikan peran pengguna Vertex AI dan peran Datastore User kepada identitas Anda.

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

Lalu jalankan perintah berikut

gcloud auth application-default login

Opsi 2) Meniru Identitas Akun Layanan untuk ADC

Jika Anda ingin menggunakan akun layanan yang dibuat di codelab ini, akun pengguna Anda harus memiliki peran Service Account Token Creator. Anda dapat memperoleh peran ini dengan menjalankan perintah berikut:

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

Selanjutnya, Anda akan menjalankan perintah berikut untuk menggunakan ADC dengan akun layanan

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

Membuat rahasia sesi lokal

Sekarang, buat rahasia sesi lokal untuk pengembangan lokal.

export SESSION_SECRET=local-secret

Menjalankan aplikasi secara lokal

Terakhir, Anda dapat memulai aplikasi dengan menjalankan skrip berikut. Skrip ini juga akan menghasilkan file output.css dari tailwindCSS.

npm run dev

Anda dapat melihat pratinjau situs dengan membuka tombol Web Preview dan memilih Preview Port 8080

pratinjau web - pratinjau pada tombol port 8080

8. Men-deploy layanan

Pertama-tama, jalankan perintah ini untuk memulai deployment dan menentukan akun layanan yang akan digunakan. Jika akun layanan tidak ditentukan, akun layanan komputasi default akan digunakan.

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

Jika Anda melihat pesan "Men-deploy dari sumber memerlukan repositori Docker Artifact Registry untuk menyimpan container yang dibangun. Repositori bernama [cloud-run-source-deploy] di region [us-central1] akan dibuat.", hit ‘y' untuk menerima dan melanjutkan.

9. Menguji layanan

Setelah di-deploy, buka URL layanan di browser web Anda. Lalu ajukan pertanyaan kepada Gemini, mis. "Saya berlatih gitar, tetapi saya juga seorang teknisi perangkat lunak. Saat melihat "C#", haruskah saya menganggapnya sebagai bahasa pemrograman atau not musik? Mana yang harus saya pilih?"

10. Selamat!

Selamat, Anda telah menyelesaikan codelab!

Sebaiknya tinjau dokumentasi Cloud Run dan Vertex AI Gemini API.

Yang telah kita bahas

  • Cara menggunakan htmx, tailwindcss, dan express.js untuk membangun layanan Cloud Run
  • Cara menggunakan library klien Vertex AI untuk mengautentikasi ke Google API
  • Cara membuat bot chat untuk berinteraksi dengan model Gemini
  • Cara men-deploy ke layanan Cloud Run tanpa file Docker
  • Cara menggunakan penyimpanan sesi ekspres yang didukung oleh Google Cloud Firestore

11. Pembersihan

Untuk menghindari tagihan yang tidak disengaja, (misalnya, jika layanan Cloud Run secara tidak sengaja dipanggil lebih sering daripada alokasi panggilan Cloud Run bulanan Anda di paket gratis), Anda dapat menghapus Cloud Run atau menghapus project yang Anda buat di Langkah 2.

Untuk menghapus layanan Cloud Run, buka Konsol Cloud Cloud Run di https://console.cloud.google.com/run dan hapus layanan chat-with-gemini. Anda juga dapat menghapus akun layanan vertex-ai-caller atau mencabut peran Pengguna Vertex AI, untuk menghindari panggilan yang tidak disengaja ke Gemini.

Jika memilih untuk menghapus seluruh project, Anda dapat membuka https://console.cloud.google.com/cloud-resource-manager, pilih project yang dibuat pada Langkah 2, lalu pilih Hapus. Jika project dihapus, Anda harus mengubah project di Cloud SDK. Anda dapat melihat daftar semua project yang tersedia dengan menjalankan gcloud projects list.