Gemini ile Bulut'ta Çoklu Modlu Asistan Oluşturma ve Dağıtma (Python)

1. Giriş

Bu codelab'de, sohbet web arayüzü şeklinde bir uygulama oluşturacaksınız. Bu uygulamayla iletişim kurabilir, bazı belgeleri veya resimleri yükleyip bunları tartışabilirsiniz. Uygulama, ön uç ve arka uç olmak üzere 2 hizmete ayrılmıştır. Bu sayede hızlı bir prototip oluşturup nasıl bir deneyim sunduğunu deneyebilir ve her ikisini de entegre etmek için API sözleşmesinin nasıl göründüğünü anlayabilirsiniz.

Bu codelab'de aşağıdaki gibi adım adım bir yaklaşım kullanacaksınız:

  1. Google Cloud projenizi hazırlayın ve gerekli tüm API'leri etkinleştirin.
  2. Gradio kitaplığını kullanarak ön uç hizmetini (sohbet arayüzü) oluşturma
  3. Gelen verileri Gemini SDK standardına göre yeniden biçimlendirecek ve Gemini API ile iletişimi sağlayacak FastAPI kullanarak arka uç hizmetini (HTTP sunucusu) oluşturun.
  4. Ortam değişkenlerini yönetme ve uygulamayı Cloud Run'a dağıtmak için gereken dosyaları ayarlama
  5. Uygulamayı Cloud Run'a dağıtma

5bcfa1cce6618305.png

Mimariye Genel Bakış

b102df2c3f1adabf.jpeg

Ön koşullar

  • Gemini API ve Google Gen AI SDK ile çalışmaya alışkın olmak
  • HTTP hizmetini kullanan temel tam yığın mimarisi hakkında bilgi sahibi olmak

Neler öğreneceksiniz?

  • Metin ve diğer veri türlerini (çok formatlı) göndermek ve metin yanıtı oluşturmak için Gemini SDK'yı kullanma
  • Sohbet bağlamını korumak için sohbet geçmişini Gemini SDK'ya nasıl aktarabilirsiniz?
  • Gradio ile ön uç web prototipi oluşturma
  • FastAPI ve Pydantic ile arka uç hizmeti geliştirme
  • Pydantic-settings ile YAML dosyasındaki ortam değişkenlerini yönetme
  • Dockerfile kullanarak uygulamayı Cloud Run'a dağıtma ve YAML dosyasıyla ortam değişkenleri sağlama

Gerekenler

  • Chrome web tarayıcısı
  • Gmail hesabı
  • Faturalandırmanın etkin olduğu bir Cloud projesi

Her seviyeden geliştirici (yeni başlayanlar dahil) için tasarlanan bu codelab'de örnek uygulamada Python kullanılmaktadır. Ancak sunulan kavramları anlamak için Python bilgisi gerekmez.

2. Başlamadan önce

Cloud Shell Düzenleyici'de Cloud Projesi Kurulumu

Bu codelab'de, faturalandırmanın etkin olduğu bir Google Cloud projenizin olduğu varsayılmaktadır. Henüz kullanmıyorsanız başlamak için aşağıdaki talimatları uygulayabilirsiniz.

  1. 2Google Cloud Console'daki proje seçici sayfasında bir Google Cloud projesi seçin veya oluşturun.
  2. Cloud projeniz için faturalandırmanın etkinleştirildiğinden emin olun. Bir projede faturalandırmanın etkin olup olmadığını kontrol etmeyi öğrenin .
  3. bq'nun önceden yüklendiği, Google Cloud'da çalışan bir komut satırı ortamı olan Cloud Shell'i kullanacaksınız. Google Cloud Console'un üst kısmından Cloud Shell'i etkinleştir'i tıklayın.

1829c3759227c19b.png

  1. Cloud Shell'e bağlandıktan sonra aşağıdaki komutu kullanarak kimliğinizin doğrulandığını ve projenin proje kimliğinize ayarlandığını kontrol edin:
gcloud auth list
  1. gcloud komutunun projeniz hakkında bilgi sahibi olduğunu onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın.
gcloud config list project
  1. Projeniz ayarlanmamışsa ayarlamak için aşağıdaki komutu kullanın:
gcloud config set project <YOUR_PROJECT_ID>

Alternatif olarak, PROJECT_ID kimliğini konsolda da görebilirsiniz.

4032c45803813f30.jpeg

Bu seçeneği tıkladığınızda sağ tarafta tüm projenizi ve proje kimliğini görürsünüz.

8dc17eb4271de6b5.jpeg

  1. Aşağıda gösterilen komutu kullanarak gerekli API'leri etkinleştirin. Bu işlem birkaç dakika sürebilir. Lütfen bekleyin.
