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

Bu codelab hakkında
schedule0 dakika
subjectSon güncelleme 29 Mart 2025
account_circleYazan: Alvin Prayuda Juniarta Dwiyantoro

1. Giriş

Bu kod laboratuvarında, sohbet web arayüzü biçiminde bir uygulama oluşturacaksınız. Bu uygulamada, uygulamayla iletişim kurabilir, bazı dokümanlar veya resimler yükleyebilir ve bunları tartışabilirsiniz. Uygulamanın kendisi 2 hizmete ayrılmıştır: ön uç ve arka uç. Bu sayede hızlı bir prototip oluşturabilir, nasıl bir deneyim sunduğunu deneyebilir ve API sözleşmesinin ikisini de entegre etmek için nasıl göründüğünü anlayabilirsiniz.

Codelab'de aşağıdaki gibi adım adım bir yaklaşım uygulayacaksınız:

  1. Google Cloud projenizi hazırlayın ve projenizdeki 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'yi kullanarak arka uç hizmetini (HTTP sunucusu) oluşturun.
  4. Uygulamayı Cloud Run'a dağıtmak için gereken ortam değişkenlerini yönetin ve gerekli dosyaları ayarlayın
  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ışma konusunda rahat olmalısınız.
  • HTTP hizmetini kullanan temel tam yığın mimarisini anlama

Neler öğreneceksiniz?

  • Metin ve diğer veri türlerini (çoklu formatlı) göndermek ve metin yanıtı oluşturmak için Gemini SDK'sını kullanma
  • Sohbet bağlamını korumak için sohbet geçmişini Gemini SDK'sında yapılandırma
  • 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 kod laboratuvarının örnek uygulamasında Python kullanılır. Ancak sunulan kavramları anlamak için Python bilgisine sahip olmanız gerekmez.

2. Başlamadan önce

Cloud Shell Düzenleyici'de Cloud projesi oluşturma

Bu kod laboratuvarında, faturalandırmanın etkin olduğu bir Google Cloud projeniz olduğu varsayılmıştır. Henüz yoksa 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. Projede faturalandırmanın etkin olup olmadığını nasıl kontrol edebileceğinizi öğrenin .
  3. Google Cloud'da çalışan ve bq ile önceden yüklenmiş 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ğrulanıp doğrulanmadığını ve projenin proje kimliğinize ayarlanıp ayarlanmadığı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 ayarlanmadıysa 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 düğmeyi tıkladığınızda projenizin tamamı ve proje kimliği sağ tarafta gösterilir.

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ğıdakine benzer bir mesaj görürsünüz:

Operation "operations/..." finished successfully.

gcloud komutunun alternatifi, her ürünü arayarak veya bu bağlantıyı kullanarak konsoldan geçmektir.

Atlanan bir API varsa uygulama sırasında istediğiniz zaman etkinleştirebilirsiniz.

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

Uygulama Çalışma Dizinini Ayarlama

  1. Düzenleyiciyi aç düğmesini tıklayın. Bu işlemle Cloud Shell Düzenleyici açılır. Kodumuzu buraya yazabiliriz b16d56e4979ec951.png
  2. Cloud Code projesinin, Cloud Shell düzenleyicisinin sol alt köşesinde (durum çubuğu) aşağıdaki resimde vurgulandığı gibi ayarlandığından ve faturalandırmanın etkinleştirildiği etkin Google Cloud projesine ayarlandığından emin olun. İstenirse Yetkilendir'i tıklayın. Cloud Shell Düzenleyici'nin başlatıldı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ştirilen projenize de yönlendirebilir.

f5003b9c38b43262.png

  1. Durum çubuğundaki etkin projeyi tıklayın ve Cloud Code pop-up'ının açılmasını bekleyin. Pop-up'ta "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) ve ardından Tamam'ı tıklayın.

8409d8db18690fdf.png

Bu noktada, yeni uygulama çalışma dizininde olmalısınız ve aşağıdaki dosyaları görebilirsiniz.

1ef5bb44f1d2c2a4.png

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

Ortam Kurulumu

Python sanal ortamını hazırlama

Sonraki adım, geliştirme ortamını hazırlamaktır. Bu kod laboratuvarında Python 3.12'yi kullanacağız ve Python sürümü ile 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'yi 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 ve .python-version, bu proje için kullanılan Python sürümünü standartlaştırır. Bu konu hakkında daha fazla bilgi edinmek için bu dokümanları inceleyebilirsiniz.
main.py
.python-version
pyproject.toml
  1. Test etmek için main.py'nin ü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ğıda gösterildiği 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 doğru şekilde ayarlandığını gösterir. uv zaten bu işlemi gerçekleştirdiği için sanal bir ortam oluşturmamız gerekmedi. Bu nedenle, bu noktadan itibaren standart Python komutu (ör. python main.py) uv run (ör. uv run main.py) ile değiştirilecek.

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

