نحوه استقرار یک برنامه چت مبتنی بر Gemini در Cloud Run

۱. مقدمه

نمای کلی

در این آزمایشگاه کد، خواهید دید که چگونه می‌توانید با استفاده از API Vertex AI Gemini و کتابخانه کلاینت Vertex AI، یک ربات چت پایه نوشته شده با Node ایجاد کنید. این برنامه از یک فروشگاه جلسه اکسپرس پشتیبانی شده توسط Google Cloud Firestore استفاده می‌کند.

آنچه یاد خواهید گرفت

  • نحوه استفاده از htmx، tailwindcss و express.js برای ساخت یک سرویس Cloud Run
  • نحوه استفاده از کتابخانه‌های کلاینت Vertex AI برای احراز هویت در APIهای گوگل
  • نحوه ایجاد یک چت‌بات برای تعامل با مدل Gemini
  • نحوه استقرار در یک سرویس ابری بدون فایل داکر
  • نحوه استفاده از یک فروشگاه جلسه اکسپرس با پشتیبانی Google Cloud Firestore

۲. تنظیمات و الزامات

پیش‌نیازها

فعال کردن پوسته ابری

  1. از کنسول ابری، روی فعال کردن پوسته ابری کلیک کنید d1264ca30785e435.png .

cb81e7c8e34bc8d.png

اگر این اولین باری است که Cloud Shell را اجرا می‌کنید، یک صفحه میانی برای توضیح آن به شما نمایش داده می‌شود. اگر با یک صفحه میانی مواجه شدید، روی ادامه کلیک کنید.

d95252b003979716.png

آماده‌سازی و اتصال به Cloud Shell فقط چند لحظه طول می‌کشد.

7833d5e1c5d18f54.png

این ماشین مجازی مجهز به تمام ابزارهای توسعه مورد نیاز است. این ماشین یک دایرکتوری خانگی پایدار ۵ گیگابایتی ارائه می‌دهد و در فضای ابری گوگل اجرا می‌شود که عملکرد شبکه و احراز هویت را تا حد زیادی افزایش می‌دهد. بخش عمده‌ای از کار شما در این آزمایشگاه کد، اگر نگوییم همه، را می‌توان با یک مرورگر انجام داد.

پس از اتصال به Cloud Shell، باید ببینید که احراز هویت شده‌اید و پروژه روی شناسه پروژه شما تنظیم شده است.

  1. برای تأیید احراز هویت، دستور زیر را در Cloud Shell اجرا کنید:
gcloud auth list

خروجی دستور

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. دستور زیر را در Cloud Shell اجرا کنید تا تأیید کنید که دستور gcloud از پروژه شما اطلاع دارد:
gcloud config list project

خروجی دستور

[core]
project = <PROJECT_ID>

اگر اینطور نیست، می‌توانید با این دستور آن را تنظیم کنید:

gcloud config set project <PROJECT_ID>

خروجی دستور

Updated property [core/project].

۳. فعال کردن APIها و تنظیم متغیرهای محیطی

فعال کردن APIها

قبل از اینکه بتوانید از این codelab استفاده کنید، باید چندین API را فعال کنید. این codelab به استفاده از APIهای زیر نیاز دارد. می‌توانید با اجرای دستور زیر این APIها را فعال کنید:

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

متغیرهای محیطی را تنظیم کنید

شما می‌توانید متغیرهای محیطی که در سراسر این آزمایشگاه کد استفاده خواهند شد را تنظیم کنید.

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"

۴. ایجاد و پیکربندی یک پروژه Firebase

  1. در کنسول Firebase ، روی افزودن پروژه کلیک کنید.
  2. برای اضافه کردن Firebase به یکی از پروژه‌های Google Cloud موجود خود، <YOUR_PROJECT_ID> را وارد کنید.
  3. در صورت درخواست، شرایط Firebase را بررسی و بپذیرید.
  4. روی ادامه کلیک کنید.
  5. برای تأیید طرح پرداخت Firebase ، روی تأیید طرح کلیک کنید.
  6. فعال کردن گوگل آنالیتیکس برای این codelab اختیاری است.
  7. روی افزودن فایربیس کلیک کنید.
  8. وقتی پروژه ایجاد شد، روی ادامه کلیک کنید.
  9. از منوی Build ، روی پایگاه داده Firestore کلیک کنید.
  10. روی ایجاد پایگاه داده کلیک کنید.
  11. منطقه خود را از منوی کشویی Location انتخاب کنید، سپس روی Next کلیک کنید.
  12. از حالت پیش‌فرض Start در حالت تولید استفاده کنید، سپس روی Create کلیک کنید.

