Gemini destekli bir sohbet uygulamasını Cloud Run'da Dağıtma

1. Giriş

Genel Bakış

Bu codelab'de, Vertex AI Gemini API ve Vertex AI istemci kitaplığını kullanarak Node'da yazılmış temel bir chat botu nasıl oluşturabileceğinizi öğreneceksiniz. Bu uygulama, Google Cloud Firestore tarafından desteklenen bir Express oturum deposu kullanıyor.

Neler öğreneceksiniz?

  • Cloud Run hizmeti oluşturmak için htmx, tailwindcss ve express.js'yi kullanma
  • Google API'lerinde kimlik doğrulaması yapmak için Vertex AI istemci kitaplıklarını kullanma
  • Gemini modeliyle etkileşim kurmak için chatbot oluşturma
  • Docker dosyası olmadan Cloud Run hizmetine dağıtma
  • Google Cloud Firestore tarafından desteklenen bir Express oturum deposunu kullanma

2. Kurulum ve Gereksinimler

Ön koşullar

Cloud Shell'i etkinleştirme

  1. Cloud Console'da Cloud Shell'i etkinleştir 'i d1264ca30785e435.png tıklayın.

cb81e7c8e34bc8d.png

Cloud Shell'i ilk kez başlatıyorsanız ne olduğunu açıklayan bir ara ekran gösterilir. Ara ekran gösterildiyse Devam'ı tıklayın.

d95252b003979716.png

Cloud Shell'in temel hazırlığı ve bağlanması yalnızca birkaç dakikanızı alır.

7833d5e1c5d18f54.png

Bu sanal makineye, ihtiyaç duyacağınız tüm geliştirme araçları yüklenmiştir. 5 GB boyutunda kalıcı bir ana dizin bulunur ve Google Cloud'da çalışır. Bu sayede ağ performansı ve kimlik doğrulama önemli ölçüde güçlenir. Bu codelab'deki çalışmalarınızın neredeyse tamamını tarayıcıyla yapabilirsiniz.

Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin, proje kimliğinize ayarlandığını görürsünüz.

  1. Kimliğinizin doğrulandığını onaylamak için Cloud Shell'de şu komutu çalıştırın:
gcloud auth list

Komut çıkışı

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. gcloud komutunun projeniz hakkında bilgi sahibi olduğunu onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın:
gcloud config list project

Komut çıkışı

[core]
project = <PROJECT_ID>

Değilse şu komutla ayarlayabilirsiniz:

gcloud config set project <PROJECT_ID>

Komut çıkışı

Updated property [core/project].

3. API'leri etkinleştirme ve ortam değişkenlerini ayarlama

API'leri etkinleştir

Bu codelab'i kullanmaya başlamadan önce etkinleştirmeniz gereken birkaç API vardır. Bu codelab'de aşağıdaki API'lerin kullanılması gerekir. Bu API'leri aşağıdaki komutu çalıştırarak etkinleştirebilirsiniz:

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

Ortam değişkenlerini ayarlama

Bu codelab boyunca kullanılacak ortam değişkenlerini ayarlayabilirsiniz.

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. Firebase projesi oluşturma ve yapılandırma

  1. Firebase konsolunda Proje ekle'yi tıklayın.
  2. Firebase'i mevcut Google Cloud projelerinizden birine eklemek için <YOUR_PROJECT_ID> değerini girin.
  3. İstenirse Firebase şartlarını inceleyip kabul edin.
  4. Devam'ı tıklayın.
  5. Firebase faturalandırma planını onaylamak için Planı Onayla'yı tıklayın.
  6. Bu codelab için Google Analytics'i etkinleştirmek isteğe bağlıdır.
  7. Firebase ekle'yi tıklayın.
  8. Proje oluşturulduktan sonra Devam'ı tıklayın.
  9. Oluştur menüsünde Firestore veritabanı'nı tıklayın.
  10. Create database'i (Veritabanı oluştur) tıklayın.
  11. Konum açılır listesinden bölgenizi seçip Sonraki'yi tıklayın.
  12. Varsayılan Üretim modunda başlat seçeneğini kullanın ve Oluştur'u tıklayın.

5. Hizmet hesabı oluşturma

