Xây dựng và triển khai Trợ lý đa phương thức trên đám mây bằng Gemini (Python)

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ tạo một ứng dụng dưới dạng giao diện web trò chuyện, trong đó bạn có thể giao tiếp với ứng dụng, tải lên một số tài liệu hoặc hình ảnh và thảo luận về chúng. Bản thân ứng dụng được chia thành 2 dịch vụ: giao diện người dùng và chương trình phụ trợ; cho phép bạn tạo một nguyên mẫu nhanh và dùng thử, đồng thời hiểu rõ hợp đồng API trông như thế nào để tích hợp cả hai.

Trong lớp học lập trình này, bạn sẽ sử dụng phương pháp từng bước như sau:

  1. Chuẩn bị dự án Google Cloud và Bật tất cả API bắt buộc trên dự án đó
  2. Xây dựng dịch vụ giao diện người dùng – giao diện trò chuyện bằng thư viện Gradio
  3. Tạo dịch vụ phụ trợ – máy chủ HTTP bằng FastAPI. Dịch vụ này sẽ định dạng lại dữ liệu đến theo tiêu chuẩn của Gemini SDK và cho phép giao tiếp với Gemini API
  4. Quản lý các biến môi trường và thiết lập các tệp bắt buộc cần thiết để triển khai ứng dụng vào Cloud Run
  5. Triển khai ứng dụng lên Cloud Run

5bcfa1cce6618305.png

Tổng quan về cấu trúc

b102df2c3f1adabf.jpeg

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

  • Thành thạo khi làm việc với Gemini APIGoogle Gen AI SDK
  • Hiểu biết về cấu trúc cơ bản của ngăn xếp đầy đủ bằng cách sử dụng dịch vụ HTTP

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

  • Cách sử dụng Gemini SDK để gửi văn bản và các loại dữ liệu khác (đa phương thức) và tạo câu trả lời bằng văn bản
  • Cách cấu trúc nhật ký trò chuyện trong Gemini SDK để duy trì ngữ cảnh trò chuyện
  • Tạo mẫu web giao diện người dùng bằng Gradio
  • Phát triển dịch vụ phụ trợ bằng FastAPIPydantic
  • Quản lý các biến môi trường trong tệp YAML bằng Pydantic-settings
  • Triển khai ứng dụng lên Cloud Run bằng Dockerfile và cung cấp các biến môi trường bằng tệp YAML

Bạn cần có

  • Trình duyệt web Chrome
  • Tài khoản Gmail
  • Một Dự án trên đám mây đã bật tính năng thanh toán

Lớp học lập trình này được thiết kế cho nhà phát triển ở mọi cấp độ (kể cả người mới bắt đầu), sử dụng Python trong ứng dụng mẫu. Tuy nhiên, bạn không cần có kiến thức về Python để hiểu các khái niệm được trình bày.

2. Trước khi bắt đầu

Thiết lập dự án trên Cloud trong Cloud Shell Editor

Lớp học lập trình này giả định rằng bạn đã có một dự án trên đám mây trên Google Cloud đã bật tính năng thanh toán. Nếu chưa có, bạn có thể làm theo hướng dẫn bên dưới để bắt đầu.

  1. 2Trong Google Cloud Console, trên trang chọn dự án, hãy chọn hoặc tạo một dự án trên Google Cloud.
  2. Đảm bảo rằng bạn đã bật tính năng thanh toán cho dự án trên đám mây của bạn. Tìm hiểu cách kiểm tra xem tính năng thanh toán có được bật trong một dự án hay không .
  3. Bạn sẽ sử dụng Cloud Shell, một môi trường dòng lệnh chạy trong Google Cloud và được tải sẵn bằng bq. Nhấp vào Kích hoạt Cloud Shell ở đầu bảng điều khiển Google Cloud.

1829c3759227c19b.png

  1. Sau khi kết nối với Cloud Shell, bạn có thể kiểm tra để đảm bảo rằng bạn đã được xác thực và dự án được đặt thành mã dự án của bạn bằng lệnh sau:
gcloud auth list
  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
  1. Nếu bạn chưa đặt dự án, hãy dùng lệnh sau để đặt:
gcloud config set project <YOUR_PROJECT_ID>

Ngoài ra, bạn cũng có thể xem mã PROJECT_ID trong bảng điều khiển

4032c45803813f30.jpeg

Nhấp vào đó, bạn sẽ thấy tất cả dự án và mã dự án ở bên phải

8dc17eb4271de6b5.jpeg

  1. Bật các API bắt buộc thông qua lệnh bên dưới. Quá trình này có thể mất vài phút, vì vậy, vui lòng kiên nhẫn chờ đợi.
gcloud services enable aiplatform.googleapis.com \
                           run.googleapis.com \
                           cloudbuild.googleapis.com \
                           cloudresourcemanager.googleapis.com

