Mem-build dan Men-deploy Asisten Multimodal di Cloud dengan Gemini (Python)

Tentang codelab ini
schedule0 menit
subjectTerakhir diperbarui 29 Maret 2025
account_circleDitulis oleh Alvin Prayuda Juniarta Dwiyantoro

Dalam codelab ini, Anda akan mem-build aplikasi dalam bentuk antarmuka web chat, tempat Anda dapat berkomunikasi dengan aplikasi, mengupload beberapa dokumen atau gambar, dan mendiskusikannya. Aplikasi itu sendiri dibagi menjadi 2 layanan: frontend dan backend; sehingga Anda dapat membuat prototipe cepat dan mencoba rasanya, serta memahami tampilan kontrak API untuk mengintegrasikan keduanya.

Melalui codelab ini, Anda akan menggunakan pendekatan langkah demi langkah sebagai berikut:

  1. Siapkan project Google Cloud Anda dan Aktifkan semua API yang diperlukan di dalamnya
  2. Mem-build layanan frontend - antarmuka chat menggunakan library Gradio
  3. Buat layanan backend - server HTTP menggunakan FastAPI yang akan memformat ulang data yang masuk ke standar Gemini SDK dan mengaktifkan komunikasi dengan Gemini API
  4. Mengelola variabel lingkungan dan menyiapkan file yang diperlukan untuk men-deploy aplikasi ke Cloud Run
  5. Men-deploy aplikasi ke Cloud Run

5bcfa1cce6618305.png

Ringkasan Arsitektur

b102df2c3f1adabf.jpeg

Prasyarat

Yang akan Anda pelajari

  • Cara menggunakan Gemini SDK untuk mengirimkan teks dan jenis data lainnya (multimodal) serta membuat respons teks
  • Cara menyusun histori chat ke dalam Gemini SDK untuk mempertahankan konteks percakapan
  • Membuat prototipe web frontend dengan Gradio
  • Pengembangan layanan backend dengan FastAPI dan Pydantic
  • Mengelola variabel lingkungan dalam file YAML dengan Pydantic-settings
  • Men-deploy aplikasi ke Cloud Run menggunakan Dockerfile dan memberikan variabel lingkungan dengan file YAML

Yang Anda butuhkan

  • Browser web Chrome
  • Akun Gmail
  • Project Cloud dengan penagihan diaktifkan

Codelab ini, yang dirancang untuk developer dari semua level (termasuk pemula), menggunakan Python dalam aplikasi contohnya. Namun, pengetahuan Python tidak diperlukan untuk memahami konsep yang disajikan.

2. Sebelum memulai

Menyiapkan Project Cloud di Editor Cloud Shell

Codelab ini mengasumsikan bahwa Anda sudah memiliki project Google Cloud dengan penagihan yang diaktifkan. Jika belum memilikinya, Anda dapat mengikuti petunjuk di bawah untuk memulai.

  1. 2Di Konsol Google Cloud, di halaman pemilih project, pilih atau buat project Google Cloud.
  2. Pastikan penagihan diaktifkan untuk project Cloud Anda. Pelajari cara memeriksa apakah penagihan telah diaktifkan pada suatu project .
  3. Anda akan menggunakan Cloud Shell, lingkungan command line yang berjalan di Google Cloud yang telah dilengkapi dengan bq. Klik Aktifkan Cloud Shell di bagian atas konsol Google Cloud.

1829c3759227c19b.png

  1. Setelah terhubung ke Cloud Shell, Anda akan memeriksa bahwa Anda sudah diautentikasi dan project ditetapkan ke project ID Anda menggunakan perintah berikut:
gcloud auth list
  1. Jalankan perintah berikut di Cloud Shell untuk mengonfirmasi bahwa perintah gcloud mengetahui project Anda.
gcloud config list project
  1. Jika project Anda belum ditetapkan, gunakan perintah berikut untuk menetapkannya:
gcloud config set project <YOUR_PROJECT_ID>

Atau, Anda juga dapat melihat ID PROJECT_ID di konsol

4032c45803813f30.jpeg

Klik project ID dan Anda akan melihat semua project dan project ID di sisi kanan