Bu hizmet hesabı, Vertex AI Gemini API'yi çağırmak için Cloud Run tarafından kullanılır. Bu hizmet hesabı, Firestore'da okuma ve yazma işlemlerinin yanı sıra Secret Manager'dan gizli anahtarları okuma iznine de sahip olur.

Öncelikle şu komutu çalıştırarak hizmet hesabını oluşturun:

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

İkinci olarak, hizmet hesabına Vertex AI Kullanıcısı rolünü verin.

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

Şimdi Secret Manager'da bir gizli anahtar oluşturun. Cloud Run hizmeti, bu gizli değere örnek başlatma sırasında çözümlenen bir ortam değişkeni olarak erişir. Gizli anahtarlar ve Cloud Run hakkında daha fazla bilgi edinebilirsiniz.

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

Ayrıca, hizmet hesabına Secret Manager'daki hızlı oturum sırrına erişim izni verin.

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

Son olarak, hizmet hesabına Firestore için okuma ve yazma erişimi verin.

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

6. Cloud Run hizmetini oluşturma

Öncelikle kaynak kodu için bir dizin oluşturun ve bu dizine gidin.

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

Ardından, aşağıdaki içeriğe sahip bir package.json dosyası oluşturun:

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

Ardından, aşağıdaki içeriğe sahip bir app.js kaynak dosyası oluşturun. Bu dosya, hizmetin giriş noktasını ve uygulamanın ana mantığını içerir.

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

tailwindCSS için tailwind.config.js dosyasını oluşturun.

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

Dağıtılan Cloud Run hizmetinin proje kimliğini ve bölgesini almak için metadataService.js dosyasını oluşturun. Bu değerler, Vertex AI istemci kitaplıklarının bir örneğini oluşturmak için kullanılır.

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