Khi thực thi lệnh thành công, bạn sẽ thấy một thông báo tương tự như thông báo dưới đây:

Operation "operations/..." finished successfully.

Bạn có thể thay thế lệnh gcloud bằng cách tìm kiếm từng sản phẩm trên bảng điều khiển hoặc sử dụng đường liên kết này.

Nếu bỏ lỡ API nào, bạn luôn có thể bật API đó trong quá trình triển khai.

Tham khảo tài liệu để biết các lệnh và cách sử dụng gcloud.

Thiết lập thư mục làm việc của ứng dụng

  1. Nhấp vào nút Open Editor (Mở trình chỉnh sửa). Thao tác này sẽ mở Cloud Shell Editor. Chúng ta có thể viết mã tại đây b16d56e4979ec951.png
  2. Đảm bảo dự án Cloud Code được đặt ở góc dưới bên trái (thanh trạng thái) của trình chỉnh sửa Cloud Shell, như được đánh dấu trong hình bên dưới và được đặt thành dự án Google Cloud đang hoạt động mà bạn đã bật tính năng thanh toán. Uỷ quyền nếu được nhắc. Có thể mất một lúc sau khi bạn khởi động Cloud Shell Editor thì nút Cloud Code – Đăng nhập mới xuất hiện. Vui lòng kiên nhẫn chờ đợi. Nếu bạn đã làm theo lệnh trước đó, nút này cũng có thể chuyển thẳng đến dự án đã kích hoạt thay vì nút đăng nhập

f5003b9c38b43262.png

  1. Nhấp vào dự án đang hoạt động đó trên thanh trạng thái và đợi cửa sổ bật lên Cloud Code mở ra. Trong cửa sổ bật lên, hãy chọn "Ứng dụng mới".

70f80078e01a02d8.png

  1. Trong danh sách ứng dụng, hãy chọn AI tạo sinh của Gemini, sau đó chọn Gemini API Python

362ff332256d6933.jpeg

85957565316308d9.jpeg

  1. Lưu ứng dụng mới với tên mà bạn muốn, trong ví dụ này, chúng ta sẽ sử dụng gemini-multimodal-chat-assistant, sau đó nhấp vào OK

8409d8db18690fdf.png

Tại thời điểm này, bạn nên đã ở trong thư mục làm việc của ứng dụng mới và thấy các tệp sau

1ef5bb44f1d2c2a4.png

Tiếp theo, chúng ta sẽ chuẩn bị môi trường Python

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

Chuẩn bị môi trường ảo Python

Bước tiếp theo là chuẩn bị môi trường phát triển. Chúng ta sẽ sử dụng Python 3.12 trong lớp học lập trình này và sử dụng trình quản lý dự án uv python để đơn giản hoá nhu cầu tạo và quản lý phiên bản python cũng như môi trường ảo

  1. Nếu bạn chưa mở cửa sổ dòng lệnh, hãy mở bằng cách nhấp vào Terminal (Cửa sổ dòng lệnh) -> New Terminal (Cửa sổ dòng lệnh mới) hoặc dùng tổ hợp phím Ctrl + Shift + C

f8457daf0bed059e.jpeg

  1. Tải uv xuống và cài đặt python 3.12 bằng lệnh sau
curl -LsSf https://astral.sh/uv/0.6.6/install.sh | sh && \
source $HOME/.local/bin/env && \
uv python install 3.12
  1. Bây giờ, hãy khởi động dự án Python bằng uv
uv init
  1. Bạn sẽ thấy main.py, .python-versionpyproject.toml được tạo trong thư mục. Bạn cần những tệp này để duy trì dự án trong thư mục. Các phần phụ thuộc và cấu hình Python có thể được chỉ định trong pyproject.toml.python-version đã chuẩn hoá phiên bản Python được dùng cho dự án này. Để đọc thêm về nội dung này, bạn có thể xem tài liệu này
main.py
.python-version
pyproject.toml
  1. Để kiểm thử, hãy ghi đè main.py bằng đoạn mã sau
def main():
   print("Hello from gemini-multimodal-chat-assistant!")

if __name__ == "__main__":
   main()
  1. Sau đó, hãy chạy lệnh sau
uv run main.py

Bạn sẽ nhận được kết quả đầu ra như bên dưới

Using CPython 3.12
Creating virtual environment at: .venv
Hello from gemini-multimodal-chat-assistant!

Điều này cho thấy dự án Python đang được thiết lập đúng cách. Chúng ta không cần tạo môi trường ảo theo cách thủ công vì uv đã xử lý việc này. Do đó, từ thời điểm này, lệnh python chuẩn (ví dụ: python main.py) sẽ được thay thế bằng uv run (ví dụ: uv run main.py).

Cài đặt các phần phụ thuộc bắt buộc

Chúng ta cũng sẽ thêm các phần phụ thuộc của gói lớp học lập trình này bằng lệnh uv. Chạy lệnh sau

