Cách triển khai ứng dụng trò chuyện sử dụng Gemini trên Cloud Run

1. Giới thiệu

Tổng quan

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách tạo một bot trò chuyện cơ bản được viết bằng nút bằng cách sử dụng Vertex AI Gemini API và thư viện ứng dụng Vertex AI. Ứng dụng này sử dụng một kho lưu trữ phiên nhanh được Google Cloud Firestore hỗ trợ.

Kiến thức bạn sẽ học được

  • Cách sử dụng htmx, tailwindcss và express.js để tạo dịch vụ Cloud Run
  • Cách sử dụng thư viện ứng dụng Vertex AI để xác thực các API của Google
  • Cách tạo bot trò chuyện để tương tác với mô hình Gemini
  • Cách triển khai cho dịch vụ chạy trên đám mây mà không cần tệp Docker
  • Cách sử dụng cửa hàng phiên nhanh được Google Cloud Firestore hỗ trợ

2. Thiết lập và yêu cầu

Điều kiện tiên quyết

Kích hoạt Cloud Shell

  1. Trong Cloud Console, hãy nhấp vào Kích hoạt Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Nếu đây là lần đầu tiên khởi động Cloud Shell, bạn sẽ thấy một màn hình trung gian mô tả về Cloud Shell. Nếu bạn nhìn thấy màn hình trung gian, hãy nhấp vào Tiếp tục.

d95252b003979716.png

Quá trình cấp phép và kết nối với Cloud Shell chỉ mất vài phút.

7833d5e1c5d18f54.pngS

Máy ảo này được tải tất cả các công cụ phát triển cần thiết. Dịch vụ này cung cấp thư mục gốc có dung lượng ổn định 5 GB và chạy trên Google Cloud, giúp nâng cao đáng kể hiệu suất và khả năng xác thực của mạng. Nhiều (nếu không nói là) tất cả công việc của bạn trong lớp học lập trình này đều có thể thực hiện bằng trình duyệt.

Sau khi kết nối với Cloud Shell, bạn sẽ thấy mình đã được xác thực và dự án được đặt thành mã dự án.

  1. Chạy lệnh sau trong Cloud Shell để xác nhận rằng bạn đã được xác thực:
gcloud auth list

Kết quả lệnh

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Chạy lệnh sau trong Cloud Shell để xác nhận rằng lệnh gcloud biết về dự án của bạn:
gcloud config list project

Kết quả lệnh

[core]
project = <PROJECT_ID>

Nếu chưa, bạn có thể thiết lập chế độ này bằng lệnh sau:

gcloud config set project <PROJECT_ID>

Kết quả lệnh

Updated property [core/project].

3. Bật API và đặt biến môi trường

Bật API

Trước khi có thể bắt đầu sử dụng lớp học lập trình này, bạn sẽ cần bật một số API. Lớp học lập trình này yêu cầu sử dụng các API sau. Bạn có thể bật các API đó bằng cách chạy lệnh sau:

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

Thiết lập biến môi trường

Bạn có thể thiết lập các biến môi trường sẽ được dùng trong suốt lớp học lập trình này.

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. Tạo và định cấu hình dự án Firebase

  1. Trong bảng điều khiển của Firebase, hãy nhấp vào Thêm dự án.
  2. Nhập <YOUR_PROJECT_ID> để thêm Firebase vào một trong các dự án hiện có trên Google Cloud
  3. Nếu được nhắc, hãy xem xét và chấp nhận các điều khoản của Firebase.
  4. Nhấp vào Tiếp tục.
  5. Nhấp vào Xác nhận gói để xác nhận gói thanh toán Firebase.
  6. Bạn không bắt buộc phải bật Google Analytics cho lớp học lập trình này.
  7. Nhấp vào Thêm Firebase.
  8. Sau khi tạo dự án, hãy nhấp vào Tiếp tục.
  9. Trên trình đơn Build (Tạo), hãy nhấp vào Firestore cơ sở dữ liệu.
  10. Nhấp vào Tạo cơ sở dữ liệu.
  11. Chọn khu vực của bạn trong trình đơn thả xuống Vị trí, sau đó nhấp vào Tiếp theo.
  12. Sử dụng Bắt đầu ở chế độ phát hành công khai mặc định, sau đó nhấp vào Tạo.

