كيفية نشر تطبيق دردشة من Gemini على Cloud Run

1. مقدمة

نظرة عامة

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية إنشاء برنامج تتبُّع أساسي للمحادثات مكتوب في العقدة باستخدام Vertex AI Gemini API ومكتبة برامج Vertex AI. يستخدم هذا التطبيق متجر جلسات Express مدعومًا من Google Cloud Firestore.

المعلومات التي ستطّلع عليها

  • كيفية استخدام htmx وtailwindcss وexpress.js لإنشاء خدمة تشغيل السحابة
  • كيفية استخدام مكتبات عملاء Vertex AI للمصادقة على واجهات Google APIs
  • كيفية إنشاء روبوت دردشة للتفاعل مع نموذج Gemini
  • كيفية النشر إلى خدمة تعمل على السحابة الإلكترونية بدون ملف Docker
  • كيفية استخدام متجر جلسات سريعة مدعوم من Google Cloud Firestore

2. الإعداد والمتطلبات

المتطلبات الأساسية

تفعيل Cloud Shell

  1. من Cloud Console، انقر على تفعيل Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

إذا كانت هذه هي المرة الأولى التي تبدأ فيها Cloud Shell، ستظهر لك شاشة وسيطة تصف ماهيتها. إذا ظهرت لك شاشة وسيطة، انقر على متابعة.

d95252b003979716.png

من المفترَض أن تستغرق عملية إدارة الحسابات والاتصال بخدمة Cloud Shell بضع دقائق فقط.

7833d5e1c5d18f54.png

يتم تحميل هذا الجهاز الافتراضي مع جميع أدوات التطوير اللازمة. وتوفّر هذه الشبكة دليلاً رئيسيًا دائمًا بسعة 5 غيغابايت ويتم تشغيله في Google Cloud، ما يحسّن بشكل كبير من أداء الشبكة والمصادقة. يمكنك تنفيذ معظم عملك، إن لم يكن كلّه، في هذا الدرس التطبيقي حول الترميز باستخدام متصفّح.

بعد الربط بخدمة 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].

3- تفعيل واجهات برمجة التطبيقات وضبط متغيرات البيئة

تفعيل واجهات برمجة التطبيقات

قبل بدء استخدام هذا الدرس التطبيقي حول الترميز، عليك تفعيل العديد من واجهات برمجة التطبيقات. يتطلّب هذا الدرس التطبيقي حول الترميز استخدام واجهات برمجة التطبيقات التالية. يمكنك تمكين واجهات برمجة التطبيقات هذه عن طريق تشغيل الأمر التالي:

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"

4. إنشاء مشروع على Firebase وإعداده

  1. في وحدة تحكُّم Firebase، انقر على إضافة مشروع.
  2. أدخِل <YOUR_PROJECT_ID>. لإضافة Firebase إلى أحد مشاريعك الحالية على Google Cloud
  3. راجِع بنود Firebase واقبلها إذا طُلب منك ذلك.
  4. انقر على متابعة.
  5. انقر على تأكيد الخطة لتأكيد خطة فوترة Firebase.
  6. يُعدّ تفعيل "إحصاءات Google" اختياريًا لهذا الدرس التطبيقي حول الترميز.
  7. انقر على إضافة Firebase.
  8. عند إنشاء المشروع، انقر على متابعة.
  9. من القائمة إنشاء، انقر على قاعدة بيانات Firestore.
  10. انقر على إنشاء قاعدة بيانات.
  11. اختر منطقتك من القائمة المنسدلة الموقع الجغرافي، ثم انقر على التالي.
  12. استخدِم الإعداد التلقائي البدء في وضع الإنتاج، ثم انقر على إنشاء.

5- إنشاء حساب خدمة

ستستخدم Cloud Run حساب الخدمة هذا لطلب بيانات Vertex AI Gemini API. سيحصل حساب الخدمة هذا أيضًا على أذونات للقراءة والكتابة في Firestore وقراءة الأسرار من Secret Manager.

أولاً، أنشئ حساب الخدمة من خلال تنفيذ الأمر التالي:

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

ثانيًا، عليك منح دور مستخدم Vertex AI لحساب الخدمة.

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

والآن، يمكنك إنشاء سر في Secret Manager. ستصل خدمة Cloud Run إلى هذا المفتاح السرّي كمتغيرات للبيئة، حيث يمكن حله في وقت بدء التشغيل على سبيل المثال. يمكنك التعرّف على مزيد من المعلومات حول العمليات السرية وتشغيل السحابة الإلكترونية.

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

وامنح حساب الخدمة إذن الوصول إلى سر الجلسة السريع في "المدير السري".

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

6- إنشاء خدمة Cloud Run

أولاً، أنشئ دليلاً لرمز المصدر والقرص المضغوط في هذا الدليل.

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 للواجهة الأمامية، التي ستستخدم 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. تشغيل الخدمة على الجهاز

أولاً، تأكَّد من أنّك في الدليل الجذر chat-with-gemini الخاص بالدرس التطبيقي حول الترميز.

cd .. && pwd

بعد ذلك، عليك تثبيت التبعيات من خلال تنفيذ الأمر التالي:

npm install

استخدام ADC عند التشغيل محليًا