spinnerSvg.js adlı bir dosya oluşturun.

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>`;

Son olarak, tailwindCSS için bir input.css dosyası oluşturun.

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

Şimdi yeni bir public dizini oluşturun.

mkdir public
cd public

Bu herkese açık dizinde, kullanıcı arabirimi için index.html dosyasını oluşturun. Bu dosya htmx kullanır.

<!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. Hizmeti yerel olarak çalıştırma

Öncelikle, codelab'iniz için kök dizinde chat-with-gemini olduğunuzdan emin olun.

cd .. && pwd

Ardından, aşağıdaki komutu çalıştırarak bağımlılıkları yükleyin:

npm install

Yerel olarak çalıştırırken ADC'yi kullanma

Cloud Shell'de çalışıyorsanız zaten bir Google Compute Engine sanal makinesinde çalışıyorsunuzdur. Bu sanal makineyle ilişkili kimlik bilgileriniz (gcloud auth list çalıştırılarak gösterildiği gibi) Uygulama Varsayılan Kimlik Bilgileri tarafından otomatik olarak kullanılacağından gcloud auth application-default login komutunu kullanmanız gerekmez. Yerel oturum gizli anahtarı oluşturma bölümüne geçebilirsiniz.

Ancak yerel terminalinizde (ör. Cloud Shell'de değil) çalışıyorsanız Google API'lerinde kimlik doğrulaması yapmak için Uygulama Varsayılan Kimlik Bilgileri'ni kullanmanız gerekir. 1) Kimlik bilgilerinizi kullanarak giriş yapabilir (hem Vertex AI Kullanıcısı hem de Veri Deposu Kullanıcısı rollerine sahip olmanız gerekir) veya 2) bu codelab'de kullanılan hizmet hesabının kimliğine bürünerek giriş yapabilirsiniz.

1. seçenek: ADC için kimlik bilgilerinizi kullanma

Kimlik bilgilerinizi kullanmak istiyorsanız öncelikle gcloud auth list komutunu çalıştırarak gcloud'da nasıl kimlik doğrulandığınızı doğrulayabilirsiniz. Ardından, kimliğinize Vertex AI Kullanıcısı rolünü vermeniz gerekebilir. Kimliğinizde Sahip rolü varsa bu Vertex AI kullanıcı rolüne zaten sahipsinizdir. Aksi takdirde, kimliğinize Vertex AI kullanıcı rolü ve veri deposu kullanıcı rolü vermek için bu komutu çalıştırabilirsiniz.

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

Ardından aşağıdaki komutu çalıştırın.

gcloud auth application-default login

2. seçenek: ADC için bir hizmet hesabının kimliğine bürünme

Bu codelab'de oluşturulan hizmet hesabını kullanmak istiyorsanız kullanıcı hesabınızın Hizmet Hesabı Jetonu Oluşturucu rolüne sahip olması gerekir. Bu rolü aşağıdaki komutu çalıştırarak edinebilirsiniz:

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

Ardından, hizmet hesabıyla ADC'yi kullanmak için aşağıdaki komutu çalıştıracaksınız.

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

Yerel oturum gizli anahtarı oluşturma

Şimdi yerel geliştirme için yerel oturum gizli anahtarı oluşturun.

export SESSION_SECRET=local-secret

Uygulamayı yerel olarak çalıştırma

Son olarak, aşağıdaki komut dosyasını çalıştırarak uygulamayı başlatabilirsiniz. Bu komut dosyası, tailwindCSS'den output.css dosyasını da oluşturur.

npm run dev

Web Önizlemesi düğmesini açıp Önizleme Bağlantı Noktası 8080'i seçerek web sitesini önizleyebilirsiniz.

web preview - preview on port 8080 button

8. Hizmeti dağıtma

Öncelikle, dağıtımı başlatmak ve kullanılacak hizmet hesabını belirtmek için bu komutu çalıştırın. Hizmet hesabı belirtilmezse varsayılan Compute hizmet hesabı kullanılır.

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

"Kaynaktan dağıtım için oluşturulan container'ları depolamak üzere bir Artifact Registry Docker deposu gerekir. [us-central1] bölgesinde [cloud-run-source-deploy] adlı bir depo oluşturulacak." mesajını gördüğünüzde "y" tuşuna basarak kabul edin ve devam edin.

9. Hizmeti test etme

Dağıtım tamamlandıktan sonra hizmet URL'sini web tarayıcınızda açın. Ardından Gemini'a soru sorun. Örneğin, "Gitar çalıyorum ama aynı zamanda yazılım mühendisiyim. "C#" ifadesini gördüğümde bunu bir programlama dili mi yoksa bir müzik notası olarak mı düşünmeliyim? Hangisini seçmeliyim?"

10. Tebrikler!

Tebrikler, codelab'i tamamladınız.

Cloud Run ve Vertex AI Gemini API'leri belgelerini incelemenizi öneririz.

İşlediğimiz konular

  • Cloud Run hizmeti oluşturmak için htmx, tailwindcss ve express.js'yi kullanma
  • Google API'lerinde kimlik doğrulaması yapmak için Vertex AI istemci kitaplıklarını kullanma
  • Gemini modeliyle etkileşim kurmak için sohbet botu oluşturma
  • Docker dosyası olmadan Cloud Run hizmetine dağıtma
  • Google Cloud Firestore tarafından desteklenen bir Express oturum deposunu kullanma

11. Temizleme

Yanlışlıkla ücretlendirilmemek için (örneğin, Cloud Run hizmetleri ücretsiz katmandaki aylık Cloud Run çağırma kotanızdan daha fazla sayıda çağrılırsa) Cloud Run'ı veya 2. adımda oluşturduğunuz projeyi silebilirsiniz.

Cloud Run hizmetini silmek için https://console.cloud.google.com/run adresinden Cloud Run Cloud Console'a gidin ve chat-with-gemini hizmetini silin. Gemini'a yanlışlıkla yapılan çağrıları önlemek için vertex-ai-caller hizmet hesabını silebilir veya Vertex AI Kullanıcısı rolünü iptal edebilirsiniz.

Projenin tamamını silmeyi tercih ederseniz https://console.cloud.google.com/cloud-resource-manager adresine gidebilir, 2. adımda oluşturduğunuz projeyi seçip Sil'i tıklayabilirsiniz. Projeyi silerseniz Cloud SDK'nızda projeleri değiştirmeniz gerekir. gcloud projects list komutunu çalıştırarak kullanılabilir tüm projelerin listesini görüntüleyebilirsiniz.