8dc17eb4271de6b5.jpeg

  1. Aktifkan API yang diperlukan melalui perintah yang ditampilkan di bawah. Tindakan ini mungkin memerlukan waktu beberapa menit, jadi harap bersabar.
gcloud services enable aiplatform.googleapis.com \
                           run.googleapis.com \
                           cloudbuild.googleapis.com \
                           cloudresourcemanager.googleapis.com

Setelah perintah berhasil dieksekusi, Anda akan melihat pesan yang mirip dengan yang ditampilkan di bawah ini:

Operation "operations/..." finished successfully.

Alternatif untuk perintah gcloud adalah melalui konsol dengan menelusuri setiap produk atau menggunakan link ini.

Jika ada API yang terlewat, Anda dapat mengaktifkannya kapan saja selama proses penerapan.

Baca dokumentasi untuk mempelajari perintah gcloud dan penggunaannya.

Menyiapkan Direktori Kerja Aplikasi

  1. Klik tombol Open Editor, tindakan ini akan membuka Cloud Shell Editor, kita dapat menulis kode di sini b16d56e4979ec951.png
  2. Pastikan project Cloud Code ditetapkan di pojok kiri bawah (status bar) editor Cloud Shell, seperti yang ditandai pada gambar di bawah dan ditetapkan ke project Google Cloud aktif tempat Anda mengaktifkan penagihan. Authorize jika diminta. Mungkin perlu waktu beberapa saat setelah menginisialisasi Editor Cloud Shell agar tombol Cloud Code - Sign In muncul. Harap bersabar. Jika Anda sudah mengikuti perintah sebelumnya, tombol tersebut juga dapat mengarah langsung ke project yang diaktifkan, bukan tombol login

f5003b9c38b43262.png

  1. Klik project aktif tersebut di status bar dan tunggu pop-up Cloud Code terbuka. Di pop-up, pilih "Aplikasi Baru".

70f80078e01a02d8.png

  1. Dari daftar aplikasi, pilih Gemini Generative AI, lalu pilih Gemini API Python

362ff332256d6933.jpeg

85957565316308d9.jpeg

  1. Simpan aplikasi baru dengan nama yang Anda sukai, dalam contoh ini kita akan menggunakan gemini-multimodal-chat-assistant , lalu klik OK

8409d8db18690fdf.png

Pada tahap ini, Anda seharusnya sudah berada di dalam direktori kerja aplikasi baru dan melihat file berikut

1ef5bb44f1d2c2a4.png

Selanjutnya, kita akan menyiapkan lingkungan python

Penyiapan Lingkungan

Menyiapkan Lingkungan Virtual Python

Langkah berikutnya adalah menyiapkan lingkungan pengembangan. Kita akan menggunakan Python 3.12 dalam codelab ini dan kita akan menggunakan pengelola project uv python untuk menyederhanakan kebutuhan pembuatan dan pengelolaan versi python serta lingkungan virtual

  1. Jika Anda belum membuka terminal, buka dengan mengklik Terminal -> New Terminal , atau gunakan Ctrl + Shift + C

f8457daf0bed059e.jpeg

  1. Download uv dan instal python 3.12 dengan perintah berikut
curl -LsSf https://astral.sh/uv/0.6.6/install.sh | sh && \
source $HOME/.local/bin/env && \
uv python install 3.12
  1. Sekarang, mari kita lakukan inisialisasi project Python menggunakan uv
uv init
  1. Anda akan melihat main.py, .python-version, dan pyproject.toml yang dibuat di direktori. File ini diperlukan untuk mengelola project di direktori. Konfigurasi dan dependensi Python dapat ditentukan di pyproject.toml dan .python-version yang menstandarkan versi Python yang digunakan untuk project ini. Untuk membaca lebih lanjut, Anda dapat melihat dokumentasi ini
main.py
.python-version
pyproject.toml
  1. Untuk mengujinya, ganti main.py dengan kode berikut
def main():
   print("Hello from gemini-multimodal-chat-assistant!")

if __name__ == "__main__":
   main()
  1. Kemudian, jalankan perintah berikut
uv run main.py

Anda akan mendapatkan output seperti yang ditunjukkan di bawah ini

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