Bu codelab paketine bağımlılıkları uv komutunu da 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 "dependencies" bölümünün önceki komutu yansıtacak şekilde güncellendiğini göreceksiniz.

Yapılandırma dosyalarını ayarlama

Şimdi bu proje için yapılandırma dosyaları oluşturmamız gerekiyor. Yapılandırma dosyaları, yeniden dağıtımda kolayca değiştirilebilecek 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ılacak bir Python paketidir.

  1. Aşağıdaki yapılandırmayı içeren settings.yaml adlı bir dosya oluşturun. Dosya->Yeni Metin Dosyası'nı tıklayın ve dosyayı aşağıdaki kodla doldurun. Ardından dosyayı settings.yaml olarak kaydedin.
VERTEXAI_LOCATION: "us-central1"
VERTEXAI_PROJECT_ID: "{YOUR-PROJECT-ID}"
BACKEND_URL: "http://localhost:8081/chat"

Lütfen VERTEXAI_PROJECT_ID değerlerini Google Cloud projesini oluştururken seçtiğiniz değerlere göre güncelleyin. Bu kod laboratuvarının VERTEXAI_LOCATION ve BACKEND_URL için önceden yapılandırılmış değerlerini 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. Dosya->Yeni Metin Dosyası'nı tıklayın ve dosyayı aşağıdaki kodla doldurun. Ardından dosyayı settings.py olarak kaydedin. Kodda, okunacak dosyanın settings.yaml adlı dosya olduğunu açıkça belirttiğimizi 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. Ardından, ortam değişkenlerini varsayılan YAML yapılandırmasına kıyasla daha yüksek önceliğe koyduğumuz için ortam değişkenlerini konsol üzerinden esnek bir şekilde güncelleyebilir ve yeniden dağıtabiliriz.

Şimdi bir sonraki adıma, hizmetleri oluşturmaya geçebiliriz.

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

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

5bcfa1cce6618305.png

Kullanıcıların metin gönderip dosya yüklemesi için bir giriş alanı içerir. Ayrıca kullanıcı, ek girişler alanında Gemini API'ye gönderilecek sistem talimatlarının üzerine de yazabilir.

Ön uç hizmetini Gradio'yu 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'da 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ıkladığınızda web arayüzünü kontrol edebilirsiniz. Alternatif olarak, Cloud Düzenleyici'nin sağ üst tarafındaki Web Önizlemesi düğmesini tıklayıp 8080 bağlantı noktasında önizle'yi seçerek de kullanıcı arayüzü uygulamasına erişebilirsiniz.

49cbdfdf77964065.jpeg

Web arayüzünü görürsünüz ancak henüz ayarlanmamış arka uç hizmeti nedeniyle sohbet göndermeye çalışırken beklenen hata alırsınız.

bd0464140308cfbe.png

Şimdi hizmetin çalışmasını bekleyin ve henüz sonlandırmayın. Bu sırada önemli kod bileşenlerini burada tartışabiliriz.

Kod Açıklaması

Web arayüzünden arka uca veri göndermek için gereken kod bu bölümdedir.

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

Gemini'ye çok modlu veri göndermek ve verileri hizmetler arasında erişilebilir hale getirmek istediğimizde kullanabileceğimiz yöntemlerden biri, verileri kodda belirtildiği gibi base64 veri türüne dönüştürmektir. Ayrıca verilerin MIME türünü de beyan etmemiz gerekir. Ancak Gemini API, mevcut MIME türlerinin tümünü destekleyemez. Bu nedenle, Gemini tarafından desteklenen MIME türlerini bilmek önemlidir. Bu türleri bu dokümanda bulabilirsiniz. Bilgileri Gemini API'nin her bir özelliğinde (ör.Vision) bulabilirsiniz.

Ayrıca, sohbet arayüzünde Gemini'ye sohbetin "hatırlatıcısı" olarak sohbet geçmişini göndermek de önemlidir. Bu web arayüzünde, Gradio tarafından web oturumu başına yönetilen sohbet geçmişini de kullanıcıdan gelen mesaj girişiyle birlikte göndeririz. Ayrıca kullanıcının sistem talimatlarını değiştirmesini ve göndermesini de sağlarız.

4. FastAPI'yi kullanarak arka uç hizmeti oluşturma

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

Yeni bir dosya oluşturun, Dosya->Yeni Metin Dosyası'nı tıklayın ve aşağıdaki kodu kopyalayıp yapıştırın,ardından 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ı unutmayın. Şimdi yeni bir terminal açmamız ve bu arka uç hizmetini çalıştırmaya çalışmamız 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 bir terminal açmak için Ctrl + Üst Karakter + C tuşlarına da 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ı