۵. یک حساب کاربری سرویس ایجاد کنید

این حساب سرویس توسط Cloud Run برای فراخوانی API مربوط به Vertex AI Gemini استفاده خواهد شد. این حساب سرویس همچنین مجوزهای خواندن و نوشتن در Firestore و خواندن اطلاعات محرمانه از Secret Manager را خواهد داشت.

ابتدا، با اجرای این دستور، حساب کاربری سرویس را ایجاد کنید:

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

دوم، نقش Vertex AI User را به حساب سرویس اعطا کنید.

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

حالا، یک راز در Secret Manager ایجاد کنید. سرویس Cloud Run به این راز به عنوان یک متغیر محیطی دسترسی خواهد داشت که در زمان راه‌اندازی اولیه‌ی نمونه، حل می‌شود. می‌توانید درباره رازها و Cloud Run بیشتر بدانید.

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

و به حساب کاربری سرویس، دسترسی به جلسه‌ی مخفی اکسپرس در Secret Manager را اعطا کنید.

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

در آخر، به حساب سرویس، دسترسی خواندن و نوشتن در Firestore را اعطا کنید.

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

۶. سرویس Cloud Run را ایجاد کنید

ابتدا، یک دایرکتوری برای کد منبع ایجاد کنید و با دستور cd به آن دایرکتوری بروید.

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

سپس، یک فایل package.json با محتوای زیر ایجاد کنید:

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

در مرحله بعد، یک فایل منبع app.js با محتوای زیر ایجاد کنید. این فایل شامل نقطه ورود سرویس و منطق اصلی برنامه است.

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

فایل tailwind.config.js را برای tailwindCSS ایجاد کنید.

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

فایل metadataService.js را برای دریافت شناسه پروژه و منطقه سرویس Cloud Run مستقر شده ایجاد کنید. این مقادیر برای نمونه‌سازی یک نمونه از کتابخانه‌های کلاینت 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;
    }
};