uv add google-genai==1.5.0 \
       gradio==5.20.1 \
       pydantic==2.10.6 \
       pydantic-settings==2.8.1 \
       pyyaml==6.0.2

Bạn sẽ thấy phần "dependencies" (các phần phụ thuộc) của pyproject.toml được cập nhật để phản ánh lệnh trước đó

Thiết lập tệp cấu hình

Bây giờ, chúng ta cần thiết lập các tệp cấu hình cho dự án này. Tệp cấu hình được dùng để lưu trữ các biến động có thể dễ dàng thay đổi khi triển khai lại. Trong dự án này, chúng ta sẽ sử dụng các tệp cấu hình dựa trên YAML với gói pydantic-settings, để có thể dễ dàng tích hợp với việc triển khai Cloud Run sau này. pydantic-settings là một gói Python có thể thực thi việc kiểm tra kiểu cho các tệp cấu hình.

  1. Tạo một tệp có tên settings.yaml với cấu hình sau. Nhấp vào File->New Text File (Tệp > Tệp văn bản mới) rồi điền mã sau. Sau đó, lưu tệp này dưới dạng settings.yaml
VERTEXAI_LOCATION: "us-central1"
VERTEXAI_PROJECT_ID: "{YOUR-PROJECT-ID}"
BACKEND_URL: "http://localhost:8081/chat"

Vui lòng cập nhật các giá trị cho VERTEXAI_PROJECT_ID theo những gì bạn đã chọn khi tạo Dự án trên Google Cloud. Trong lớp học lập trình này, chúng ta sẽ sử dụng các giá trị được định cấu hình sẵn cho VERTEXAI_LOCATIONBACKEND_URL .

  1. Sau đó, hãy tạo tệp python settings.py. Mô-đun này sẽ đóng vai trò là mục nhập theo chương trình cho các giá trị cấu hình trong tệp cấu hình của chúng ta. Nhấp vào File->New Text File (Tệp > Tệp văn bản mới) rồi điền mã sau. Sau đó, hãy lưu tệp này dưới dạng settings.py. Bạn có thể thấy trong mã rằng chúng ta đã đặt rõ ràng tệp có tên settings.yaml là tệp sẽ được đọc
from pydantic_settings import (
    BaseSettings,
    SettingsConfigDict,
    YamlConfigSettingsSource,
    PydanticBaseSettingsSource,
)
from typing import Type, Tuple

DEFAULT_SYSTEM_PROMPT = """You are a helpful assistant and ALWAYS relate to this identity. 
You are expert at analyzing given documents or images.
"""

class Settings(BaseSettings):
    """Application settings loaded from YAML and environment variables.

    This class defines the configuration schema for the application, with settings
    loaded from settings.yaml file and overridable via environment variables.

    Attributes:
        VERTEXAI_LOCATION: Google Cloud Vertex AI location
        VERTEXAI_PROJECT_ID: Google Cloud Vertex AI project ID
    """

    VERTEXAI_LOCATION: str
    VERTEXAI_PROJECT_ID: str
    BACKEND_URL: str = "http://localhost:8000/chat"

    model_config = SettingsConfigDict(
        yaml_file="settings.yaml", yaml_file_encoding="utf-8"
    )

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: Type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> Tuple[PydanticBaseSettingsSource, ...]:
        """Customize the settings sources and their priority order.

        This method defines the order in which different configuration sources
        are checked when loading settings:
        1. Constructor-provided values
        2. YAML configuration file
        3. Environment variables

        Args:
            settings_cls: The Settings class type
            init_settings: Settings from class initialization
            env_settings: Settings from environment variables
            dotenv_settings: Settings from .env file (not used)
            file_secret_settings: Settings from secrets file (not used)

        Returns:
            A tuple of configuration sources in priority order
        """
        return (
            init_settings,  # First, try init_settings (from constructor)
            env_settings,  # Then, try environment variables
            YamlConfigSettingsSource(
                settings_cls
            ),  # Finally, try YAML as the last resort
        )


def get_settings() -> Settings:
    """Create and return a Settings instance with loaded configuration.

    Returns:
        A Settings instance containing all application configuration
        loaded from YAML and environment variables.
    """
    return Settings()

Những cấu hình này cho phép chúng tôi linh hoạt cập nhật thời gian chạy. Trong quá trình triển khai ban đầu, chúng ta sẽ dựa vào cấu hình settings.yaml để có cấu hình mặc định đầu tiên. Sau đó, chúng ta có thể linh hoạt cập nhật các biến môi trường thông qua bảng điều khiển và triển khai lại vì chúng ta đặt các biến môi trường ở mức độ ưu tiên cao hơn so với cấu hình YAML mặc định

Giờ đây, chúng ta có thể chuyển sang bước tiếp theo là tạo các dịch vụ

3. Tạo dịch vụ giao diện người dùng bằng Gradio

Chúng ta sẽ xây dựng một giao diện web trò chuyện có dạng như sau