Ini menunjukkan bahwa project python disiapkan dengan benar. Kita tidak perlu membuat lingkungan virtual secara manual karena uv sudah menanganinya. Jadi, mulai saat ini, perintah python standar (Misalnya, python main.py ) akan diganti dengan uv run (Misalnya, uv run main.py ).

Menginstal Dependensi yang Diperlukan

Kita juga akan menambahkan dependensi paket codelab ini menggunakan perintah uv. Jalankan perintah berikut

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

Anda akan melihat bahwa bagian "dependencies" pyproject.toml akan diperbarui untuk mencerminkan perintah sebelumnya

Menyiapkan File Konfigurasi

Sekarang kita harus menyiapkan file konfigurasi untuk project ini. File konfigurasi digunakan untuk menyimpan variabel dinamis yang dapat dengan mudah diubah saat deployment ulang. Dalam project ini, kita akan menggunakan file konfigurasi berbasis YAML dengan paket pydantic-settings, sehingga dapat diintegrasikan dengan mudah dengan deployment Cloud Run nanti. pydantic-settings adalah paket python yang dapat menerapkan pemeriksaan jenis untuk file konfigurasi.

  1. Buat file bernama settings.yaml dengan konfigurasi berikut. Klik File->New Text File dan isi dengan kode berikut. Kemudian, simpan sebagai settings.yaml
VERTEXAI_LOCATION: "us-central1"
VERTEXAI_PROJECT_ID: "{YOUR-PROJECT-ID}"
BACKEND_URL: "http://localhost:8081/chat"

Perbarui nilai untuk VERTEXAI_PROJECT_ID sesuai dengan yang telah Anda pilih saat membuat Project Google Cloud. Untuk codelab ini, kita akan menggunakan nilai yang telah dikonfigurasi sebelumnya untuk VERTEXAI_LOCATION dan BACKEND_URL .

  1. Kemudian, buat file python settings.py, modul ini akan berfungsi sebagai entri terprogram untuk nilai konfigurasi dalam file konfigurasi kita. Klik File->New Text File dan isi dengan kode berikut. Kemudian, simpan sebagai settings.py. Anda dapat melihat dalam kode bahwa kita secara eksplisit menetapkan file bernama settings.yaml adalah file yang akan dibaca
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()

Konfigurasi ini memungkinkan kita memperbarui runtime secara fleksibel. Pada deployment awal, kita akan mengandalkan konfigurasi settings.yaml sehingga kita memiliki konfigurasi default pertama. Setelah itu, kita dapat memperbarui variabel lingkungan secara fleksibel melalui konsol dan men-deploy ulang karena kita menempatkan variabel lingkungan dalam prioritas yang lebih tinggi dibandingkan dengan konfigurasi YAML default

Sekarang kita dapat melanjutkan ke langkah berikutnya, yaitu mem-build layanan

3. Mem-build Layanan Frontend menggunakan Gradio

Kita akan membuat antarmuka web chat yang terlihat seperti ini

5bcfa1cce6618305.png

Halaman ini berisi kolom input bagi pengguna untuk mengirim teks dan mengupload file. Selain itu, pengguna juga dapat menimpa petunjuk sistem yang akan dikirim ke Gemini API di kolom input tambahan

Kita akan mem-build layanan frontend menggunakan Gradio. Ganti nama main.py menjadi frontend.py dan ganti kode menggunakan kode berikut

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,
    )

Setelah itu, kita dapat mencoba menjalankan layanan frontend dengan perintah berikut. Jangan lupa untuk mengganti nama file main.py menjadi frontend.py

uv run frontend.py

Anda akan melihat output yang mirip dengan ini di konsol cloud

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

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

Setelah itu, Anda dapat memeriksa antarmuka web saat ctrl+mengklik link URL lokal. Atau, Anda juga dapat mengakses aplikasi frontend dengan mengklik tombol Web Preview di sisi kanan atas Cloud Editor, lalu memilih Preview on port 8080

49cbdfdf77964065.jpeg

Anda akan melihat antarmuka web, tetapi akan mendapatkan error yang diharapkan saat mencoba mengirim chat karena layanan backend belum disiapkan

