Cách sử dụng Cloud Run bằng cách gọi hàm Gemini

1. Giới thiệu

Tổng quan

Trong lớp học lập trình này, bạn sẽ thấy cách cấp cho Gemini quyền truy cập vào dữ liệu theo thời gian thực bằng cách sử dụng một tính năng mới có tên là Gọi hàm. Để mô phỏng dữ liệu theo thời gian thực, bạn sẽ tạo một điểm cuối dịch vụ thời tiết trả về thông tin thời tiết hiện tại cho 2 vị trí. Sau đó, bạn sẽ tạo một ứng dụng trò chuyện do Gemini cung cấp, sử dụng tính năng gọi hàm để truy xuất thông tin thời tiết hiện tại.

Hãy xem nhanh hình ảnh để hiểu về tính năng Gọi hàm.

  • Câu lệnh yêu cầu thông tin về vị trí thời tiết hiện tại tại một vị trí nhất định
  • Câu lệnh này và hợp đồng hàm cho getWeather() sẽ được gửi đến Gemini
  • Gemini yêu cầu ứng dụng chatbot gọi "getWeather(Seattle)" thay cho ứng dụng này
  • Ứng dụng gửi lại kết quả (40 độ F và trời mưa)
  • Gemini gửi kết quả lại cho người gọi

Tóm lại, Gemini không gọi Hàm. Bạn, với tư cách là nhà phát triển, phải gọi hàm này và gửi kết quả về cho Gemini.

Sơ đồ quy trình Gọi hàm

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

  • Cách hoạt động của tính năng gọi hàm của Gemini
  • Cách triển khai một ứng dụng chatbot dựa trên Gemini dưới dạng một dịch vụ Cloud Run

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 bạ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 thấy màn hình trung gian, hãy nhấp vào Tiếp tục.

d95252b003979716.png

Quá trình cung cấp và kết nối với Cloud Shell chỉ mất vài giây.

7833d5e1c5d18f54.png

Máy ảo này được trang bị tất cả các công cụ phát triển cần thiết. Nền tảng này cung cấp một thư mục chính có dung lượng 5 GB và chạy trong Google Cloud, giúp tăng cường đáng kể hiệu suất mạng và hoạt động xác thực. Bạn có thể thực hiện hầu hết, nếu không muốn nói là tất cả, công việc của mình trong lớp học lập trình này bằng trình duyệt.

Sau khi kết nối với Cloud Shell, bạn sẽ thấy rằng mình đã được xác thực và dự án được đặt thành mã dự án của bạ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

Đầu ra của 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

Đầu ra của lệnh

[core]
project = <PROJECT_ID>

Nếu không, bạn có thể đặt nó bằng lệnh sau:

gcloud config set project <PROJECT_ID>

Đầu ra của lệnh

Updated property [core/project].

3. Thiết lập các biến môi trường và bật API

Thiết lập các 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>
WEATHER_SERVICE=weatherservice
FRONTEND=frontend
SERVICE_ACCOUNT="vertex-ai-caller"
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

Bật API

Để có thể bắt đầu sử dụng lớp học lập trình này, bạn cần bật một số API. Lớp học lập trình này yêu cầu bạn 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

4. Tạo tài khoản dịch vụ để gọi Vertex AI

Cloud Run sẽ dùng tài khoản dịch vụ này để gọi Vertex AI Gemini API.

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

5. Tạo dịch vụ Cloud Run phụ trợ

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

mkdir -p gemini-function-calling/weatherservice gemini-function-calling/frontend && cd gemini-function-calling/weatherservice

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

{
    "name": "weatherservice",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "express": "^4.18.3"
    }
}

Tiếp theo, hãy tạo một tệp nguồn app.js có nội dung 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.get("/getweather", (req, res) => {
    const location = req.query.location;
    let temp, conditions;

    if (location == "New Orleans") {
        temp = 99;
        conditions = "hot and humid";
    } else if (location == "Seattle") {
        temp = 40;
        conditions = "rainy and overcast";
    } else {
        res.status(400).send("there is no data for the requested location");
    }

    res.json({
        weather: temp,
        location: location,
        conditions: conditions
    });
});

const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
    console.log(`weather service: listening on port ${port}`);
});

