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

1. Giriş

Genel Bakış

Bu codelab'de, Vertex AI Gemini API'yi ve Vertex AI istemci kitaplığını kullanarak düğümde yazılmış temel bir chat bot'u nasıl oluşturacağınızı öğreneceksiniz. Bu uygulama, Google Cloud Firestore tarafından desteklenen bir ekspres oturum deposu kullanır.

Neler öğreneceksiniz?

  • Cloud Run hizmeti derlemek için htmx, tailwindcss ve express.js'yi kullanma
  • Google API'lerinde kimlik doğrulamak için Vertex AI istemci kitaplıklarını kullanma
  • Gemini modeliyle etkileşimde bulunmak için chatbot oluşturma
  • Docker dosyası olmadan Cloud Run hizmetine dağıtım yapma
  • Google Cloud Firestore tarafından desteklenen ekspres oturum deposu nasıl kullanılır?

2. Kurulum ve Gereksinimler

Ön koşullar

  • Cloud Console'a giriş yaptınız.
  • Daha önce bir Cloud Run hizmeti dağıttınız. Örneğin, başlamak için kaynak kodundan web hizmeti dağıtma başlıklı makaledeki adımları uygulayabilirsiniz.

Cloud Shell'i etkinleştirme

  1. Cloud Console'da, Cloud Shell'i etkinleştir d1264ca30785e435.png simgesini 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 bir ekran görüntülendiyse Devam'ı tıklayın.

d95252b003979716.png

Temel hazırlık ve Cloud Shell'e bağlanmak yalnızca birkaç dakika sürer.

7833d5e1c5d18f54.png

Gereken tüm geliştirme araçları bu sanal makinede yüklüdür. 5 GB boyutunda kalıcı bir ana dizin sunar ve Google Cloud'da çalışarak ağ performansını ve kimlik doğrulamasını büyük ölçüde iyileştirir. Bu codelab'deki çalışmalarınızın tamamı olmasa bile büyük bir kısmı tarayıcıyla yapılabilir.

Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin proje kimliğinize ayarlandığını göreceksiniz.

  1. Kimlik doğrulamanızın tamamlandığını onaylamak için Cloud Shell'de aşağıdaki 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 projenizi bildiğini onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın:
gcloud config list project

Komut çıkışı

[core]
project = <PROJECT_ID>

Doğru değilse aşağıdaki komutla ayarlayabilirsiniz:

gcloud config set project <PROJECT_ID>

Komut çıkışı

Updated property [core/project].

3. API'leri Etkinleştirin ve Ortam Değişkenlerini Ayarlayın

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 şu 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'de 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. <YOUR_PROJECT_ID> değerini girin mevcut Google Cloud projelerinizden birine Firebase'i ekleyin
  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'i ekle'yi tıklayın.
  8. Proje oluşturulduktan sonra Continue (Devam) seçeneğini tıklayın.
  9. Build (Derleme) menüsünde, Firestore database'i (Firestore veritabanı) tıklayın.
  10. Create database'i (Veritabanı oluştur) tıklayın.
  11. Konum açılır menüsünden bölgenizi seçip İleri'yi tıklayın.
  12. Varsayılan Üretim modunda başlat seçeneğini kullanın, ardından Oluştur'u tıklayın.

5. Hizmet hesabı oluşturma

Bu hizmet hesabı, Cloud Run tarafından Vertex AI Gemini API'yi çağırmak için kullanılır. Bu hizmet hesabı ayrıca Firestore okuma, yazma ve Secret Manager'daki 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"

Ardından, 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 gizli anahtar oluşturun. Cloud Run hizmeti, bu gizli anahtara bir ortam değişkeni olarak erişir ve bu değişken, örnek başlatma sırasında çözülür. 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'da açık oturum gizli anahtarı için 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'a 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

İlk olarak, kaynak kodu için bir dizin oluşturun ve bu dizin için cd'yi kullanın.

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

Sonra, aşağıdaki içerikle 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 ö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ştur

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

Ayrıca bu herkese açık dizinde, kullanıcı arabirimi için htmx kullanacak olan index.html dosyasını oluşturun.

<!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'inizin kök dizininde (chat-with-gemini) olduğunuzdan emin olun.

cd .. && pwd

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

npm install

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

Cloud Shell'de çalışıyorsanız zaten bir Google Compute Engine sanal makinesinde çalıştırıyorsunuz demektir. Bu sanal makineyle ilişkili kimlik bilgileriniz (gcloud auth list çalıştırıldığında gösterildiği şekilde), otomatik olarak Uygulama Varsayılan Kimlik Bilgileri tarafından kullanılacağından gcloud auth application-default login komutunun kullanılması 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ğrulamak için Uygulama Varsayılan Kimlik Bilgilerini kullanmanız gerekir. 1) Kimlik bilgilerinizi kullanarak giriş yapabilirsiniz (hem Vertex AI Kullanıcısı hem de Datastore Kullanıcısı rollerine sahipseniz) 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 gcloud'da kimlik doğrulamanın nasıl yapıldığını doğrulamak için önce gcloud auth list komutunu çalıştırabilirsiniz. Ardından, kimliğinize Vertex AI Kullanıcısı rolünü vermeniz gerekebilir. Kimliğiniz Sahip rolündeyse Vertex AI kullanıcı rolüne zaten sahipsiniz demektir. Aksi takdirde, kimliğinize Vertex AI kullanıcı rolünü ve Datastore Kullanıcısı rolünü 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ü almak için şu komutu çalıştırın:

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

Ardından, ADC'yi hizmet hesabıyla 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 bir 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 çıkış.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 önizlemesi - 8080 bağlantı noktasında önizle düğmesi

8. Hizmeti dağıtma

Öncelikle bu komutu çalıştırarak dağıtımı başlatın ve kullanılacak hizmet hesabını belirtin. Bir hizmet hesabı belirtilmemişse 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 yapmak için derlenen container'ları depolamak için bir Artifact Registry Docker deposu gerekir. [us-central1] bölgesinde [cloud-run-source-deploy] adlı bir depo oluşturulacak.", "y"ye basın kabul edip devam edin.

9. Hizmeti test etme

Dağıtım tamamlandığında web tarayıcınızda hizmet URL'sini açın. Ardından Gemini'a bir soru sorun, ör. "Gitar çalıyorum. Aynı zamanda yazılım mühendisiyim. "C#" ifadesini gördüğümde bunu bir programlama dili mi yoksa 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.

İşlediklerimiz

  • Cloud Run hizmeti derlemek için htmx, tailwindcss ve express.js'yi kullanma
  • Google API'lerinde kimlik doğrulamak için Vertex AI istemci kitaplıklarını kullanma
  • Gemini modeliyle etkileşimde bulunmak için chat bot oluşturma
  • Docker dosyası olmadan Cloud Run hizmetine dağıtım yapma
  • Google Cloud Firestore tarafından desteklenen ekspres oturum deposu nasıl kullanılır?

11. Temizleme

Yanlışlıkla yapılan ücretleri önlemek için (örneğin, Cloud Run hizmetleri yanlışlıkla ücretsiz katmandaki aylık Cloud Run çağırma tahsisinizden daha fazla kez çağrıldıysa) Cloud Run'ı silebilir 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 gidip chat-with-gemini hizmetini silin. Dilerseniz 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 gidip 2. adımda oluşturduğunuz projeyi, ardından Sil'i seçebilirsiniz. Projeyi silerseniz Cloud SDK'nızdaki projeleri değiştirmeniz gerekir. gcloud projects list komutunu çalıştırarak mevcut tüm projelerin listesini görüntüleyebilirsiniz.