bd0464140308cfbe.png

Sekarang, biarkan layanan berjalan dan jangan hentikan dulu. Sementara itu, kita dapat membahas komponen kode yang penting di sini

Penjelasan Kode

Kode untuk mengirim data dari antarmuka web ke backend ada di bagian ini

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
    ... 

Saat ingin mengirim data multimodal ke Gemini, dan membuat data dapat diakses di antara layanan, salah satu mekanisme yang dapat kita lakukan adalah mengonversi data menjadi jenis data base64 seperti yang dideklarasikan dalam kode. Kita juga perlu mendeklarasikan jenis MIME data. Namun, Gemini API tidak dapat mendukung semua jenis MIME yang ada. Oleh karena itu, penting untuk mengetahui jenis MIME yang didukung oleh Gemini yang dapat dibaca di dokumentasi ini. Anda dapat menemukan informasi di setiap kemampuan Gemini API (misalnya, Vision)

Selain itu, di antarmuka chat, Anda juga harus mengirim histori chat sebagai konteks tambahan untuk memberi Gemini "kenangan" percakapan. Jadi, di antarmuka web ini, kita juga mengirim histori chat yang dikelola per sesi web oleh Gradio dan mengirimnya bersama dengan input pesan dari pengguna. Selain itu, kami juga memungkinkan pengguna mengubah petunjuk sistem dan mengirimkannya juga

4. Mem-build Layanan Backend menggunakan FastAPI

Selanjutnya, kita harus mem-build backend yang dapat menangani payload yang telah dibahas sebelumnya, pesan pengguna terakhir, histori chat, dan petunjuk sistem. Kita akan menggunakan FastAPI untuk membuat layanan backend HTTP.

Buat file baru, Klik File->New Text File, lalu salin dan tempel kode berikut, lalu simpan sebagai 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)

Jangan lupa untuk menyimpannya sebagai backend.py . Setelah itu, kita dapat mencoba menjalankan layanan backend. Ingat bahwa pada langkah sebelumnya kita menjalankan layanan frontend dengan benar, sekarang kita harus membuka terminal baru dan mencoba menjalankan layanan backend ini

  1. Buat terminal baru. Buka terminal di area bawah dan temukan tombol "+" untuk membuat terminal baru. Atau, Anda dapat menekan Ctrl + Shift + C untuk membuka terminal baru

3e52a362475553dc.jpeg

  1. Setelah itu, pastikan Anda berada di direktori kerja gemini-multimodal-chat-assistant, lalu jalankan perintah berikut
uv run backend.py
  1. Jika berhasil, output akan ditampilkan seperti ini
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)

Penjelasan Kode

Menentukan Rute HTTP untuk Menerima Permintaan Chat

Di FastAPI, kita menentukan rute menggunakan dekorator app. Kita juga menggunakan Pydantic untuk menentukan kontrak API. Kita menentukan bahwa rute untuk menghasilkan respons berada di rute /chat dengan metode POST. Fungsi ini dideklarasikan dalam kode berikut

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
    ...

Menyiapkan Format Histori Chat Gemini SDK

Salah satu hal penting yang perlu dipahami adalah cara kita dapat menyusun ulang histori chat sehingga dapat disisipkan sebagai nilai argumen history saat kita melakukan inisialisasi klien Gemini nanti. Anda dapat memeriksa kode di bawah ini

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

Untuk menyediakan histori chat ke Gemini SDK, kita perlu memformat data dalam jenis data List[Content]. Setiap Konten harus memiliki minimal nilai peran dan bagian. Peran mengacu pada sumber pesan, baik pengguna maupun model. Di mana bagian mengacu pada perintah itu sendiri, yang dapat berupa teks saja, atau kombinasi dari berbagai modalitas. Lihat cara menyusun argumen Konten secara mendetail di dokumentasi ini

Menangani Data Non-teks ( Multimodal)

Seperti yang disebutkan sebelumnya di bagian frontend, salah satu cara untuk mengirim data non-teks atau multimodal adalah dengan mengirim data sebagai string base64. Kita juga perlu menentukan jenis MIME untuk data agar dapat ditafsirkan dengan benar, misalnya memberikan jenis MIME image/jpeg jika kita mengirim data gambar dengan akhiran .jpg.