5bcfa1cce6618305.png

Nó chứa một trường nhập dữ liệu để người dùng gửi văn bản và tải tệp lên. Ngoài ra, người dùng cũng có thể ghi đè chỉ dẫn hệ thống sẽ được gửi đến Gemini API trong trường dữ liệu đầu vào bổ sung

Chúng ta sẽ tạo dịch vụ giao diện người dùng bằng Gradio. Đổi tên main.py thành frontend.py và ghi đè mã bằng mã sau

import gradio as gr
import requests
import base64
from pathlib import Path
from typing import List, Dict, Any
from settings import get_settings, DEFAULT_SYSTEM_PROMPT

settings = get_settings()

IMAGE_SUFFIX_MIME_MAP = {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".heic": "image/heic",
    ".heif": "image/heif",
    ".webp": "image/webp",
}
DOCUMENT_SUFFIX_MIME_MAP = {
    ".pdf": "application/pdf",
}


def get_mime_type(filepath: str) -> str:
    """Get the MIME type for a file based on its extension.

    Args:
        filepath: Path to the file.

    Returns:
        str: The MIME type of the file.

    Raises:
        ValueError: If the file type is not supported.
    """
    filepath = Path(filepath)
    suffix = filepath.suffix

    # modify ".jpg" suffix to ".jpeg" to unify the mime type
    suffix = suffix if suffix != ".jpg" else ".jpeg"

    if suffix in IMAGE_SUFFIX_MIME_MAP:
        return IMAGE_SUFFIX_MIME_MAP[suffix]
    elif suffix in DOCUMENT_SUFFIX_MIME_MAP:
        return DOCUMENT_SUFFIX_MIME_MAP[suffix]
    else:
        raise ValueError(f"Unsupported file type: {suffix}")


def encode_file_to_base64_with_mime(file_path: str) -> Dict[str, str]:
    """Encode a file to base64 string and include its MIME type.

    Args:
        file_path: Path to the file to encode.

    Returns:
        Dict[str, str]: Dictionary with 'data' and 'mime_type' keys.
    """
    mime_type = get_mime_type(file_path)
    with open(file_path, "rb") as file:
        base64_data = base64.b64encode(file.read()).decode("utf-8")

    return {"data": base64_data, "mime_type": mime_type}


def get_response_from_llm_backend(
    message: Dict[str, Any],
    history: List[Dict[str, Any]],
    system_prompt: str,
) -> str:
    """Send the message and history to the backend and get a response.

    Args:
        message: Dictionary containing the current message with 'text' and optional 'files' keys.
        history: List of previous message dictionaries in the conversation.
        system_prompt: The system prompt to be sent to the backend.

    Returns:
        str: The text response from the backend service.
    """

    # Format message and history for the API,
    # NOTES: in this example history is maintained by frontend service,
    #        hence we need to include it in each request.
    #        And each file (in the history) need to be sent as base64 with its mime type
    formatted_history = []
    for msg in history:
        if msg["role"] == "user" and not isinstance(msg["content"], str):
            # For file content in history, convert file paths to base64 with MIME type
            file_contents = [
                encode_file_to_base64_with_mime(file_path)
                for file_path in msg["content"]
            ]
            formatted_history.append({"role": msg["role"], "content": file_contents})
        else:
            formatted_history.append({"role": msg["role"], "content": msg["content"]})

    # Extract files and convert to base64 with MIME type
    files_with_mime = []
    if uploaded_files := message.get("files", []):
        for file_path in uploaded_files:
            files_with_mime.append(encode_file_to_base64_with_mime(file_path))

    # Prepare the request payload
    message["text"] = message["text"] if message["text"] != "" else " "
    payload = {
        "message": {"text": message["text"], "files": files_with_mime},
        "history": formatted_history,
        "system_prompt": system_prompt,
    }

    # Send request to backend
    try:
        response = requests.post(settings.BACKEND_URL, json=payload)
        response.raise_for_status()  # Raise exception for HTTP errors

        result = response.json()
        if error := result.get("error"):
            return f"Error: {error}"

        return result.get("response", "No response received from backend")
    except requests.exceptions.RequestException as e:
        return f"Error connecting to backend service: {str(e)}"


if __name__ == "__main__":
    demo = gr.ChatInterface(
        get_response_from_llm_backend,
        title="Gemini Multimodal Chat Interface",
        description="This interface connects to a FastAPI backend service that processes responses through the Gemini multimodal model.",
        type="messages",
        multimodal=True,
        textbox=gr.MultimodalTextbox(file_count="multiple"),
        additional_inputs=[
            gr.Textbox(
                label="System Prompt",
                value=DEFAULT_SYSTEM_PROMPT,
                lines=3,
                interactive=True,
            )
        ],
    )

    demo.launch(
        server_name="0.0.0.0",
        server_port=8080,
    )