gcloud services enable aiplatform.googleapis.com \
                           run.googleapis.com \
                           cloudbuild.googleapis.com \
                           cloudresourcemanager.googleapis.com

Komut başarıyla yürütüldüğünde aşağıda gösterilene benzer bir mesaj görürsünüz:

Operation "operations/..." finished successfully.

Gcloud komutuna alternatif olarak, her ürünü arayarak veya bu bağlantıyı kullanarak konsolu kullanabilirsiniz.

Herhangi bir API atlanırsa uygulama sırasında istediğiniz zaman etkinleştirebilirsiniz.

gcloud komutları ve kullanımı için belgelere bakın.

Uygulama Çalışma Dizini Kurulumu

  1. Open Editor (Düzenleyiciyi Aç) düğmesini tıklayın. Bu işlemle Cloud Shell Editor açılır. Kodumuzu burada yazabiliriz. b16d56e4979ec951.png
  2. Cloud Shell düzenleyicisinin sol alt köşesinde (durum çubuğu) Cloud Code projesinin ayarlandığından emin olun. Bu proje, aşağıdaki resimde vurgulandığı gibi faturalandırmanın etkinleştirildiği etkin Google Cloud projesi olarak ayarlanmalıdır. İstenirse Yetkilendir'i tıklayın. Cloud Shell Düzenleyici'yi başlattıktan sonra Cloud Code - Sign In (Cloud Code - Oturum Aç) düğmesinin görünmesi biraz zaman alabilir. Lütfen bekleyin. Önceki komutu zaten uyguladıysanız düğme, oturum açma düğmesi yerine doğrudan etkinleştirilmiş projenize de yönlendirebilir.

f5003b9c38b43262.png

  1. Durum çubuğunda etkin projeyi tıklayın ve Cloud Code pop-up'ının açılmasını bekleyin. Pop-up pencerede "Yeni Uygulama"yı seçin.

70f80078e01a02d8.png

  1. Uygulama listesinden Gemini Üretken Yapay Zeka'yı, ardından Gemini API Python'u seçin.

362ff332256d6933.jpeg

85957565316308d9.jpeg

  1. Yeni uygulamayı istediğiniz adla kaydedin. Bu örnekte gemini-multimodal-chat-assistant adını kullanacağız. Ardından Tamam'ı tıklayın.

8409d8db18690fdf.png

Bu noktada, yeni uygulama çalışma dizininde olmanız ve aşağıdaki dosyaları görmeniz gerekir.

1ef5bb44f1d2c2a4.png

Ardından, Python ortamımızı hazırlayacağız.

Ortam Kurulumu

Python sanal ortamını hazırlama

Bir sonraki adım, geliştirme ortamını hazırlamaktır. Bu codelab'de Python 3.12'yi kullanacağız. Python sürümü ve sanal ortam oluşturma ve yönetme ihtiyacını basitleştirmek için uv python proje yöneticisini kullanacağız.

  1. Terminali henüz açmadıysanız Terminal -> Yeni Terminal'i tıklayarak açın veya Ctrl + Üst Karakter + C tuşlarını kullanın.

f8457daf0bed059e.jpeg

  1. uv'yı indirin ve aşağıdaki komutla Python 3.12'yi yükleyin.
curl -LsSf https://astral.sh/uv/0.6.6/install.sh | sh && \
source $HOME/.local/bin/env && \
uv python install 3.12
  1. Şimdi uv kullanarak Python projesini başlatalım.
uv init
  1. Dizinde main.py, .python-version ve pyproject.toml dosyalarının oluşturulduğunu görürsünüz. Bu dosyalar, projenin dizinde tutulması için gereklidir. Python bağımlılıkları ve yapılandırmaları pyproject.toml dosyasında belirtilebilir. .python-version dosyası ise bu proje için kullanılan Python sürümünü standartlaştırır. Bu konu hakkında daha fazla bilgi için belgeleri inceleyebilirsiniz.
main.py
.python-version
pyproject.toml
  1. Test etmek için main.py dosyasının üzerine aşağıdaki kodu yazın.
def main():
   print("Hello from gemini-multimodal-chat-assistant!")

if __name__ == "__main__":
   main()
  1. Ardından, aşağıdaki komutu çalıştırın.
uv run main.py

Aşağıdaki gibi bir çıkış alırsınız.

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

Bu, Python projesinin düzgün şekilde ayarlandığını gösterir. uv bu işlemi zaten yaptığından sanal ortamı manuel olarak oluşturmamız gerekmedi. Bu noktadan itibaren standart Python komutu (ör. python main.py) uv run (ör. uv run main.py) ile değiştirilir.