Bagian kode ini mengonversi data base64 menjadi format Part.from_bytes dari 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. Pengujian Integrasi

Sekarang, Anda akan memiliki beberapa layanan yang berjalan di tab konsol cloud yang berbeda:

  • Layanan frontend berjalan di port 8080
* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.
  • Layanan backend berjalan di port 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)

Pada status saat ini, Anda seharusnya dapat mengirim dokumen di chat dengan lancar bersama asisten dari aplikasi web di port 8080. Anda dapat mulai bereksperimen dengan mengupload file dan mengajukan pertanyaan. Perhatikan bahwa jenis file tertentu belum didukung dan akan menampilkan Error.

Anda juga dapat mengedit petunjuk sistem dari kolom Input Tambahan di bawah kotak teks

ee9c849a276d378.png

6. Men-deploy ke Cloud Run

Sekarang, tentu saja kita ingin menampilkan aplikasi yang luar biasa ini kepada orang lain. Untuk melakukannya, kita dapat memaketkan aplikasi ini dan men-deploy-nya ke Cloud Run sebagai layanan publik yang dapat diakses oleh orang lain. Untuk melakukannya, mari kita bahas kembali arsitektur

b102df2c3f1adabf.jpeg

Dalam codelab ini, kita akan menempatkan layanan frontend dan backend dalam 1 penampung. Kita memerlukan bantuan supervisord untuk mengelola kedua layanan tersebut.

Buat file baru, Klik File->New Text File, lalu salin dan tempel kode berikut, lalu simpan sebagai 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

Selanjutnya, kita memerlukan Dockerfile. Klik File->New Text File, lalu salin dan tempel kode berikut,lalu simpan sebagai 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"]

Pada tahap ini, kita sudah memiliki semua file yang diperlukan untuk men-deploy aplikasi ke Cloud Run. Mari kita deploy. Buka Terminal Cloud Shell dan pastikan project saat ini dikonfigurasi ke project aktif Anda. Jika tidak, Anda harus menggunakan perintah gcloud configure untuk menetapkan project ID:

gcloud config set project [PROJECT_ID]

Kemudian, jalankan perintah berikut untuk men-deploynya ke Cloud Run.

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

Anda akan diminta untuk memasukkan nama layanan, misalnya "gemini-multimodal-chat-assistant". Karena kita memiliki Dockerfile di direktori kerja aplikasi, Dockerfile akan mem-build container Docker dan mendorongnya ke Artifact Registry. Tindakan ini juga akan meminta Anda untuk membuat repositori Artifact Registry di region tersebut. Jawab "Y" untuk hal ini. Selain itu, ucapkan "y" saat diminta apakah Anda ingin mengizinkan pemanggilan yang tidak diautentikasi. Perhatikan bahwa kami mengizinkan akses yang tidak diautentikasi di sini karena ini adalah aplikasi demo. Rekomendasinya adalah menggunakan autentikasi yang sesuai untuk aplikasi perusahaan dan produksi Anda.

Setelah deployment selesai, Anda akan mendapatkan link yang mirip dengan link di bawah ini:

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

Gunakan aplikasi Anda dari jendela Samaran atau perangkat seluler. Halaman tersebut seharusnya sudah aktif.

7. Tantangan

Sekarang saatnya Anda bersinar dan mengasah keterampilan eksplorasi Anda. Apakah Anda memiliki kemampuan untuk mengubah kode agar asisten dapat mendukung pembacaan file audio atau mungkin file video?

8. Pembersihan

Agar tidak menimbulkan biaya pada akun Google Cloud Anda untuk resource yang digunakan dalam codelab ini, ikuti langkah-langkah berikut:

  1. Di konsol Google Cloud, buka halaman Manage resources.
  2. Dalam daftar project, pilih project yang ingin Anda hapus, lalu klik Delete.
  3. Pada dialog, ketik project ID, lalu klik Shut down untuk menghapus project.
  4. Atau, Anda dapat membuka Cloud Run di konsol, memilih layanan yang baru saja di-deploy, lalu menghapusnya.