Membangun Aplikasi Chat AI Hybrid di GKE dengan Gemma dan Gemini

1. Pengantar

Ringkasan

Di lab ini, Anda akan membangun dan men-deploy aplikasi chat berteknologi AI full-stack di Google Kubernetes Engine (GKE). Aplikasi "hibrida" ini menunjukkan pola arsitektur yang canggih: kemampuan untuk beralih dengan lancar antara model terbuka yang dihosting sendiri (Gemma 3 12B) yang berjalan langsung di cluster Anda dan layanan AI terkelola (Gemini 2.5 Flash melalui Vertex AI).

                                   +----------------------+
                                   |   User (Web Browser) |
                                   +-----------+----------+
                                               |
                                               v
+----------------------------------------------+---------------------------------------------+
| Google Cloud Platform                        |                                             |
|   |                                  +-------+-------+                                     |
|   |                                  | Load Balancer |                                     |
|   |                                  +-------+-------+                                     |
|   |                                          v                                             |
|   +------------------------------------------+-----------------------------------------+   |
|   |  Google Kubernetes Engine (GKE)          |                                         |   |
|   |                                          v                                         |   |
|   |                              +-----------+-----------+                             |   |
|   |                              |    Gradio Chat App    |                             |   |
|   |                              +--+-----------------+--+                             |   |
|   |                                 |                 |                                |   |
|   |                   (Self-hosted) |                 | (Managed via SDK)              |   |
|   |                                 v                 |                                |   |
|   |                  +--------------+---+             |                                |   |
|   |                  | Gemma 3 Model    |             |                                |   |
|   |                  | (GPU Node)       |             |                                |   |
|   |                  +------------------+             |                                |   |
|   +---------------------------------------------------|--------------------------------+   |
|                                                       |                                    |
|                                                       v                                    |
|                                            +----------+-----------+                        |
|                                            | Vertex AI (Gemini)   |                        |
|                                            +----------------------+                        |
|                                                       | (Save History)                     |
|                                                       v                                    |
|                                            +----------+-----------+                        |
|                                            | Firestore Database   |                        |
|                                            +----------------------+                        |
+--------------------------------------------------------------------------------------------+

Anda akan menggunakan Terraform untuk menyediakan infrastruktur, termasuk cluster Autopilot GKE dan database Firestore untuk menyimpan histori sesi chat. Kemudian, Anda akan menyelesaikan kode aplikasi Python untuk menangani percakapan multi-turn, berinteraksi dengan kedua model AI, dan men-deploy aplikasi akhir menggunakan Cloud Build dan Skaffold.

Yang akan Anda pelajari

  • Menyediakan infrastruktur GKE dan Firestore menggunakan Terraform.
  • Men-deploy model bahasa besar (Gemma) di GKE Autopilot menggunakan manifes Kubernetes.
  • Terapkan antarmuka chat Gradio di Python yang dapat beralih di antara berbagai backend AI.
  • Gunakan Firestore untuk menyimpan dan mengambil histori sesi chat.
  • Mengonfigurasi Workload Identity untuk memberikan akses yang aman bagi workload GKE Anda ke layanan Google Cloud (Vertex AI, Firestore).

Prasyarat

  • Project Google Cloud yang mengaktifkan penagihan.
  • Pemahaman dasar tentang Python, Kubernetes, dan alat command line standar.
  • Token Hugging Face dengan akses ke model Gemma.

2. Penyiapan Project

  1. Jika belum memiliki Akun Google, Anda harus membuat Akun Google.
    • Gunakan akun pribadi, bukan akun kantor atau sekolah. Akun kantor dan sekolah mungkin memiliki batasan yang mencegah Anda mengaktifkan API yang diperlukan untuk lab ini.
  2. Login ke Konsol Google Cloud.
  3. Aktifkan penagihan di Konsol Cloud.
    • Menyelesaikan lab ini akan dikenai biaya kurang dari $1 USD untuk resource Cloud.
    • Anda dapat mengikuti langkah-langkah di akhir lab ini untuk menghapus resource agar tidak dikenai biaya lebih lanjut.
    • Pengguna baru memenuhi syarat untuk mengikuti Uji Coba Gratis senilai$300 USD.
  4. Buat project baru atau pilih untuk menggunakan kembali project yang ada.