5. Tạo một tài khoản dịch vụ

Tài khoản dịch vụ này sẽ được Cloud Run sử dụng để gọi Vertex AI Gemini API. Tài khoản dịch vụ này cũng sẽ có quyền đọc và ghi vào Firestore cũng như đọc các khoá bí mật của Secret Manager.

Trước tiên, hãy tạo tài khoản dịch vụ bằng cách chạy lệnh sau:

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

Thứ hai, cấp vai trò Người dùng Vertex AI cho tài khoản dịch vụ.

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

Bây giờ, hãy tạo một khoá bí mật trong Trình quản lý bí mật. Dịch vụ Cloud Run sẽ truy cập vào mã thông báo bí mật này dưới dạng các biến môi trường. Biến này sẽ được phân giải tại thời điểm khởi động thực thể. Bạn có thể tìm hiểu thêm về bí mật và Cloud Run.

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

Đồng thời, cấp cho tài khoản dịch vụ quyền truy cập vào khoá bí mật phiên nhanh trong Trình quản lý bí mật.

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

Cuối cùng, hãy cấp cho Firestore quyền đọc và ghi tài khoản dịch vụ.

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

6. Tạo dịch vụ Cloud Run

Trước tiên, tạo một thư mục cho mã nguồn và cd vào thư mục đó.

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

Sau đó, hãy tạo một tệp package.json với nội dung sau:

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

Tiếp theo, hãy tạo một tệp nguồn app.js có nội dung như bên dưới. Tệp này chứa điểm truy cập cho dịch vụ và chứa logic chính cho ứng dụng.

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

Tạo tệp tailwind.config.js cho tailwindCSS.

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

Tạo tệp metadataService.js để lấy mã dự án và khu vực cho dịch vụ Cloud Run đã triển khai. Các giá trị này sẽ được dùng để tạo thực thể cho một thực thể của thư viện ứng dụng 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;
    }
};