Chat İsteğini Almak İçin HTTP Rotasını Tanımlama

FastAPI'de yolu app süsleyicisini kullanarak tanımlarız. API sözleşmesini tanımlamak için Pydantic'i de kullanırız. Yanıtın oluşturulacağı rotanın POST yöntemiyle /chat yolunda olduğunu belirtiriz. Aşağıdaki kodda tanımlanan işlevler

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 konulardan biri, sohbet geçmişini nasıl yeniden yapılandırabileceğimizdir. Böylece, daha sonra bir Gemini istemcisini başlattığımızda history bağımsız değişken değeri olarak eklenebilir. 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'sına sohbet geçmişini sağlamak için verileri List[Content] veri türüne göre biçimlendirmemiz gerekir. Her İçerik en az bir rol ve bölüm değerine sahip olmalıdır. rol, mesajın kaynağını (kullanıcı veya model) belirtir. Burada parts, istemle ilgilidir. İstem yalnızca metin veya farklı modalitelerin bir kombinasyonu olabilir. İçerik bağımsız değişkenlerinin nasıl yapılandırılacağını bu belgede ayrıntılı olarak görebilirsiniz.

Metin dışı ( çok formatlı) verileri işleme

Kullanıcı arayüzü bölümünde daha önce de belirtildiği gibi, metin olmayan veya çok modlu verileri göndermenin yollarından biri, verileri base64 dizesi olarak göndermektir. Ayrıca, verilerin doğru şekilde yorumlanabilmesi için verilerin MIME türünü de belirtmemiz gerekir. Örneğin, resim verilerini .jpg son ekiyle gönderiyorsak image/jpeg MIME türünü sağlamamız gerekir.

Kodun bu kısmı, base64 verilerini Gemini SDK'sındaki 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ırabilirsiniz:

  • 8080 bağlantı noktasında çalışan ön uç hizmeti
* 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ışı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 asistanla sohbet ederken belgelerinizi sorunsuz bir şekilde gönderebilirsiniz. Dosya yükleyerek ve soru sorarak denemeye başlayabilirsiniz. Belirli dosya türlerinin henüz desteklenmediği ve hata oluşturduğunu unutmayın.

Sistem talimatlarını, metin kutusunun altındaki Ek Girişler alanından da düzenleyebilirsiniz.

ee9c849a276d378.png

6. Cloud Run'a dağıtma

Elbette bu muhteşem uygulamayı diğer kullanıcılara da göstermek istiyoruz. Bunu yapmak için bu uygulamayı paketleyebilir ve diğer kullanıcıların erişebileceği herkese açık bir hizmet olarak Cloud Run'a dağıtabiliriz. Bunu yapmak için mimariyi tekrar gözden geçirelim.

b102df2c3f1adabf.jpeg

Bu codelab'de hem ön uç hem de arka uç hizmetini 1 kapsayıcıya yerleştireceğiz. Her iki hizmeti de yönetmek için gözetimli kullanıcının yardımına ihtiyacımız var.

Yeni bir dosya oluşturun, Dosya->Yeni Metin Dosyası'nı tıklayın ve aşağıdaki kodu kopyalayıp yapıştırın,ardından 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'imize ihtiyacımız olacak. Dosya->Yeni Metin Dosyası'nı 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 aşamada, uygulamalarımızı Cloud Run'a dağıtmak için gereken tüm dosyalara sahibiz. Şimdi dağıtalım. Cloud Shell Terminal'e gidin ve mevcut projenin etkin projenizle 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". Uygulama çalışma dizinimizde Dockerfile bulunduğundan Docker container'ı oluşturur ve Artifact Registry'ye aktarır. Ayrıca, Artifact Registry deposunun bölgede oluşturulacağını belirten bir istem gösterilir. Bu isteme "Evet" yanıtını verin. Ayrıca, kimliği doğrulanmamış çağrılara izin vermek isteyip istemediğiniz sorulduğunda "y" deyin. Bu bir demo uygulama olduğu için kimlik doğrulaması yapılmayan erişime izin verildiğini unutmayın. Kuruluş ve üretim uygulamalarınızda uygun kimlik doğrulamayı 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ştur.

7. Zorluk

Şimdi keşif becerilerinizi geliştirme ve öne çıkma zamanı. Asistanın ses dosyalarını veya video dosyalarını okumasını desteklemesi için kodu değiştirmek için gerekenlere sahip misiniz?

8. Temizleme

Bu kod laboratuvarında 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 ardından Sil'i tıklayın.
  3. İletişim kutusuna proje kimliğini yazın ve projeyi silmek için Kapat'ı tıklayın.
  4. Alternatif olarak, konsolda Cloud Run'a gidip yeni dağıttığınız hizmeti seçip silebilirsiniz.