Gerekli Bağımlılıkları Yükleme

Bu codelab paket bağımlılıklarını da uv komutunu kullanarak ekleyeceğiz. Aşağıdaki komutu çalıştırın:

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

pyproject.toml dosyasının "dependencies" bölümünün, önceki komutu yansıtacak şekilde güncellendiğini görürsünüz.

Yapılandırma dosyalarını ayarlama

Şimdi bu proje için yapılandırma dosyalarını ayarlamamız gerekiyor. Yapılandırma dosyaları, yeniden dağıtım sırasında kolayca değiştirilebilen dinamik değişkenleri depolamak için kullanılır. Bu projede, pydantic-settings paketiyle YAML tabanlı yapılandırma dosyaları kullanacağız. Böylece, daha sonra Cloud Run dağıtımıyla kolayca entegre edilebilir. pydantic-settings, yapılandırma dosyaları için tür kontrolünü zorunlu kılabilen bir Python paketidir.

  1. Aşağıdaki yapılandırmayla settings.yaml adlı bir dosya oluşturun. File->New Text File'ı (Dosya->Yeni Metin Dosyası) tıklayın ve aşağıdaki kodu girin. Ardından settings.yaml olarak kaydedin.
VERTEXAI_LOCATION: "us-central1"
VERTEXAI_PROJECT_ID: "{YOUR-PROJECT-ID}"
BACKEND_URL: "http://localhost:8081/chat"

Lütfen Google Cloud projesini oluştururken seçtiğiniz değerlere göre VERTEXAI_PROJECT_ID değerlerini güncelleyin. Bu codelab'de VERTEXAI_LOCATION ve BACKEND_URL için önceden yapılandırılmış değerleri kullanacağız .

  1. Ardından, settings.py adlı bir Python dosyası oluşturun. Bu modül, yapılandırma dosyalarımızdaki yapılandırma değerleri için programatik giriş görevi görür. File->New Text File'ı (Dosya->Yeni Metin Dosyası) tıklayın ve aşağıdaki kodu girin. Ardından settings.py olarak kaydedin. Kodda, settings.yaml adlı dosyanın okunacak dosya olduğunu açıkça ayarladığımızı görebilirsiniz.
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()

Bu yapılandırmalar, çalışma zamanımızı esnek bir şekilde güncellememize olanak tanır. İlk dağıtımda, ilk varsayılan yapılandırmaya sahip olmak için settings.yaml yapılandırmasını kullanırız. Bunu yaptıktan sonra, ortam değişkenlerini varsayılan YAML yapılandırmasına göre daha yüksek önceliğe yerleştirdiğimiz için ortam değişkenlerini konsol üzerinden esnek bir şekilde güncelleyebilir ve yeniden dağıtabiliriz.

Şimdi bir sonraki adıma geçebiliriz: hizmetleri oluşturma

3. Gradio kullanarak ön uç hizmeti oluşturma

Aşağıdaki gibi bir sohbet web arayüzü oluşturacağız.

5bcfa1cce6618305.png

Kullanıcıların metin göndermesine ve dosya yüklemesine olanak tanıyan bir giriş alanı içerir. Ayrıca kullanıcı, ek girişler alanında Gemini API'ye gönderilecek sistem talimatının üzerine de yazabilir.

Ön uç hizmetini Gradio kullanarak oluşturacağız. main.py dosyasını frontend.py olarak yeniden adlandırın ve aşağıdaki kodu kullanarak kodun üzerine yazın.

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

Ardından, ön uç hizmetini aşağıdaki komutla çalıştırmayı deneyebiliriz. main.py dosyasını frontend.py olarak yeniden adlandırmayı unutmayın.

uv run frontend.py

Cloud Console'unuzda buna benzer bir çıkış görürsünüz.

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

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

Ardından, yerel URL bağlantısını Ctrl+tıklayarak web arayüzünü kontrol edebilirsiniz. Alternatif olarak, Cloud Editor'ün sağ üst tarafındaki Web Önizlemesi düğmesini tıklayıp 8080 bağlantı noktasında önizle'yi seçerek de ön uç uygulamasına erişebilirsiniz.

49cbdfdf77964065.jpeg

Web arayüzünü görürsünüz ancak henüz ayarlanmamış olan arka uç hizmeti nedeniyle sohbet göndermeye çalıştığınızda beklenen hatayı alırsınız.

bd0464140308cfbe.png

Şimdi hizmetin çalışmasına izin verin ve henüz sonlandırmayın. Bu arada, önemli kod bileşenlerini burada tartışabiliriz.