Sau đó, chúng ta có thể thử chạy dịch vụ giao diện người dùng bằng lệnh sau. Đừng quên đổi tên tệp main.py thành frontend.py

uv run frontend.py

Bạn sẽ thấy kết quả tương tự như kết quả này trong Cloud Console

* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.

Sau đó, bạn có thể kiểm tra giao diện web khi nhấn ctrl+nhấp vào đường liên kết URL cục bộ. Ngoài ra, bạn cũng có thể truy cập vào ứng dụng giao diện người dùng bằng cách nhấp vào nút Xem trước trên web ở phía trên cùng bên phải của Cloud Editor, rồi chọn Xem trước trên cổng 8080

49cbdfdf77964065.jpeg

Bạn sẽ thấy giao diện web, tuy nhiên, bạn sẽ gặp phải lỗi dự kiến khi cố gắng gửi tin nhắn trò chuyện do dịch vụ phụ trợ chưa được thiết lập

bd0464140308cfbe.png

Bây giờ, hãy chạy dịch vụ và đừng tắt dịch vụ ngay. Trong thời gian chờ đợi, chúng ta có thể thảo luận về các thành phần quan trọng của mã tại đây

Giải thích mã

Mã để gửi dữ liệu từ giao diện web đến phần phụ trợ nằm ở phần này

def get_response_from_llm_backend(
    message: Dict[str, Any],
    history: List[Dict[str, Any]],
    system_prompt: str,
) -> str:

    ... 
    # Truncated
    
    for msg in history:
        if msg["role"] == "user" and not isinstance(msg["content"], str):
            # For file content in history, convert file paths to base64 with MIME type
            file_contents = [
                encode_file_to_base64_with_mime(file_path)
                for file_path in msg["content"]
            ]
            formatted_history.append({"role": msg["role"], "content": file_contents})
        else:
            formatted_history.append({"role": msg["role"], "content": msg["content"]})

    # Extract files and convert to base64 with MIME type
    files_with_mime = []
    if uploaded_files := message.get("files", []):
        for file_path in uploaded_files:
            files_with_mime.append(encode_file_to_base64_with_mime(file_path))

    # Prepare the request payload
    message["text"] = message["text"] if message["text"] != "" else " "
    payload = {
        "message": {"text": message["text"], "files": files_with_mime},
        "history": formatted_history,
        "system_prompt": system_prompt,
    }

    # Truncated
    ... 

Khi muốn gửi dữ liệu đa phương thức đến Gemini và giúp các dịch vụ truy cập được vào dữ liệu đó, một cơ chế mà chúng ta có thể sử dụng là chuyển đổi dữ liệu thành kiểu dữ liệu base64 như được khai báo trong mã. Chúng ta cũng cần khai báo loại MIME của dữ liệu. Tuy nhiên, Gemini API không thể hỗ trợ tất cả các loại MIME hiện có. Do đó, bạn cần biết những loại MIME được Gemini hỗ trợ mà bạn có thể đọc trên tài liệu này. Bạn có thể tìm thấy thông tin trong từng chức năng của Gemini API (ví dụ: Vision)

Ngoài ra, trong giao diện trò chuyện, bạn cũng cần gửi nhật ký trò chuyện làm bối cảnh bổ sung để Gemini có "bộ nhớ" về cuộc trò chuyện. Vì vậy, trong giao diện web này, chúng tôi cũng gửi nhật ký trò chuyện do Gradio quản lý theo từng phiên web và gửi nhật ký đó cùng với nội dung tin nhắn mà người dùng nhập. Ngoài ra, chúng tôi cũng cho phép người dùng sửa đổi chỉ dẫn hệ thống và gửi chỉ dẫn đó

4. Tạo dịch vụ phụ trợ bằng FastAPI

Tiếp theo, chúng ta sẽ cần xây dựng phần phụ trợ có thể xử lý tải trọng đã thảo luận trước đó, tin nhắn cuối cùng của người dùng, nhật ký trò chuyệnhướng dẫn hệ thống. Chúng ta sẽ sử dụng FastAPI để tạo dịch vụ phụ trợ HTTP.

Tạo tệp mới, nhấp vào File->New Text File (Tệp->Tệp văn bản mới), rồi sao chép và dán mã sau, sau đó lưu tệp dưới dạng backend.py

import base64
from fastapi import FastAPI, Body
from google.genai.types import Content, Part
from google.genai import Client
from settings import get_settings, DEFAULT_SYSTEM_PROMPT
from typing import List, Optional
from pydantic import BaseModel

app = FastAPI(title="Gemini Multimodal Service")

settings = get_settings()
GENAI_CLIENT = Client(
    location=settings.VERTEXAI_LOCATION,
    project=settings.VERTEXAI_PROJECT_ID,
    vertexai=True,
)
GEMINI_MODEL_NAME = "gemini-2.0-flash-001"