Buka Cloud Shell Editor

  1. Klik link ini untuk langsung membuka Cloud Shell Editor
  2. Jika diminta untuk memberikan otorisasi kapan saja hari ini, klik Authorize untuk melanjutkan. Klik untuk memberikan otorisasi pada Cloud Shell
  3. Jika terminal tidak muncul di bagian bawah layar, buka terminal:
    • Klik Lihat
    • Klik TerminalMembuka terminal baru di Cloud Shell Editor
  4. Di terminal, tetapkan project Anda dengan perintah ini:
    • Format:
      gcloud config set project [PROJECT_ID]
      
    • Contoh:
      gcloud config set project lab-project-id-example
      
    • Jika Anda tidak ingat project ID Anda:
      • Anda dapat mencantumkan semua project ID Anda dengan:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Menetapkan project ID di terminal Cloud Shell Editor
  5. Anda akan melihat pesan ini:
    Updated property [core/project].
    
    Jika Anda melihat WARNING dan diminta Do you want to continue (Y/n)?, berarti Anda kemungkinan telah memasukkan ID project dengan salah. Tekan n, tekan Enter, lalu coba jalankan perintah gcloud config set project lagi.

Membuat Clone Repositori

Di terminal Cloud Shell, clone repositori project dan buka direktori project:

git clone https://github.com/GoogleCloudPlatform/devrel-demos.git
cd devrel-demos/containers/gradio-chat-gke

Luangkan waktu sejenak untuk mempelajari struktur project:

gradio-chat-gke/
├── app/
   ├── app.py                # Main application logic (you will edit this)
   ├── requirements.txt      # Python dependencies
   └── themes.py             # UI theming
├── deploy/
   ├── chat-deploy.yaml      # Kubernetes deployment for the chat app
   ├── Dockerfile            # Container definition for the chat app
   └── gemma3-12b-deploy.yaml# Kubernetes deployment for Gemma model
├── infra/
   └── main.tf               # Terraform infrastructure definition
└── skaffold.yaml             # Skaffold configuration for building/deploying

Menetapkan Variabel Lingkungan

Siapkan variabel lingkungan untuk project ID dan nomor project Anda. Variabel ini akan digunakan oleh Terraform dan perintah berikutnya.

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format="value(projectNumber)")
export REGION=us-central1

Aktifkan Cloud Resource Manager API

Terraform memerlukan Cloud Resource Manager API diaktifkan untuk mengelola resource project Anda, jadi kita perlu mengaktifkannya terlebih dahulu. Selanjutnya, kita akan men-deploy aplikasi chat dengan Skaffold, yang menggunakan Cloud Build untuk membangun image container kita. Sekarang kita akan mengaktifkan Storage API dan membuat bucket yang diperlukan untuk Cloud Build. Kita akan menggunakan Terraform itu sendiri untuk mengaktifkan API lainnya yang diperlukan untuk project ini.

gcloud services enable cloudresourcemanager.googleapis.com storage-api.googleapis.com

Membuat Bucket Staging Cloud Build

Skaffold menggunakan Google Cloud Build, yang memerlukan bucket Cloud Storage untuk menyiapkan kode sumber Anda.

Buat sekarang untuk memastikan keberadaannya:

gcloud storage buckets create gs://${GOOGLE_CLOUD_PROJECT}_cloudbuild

(Jika Anda mendapatkan error yang menyatakan bahwa bucket sudah ada, Anda dapat mengabaikannya dengan aman).

3. Menyediakan Infrastruktur dengan Terraform

Kita akan menggunakan Terraform untuk menyiapkan resource Google Cloud yang diperlukan. Hal ini memastikan lingkungan yang dapat direproduksi dan konsisten.

  1. Buka direktori infrastruktur:
    cd infra
    

File ini menentukan API tambahan yang akan kita perlukan untuk project ini: cloudbuild, artifactregistry, container (gke), firestore, dan aiplatform (vertexai). Lihat di file atau di bawah untuk melihat cara API diaktifkan melalui Terraform:

resource "google_project_service" "cloudbuild" {
  service            = "cloudbuild.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "artifactregistry" {
  service            = "artifactregistry.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "container" {
  service            = "container.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "firestore" {
  service            = "firestore.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "vertexai" {
  service            = "aiplatform.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

Tentukan Cluster GKE

Buka infra/main.tf di editor Anda. Anda akan melihat beberapa komentar # TODO. Anda dapat membukanya secara manual atau menggunakan perintah ini untuk membuka file di editor:

cloudshell edit main.tf

Pertama, kita perlu menentukan cluster Kubernetes. Kita akan menggunakan GKE Autopilot, yang ideal untuk workload AI karena menangani pengelolaan node secara otomatis.

Temukan # TODO: Create a GKE Autopilot Cluster dan tambahkan blok berikut di bawahnya:

# Create a GKE Autopilot Cluster
resource "google_container_cluster" "primary" {
  name     = var.cluster_name
  location = var.region
  project  = var.project_id

  # Enable Autopilot mode
  enable_autopilot = true

  deletion_protection = false

  # Networking
  network    = "default"
  subnetwork = "projects/${var.project_id}/regions/${var.region}/subnetworks/default"

  # Timeout for cluster creation
  timeouts {
    create = "30m"
    update = "30m"
  }

  depends_on = [google_project_service.container]
}

Perhatikan enable_autopilot = true. Satu baris ini menyelamatkan kita dari pengelolaan node pool, penskalaan otomatis, dan pengemasan bin workload GPU.

Tentukan Database Firestore

Selanjutnya, kita memerlukan tempat untuk menyimpan histori chat. Firestore adalah database NoSQL serverless yang sangat sesuai dengan kebutuhan ini.

Temukan # TODO: Create a Firestore Database dan tambahkan:

resource "google_firestore_database" "database" {
  project     = var.project_id
  name        = "chat-app-db"
  location_id = "nam5"
  type        = "FIRESTORE_NATIVE"

  depends_on = [google_project_service.firestore]
}

Setelah menambahkan resource database, temukan # TODO: Create an initial Firestore Document dan tambahkan blok berikut. Resource ini membuat dokumen placeholder awal dalam koleksi kita, yang berguna untuk menginisialisasi struktur database.

resource "google_firestore_document" "initial_document" {
  project     = var.project_id
  collection  = "chat_sessions"
  document_id = "initialize"
  fields = <<EOF
  EOF

  depends_on = [google_firestore_database.database]
}

Tentukan Workload Identity

Terakhir, kita perlu mengonfigurasi keamanan. Kita ingin pod Kubernetes kita dapat mengakses Vertex AI dan Firestore tanpa kita harus mengelola kunci API atau rahasia apa pun. Kita melakukannya dengan Workload Identity.

Kita akan memberikan peran IAM yang diperlukan ke Akun Layanan Kubernetes (KSA) yang akan digunakan aplikasi kita.

Catatan: Akun Layanan Kubernetes (gradio-chat-ksa) yang dirujuk dalam binding ini belum ada. Objek ini akan dibuat nanti saat kita men-deploy aplikasi ke cluster. Tidak masalah (dan merupakan praktik umum) untuk melakukan pra-penyediaan binding IAM ini.

Temukan # TODO: Configure Workload Identity IAM bindings dan tambahkan:

locals {
  ksa_principal = "principal://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${var.project_id}.svc.id.goog/subject/ns/default/sa/gradio-chat-ksa"
}

resource "google_project_iam_member" "ksa_token_creator" {
  project = var.project_id
  role    = "roles/iam.serviceAccountTokenCreator"
  member  = local.ksa_principal
}

resource "google_project_iam_member" "ksa_vertex_user" {
  project = var.project_id
  role    = "roles/aiplatform.user"
  member  = local.ksa_principal
}

resource "google_project_iam_member" "ksa_datastore_user" {
  project = var.project_id
  role    = "roles/datastore.user"
  member  = local.ksa_principal
}

Terapkan Konfigurasi

Setelah infrastruktur kita ditentukan, mari kita sediakan.

  1. Pertama-tama, kita perlu menetapkan beberapa variabel yang akan digunakan Terraform. Kita akan melakukannya menggunakan variabel lingkungan:
export TF_VAR_project_id=$(gcloud config get-value project)
export TF_VAR_project_number=$(gcloud projects describe $TF_VAR_project_id --format="value(projectNumber)")
export TF_VAR_region="us-central1"
  1. Lakukan inisialisasi Terraform:
terraform init
  1. Gunakan terraform plan untuk melihat pratinjau resource yang akan dibuat.
terraform plan
  1. Terapkan konfigurasi. Jika diminta, ketik yes untuk mengonfirmasi.
terraform apply

Catatan: Penyediaan cluster GKE dapat memerlukan waktu 10-15 menit. Sambil menunggu, Anda dapat melanjutkan untuk meninjau kode aplikasi di bagian berikutnya.

  1. Setelah selesai, konfigurasi kubectl untuk berkomunikasi dengan cluster baru Anda:
gcloud container clusters get-credentials gradio-chat-cluster --region us-central1 --project $TF_VAR_project_id

4. Men-deploy Gemma yang Dihosting Sendiri di GKE

Selanjutnya, kita akan men-deploy model Gemma 3 12B langsung ke cluster GKE Anda. Hal ini memungkinkan inferensi latensi rendah dan kontrol penuh atas lingkungan eksekusi model.

Mengonfigurasi Kredensial Hugging Face

Untuk mendownload model Gemma, cluster Anda memerlukan autentikasi dengan Hugging Face.

  1. Pastikan Anda memiliki token Hugging Face.
  2. Buat Secret Kubernetes dengan token Anda- Ganti [YOUR_HF_TOKEN] dengan token Anda yang sebenarnya:
    kubectl create secret generic hf-secret --from-literal=hf_api_token=[YOUR_HF_TOKEN]
    

Men-deploy Model

Kita akan menggunakan Deployment Kubernetes standar untuk menjalankan model. Manifes terletak di deploy/gemma3-12b-deploy.yaml. Anda dapat membukanya secara manual atau menggunakan perintah ini untuk membuka file di editor:

cd ../deploy
cloudshell edit gemma3-12b-deploy.yaml

Luangkan waktu sejenak untuk memeriksa file ini. Perhatikan bagian resources:

        resources:
          requests:
            nvidia.com/gpu: 4
      nodeSelector:
        cloud.google.com/gke-accelerator: nvidia-l4

Ini adalah infrastruktur AI deklaratif. Kita memberi tahu GKE Autopilot bahwa pod tertentu ini memerlukan 4 GPU NVIDIA L4. Autopilot akan menemukan atau menyediakan node yang memenuhi persyaratan ini secara persis. Jika node tidak tersedia untuk penyediaan, node akan terus mencoba hingga node yang memenuhi persyaratan tersedia.

  1. Terapkan manifes deployment:
    cd ..
    kubectl apply -f deploy/gemma3-12b-deploy.yaml
    
    Tindakan ini akan memulai proses mendownload bobot model dan memulai server inferensi. Umumnya, proses ini dapat memerlukan waktu beberapa menit. Deployment Gemma ini menggunakan GPU, yang dapat mengalami kekurangan ketersediaan. Jika GPU tidak tersedia, pod gemma akan tetap "tertunda" hingga tersedia, dan Konsol Google Cloud akan menampilkan error seperti "Cannot schedule pods: Preemption is not helpful for scheduling." dan/atau "Cannot schedule pods: node(s) didn't match Pod's node affinity/selector." Artinya, GKE belum dapat memperoleh GPU untuk Anda. Proses ini akan terus mencoba hingga dapat memperoleh GPU. Proses ini dapat memerlukan waktu beberapa menit, atau beberapa hari, bergantung pada ketersediaan GPU. Anda dapat memeriksa statusnya dengan:
    kubectl get pods
    
    Anda dapat melanjutkan untuk men-deploy aplikasi meskipun pod gemma belum aktif dan berjalan. Aplikasi chat akan terhubung ke layanan gemma setiap kali layanan tersebut tersedia. Perhatikan bahwa Anda tidak akan dapat berinteraksi dengan Gemma melalui aplikasi chat Anda hingga pod menampilkan status gemma dan Running.1/1 Namun, Anda dapat melakukan percakapan dengan Gemini untuk sementara.

5. Membangun Aplikasi Chat

Sekarang, mari kita selesaikan aplikasi Python. Buka app/app.py di Cloud Shell Editor. Anda akan menemukan beberapa blok # TODO yang perlu diisi agar aplikasi berfungsi.

cloudshell edit app/app.py

Langkah 1: Memproses Histori Percakapan

LLM memerlukan histori percakapan yang diformat secara khusus agar dapat memahami siapa yang mengatakan apa.

Pola "Penerjemah Universal": Perhatikan bahwa kita akan menulis dua fungsi berbeda untuk memproses histori chat yang sama. Ini adalah pola utama dalam aplikasi multi-model.

  • Sumber Tepercaya (Gradio): Aplikasi kita menyimpan histori dalam format sederhana dan generik: [[user_msg1, bot_msg1], ...].
  • Target 1 (Gemma): Perlu dikonversi menjadi satu string mentah dengan token khusus tertentu.
  • Target 2 (Gemini): Perlu dikonversi menjadi daftar objek API terstruktur.

Dengan memformat ulang histori umum ke dalam format target di setiap giliran, kita dapat beralih antar-model dengan lancar. Untuk menambahkan model lain nanti, Anda harus menulis fungsi pemrosesan baru untuk format spesifiknya.

Untuk Gemma (Dihosting Sendiri)

Memahami Template Chat: Saat menghosting model terbuka Anda sendiri, biasanya Anda perlu memformat perintah secara manual menjadi string tertentu yang dilatih model untuk dikenali sebagai percakapan. Hal ini dikenal sebagai "template chat".

Cari fungsi process_message_gemma di app.py dan ganti dengan kode berikut:

# This function takes a user's message and the conversation history as input.
#   Its job is to format these elements into a single,
#   structured prompt that can be understood by the language model (LLM).
#   This structured format helps the LLM maintain context and generate more relevant responses.
def process_message_gemma(message, history):
    user_prompt_format = "User's Turn:\n>>> {prompt}\n"
    assistant_prompt_format = "Assistant's Turn:\n>>> {prompt}\n"

    history_message = ""
    for user_turn, assistant_turn in history:
        history_message += user_prompt_format.format(prompt=user_turn)
        history_message += assistant_prompt_format.format(prompt=assistant_turn)

    # Format the new user message
    new_user_message = user_prompt_format.format(prompt=message)
    # Create a new aggregated message to be used as a single flat string in a json object sent to the LLM
    aggregated_message = (
        history_message + new_user_message + assistant_prompt_format.format(prompt="")
    )
    return aggregated_message

Untuk Gemini (Dikelola)

Layanan terkelola sering kali lebih memilih objek terstruktur daripada string mentah. Kita memerlukan fungsi terpisah untuk memformat histori menjadi objek types.Content untuk Gemini SDK.

Temukan process_message_gemini dan ganti dengan:

def process_message_gemini(message, history):
    contents = []
    for user_turn, model_turn in history:
        contents.append(
            types.Content(role="user", parts=[types.Part.from_text(text=user_turn)])
        )
        contents.append(
            types.Content(role="model", parts=[types.Part.from_text(text=model_turn)])
        )

    contents.append(
        types.Content(role="user", parts=[types.Part.from_text(text=message)])
    )
    return contents

Langkah 2: Panggil Model Gemma yang Dihosting Sendiri

Kita perlu mengirim perintah yang diformat ke layanan Gemma yang berjalan di cluster kita. Kita akan menggunakan permintaan POST HTTP standar ke nama DNS internal layanan.

Temukan fungsi call_gemma_model dan ganti dengan:

# Construct the request, send it to Gemma, return the model's response
# aggregated_message = current user message + history
def call_gemma_model(aggregated_message, model_temperature, top_p, max_tokens):
    json_message = {
        "prompt": aggregated_message,
        "temperature": model_temperature,
        "top_p": top_p,
        "max_tokens": max_tokens,
        "stop": ["User's Turn:"],
    }

    # Log what will be sent to the LLM
    print("*** JSON request: " + str(json_message))

    # Send the constructed json with the user prompt to the model and put the model's response in the json_data variable
    json_data = post_request(json_message)

    # The response from the model is a list of predictions. We'll take the first result.
    raw_output = json_data["predictions"][0]

    # The vLLM server returns the full prompt in the response. We need to extract
    # just the newly generated text from the model.
    assistant_turn_marker = "Assistant's Turn:\n>>>"
    marker_pos = raw_output.rfind(assistant_turn_marker)

    if marker_pos != -1:
        output = raw_output[marker_pos + len(assistant_turn_marker) :]
    else:
        output = raw_output

    # Clean up potential over-generation
    stop_marker = "User's Turn:"
    stop_pos = output.lower().find(stop_marker.lower())
    if stop_pos != -1:
        output = output[:stop_pos]

    return output.strip()

Langkah 3: Panggil Model Gemini Vertex AI

Untuk model terkelola, kita akan menggunakan Google GenAI SDK. Hal ini jauh lebih sederhana karena menangani panggilan jaringan untuk kita.

Temukan fungsi call_gemini_model dan ganti dengan:

# Send a request to Gemini via the VertexAI API. Return the model's response
# contents = list of types.Content objects
def call_gemini_model(contents, model_temperature, top_p, max_tokens):
    gemini_model = "gemini-2.5-flash"

    response = client.models.generate_content(
        model=gemini_model,
        contents=contents,
        config={
            "temperature": model_temperature,
            "max_output_tokens": max_tokens,
            "top_p": top_p,
        },
    )
    return response.text

Langkah 4: Terapkan Antarmuka Inferensi Utama

Terakhir, kita memerlukan fungsi orkestrator utama yang dipanggil Gradio. Fungsi ini harus:

  1. Lakukan inisialisasi histori jika kosong.
  2. Proses pesan.
  3. Mengarahkan permintaan ke model yang dipilih (Gemma atau Gemini).
  4. Simpan interaksi ke Firestore.
  5. Menampilkan respons ke UI.

Gradio dan Pengelolaan Status: ChatInterface Gradio secara otomatis menangani status tingkat sesi (menampilkan pesan di browser). Namun, tidak ada dukungan bawaan untuk database eksternal.

Untuk mempertahankan histori chat dalam jangka panjang, kami menggunakan pola standar: kami terhubung ke fungsi inference_interface. Dengan menerima request: gr.Request sebagai argumen, Gradio akan otomatis meneruskan detail sesi pengguna saat ini kepada kita. Kami menggunakan ID ini untuk membuat dokumen Firestore unik bagi setiap pengguna, sehingga percakapan tidak tercampur dalam lingkungan multi-pengguna.

Temukan fungsi inference_interface dan ganti dengan:

# This is the primary chat function. Every time a user sends a message, gradio calls this function,
# which sends the user's input to the appropriate AI (as indicated on the user interface), updates
# the chat history for future use during this session, and records the chat history in Firestore.
def inference_interface(
    message,
    history,
    model_name,
    model_temperature,
    top_p,
    max_tokens,
    request: gr.Request,
):

    # set history to empty array
    if history is None:
        history = []

    # Get or create session document
    session_hash = request.session_hash
    doc_id = f"session-{session_hash}"
    doc_ref = db.collection("chat_sessions").document(doc_id)

    # Create the session document if it doesn't exist
    if not doc_ref.get().exists:
        doc_ref.set({"Session start": datetime.datetime.now()})

    # Log info
    print("Model: " + model_name)
    print("LLM Engine: " + llm_engine)
    print("* History: " + str(history))

    # Pass the message and history to the appropriate model, as indicated by the user via the ui
    if model_name == "Gemma3 12b it":
        aggregated_message = process_message_gemma(message, history)
        output = call_gemma_model(
            aggregated_message, model_temperature, top_p, max_tokens
        )

    elif model_name == "Gemini":
        gemini_contents = process_message_gemini(message, history)
        output = call_gemini_model(
            gemini_contents, model_temperature, top_p, max_tokens
        )

    else:
        # Handle the case where no valid model is selected
        output = "Error: Invalid model selected."

    interaction = {"user": message, model_name: output}

    # Log the updated chat history
    print("* History: " + str(history) + " " + str(interaction))

    # Save the updated history to Firestore
    save_chat_history(interaction, doc_ref)

    return output

6. Periksa File app.py Anda

Pada tahap ini, aplikasi chat berbasis gradio Anda sudah siap di-deploy. Pastikan file tersebut sama persis dengan file lengkap berikut.

Pemecahan masalah: Jika Anda men-deploy aplikasi dan mendapatkan error "refused to connect" atau "This site can't be reached" saat mencoba terhubung ke aplikasi, coba ulangi langkah-langkah dari titik ini, dimulai dengan menyalin seluruh file ini dan menempelkannya ke app.py.

# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime

import google.auth
import google.cloud
import gradio as gr
import requests
import themes
from google import genai

from google.cloud import firestore
from google.genai import types

## Do one-time initialization things

## grab the project id from google auth
_, project = google.auth.default()
print(f"Project: {project}")

# Set initial values for model
llm_engine = "vllm"
host = "http://gemma-service:8000"
context_path = "/generate"

# initialize vertex for interacting with Gemini
client = genai.Client(
    vertexai=True,
    project=project,
    location="global",
)

# Initialize Firestore client
db = firestore.Client(database="chat-app-db")


# This is the primary chat function. Every time a user sends a message, gradio calls this function,
# which sends the user's input to the appropriate AI (as indicated on the user interface), updates
# the chat history for future use during this session, and records the chat history in Firestore.
def inference_interface(
    message,
    history,
    model_name,
    model_temperature,
    top_p,
    max_tokens,
    request: gr.Request,
):

    # set history to empty array
    if history is None:
        history = []

    # Get or create session document
    session_hash = request.session_hash
    doc_id = f"session-{session_hash}"
    doc_ref = db.collection("chat_sessions").document(doc_id)

    # Create the session document if it doesn't exist
    if not doc_ref.get().exists:
        doc_ref.set({"Session start": datetime.datetime.now()})

    # Log info
    print("Model: " + model_name)
    print("LLM Engine: " + llm_engine)
    print("* History: " + str(history))

    # Pass the message and history to the appropriate model, as indicated by the user via the ui
    if model_name == "Gemma3 12b it":
        aggregated_message = process_message_gemma(message, history)
        output = call_gemma_model(
            aggregated_message, model_temperature, top_p, max_tokens
        )

    elif model_name == "Gemini":
        gemini_contents = process_message_gemini(message, history)
        output = call_gemini_model(
            gemini_contents, model_temperature, top_p, max_tokens
        )

    else:
        # Handle the case where no valid model is selected
        output = "Error: Invalid model selected."

    interaction = {"user": message, model_name: output}

    # Log the updated chat history
    print("* History: " + str(history) + " " + str(interaction))

    # Save the updated history to Firestore
    save_chat_history(interaction, doc_ref)

    return output


# Construct the request, send it to Gemma, return the model's response
# aggregated_message = current user message + history
def call_gemma_model(aggregated_message, model_temperature, top_p, max_tokens):
    json_message = {
        "prompt": aggregated_message,
        "temperature": model_temperature,
        "top_p": top_p,
        "max_tokens": max_tokens,
        "stop": ["User's Turn:"],
    }

    # Log what will be sent to the LLM
    print("*** JSON request: " + str(json_message))  # Log the JSON request

    # Send the constructed json with the user prompt to the model and put the model's response in the json_data variable
    json_data = post_request(json_message)

    # The response from the model is a list of predictions.
    # We'll take the first result.
    raw_output = json_data["predictions"][0]

    # The vLLM server returns the full prompt in the response. We need to extract
    # just the newly generated text from the model. The prompt ends with
    # "Assistant's Turn:\n>>>", so we find the last occurrence of that and
    # take everything after it.
    assistant_turn_marker = "Assistant's Turn:\n>>>"
    marker_pos = raw_output.rfind(assistant_turn_marker)

    if marker_pos != -1:
        # Get the text generated by the assistant
        output = raw_output[marker_pos + len(assistant_turn_marker) :]
    else:
        # Fallback in case the marker isn't found
        output = raw_output

    # The model sometimes continues the conversation and includes the next user's turn.
    # The 'stop' parameter is a good hint, but we parse the output as a safeguard.
    stop_marker = "User's Turn:"
    stop_pos = output.lower().find(stop_marker.lower())
    if stop_pos != -1:
        output = output[:stop_pos]

    # The model also sometimes prefixes its response with "Output:". We'll remove this.
    output = output.lstrip()
    prefix_marker = "Output:"
    if output.lower().startswith(prefix_marker.lower()):
        output = output[len(prefix_marker) :]

    return output.strip()


# Send a request to Gemini via the VertexAI API. Return the model's response
# contents = list of types.Content objects
def call_gemini_model(contents, model_temperature, top_p, max_tokens):
    gemini_model = "gemini-2.5-flash"

    response = client.models.generate_content(
        model=gemini_model,
        contents=contents,
        config={
            "temperature": model_temperature,
            "max_output_tokens": max_tokens,
            "top_p": top_p,
        },
    )
    output = response.text  # Extract the generated text
    # Consider handling additional response attributes (safety, usage, etc.)
    return output


def process_message_gemini(message, history):
    contents = []
    for user_turn, model_turn in history:
        contents.append(
            types.Content(role="user", parts=[types.Part.from_text(text=user_turn)])
        )
        contents.append(
            types.Content(role="model", parts=[types.Part.from_text(text=model_turn)])
        )

    contents.append(
        types.Content(role="user", parts=[types.Part.from_text(text=message)])
    )
    return contents


# This function takes a user's message and the conversation history as input.
#   Its job is to format these elements into a single,
#   structured prompt that can be understood by the language model (LLM).
#   This structured format helps the LLM maintain context and generate more relevant responses.
def process_message_gemma(message, history):
    user_prompt_format = "User's Turn:\n>>> {prompt}\n"
    assistant_prompt_format = "Assistant's Turn:\n>>> {prompt}\n"

    history_message = ""
    for user_turn, assistant_turn in history:
        history_message += user_prompt_format.format(prompt=user_turn)
        history_message += assistant_prompt_format.format(prompt=assistant_turn)

    # Format the new user message
    new_user_message = user_prompt_format.format(prompt=message)
    # Create a new aggregated message to be used as a single flat string in a json object sent to the LLM
    aggregated_message = (
        history_message + new_user_message + assistant_prompt_format.format(prompt="")
    )
    return aggregated_message


# Function to save chat history to Firestore
def save_chat_history(interaction, doc_ref):
    timestamp_str = str(datetime.datetime.now())

    # Save the chat history, merging with existing data
    doc_ref.update({timestamp_str: interaction})

    print("Chat history saved successfully!")  # Optional: Log success


# Send the json message to the model and return the model's response. This is used for Gemma but not Gemini. It could also be used for other models.
def post_request(json_message):
    print("*** Request" + str(json_message), flush=True)
    # Set a timeout and check for HTTP errors. This will raise an exception on a bad status code (4xx or 5xx).
    response = requests.post(host + context_path, json=json_message, timeout=60)
    response.raise_for_status()
    json_data = response.json()
    print("*** Output: " + str(json_data), flush=True)
    return json_data


# custom css to hide default footer
css = """
footer {display: none !important;} .gradio-container {min-height: 0px !important;}
"""

# Add a dropdown to select the model to chat with
model_dropdown = gr.Dropdown(
    ["Gemma3 12b it", "Gemini"],
    label="Model",
    info="Select the model you would like to chat with.",
    value="Gemma3 12b it",
)

# Make the model temperature, top_p, and max tokents modifiable via sliders in the GUI
model_temperature = gr.Slider(
    minimum=0.1, maximum=1.0, value=0.9, label="Temperature", render=False
)
top_p = gr.Slider(minimum=0.1, maximum=1.0, value=0.95, label="Top_p", render=False)
max_tokens = gr.Slider(
    minimum=1, maximum=4096, value=1024, label="Max Tokens", render=False
)

# Call gradio to create the chat interface
app = gr.ChatInterface(
    inference_interface,
    additional_inputs=[model_dropdown, model_temperature, top_p, max_tokens],
    theme=themes.google_theme(),
    css=css,
    title="Chat with AI",
)

app.launch(server_name="0.0.0.0", allowed_paths=["images"])

7. Men-deploy Aplikasi Chat

Kita akan menggunakan Skaffold untuk membangun image container dan men-deploy-nya ke cluster. Skaffold adalah alat command line yang mengatur dan mengotomatiskan proses membangun, mengirim, dan men-deploy aplikasi ke Kubernetes. Hal ini menyederhanakan alur kerja pengembangan dengan memungkinkan Anda memicu seluruh proses ini dengan satu perintah, sehingga ideal untuk melakukan iterasi pada aplikasi Anda.

Catatan: perintah ini juga akan men-deploy akun Layanan Kubernetes yang kita perlukan untuk Workload Identity. Anda dapat melihat definisinya di file deploy/chat-deploy.yaml. Lihat definisinya di sini, sebagai referensi:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: gradio-chat-ksa

Jalankan Skaffold untuk mem-build dan men-deploy:

skaffold run --default-repo=us-central1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/chat-app-repo

Skaffold akan menggunakan Cloud Build untuk membangun image container, mengirimkannya ke Artifact Registry yang dibuat oleh Terraform, lalu menerapkan manifes Kubernetes ke cluster Anda.

8. Menguji Aplikasi

  1. Tunggu hingga layanan aplikasi chat mendapatkan alamat IP eksternal:
    kubectl get svc gradio-chat-service --watch
    
    Setelah EXTERNAL-IP berubah dari pending menjadi alamat IP yang sebenarnya, tekan Ctrl+C untuk berhenti melihat.
  2. Buka browser web, lalu buka http://[EXTERNAL-IP]:7860.
  3. Coba berinteraksi dengan model. Aplikasi dikonfigurasi agar Anda dapat melakukan chat dengan model Gemma yang dihosting secara lokal secara default. Jika Anda ingin memulai percakapan dengan Gemini, ubah model di dropdown "Input Tambahan". Sebagai contoh, coba tanyakan kepada AI: "Ceritakan lelucon Kubernetes."

Pemecahan masalah:

  1. Jika Anda mendapatkan error seperti "Situs ini tidak dapat dijangkau" atau "[EXTERNAL-IP] menolak untuk terhubung", mungkin ada yang salah dengan file app.py Anda. Kembali ke langkah berjudul "Periksa File app.py Anda" dan ulangi langkah-langkah dari sana.
  2. UI secara default menggunakan model "Gemma3 12b it". Jika Anda langsung mendapatkan error, kemungkinan karena pod Gemma belum siap. Tips: Anda dapat mengubah dropdown ke "Gemini" untuk menguji interaksi dengan aplikasi chat sambil menunggu Gemma diinisialisasi.

Uji Gemma: Pastikan "Gemma3 12b it" dipilih di dropdown dan kirim pesan (misalnya, "Ceritakan lelucon tentang Kubernetes").

Menguji Gemini: Ubah dropdown ke "Gemini" dan ajukan pertanyaan lain (misalnya, "Apa perbedaan antara pod dan node?").

Verifikasi Histori: Setelah Anda berhasil melakukan percakapan dengan model (baik Gemma maupun Gemini) di aplikasi chat, periksa database "chat-app-db" Anda di Firestore untuk melihat log percakapan. Jika Anda dapat melakukan percakapan dengan kedua model, perhatikan bahwa histori percakapan dipertahankan meskipun saat beralih model.

9. Melangkah Lebih Jauh

Setelah Anda memiliki aplikasi chat hybrid yang berfungsi, pertimbangkan tantangan berikut untuk memperdalam pemahaman Anda:

  1. Persona Kustom: Coba ubah fungsi process_message_gemma dan process_message_gemini untuk menyertakan "perintah sistem" di awal. Misalnya, beri tahu model "Anda adalah asisten bajak laut yang membantu" dan lihat bagaimana responsnya berubah.
  2. Identitas Pengguna Persisten: Saat ini, aplikasi membuat UUID acak baru untuk setiap sesi. Bagaimana cara mengintegrasikan sistem autentikasi yang sebenarnya (seperti Login dengan Google) sehingga pengguna dapat melihat histori percakapan sebelumnya di berbagai perangkat?
  3. Eksperimen Model: Coba ubah penggeser temperature di UI. Bagaimana pengaruh temperatur tinggi (mendekati 1,0) terhadap kreativitas vs. akurasi respons dibandingkan dengan temperatur rendah (mendekati 0,1)?

10. Kesimpulan

Selamat! Anda telah berhasil membuat aplikasi AI hibrida. Anda telah mempelajari cara:

  • Gunakan Terraform untuk infrastruktur sebagai kode di Google Cloud.
  • Hosting LLM berat terbuka Anda sendiri di GKE untuk mendapatkan kontrol penuh.
  • Integrasikan layanan AI terkelola seperti Vertex AI untuk mendapatkan fleksibilitas.
  • Bangun aplikasi stateful menggunakan Firestore untuk persistensi.
  • Amankan workload Anda menggunakan Workload Identity.

Pembersihan

Agar tidak dikenai biaya, hancurkan resource yang Anda buat:

cd infra
terraform destroy -var="project_id=$GOOGLE_CLOUD_PROJECT" -var="project_number=$PROJECT_NUMBER" -var="region=$REGION"