Kod Açıklaması (Code Explanation)

Web arayüzünden arka uca veri gönderme kodu bu bölümde yer alır.

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

Çok formatlı verileri Gemini'a göndermek ve hizmetler arasında erişilebilir hale getirmek istediğimizde, verileri kodda belirtildiği gibi base64 veri türüne dönüştürebiliriz. Ayrıca verilerin MIME türünü de beyan etmemiz gerekir. Ancak Gemini API, mevcut tüm MIME türlerini destekleyemez. Bu nedenle, Gemini tarafından desteklenen ve bu belgede okunabilen MIME türlerini bilmek önemlidir. Bilgileri Gemini API'nin her bir özelliğinde (ör.Vision) bulabilirsiniz.

Ayrıca, sohbet arayüzünde Gemini'a sohbetin "hafızasını" vermek için sohbet geçmişini ek bağlam olarak göndermek de önemlidir. Bu nedenle, bu web arayüzünde Gradio tarafından web oturumu başına yönetilen sohbet geçmişini de göndeririz ve bunu kullanıcının mesaj girişiyle birlikte göndeririz. Ayrıca, kullanıcının sistem talimatını değiştirmesine ve bunu göndermesine de olanak tanırız.

4. FastAPI kullanarak arka uç hizmeti oluşturma

Ardından, daha önce bahsedilen yükü, son kullanıcı mesajını, sohbet geçmişini ve sistem talimatını işleyebilecek arka ucu oluşturmamız gerekir. HTTP arka uç hizmetini oluşturmak için FastAPI'yi kullanacağız.

Yeni dosya oluşturun, Dosya->Yeni Metin Dosyası'nı tıklayın,aşağıdaki kodu kopyalayıp yapıştırın ve backend.py olarak kaydedin.

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)

Dosyayı backend.py olarak kaydetmeyi unutmayın. Ardından arka uç hizmetini çalıştırmayı deneyebiliriz. Önceki adımda ön uç hizmetini doğru şekilde çalıştırdığımızı hatırlayın. Şimdi yeni bir terminal açıp bu arka uç hizmetini çalıştırmayı denememiz gerekiyor.

  1. Yeni bir terminal oluşturun. Alt kısımdaki terminalinize gidin ve yeni bir terminal oluşturmak için "+" düğmesini bulun. Alternatif olarak, yeni terminali açmak için Ctrl + Üst Karakter + C tuşlarına basabilirsiniz.

3e52a362475553dc.jpeg

  1. Ardından, gemini-multimodal-chat-assistant çalışma dizininde olduğunuzdan emin olun ve aşağıdaki komutu çalıştırın.
uv run backend.py
  1. İşlem başarılı olursa aşağıdaki gibi bir çıkış gösterilir.
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)

Kod Açıklaması (Code Explanation)

Sohbet isteği almak için HTTP rotasını tanımlama

FastAPI'de rotayı app dekoratörünü kullanarak tanımlarız. API sözleşmesini tanımlamak için Pydantic'i de kullanırız. Yanıt oluşturma rotasının POST yöntemiyle /chat rotasında olduğunu belirtiyoruz. Bu işlevler aşağıdaki kodda belirtilmiştir.

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

Gemini SDK Sohbet Geçmişi Biçimini Hazırlama

Anlaşılması gereken önemli noktalardan biri, sohbet geçmişini daha sonra bir Gemini istemcisini başlatırken history bağımsız değişken değeri olarak eklenebilecek şekilde nasıl yeniden yapılandırabileceğimizdir. Aşağıdaki kodu inceleyebilirsiniz.

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

Gemini SDK'ya sohbet geçmişi sağlamak için verileri List[Content] veri türünde biçimlendirmemiz gerekir. Her Content (İçerik) en az bir role (rol) ve parts (parçalar) değerine sahip olmalıdır. role, mesajın kaynağını (user veya model) ifade eder. Burada bölümler, istemin kendisini ifade eder. Bu bölüm yalnızca metin veya farklı biçimlerin bir kombinasyonu olabilir. İçerik bağımsız değişkenlerinin nasıl yapılandırılacağı hakkında ayrıntılı bilgiyi bu belgede bulabilirsiniz.

Metin Olmayan ( Çok Formatlı) Verileri İşleme

Ön uç bölümünde daha önce belirtildiği gibi, metin dışı veya çok formatlı verileri göndermenin yollarından biri verileri base64 dizesi olarak göndermektir. Verilerin doğru şekilde yorumlanabilmesi için MIME türünü de belirtmemiz gerekir. Örneğin, .jpg sonekiyle resim verileri gönderiyorsak image/jpeg MIME türünü sağlamamız gerekir.