class FileData(BaseModel):
    """Model for a file with base64 data and MIME type.

    Attributes:
        data: Base64 encoded string of the file content.
        mime_type: The MIME type of the file.
    """

    data: str
    mime_type: str


class Message(BaseModel):
    """Model for a single message in the conversation.

    Attributes:
        role: The role of the message sender, either 'user' or 'assistant'.
        content: The text content of the message or a list of file data objects.
    """

    role: str
    content: str | List[FileData]


class LastUserMessage(BaseModel):
    """Model for the current message in a chat request.

    Attributes:
        text: The text content of the message.
        files: List of file data objects containing base64 data and MIME type.
    """

    text: str
    files: List[FileData] = []


class ChatRequest(BaseModel):
    """Model for a chat request.

    Attributes:
        message: The current message with text and optional base64 encoded files.
        history: List of previous messages in the conversation.
        system_prompt: Optional system prompt to be used in the chat.
    """

    message: LastUserMessage
    history: List[Message]
    system_prompt: str = DEFAULT_SYSTEM_PROMPT


class ChatResponse(BaseModel):
    """Model for a chat response.

    Attributes:
        response: The text response from the model.
        error: Optional error message if something went wrong.
    """

    response: str
    error: Optional[str] = None


def handle_multimodal_data(file_data: FileData) -> Part:
    """Converts Multimodal data to a Google Gemini Part object.

    Args:
        file_data: FileData object with base64 data and MIME type.

    Returns:
        Part: A Google Gemini Part object containing the file data.
    """
    data = base64.b64decode(file_data.data)  # decode base64 string to bytes
    return Part.from_bytes(data=data, mime_type=file_data.mime_type)


def format_message_history_to_gemini_standard(
    message_history: List[Message],
) -> List[Content]:
    """Converts message history format to Google Gemini Content format.

    Args:
        message_history: List of message objects from the chat history.
            Each message contains 'role' and 'content' attributes.

    Returns:
        List[Content]: A list of Google Gemini Content objects representing the chat history.

    Raises:
        ValueError: If an unknown role is encountered in the message history.
    """
    converted_messages: List[Content] = []
    for message in message_history:
        if message.role == "assistant":
            converted_messages.append(
                Content(role="model", parts=[Part.from_text(text=message.content)])
            )
        elif message.role == "user":
            # Text-only messages
            if isinstance(message.content, str):
                converted_messages.append(
                    Content(role="user", parts=[Part.from_text(text=message.content)])
                )

            # Messages with files
            elif isinstance(message.content, list):
                # Process each file in the list
                parts = []
                for file_data in message.content:
                    for file_data in message.content:
                        parts.append(handle_multimodal_data(file_data))

                # Add the parts to a Content object
                if parts:
                    converted_messages.append(Content(role="user", parts=parts))

            else:
                raise ValueError(f"Unexpected content format: {type(message.content)}")

        else:
            raise ValueError(f"Unknown role: {message.role}")

    return converted_messages


@app.post("/chat", response_model=ChatResponse)
async def chat(
    request: ChatRequest = Body(...),
) -> ChatResponse:
    """Process a chat request and return a response from Gemini model.

    Args:
        request: The chat request containing message and history.

    Returns:
        ChatResponse: The model's response to the chat request.
    """
    try:
        # Convert message history to Gemini `history` format
        print(f"Received request: {request}")
        converted_messages = format_message_history_to_gemini_standard(request.history)

        # Create chat model
        chat_model = GENAI_CLIENT.chats.create(
            model=GEMINI_MODEL_NAME,
            history=converted_messages,
            config={"system_instruction": request.system_prompt},
        )

        # Prepare multimodal content
        content_parts = []

        # Handle any base64 encoded files in the current message
        if request.message.files:
            for file_data in request.message.files:
                content_parts.append(handle_multimodal_data(file_data))

        # Add text content
        content_parts.append(Part.from_text(text=request.message.text))

        # Send message to Gemini
        response = chat_model.send_message(content_parts)
        print(f"Generated response: {response}")

        return ChatResponse(response=response.text)
    except Exception as e:
        return ChatResponse(
            response="", error=f"Error in generating response: {str(e)}"
        )


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8081)

Đừng quên lưu tệp này dưới dạng backend.py. Sau đó, chúng ta có thể thử chạy dịch vụ phụ trợ. Hãy nhớ rằng ở bước trước, chúng ta đã chạy dịch vụ giao diện người dùng, giờ đây, chúng ta sẽ cần mở một thiết bị đầu cuối mới và thử chạy dịch vụ phụ trợ này

  1. Tạo một thiết bị đầu cuối mới. Chuyển đến thiết bị đầu cuối ở khu vực dưới cùng và tìm nút "+" để tạo một thiết bị đầu cuối mới. Hoặc bạn có thể nhấn tổ hợp phím Ctrl + Shift + C để mở thiết bị đầu cuối mới