فایلی به نام 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>`;

در نهایت، یک فایل input.css برای tailwindCSS ایجاد کنید.

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

حالا، یک دایرکتوری public جدید ایجاد کنید.

mkdir public
cd public

و درون آن دایرکتوری عمومی، فایل index.html را برای بخش کاربری (front end) ایجاد کنید که از 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>

۷. سرویس را به صورت محلی اجرا کنید

ابتدا مطمئن شوید که در دایرکتوری ریشه chat-with-gemini مربوط به codelab خود هستید.

cd .. && pwd

در مرحله بعد، با اجرای دستور زیر، وابستگی‌ها را نصب کنید:

npm install

استفاده از ADC هنگام اجرای محلی

اگر در Cloud Shell اجرا می‌کنید، در حال حاضر روی یک ماشین مجازی Google Compute Engine در حال اجرا هستید. اعتبارنامه‌های شما که با این ماشین مجازی مرتبط هستند (همانطور که با اجرای gcloud auth list نشان داده شده است) به طور خودکار توسط Application Default Credentials استفاده می‌شوند، بنابراین نیازی به استفاده از دستور gcloud auth application-default login نیست. می‌توانید به بخش Create a local session secret بروید.

با این حال، اگر برنامه را روی ترمینال محلی خود اجرا می‌کنید (یعنی در Cloud Shell نیستید)، برای احراز هویت در APIهای گوگل باید از Application Default Credentials استفاده کنید. می‌توانید ۱) با استفاده از اعتبارنامه‌های خود وارد شوید (به شرطی که هر دو نقش Vertex AI User و Datastore User را داشته باشید) یا ۲) می‌توانید با جعل هویت حساب کاربری سرویس مورد استفاده در این codelab وارد شوید.

گزینه ۱) استفاده از اعتبارنامه‌هایتان برای ADC

اگر می‌خواهید از اعتبارنامه‌های خود استفاده کنید، ابتدا می‌توانید gcloud auth list اجرا کنید تا نحوه احراز هویت خود در gcloud را تأیید کنید. در مرحله بعد، ممکن است لازم باشد به هویت خود نقش Vertex AI User را اعطا کنید. اگر هویت شما نقش Owner را دارد، از قبل این نقش کاربری Vertex AI را دارید. در غیر این صورت، می‌توانید این دستور را اجرا کنید تا نقش کاربری Vertex AI و نقش کاربری 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

سپس دستور زیر را اجرا کنید

gcloud auth application-default login

گزینه ۲) جعل هویت یک حساب کاربری سرویس برای ADC

اگر می‌خواهید از حساب کاربری سرویس ایجاد شده در این codelab استفاده کنید، حساب کاربری شما باید نقش Service Account Token Creator را داشته باشد. می‌توانید این نقش را با اجرای دستور زیر به دست آورید:

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

در مرحله بعد، دستور زیر را برای استفاده از ADC با حساب سرویس اجرا خواهید کرد.

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

یک راز جلسه محلی ایجاد کنید

اکنون، یک راز جلسه محلی برای توسعه محلی ایجاد کنید.

export SESSION_SECRET=local-secret

اجرای برنامه به صورت محلی

در نهایت، می‌توانید برنامه را با اجرای اسکریپت زیر شروع کنید. این اسکریپت همچنین فایل output.css را از tailwindCSS تولید می‌کند.

npm run dev

شما می‌توانید با باز کردن دکمه پیش‌نمایش وب و انتخاب پیش‌نمایش پورت ۸۰۸۰، پیش‌نمایش وب‌سایت را مشاهده کنید.

پیش‌نمایش وب - دکمه پیش‌نمایش روی پورت ۸۰۸۰

۸. سرویس را مستقر کنید

ابتدا، این دستور را اجرا کنید تا استقرار شروع شود و حساب سرویس مورد استفاده را مشخص کنید. اگر حساب سرویس مشخص نشده باشد، از حساب سرویس محاسبه پیش‌فرض استفاده می‌شود.

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

اگر از شما پرسیده شد که «استقرار از منبع نیاز به یک مخزن Docker در رجیستری Artifact برای ذخیره کانتینرهای ساخته شده دارد. یک مخزن با نام [cloud-run-source-deploy] در منطقه [us-central1] ایجاد خواهد شد.» برای پذیرش و ادامه، «y» را فشار دهید.

۹. سرویس را آزمایش کنید

پس از استقرار، URL سرویس را در مرورگر وب خود باز کنید. سپس از Gemini یک سؤال بپرسید، مثلاً «من گیتار تمرین می‌کنم اما مهندس نرم‌افزار هم هستم. وقتی «C#» را می‌بینم، آیا باید آن را به عنوان یک زبان برنامه‌نویسی یا یک نت موسیقی در نظر بگیرم؟ کدام یک را باید انتخاب کنم؟»

۱۰. تبریک می‌گویم!

تبریک می‌گویم که آزمایشگاه کد را تمام کردید!

توصیه می‌کنیم مستندات Cloud Run و Vertex AI Gemini APIs را بررسی کنید.

آنچه ما پوشش داده‌ایم

  • نحوه استفاده از htmx، tailwindcss و express.js برای ساخت یک سرویس Cloud Run
  • نحوه استفاده از کتابخانه‌های کلاینت Vertex AI برای احراز هویت در APIهای گوگل
  • نحوه ایجاد یک ربات چت برای تعامل با مدل Gemini
  • نحوه استقرار در یک سرویس ابری بدون فایل داکر
  • نحوه استفاده از یک فروشگاه جلسه اکسپرس با پشتیبانی Google Cloud Firestore

۱۱. تمیز کردن

برای جلوگیری از هزینه‌های ناخواسته، (برای مثال، اگر سرویس‌های Cloud Run سهواً بیشتر از تخصیص فراخوانی ماهانه Cloud Run شما در سطح رایگان فراخوانی شوند)، می‌توانید Cloud Run یا پروژه‌ای را که در مرحله 2 ایجاد کرده‌اید، حذف کنید.

برای حذف سرویس Cloud Run، به کنسول Cloud Run در آدرس https://console.cloud.google.com/run بروید و سرویس chat-with-gemini را حذف کنید. همچنین می‌توانید حساب سرویس vertex-ai-caller را حذف کنید یا نقش Vertex AI User را لغو کنید تا از هرگونه تماس ناخواسته با Gemini جلوگیری شود.

اگر تصمیم به حذف کل پروژه دارید، می‌توانید به آدرس https://console.cloud.google.com/cloud-resource-manager بروید، پروژه‌ای را که در مرحله ۲ ایجاد کرده‌اید انتخاب کنید و گزینه Delete را انتخاب کنید. اگر پروژه را حذف کنید، باید پروژه‌ها را در Cloud SDK خود تغییر دهید. می‌توانید با اجرای gcloud projects list لیست تمام پروژه‌های موجود را مشاهده کنید.