Kodun bu kısmı, base64 verilerini Gemini SDK'dan Part.from_bytes biçimine dönüştürür.

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

Artık farklı Cloud Console sekmelerinde birden fazla hizmet çalıştırıyor olmanız gerekir:

  • Frontend hizmeti 8080 numaralı bağlantı noktasında çalışır.
* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.
  • Arka uç hizmeti 8081 numaralı bağlantı noktasında çalıştırılır.
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)

Şu anda, 8080 bağlantı noktasındaki web uygulamasından belgelerinizi asistanla sorunsuz bir şekilde sohbet ederek gönderebilirsiniz. Dosya yükleyip soru sorarak denemeye başlayabilirsiniz. Bazı dosya türlerinin henüz desteklenmediğini ve hata oluşturacağını unutmayın.

Ayrıca, metin kutusunun altındaki Ek Girişler alanından sistem talimatlarını düzenleyebilirsiniz.

ee9c849a276d378.png

6. Cloud Run'a dağıtma

Elbette bu harika uygulamayı diğer kullanıcılara da göstermek istiyoruz. Bunu yapmak için bu uygulamayı paketleyip başkalarının erişebileceği herkese açık bir hizmet olarak Cloud Run'a dağıtabiliriz. Bunu yapmak için mimariye tekrar göz atalım.

b102df2c3f1adabf.jpeg

Bu codelab'de hem ön uç hem de arka uç hizmetini tek bir kapsayıcıya yerleştireceğiz. Her iki hizmeti de yönetmek için supervisord'un yardımına ihtiyacımız olacak.

Yeni dosya oluşturun, Dosya->Yeni Metin Dosyası'nı tıklayın,aşağıdaki kodu kopyalayıp yapıştırın ve supervisord.conf olarak kaydedin.

[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

Ardından, Dockerfile'a ihtiyacımız olacak. File->New Text File'ı (Dosya->Yeni Metin Dosyası) tıklayın ve aşağıdaki kodu kopyalayıp yapıştırın, ardından Dockerfile olarak kaydedin.

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"]

Bu noktada, uygulamalarımızı Cloud Run'a dağıtmak için gereken tüm dosyalara sahibiz. Şimdi dağıtalım. Cloud Shell Terminali'ne gidin ve mevcut projenin etkin projeniz olarak yapılandırıldığından emin olun. Aksi takdirde, proje kimliğini ayarlamak için gcloud configure komutunu kullanmanız gerekir:

gcloud config set project [PROJECT_ID]

Ardından, Cloud Run'a dağıtmak için aşağıdaki komutu çalıştırın.

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

Hizmetiniz için bir ad girmeniz istenir. Örneğin, "gemini-multimodal-chat-assistant" adını girebilirsiniz. Uygulama çalışma dizinimizde Dockerfile olduğu için Docker container'ı oluşturulur ve Artifact Registry'ye aktarılır. Ayrıca, Artifact Registry deposunu bölgede oluşturacağını belirten bir istem gösterir. Bu isteme "Y" yanıtını verin. Ayrıca, kimliği doğrulanmamış çağrılara izin vermek isteyip istemediğiniz sorulduğunda "y" deyin. Bunun bir demo uygulaması olması nedeniyle burada kimliği doğrulanmamış erişime izin verildiğini unutmayın. Kurumsal ve üretim uygulamalarınız için uygun kimlik doğrulama yöntemini kullanmanız önerilir.

Dağıtım tamamlandığında aşağıdakine benzer bir bağlantı alırsınız:

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

Uygulamanızı gizli pencereden veya mobil cihazınızdan kullanmaya devam edebilirsiniz. Bu özellik zaten kullanıma sunulmuş olmalıdır.

7. Zorluk

Şimdi keşif becerilerinizi geliştirme ve parlatma zamanı. Asistanın ses dosyalarını veya video dosyalarını okumayı desteklemesi için kodu değiştirecek bilgiye sahip misiniz?

8. Temizleme

Bu codelab'de kullanılan kaynaklar için Google Cloud hesabınızın ücretlendirilmesini istemiyorsanız şu adımları uygulayın:

  1. Google Cloud Console'da Kaynakları yönetin sayfasına gidin.
  2. Proje listesinde silmek istediğiniz projeyi seçin ve Sil'i tıklayın.
  3. İletişim kutusunda proje kimliğini yazın ve projeyi silmek için Kapat'ı tıklayın.
  4. Alternatif olarak, konsolda Cloud Run'a gidebilir, yeni dağıttığınız hizmeti seçip silebilirsiniz.