إذا كنت تستخدم Cloud Shell، يعني هذا أنّك تعمل على جهاز افتراضي في Google Compute Engine. سيتم تلقائيًا استخدام بيانات الاعتماد المرتبطة بهذا الجهاز الافتراضي (كما هو موضّح عند تشغيل gcloud auth list) من خلال "بيانات الاعتماد التلقائية للتطبيق"، لذلك ليس من الضروري استخدام الأمر gcloud auth application-default login. يمكنك التخطّي للأسفل إلى القسم إنشاء جلسة سرية لجلسة محلية.

ومع ذلك، إذا كنت تستخدم الوحدة الطرفية المحلية (أي ليس في Cloud Shell)، فستحتاج إلى استخدام "بيانات الاعتماد التلقائية للتطبيق" لمصادقة Google APIs. يمكنك 1) تسجيل الدخول باستخدام بيانات الاعتماد (شرط أن يكون لديك دور مستخدم Vertex AI ودور مستخدم Datastore) أو 2) تسجيل الدخول من خلال انتحال هوية حساب الخدمة المستخدَم في هذا الدرس التطبيقي حول الترميز.

الخيار 1) استخدام بيانات الاعتماد الخاصة بك لـ ADC

إذا أردت استخدام بيانات الاعتماد، يمكنك أولاً تشغيل gcloud auth list للتحقّق من كيفية مصادقتك في gcloud. بعد ذلك، قد تحتاج إلى منح هويتك دور مستخدم Vertex AI. إذا كان لديك دور "المالك"، سيكون لديك دور مستخدم Vertex AI هذا. وإذا لم يكن الأمر كذلك، يمكنك تنفيذ هذا الأمر لمنح هويتك دور مستخدم Vertex AI ودور "مستخدم تخزين البيانات".

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

الخيار 2) انتحال هوية حساب خدمة لـ ADC

إذا أردت استخدام حساب الخدمة الذي تم إنشاؤه في هذا الدرس التطبيقي حول الترميز، يجب أن يكون لحساب المستخدم دور "منشئ الرموز المميّزة لحساب الخدمة". يمكنك الحصول على هذا الدور من خلال تنفيذ الأمر التالي:

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

تشغيل التطبيق على الجهاز

وأخيرًا، يمكنك تشغيل التطبيق من خلال تشغيل النص البرمجي التالي. سينشئ هذا النص البرمجي أيضًا ملف exit.css من tailwindCSS.

npm run dev

يمكنك معاينة الموقع الإلكتروني من خلال فتح زر "معاينة الويب" واختيار "معاينة المنفذ 8080"

معاينة الويب - زر المعاينة على المنفذ 8080

8. نشر الخدمة

أولاً، شغِّل هذا الأمر لبدء عملية النشر واختَر حساب الخدمة المطلوب استخدامه. إذا لم يتم تحديد حساب خدمة، يتم استخدام حساب خدمة Compute Engine التلقائي.

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

إذا طُلب منك أنّ "النشر من المصدر يتطلّب مستودع Artifact Registry Docker لتخزين الحاويات المُضمنة. سيتم إنشاء مستودع باسم [cloud-run-source-deploy] في المنطقة [us-central1]، والنقر على "y" للقبول والمتابعة.

9. اختبار الخدمة

بعد نشر عنوان URL، افتح عنوان URL الخاص بالخدمة في متصفّح الويب. بعد ذلك، يمكنك طرح سؤال على Gemini، مثلاً "أمارس العزف على الغيتار، ولكني أيضًا مهندسة برمجيات. عندما أرى "C# "، هل يجب أن أعتبرها لغة برمجة أم نوتة موسيقية؟ أيهما ينبغي أن أختار؟"

10. تهانينا

تهانينا على إكمال الدرس التطبيقي حول الترميز.

ننصحك بمراجعة المستندَين Cloud Run وVertex AI Gemini API.

النقاط التي تناولناها

  • كيفية استخدام htmx وtailwindcss وexpress.js لإنشاء خدمة تشغيل السحابة
  • كيفية استخدام مكتبات عملاء Vertex AI للمصادقة على واجهات Google APIs
  • كيفية إنشاء روبوت دردشة للتفاعل مع نموذج Gemini
  • كيفية النشر إلى خدمة تعمل على السحابة الإلكترونية بدون ملف Docker
  • كيفية استخدام متجر جلسات سريعة مدعوم من Google Cloud Firestore

11. تَنظيم

لتجنب دفع رسوم غير مقصودة، (على سبيل المثال، إذا تم استدعاء خدمات Cloud Run عن غير قصد أكثر من تخصيص استدعاء Cloud Run الشهري في الفئة المجانية)، يمكنك إما حذف Cloud Run أو حذف المشروع الذي أنشأته في الخطوة 2.

لحذف خدمة Cloud Run، انتقِل إلى Cloud Run Console على https://console.cloud.google.com/run واحذف خدمة chat-with-gemini. يمكنك أيضًا حذف حساب خدمة "vertex-ai-caller" أو إبطال دور مستخدم Vertex AI، وذلك لتجنُّب تلقّي أي مكالمات غير مقصودة إلى Gemini.

إذا اخترت حذف المشروع بالكامل، يمكنك الانتقال إلى https://console.cloud.google.com/cloud-resource-manager، واختيار المشروع الذي أنشأته في الخطوة الثانية، ثم اختيار "حذف". إذا حذفت المشروع، ستحتاج إلى تغيير المشاريع في حزمة تطوير البرامج (SDK) للسحابة الإلكترونية. يمكنك عرض قائمة بجميع المشاريع المتاحة من خلال تشغيل gcloud projects list.