3e52a362475553dc.jpeg

  1. Sau đó, hãy đảm bảo rằng bạn đang ở trong thư mục làm việc gemini-multimodal-chat-assistant rồi chạy lệnh sau
uv run backend.py
  1. Nếu thành công, bạn sẽ thấy kết quả như sau
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

Giải thích mã

Xác định tuyến HTTP để nhận yêu cầu trò chuyện

Trong FastAPI, chúng ta xác định tuyến bằng cách sử dụng trình trang trí app. Chúng tôi cũng sử dụng Pydantic để xác định hợp đồng API. Chúng ta chỉ định rằng tuyến đường để tạo phản hồi nằm trong tuyến đường /chat bằng phương thức POST. Các chức năng này được khai báo trong mã sau

class FileData(BaseModel):
    data: str
    mime_type: str

class Message(BaseModel):
    role: str
    content: str | List[FileData]

class LastUserMessage(BaseModel):
    text: str
    files: List[FileData] = []

class ChatRequest(BaseModel):
    message: LastUserMessage
    history: List[Message]
    system_prompt: str = DEFAULT_SYSTEM_PROMPT

class ChatResponse(BaseModel):
    response: str
    error: Optional[str] = None

    ...

@app.post("/chat", response_model=ChatResponse)
async def chat(
    request: ChatRequest = Body(...),
) -> ChatResponse:
    
    # Truncated
    ...

Chuẩn bị định dạng nhật ký trò chuyện của Gemini SDK

Một trong những điều quan trọng cần hiểu là cách chúng ta có thể tái cấu trúc nhật ký trò chuyện để có thể chèn nhật ký đó làm giá trị đối số history khi khởi tạo một ứng dụng Gemini sau này. Bạn có thể kiểm tra mã bên dưới

def format_message_history_to_gemini_standard(
    message_history: List[Message],
) -> List[Content]:
    
    ...
    # Truncated    

    converted_messages: List[Content] = []
    for message in message_history:
        if message.role == "assistant":
            converted_messages.append(
                Content(role="model", parts=[Part.from_text(text=message.content)])
            )
        elif message.role == "user":
            # Text-only messages
            if isinstance(message.content, str):
                converted_messages.append(
                    Content(role="user", parts=[Part.from_text(text=message.content)])
                )

            # Messages with files
            elif isinstance(message.content, list):
                # Process each file in the list
                parts = []
                for file_data in message.content:
                    parts.append(handle_multimodal_data(file_data))

                # Add the parts to a Content object
                if parts:
                    converted_messages.append(Content(role="user", parts=parts))
    
    #Truncated
    ...

    return converted_messages

Để cung cấp nhật ký trò chuyện cho Gemini SDK, chúng ta cần định dạng dữ liệu theo kiểu dữ liệu List[Content]. Mỗi Nội dung phải có ít nhất một giá trị vai tròphần. vai trò đề cập đến nguồn của thông báo, cho dù đó là người dùng hay mô hình. Trong đó, parts đề cập đến chính câu lệnh, chỉ có thể là văn bản hoặc kết hợp nhiều phương thức. Xem cách cấu trúc các đối số Nội dung một cách chi tiết trong tài liệu này

Xử lý dữ liệu không phải dạng văn bản ( đa phương thức)

Như đã đề cập trước đó trong phần giao diện người dùng, một trong những cách gửi dữ liệu không phải dạng văn bản hoặc dữ liệu đa phương thức là gửi dữ liệu dưới dạng chuỗi base64. Chúng ta cũng cần chỉ định loại MIME cho dữ liệu để có thể diễn giải chính xác, ví dụ: cung cấp loại MIME image/jpeg nếu chúng ta gửi dữ liệu hình ảnh có hậu tố .jpg.

Phần mã này chuyển đổi dữ liệu base64 thành định dạng Part.from_bytes từ Gemini SDK

def handle_multimodal_data(file_data: FileData) -> Part:
    """Converts Multimodal data to a Google Gemini Part object.

    Args:
        file_data: FileData object with base64 data and MIME type.

    Returns:
        Part: A Google Gemini Part object containing the file data.
    """
    data = base64.b64decode(file_data.data)  # decode base64 string to bytes
    return Part.from_bytes(data=data, mime_type=file_data.mime_type)

5. Kiểm thử tích hợp

Giờ đây, bạn sẽ có nhiều dịch vụ chạy trong thẻ bảng điều khiển đám mây khác nhau:

  • Dịch vụ giao diện người dùng chạy ở cổng 8080
* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.
  • Dịch vụ phụ trợ chạy ở cổng 8081
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

Hiện tại, bạn có thể gửi tài liệu của mình trong cuộc trò chuyện một cách liền mạch với trợ lý từ ứng dụng web trên cổng 8080. Bạn có thể bắt đầu thử nghiệm bằng cách tải tệp lên và đặt câu hỏi! Xin lưu ý rằng một số loại tệp chưa được hỗ trợ và sẽ gây ra Lỗi.