app.get("/", (req, res) => {
    res.send("welcome to hard-coded weather!");
});

Triển khai Dịch vụ thời tiết

Bạn có thể dùng lệnh này để triển khai dịch vụ thời tiết.

gcloud run deploy $WEATHER_SERVICE \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Kiểm thử Dịch vụ thời tiết

Bạn có thể xác minh thời tiết cho 2 vị trí bằng cách sử dụng curl:

WEATHER_SERVICE_URL=$(gcloud run services describe $WEATHER_SERVICE \
              --platform managed \
              --region=$REGION \
              --format='value(status.url)')

curl $WEATHER_SERVICE_URL/getweather?location=Seattle

curl $WEATHER_SERVICE_URL/getweather?location\=New%20Orleans

Seattle có nhiệt độ 40 độ F và trời mưa, còn New Orleans có nhiệt độ 99 độ F và luôn ẩm ướt.

6. Tạo dịch vụ Giao diện người dùng

Trước tiên, hãy chuyển đến thư mục frontend.

cd gemini-function-calling/frontend

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

{
  "name": "demo1",
  "version": "1.0.0",
  "description": "",
  "main": "index.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/vertexai": "^0.4.0",
    "axios": "^1.6.7",
    "express": "^4.18.2",
    "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 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");

const expressWs = require("express-ws")(app);

app.use(express.static("public"));

const {
    VertexAI,
    FunctionDeclarationSchemaType
} = require("@google-cloud/vertexai");

// get project and location from metadata service
const metadataService = require("./metadataService.js");

// instance of Gemini model
let generativeModel;

// 1: define the function
const functionDeclarations = [
    {
        function_declarations: [
            {
                name: "getweather",
                description: "get weather for a given location",
                parameters: {
                    type: FunctionDeclarationSchemaType.OBJECT,
                    properties: {
                        location: {
                            type: FunctionDeclarationSchemaType.STRING
                        },
                        degrees: {
                            type: FunctionDeclarationSchemaType.NUMBER,
                            "description":
                                "current temperature in fahrenheit"
                        },
                        conditions: {
                            type: FunctionDeclarationSchemaType.STRING,
                            "description":
                                "how the weather feels subjectively"
                        }
                    },
                    required: ["location"]
                }
            }
        ]
    }
];

// on instance startup
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
    console.log(`demo1: listening on port ${port}`);

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

const axios = require("axios");
const baseUrl = "https://weatherservice-k6msmyp47q-uc.a.run.app";