Tạo một tệp có tên là 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>`;

Cuối cùng, hãy tạo một tệp input.css cho tailwindCSS.

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

Bây giờ, hãy tạo một thư mục public mới.

mkdir public
cd public

Và trong thư mục công khai đó, hãy tạo tệp index.html cho giao diện người dùng, tệp này sẽ sử dụng 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. Chạy dịch vụ trên máy

Trước tiên, hãy đảm bảo bạn đang ở trong thư mục gốc chat-with-gemini cho lớp học lập trình của mình.

cd .. && pwd

Tiếp theo, hãy cài đặt các phần phụ thuộc bằng cách chạy lệnh sau:

npm install

Sử dụng ADC khi chạy cục bộ

Nếu đang chạy trong Cloud Shell, tức là bạn đã chạy trên một máy ảo Google Compute Engine. Thông tin đăng nhập được liên kết với máy ảo này (như minh hoạ trong quá trình chạy gcloud auth list) sẽ tự động được Thông tin xác thực mặc định của ứng dụng sử dụng. Vì vậy, bạn không cần phải sử dụng lệnh gcloud auth application-default login. Bạn có thể chuyển đến phần Tạo mã thông báo bí mật cho phiên cục bộ

Tuy nhiên, nếu đang chạy trên thiết bị đầu cuối cục bộ (tức là không chạy trong Cloud Shell), bạn sẽ cần phải sử dụng Thông tin xác thực mặc định của ứng dụng để xác thực với các API của Google. Bạn có thể 1) đăng nhập bằng thông tin đăng nhập của mình (miễn là bạn có cả vai trò Người dùng Vertex AI và Người dùng kho dữ liệu) hoặc 2) bạn có thể đăng nhập bằng cách mạo danh tài khoản dịch vụ dùng trong lớp học lập trình này.

Phương án 1) Sử dụng thông tin xác thực của bạn cho ADC

Nếu muốn sử dụng thông tin đăng nhập của mình thì trước tiên, bạn có thể chạy gcloud auth list để xác minh cách bạn được xác thực trong gcloud. Tiếp theo, bạn có thể cần cấp vai trò Người dùng Vertex AI cho danh tính của bạn. Nếu danh tính của bạn có vai trò Chủ sở hữu thì bạn đã có vai trò người dùng Vertex AI này. Nếu không, bạn có thể chạy lệnh này để cấp vai trò của người dùng trong Vertex AI danh tính và vai trò Người dùng 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

Sau đó chạy lệnh sau

gcloud auth application-default login

Lựa chọn 2) Mạo danh một tài khoản dịch vụ của ADC

Nếu muốn sử dụng tài khoản dịch vụ được tạo trong lớp học lập trình này, tài khoản người dùng của bạn cần phải có vai trò Người tạo mã thông báo tài khoản dịch vụ. Bạn có thể có được vai trò này bằng cách chạy lệnh sau:

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

Tiếp theo, bạn sẽ chạy lệnh sau để sử dụng ADC với tài khoản dịch vụ

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

Tạo một mã thông báo bí mật cho phiên cục bộ

Bây giờ, hãy tạo một bí mật phiên cục bộ để phát triển cục bộ.

export SESSION_SECRET=local-secret

Chạy ứng dụng trên máy

Cuối cùng, bạn có thể khởi động ứng dụng bằng cách chạy tập lệnh sau. Tập lệnh này cũng sẽ tạo tệp output.css từ tailwindCSS.

npm run dev

Bạn có thể xem trước trang web bằng cách mở nút Web Preview và chọn Preview Port 8080

bản xem trước trên web – bản xem trước trên nút cổng 8080

8. Triển khai dịch vụ

Trước tiên, hãy chạy lệnh này để bắt đầu triển khai và chỉ định tài khoản dịch vụ sẽ được sử dụng. Nếu bạn không chỉ định tài khoản dịch vụ, thì tài khoản dịch vụ mặc định của Compute Engine sẽ được sử dụng.

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

Nếu bạn thấy lời nhắc "Triển khai từ nguồn yêu cầu kho lưu trữ Artifact Registry Docker để lưu trữ các vùng chứa đã dựng. Hệ thống sẽ tạo một kho lưu trữ có tên [cloud-run-source-perform] tại khu vực [us-central1].", hãy nhấn vào "y" để chấp nhận và tiếp tục.

9. Kiểm thử dịch vụ

Sau khi triển khai, hãy mở URL dịch vụ trong trình duyệt web. Sau đó, hãy đặt câu hỏi cho Gemini, ví dụ: "Tôi tập ghi-ta nhưng cũng là kỹ sư phần mềm. Khi thấy "C#", tôi nên nghĩ ngôn ngữ đó là ngôn ngữ lập trình hay nốt nhạc? Tôi nên chọn mục nào?"

10. Xin chúc mừng!

Chúc mừng bạn đã hoàn thành lớp học lập trình!

Bạn nên tham khảo tài liệu về Cloud RunAPI Gemini của Vertex AI.

Nội dung đã đề cập

  • Cách sử dụng htmx, tailwindcss và express.js để tạo dịch vụ Cloud Run
  • Cách sử dụng thư viện ứng dụng Vertex AI để xác thực các API của Google
  • Cách tạo bot trò chuyện để tương tác với mô hình Gemini
  • Cách triển khai cho dịch vụ chạy trên đám mây mà không cần tệp Docker
  • Cách sử dụng cửa hàng phiên nhanh được Google Cloud Firestore hỗ trợ

11. Dọn dẹp

Để tránh các khoản phí vô tình (ví dụ: nếu các dịch vụ Cloud Run vô tình bị gọi nhiều lần hơn mức phân bổ lệnh gọi Cloud Run hằng tháng của bạn ở cấp miễn phí), bạn có thể xoá Cloud Run hoặc xoá dự án bạn đã tạo ở Bước 2.

Để xoá dịch vụ Cloud Run, hãy truy cập vào Cloud Run Cloud Console tại https://console.cloud.google.com/run rồi xoá dịch vụ chat-with-gemini. Bạn cũng nên xoá tài khoản dịch vụ vertex-ai-caller hoặc thu hồi vai trò của Người dùng Vertex AI để tránh việc vô tình gọi đến Gemini.

Nếu chọn xoá toàn bộ dự án, bạn có thể truy cập vào https://console.cloud.google.com/cloud-resource-manager, chọn dự án mà bạn đã tạo ở Bước 2 rồi chọn Xoá. Nếu xoá dự án, bạn sẽ phải thay đổi các dự án trong Cloud SDK của mình. Bạn có thể xem danh sách tất cả dự án hiện có bằng cách chạy gcloud projects list.