Bạn cũng có thể chỉnh sửa chỉ dẫn cho hệ thống trong trường Thông tin đầu vào bổ sung bên dưới hộp văn bản

ee9c849a276d378.png

6. Triển khai lên Cloud Run

Tất nhiên, chúng ta muốn giới thiệu ứng dụng tuyệt vời này cho những người khác. Để làm như vậy, chúng ta có thể đóng gói ứng dụng này và triển khai ứng dụng đó vào Cloud Run dưới dạng một dịch vụ công cộng mà người khác có thể truy cập. Để làm được điều đó, hãy xem lại cấu trúc

b102df2c3f1adabf.jpeg

Trong lớp học lập trình này, chúng ta sẽ đặt cả dịch vụ giao diện người dùng và dịch vụ phụ trợ vào 1 vùng chứa. Chúng ta sẽ cần sự trợ giúp của supervisord để quản lý cả hai dịch vụ.

Tạo tệp mới, nhấp vào File->New Text File (Tệp->Tệp văn bản mới), rồi sao chép và dán mã sau, sau đó lưu tệp dưới dạng supervisord.conf

[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid

[program:backend]
command=uv run backend.py
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=10
startretries=3

[program:frontend]
command=uv run frontend.py
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
startsecs=10
startretries=3

Tiếp theo, chúng ta sẽ cần Dockerfile. Nhấp vào File->New Text File (Tệp->Tệp văn bản mới), rồi sao chép và dán mã sau,sau đó lưu dưới dạng Dockerfile

FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.6.6 /uv /uvx /bin/

RUN apt-get update && apt-get install -y \
    supervisor curl \
    && rm -rf /var/lib/apt/lists/*

ADD . /app
WORKDIR /app

RUN uv sync --frozen

EXPOSE 8080

# Copy supervisord configuration
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

ENV PYTHONUNBUFFERED=1

ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Đến đây, chúng ta đã có tất cả các tệp cần thiết để triển khai ứng dụng lên Cloud Run. Hãy triển khai ứng dụng. Chuyển đến Cloud Shell Terminal và đảm bảo dự án hiện tại được định cấu hình cho dự án đang hoạt động của bạn. Nếu không, bạn phải dùng lệnh gcloud configure để đặt mã dự án:

gcloud config set project [PROJECT_ID]

Sau đó, hãy chạy lệnh sau để triển khai ứng dụng này vào Cloud Run.

gcloud run deploy --source . \
                  --env-vars-file settings.yaml \
                  --port 8080 \
                  --region us-central1

Thao tác này sẽ nhắc bạn nhập tên cho dịch vụ của mình, giả sử là "gemini-multimodal-chat-assistant". Vì chúng ta có Dockerfile trong thư mục làm việc của ứng dụng, nên nó sẽ tạo vùng chứa Docker và đẩy vùng chứa đó vào Artifact Registry. Thao tác này cũng sẽ nhắc bạn rằng nó sẽ tạo kho lưu trữ Artifact Registry trong khu vực, hãy trả lời "Y" cho lời nhắc này. Ngoài ra, hãy nói "y" khi được hỏi liệu bạn có muốn cho phép các lệnh gọi chưa được xác thực hay không. Xin lưu ý rằng chúng tôi đang cho phép truy cập chưa xác thực tại đây vì đây là một ứng dụng minh hoạ. Bạn nên sử dụng phương thức xác thực phù hợp cho các ứng dụng doanh nghiệp và ứng dụng phát hành công khai.

Sau khi quá trình triển khai hoàn tất, bạn sẽ nhận được một đường liên kết tương tự như đường liên kết bên dưới:

https://gemini-multimodal-chat-assistant-*******.us-central1.run.app

Hãy tiếp tục sử dụng ứng dụng của bạn trong cửa sổ Ẩn danh hoặc trên thiết bị di động. Nội dung đó đã được xuất bản.

7. Thách thức

Giờ là lúc bạn thể hiện và trau dồi kỹ năng khám phá của mình. Bạn có đủ khả năng để thay đổi mã sao cho trợ lý có thể hỗ trợ đọc tệp âm thanh hoặc có thể là tệp video không?

8. Dọn dẹp

Để tránh phát sinh phí cho tài khoản Google Cloud của bạn đối với các tài nguyên được dùng trong lớp học lập trình này, hãy làm theo các bước sau:

  1. Trong bảng điều khiển Google Cloud, hãy chuyển đến trang Quản lý tài nguyên.
  2. Trong danh sách dự án, hãy chọn dự án mà bạn muốn xoá, rồi nhấp vào Xoá.
  3. Trong hộp thoại, hãy nhập mã dự án rồi nhấp vào Tắt để xoá dự án.
  4. Ngoài ra, bạn có thể chuyển đến Cloud Run trên bảng điều khiển, chọn dịch vụ bạn vừa triển khai rồi xoá.