app.ws("/sendMessage", async function (ws, req) {

    // this chat history will be pinned to the current 
    // Cloud Run instance. Consider using Firestore &
    // Firebase anonymous auth instead.

    // start ephemeral chat session with Gemini
    const chatWithModel = generativeModel.startChat({
        tools: functionDeclarations
    });

    ws.on("message", async function (message) {
        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);

        // Function calling demo
        let response1 = await results.response;
        let data = response1.candidates[0].content.parts[0];

        let methodToCall = data.functionCall;
        if (methodToCall === undefined) {
            console.log("Gemini says: ", data.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">
                        ${data.text}
                    </div>`);

            // bail out - Gemini doesn't want to return a function
            return;
        }

        // otherwise Gemini wants to call a function
        console.log(
            "Gemini wants to call: " +
                methodToCall.name +
                " with args: " +
                util.inspect(methodToCall.args, {
                    showHidden: false,
                    depth: null,
                    colors: true
                })
        );

        // make the external call
        let jsonReturned;
        try {
            const responseFunctionCalling = await axios.get(
                baseUrl + "/" + methodToCall.name,

                {
                    params: {
                        location: methodToCall.args.location
                    }
                }
            );
            jsonReturned = responseFunctionCalling.data;
        } catch (ex) {
            // in case an invalid location was provided
            jsonReturned = ex.response.data;
        }

        console.log("jsonReturned: ", jsonReturned);

        // tell the model what function we just called
        const functionResponseParts = [
            {
                functionResponse: {
                    name: methodToCall.name,
                    response: {
                        name: methodToCall.name,
                        content: { jsonReturned }
                    }
                }
            }
        ];

        // // Send a follow up message with a FunctionResponse
        const result2 = await chatWithModel.sendMessage(
            functionResponseParts
        );

        // This should include a text response from the model using the response content
        // provided above
        const response2 = await result2.response;
        let answer = response2.candidates[0].content.parts[0].text;
        console.log("answer: ", answer);

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

    ws.on("close", () => {
        console.log("WebSocket was closed");
    });
});

function sleep(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

Tạo tệp input.css cho tailwindCSS.

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

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

Tạo một thư mục public mới.

mkdir public
cd public

Giờ 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 2</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
                        >
What&apos;s is the current weather in Seattle?</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ụ Frontend cục bộ

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

cd .. && pwd

Sau đó, 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 đang chạy trên một máy ảo Google Compute Engine. Thông tin xác thực của bạn được liên kết với máy ảo này (như khi chạy gcloud auth list) sẽ được Thông tin xác thực mặc định của ứng dụng tự động sử dụng, nên bạn không cần dùng lệnh gcloud auth application-default login. Bạn có thể chuyển xuống phần Chạy ứng dụng 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 phải trong Cloud Shell), bạn sẽ cần 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ụ được dùng trong lớp học lập trình này.

Cách 1) Sử dụng thông tin đăng nhập của bạn cho ADC

Nếu muốn sử dụng thông tin đăng nhập của mình, 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 cho danh tính của mình vai trò Người dùng Vertex AI. 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 cho danh tính của mình vai trò người dùng Vertex AI 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 đó, hãy chạy lệnh sau

gcloud auth application-default login

Cách 2) Mạo danh tài khoản dịch vụ cho 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 có vai trò Trình tạo mã thông báo tài khoản dịch vụ. Bạn có thể nhận đượ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

Chạy ứng dụng cục bộ

Cuối cùng, bạn có thể chạy ứng dụng bằng cách chạy tập lệnh sau. Tập lệnh dev 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 (Xem trước trang web) rồi chọn Preview Port 8080 (Xem trước cổng 8080)

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

8. Triển khai và kiểm thử dịch vụ Giao diện người dùng

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 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ụ tính toán mặc định sẽ được dùng.

gcloud run deploy $FRONTEND \
  --service-account $SERVICE_ACCOUNT_ADDRESS \
  --source . \
  --region $REGION \
  --allow-unauthenticated

Mở URL dịch vụ cho giao diện người dùng trong trình duyệt. Đặt câu hỏi "Thời tiết hiện tại ở Seattle như thế nào?" và Gemini sẽ trả lời "Hiện tại, trời mưa và nhiệt độ là 40 độ." Nếu bạn hỏi "Thời tiết hiện tại ở Boston như thế nào?" Gemini sẽ trả lời "Tôi không thể thực hiện yêu cầu này. API thời tiết hiện có không có dữ liệu cho Boston."

9. Xin chúc mừng!

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

Bạn nên xem tài liệu về Cloud Run, Vertex AI Gemini APIlệnh gọi hàm.

Nội dung đã đề cập

  • Cách hoạt động của tính năng gọi hàm của Gemini
  • Cách triển khai một ứng dụng chatbot dựa trên Gemini dưới dạng một dịch vụ Cloud Run

10. Dọn dẹp

Để tránh bị tính phí ngoài ý muốn (ví dụ: nếu dịch vụ Cloud Run này được gọi nhiều lần hơn hạn mức gọi Cloud Run hằng tháng của bạn trong bậc miễn phí), bạn có thể xoá dịch vụ Cloud Run hoặc xoá dự án mà bạn đã tạo ở Bước 2.

Để xoá các dịch vụ Cloud Run, hãy truy cập vào Cloud Console của Cloud Run tại https://console.cloud.google.com/functions/ rồi xoá các dịch vụ $WEATHER_SERVICE và $FRONTEND mà bạn đã tạo trong lớp học lập trình này.

Bạn cũng có thể xoá tài khoản dịch vụ vertex-ai-caller hoặc thu hồi vai trò Người dùng Vertex AI để tránh vô tình gọi 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 bạn đã tạo ở Bước 2 rồi chọn Xoá. Nếu xoá dự án, bạn sẽ cần thay đổi dự án trong Cloud SDK. Bạn có thể xem danh sách tất cả các dự án có sẵn bằng cách chạy gcloud projects list.