Crie e implante um assistente multimodal no Cloud com o Gemini (Python)

Sobre este codelab
schedule0 minuto
subjectÚltimo 29 de março de 2025 atualizado
account_circleEscrito por Alvin Prayuda Juniarta Dwiyantoro

Neste codelab, você vai criar um aplicativo na forma de uma interface da Web de chat, em que poderá se comunicar, fazer upload de documentos ou imagens e discutir sobre eles. O aplicativo é dividido em dois serviços: front-end e back-end. Isso permite que você crie um protótipo rápido e teste como ele funciona, além de entender como o contrato da API parece integrar os dois.

No codelab, você vai usar uma abordagem detalhada da seguinte forma:

  1. Prepare seu projeto do Google Cloud e ative todas as APIs necessárias nele
  2. Criar o serviço de front-end: interface de chat usando a biblioteca Gradio
  3. Crie o serviço de back-end: servidor HTTP usando a FastAPI, que reformata os dados recebidos para o padrão do SDK Gemini e ativa a comunicação com a API Gemini.
  4. Gerenciar variáveis de ambiente e configurar os arquivos necessários para implantar o aplicativo no Cloud Run
  5. Implantar o aplicativo no Cloud Run

5bcfa1cce6618305.png

Visão geral da arquitetura

b102df2c3f1adabf.jpeg

Pré-requisitos

O que você vai aprender

  • Como usar o SDK do Gemini para enviar texto e outros tipos de dados (multimodal) e gerar respostas de texto
  • Como estruturar o histórico de conversas no SDK do Gemini para manter o contexto da conversa
  • Prototipagem de front-end da Web com o Gradio
  • Desenvolvimento de serviços de back-end com a FastAPI e a Pydantic
  • Gerenciar variáveis de ambiente no arquivo YAML com o Pydantic-settings
  • Implantar o aplicativo no Cloud Run usando o Dockerfile e fornecer variáveis de ambiente com o arquivo YAML

O que é necessário

  • Navegador da Web Google Chrome
  • Uma conta do Gmail
  • Um projeto do Cloud com faturamento ativado

Este codelab, desenvolvido para desenvolvedores de todos os níveis (inclusive iniciantes), usa o Python no aplicativo de exemplo. No entanto, não é necessário ter conhecimento de Python para entender os conceitos apresentados.

2. Antes de começar

Configurar o projeto do Cloud no editor do Cloud Shell

Este codelab pressupõe que você já tenha um projeto do Google Cloud com o faturamento ativado. Se você ainda não tem uma, siga as instruções abaixo para começar.

  1. 2No console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud.
  2. Verifique se o faturamento está ativado para seu projeto do Cloud. Saiba como verificar se o faturamento está ativado em um projeto .
  3. Você vai usar o Cloud Shell, um ambiente de linha de comando executado no Google Cloud que vem pré-carregado com bq. Clique em "Ativar o Cloud Shell" na parte de cima do console do Google Cloud.

1829c3759227c19b.png

  1. Depois de se conectar ao Cloud Shell, verifique se você já está autenticado e se o projeto está definido como seu ID do projeto usando o seguinte comando:
gcloud auth list
  1. Execute o comando a seguir no Cloud Shell para confirmar se o comando gcloud sabe sobre seu projeto.
gcloud config list project
  1. Se o projeto não estiver definido, use este comando:
gcloud config set project <YOUR_PROJECT_ID>

Também é possível conferir o ID do PROJECT_ID no console.

4032c45803813f30.jpeg

Clique nele e você vai encontrar todo o projeto e o ID do projeto no lado direito.

8dc17eb4271de6b5.jpeg

  1. Ative as APIs necessárias usando o comando mostrado abaixo. Isso pode levar alguns minutos. Tenha paciência.
gcloud services enable aiplatform.googleapis.com \
                           run.googleapis.com \
                           cloudbuild.googleapis.com \
                           cloudresourcemanager.googleapis.com

Se o comando for executado com sucesso, uma mensagem semelhante à mostrada abaixo vai aparecer:

Operation "operations/..." finished successfully.

A alternativa ao comando gcloud é pesquisar cada produto no console ou usar este link.

Se alguma API for perdida, você poderá ativá-la durante a implementação.

Consulte a documentação para ver o uso e os comandos gcloud.

Configurar o diretório de trabalho do aplicativo

  1. Clique no botão "Abrir editor" para abrir o editor do Cloud Shell. Você pode escrever seu código aqui b16d56e4979ec951.png
  2. Verifique se o projeto do Cloud Code está definido no canto inferior esquerdo (barra de status) do editor do Cloud Shell, conforme destacado na imagem abaixo, e se está definido como o projeto ativo do Google Cloud em que você ativou o faturamento. Autorizar, se necessário. Pode levar um tempo após a inicialização do editor do Cloud Shell para que o botão Cloud Code - Fazer login apareça. Seja paciente. Se você já seguiu o comando anterior, o botão também pode apontar diretamente para o projeto ativado, em vez do botão de login.

f5003b9c38b43262.png

  1. Clique no projeto ativo na barra de status e aguarde a abertura do pop-up do Cloud Code. No pop-up, selecione "Novo aplicativo".

70f80078e01a02d8.png

  1. Na lista de aplicativos, escolha IA generativa Gemini e API Gemini Python.

362ff332256d6933.jpeg

85957565316308d9.jpeg

  1. Salve o novo aplicativo com o nome que preferir. Neste exemplo, usaremos gemini-multimodal-chat-assistant e clique em OK.

8409d8db18690fdf.png

Neste ponto, você já deve estar no novo diretório de trabalho do aplicativo e ver os seguintes arquivos:

1ef5bb44f1d2c2a4.png

Em seguida, vamos preparar nosso ambiente Python.

Configuração do ambiente

Preparar o ambiente virtual do Python

A próxima etapa é preparar o ambiente de desenvolvimento. Usaremos o Python 3.12 neste codelab e o uv python project manager para simplificar a necessidade de criar e gerenciar a versão do Python e o ambiente virtual.

  1. Se você ainda não abriu o terminal, clique em Terminal -> Novo terminal ou use Ctrl + Shift + C.

f8457daf0bed059e.jpeg

  1. Faça o download de uv e instale o Python 3.12 com o seguinte comando
curl -LsSf https://astral.sh/uv/0.6.6/install.sh | sh && \
source $HOME/.local/bin/env && \
uv python install 3.12
  1. Agora vamos inicializar o projeto Python usando uv.
uv init
  1. Você vai encontrar main.py, .python-version e pyproject.toml criados no diretório. Esses arquivos são necessários para manter o projeto no diretório. As dependências e configurações do Python podem ser especificadas em pyproject.toml e .python-version padronizaram a versão do Python usada para este projeto. Para saber mais, consulte esta documentação.
main.py
.python-version
pyproject.toml
  1. Para testar, substitua o main.py pelo código a seguir
def main():
   print("Hello from gemini-multimodal-chat-assistant!")

if __name__ == "__main__":
   main()
  1. Em seguida, execute o comando a seguir:
uv run main.py

O resultado será exibido como mostrado abaixo.

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

Isso mostra que o projeto Python está sendo configurado corretamente. Não precisamos criar um ambiente virtual manualmente, porque o uv já faz isso. A partir desse ponto, o comando padrão do Python (por exemplo, python main.py) será substituído por uv run (por exemplo, uv run main.py).

Instalar as dependências necessárias

Também vamos adicionar as dependências do pacote deste codelab usando o comando uv. Execute o comando a seguir

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

A seção "dependencies" do pyproject.toml será atualizada para refletir o comando anterior.

Arquivos de configuração do Setup

Agora vamos configurar os arquivos de configuração para este projeto. Os arquivos de configuração são usados para armazenar variáveis dinâmicas que podem ser facilmente alteradas na nova implantação. Neste projeto, vamos usar arquivos de configuração baseados em YAML com o pacote pydantic-settings para que ele possa ser integrado facilmente à implantação do Cloud Run mais tarde. pydantic-settings é um pacote Python que pode aplicar a verificação de tipo para os arquivos de configuração.

  1. Crie um arquivo chamado settings.yaml com a seguinte configuração. Clique em File->New Text File e preencha com o código abaixo. Em seguida, salve como settings.yaml.
VERTEXAI_LOCATION: "us-central1"
VERTEXAI_PROJECT_ID: "{YOUR-PROJECT-ID}"
BACKEND_URL: "http://localhost:8081/chat"

Atualize os valores de VERTEXAI_PROJECT_ID de acordo com o que você selecionou ao criar o projeto do Google Cloud. Neste codelab, vamos usar os valores pré-configurados para VERTEXAI_LOCATION e BACKEND_URL .

  1. Em seguida, crie o arquivo python settings.py. Esse módulo vai atuar como entrada programática para os valores de configuração nos arquivos de configuração. Clique em File->New Text File e preencha com o código abaixo. Em seguida, salve como settings.py. No código, definimos explicitamente que o arquivo chamado settings.yaml é o que será lido.
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()

Essas configurações permitem que atualizemos o ambiente de execução com flexibilidade. Na implantação inicial, vamos usar a configuração settings.yaml para ter a primeira configuração padrão. Depois disso, podemos atualizar as variáveis de ambiente de forma flexível pelo console e reimplantar, colocando as variáveis de ambiente na prioridade mais alta em comparação com a configuração YAML padrão.

Agora podemos passar para a próxima etapa, criando os serviços.

3. Criar um serviço de front-end usando o Gradio

Vamos criar uma interface da Web de chat com esta aparência

5bcfa1cce6618305.png

Ele contém um campo de entrada para os usuários enviarem texto e fazerem upload de arquivos. Além disso, o usuário também pode substituir a instrução do sistema que será enviada para a API Gemini no campo de entradas adicionais.

Vamos criar o serviço de front-end usando o Gradio. Renomeie main.py para frontend.py e substitua o código usando o código a seguir

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

Depois disso, podemos tentar executar o serviço de front-end com o comando abaixo. Não se esqueça de renomear o arquivo main.py para frontend.py.

uv run frontend.py

Você vai encontrar um resultado semelhante a este no console do Cloud

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

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

Depois disso, você pode verificar a interface da Web ao pressionar Ctrl + clicar no link do URL local. Como alternativa, você também pode acessar o aplicativo de front-end clicando no botão Visualização da Web no canto superior direito do Cloud Editor e selecionando Visualizar na porta 8080.

49cbdfdf77964065.jpeg

A interface da Web aparece, mas você recebe um erro esperado ao tentar enviar o chat porque o serviço de back-end ainda não está configurado.

bd0464140308cfbe.png

Agora, deixe o serviço em execução e não o encerre ainda. Enquanto isso, podemos discutir os componentes importantes do código aqui.

Explicação do código

O código para enviar dados da interface da Web para o back-end está nesta parte

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

Quando queremos enviar dados multimodais para o Gemini e torná-los acessíveis entre os serviços, um mecanismo que podemos usar é converter os dados para o tipo de dados base64, conforme declarado no código. Também precisamos declarar o tipo MIME dos dados. No entanto, a API Gemini não oferece suporte a todos os tipos MIME existentes. Por isso, é importante saber quais tipos MIME compatíveis com o Gemini podem ser lidos nesta documentação. Você pode encontrar as informações em cada um dos recursos da API Gemini (por exemplo, Vision).

Além disso, em uma interface de chat, também é importante enviar o histórico de chat como contexto adicional para que o Gemini tenha uma "memória" da conversa. Nesta interface da Web, também enviamos o histórico de chat gerenciado por sessão da Web pelo Gradio e o enviamos junto com a mensagem inserida pelo usuário. Além disso, também permitimos que o usuário modifique a instrução do sistema e a envie.

4. Criar um serviço de back-end usando a FastAPI

Em seguida, vamos precisar criar o back-end que pode processar o payload discutido anteriormente, a última mensagem do usuário, o histórico de bate-papo e a instrução do sistema. Vamos usar a FastAPI para criar o serviço de back-end HTTP.

Crie um novo arquivo, clique em File->New Text File e copie e cole o código a seguir. Em seguida,salve como backend.py.

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

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

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


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

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

    data: str
    mime_type: str


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

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

    role: str
    content: str | List[FileData]


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

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

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


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

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

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


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

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

    response: str
    error: Optional[str] = None


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

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

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


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

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

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

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

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

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

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

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

    return converted_messages


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

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

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

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

        # Prepare multimodal content
        content_parts = []

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

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

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

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


if __name__ == "__main__":
    import uvicorn

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

Não se esqueça de salvar como backend.py. Depois disso, podemos tentar executar o serviço de back-end. Lembre-se de que na etapa anterior, executamos o serviço de front-end. Agora, precisamos abrir um novo terminal e tentar executar o serviço de back-end.

  1. Crie um terminal. Navegue até o terminal na área de baixo e encontre o botão "+" para criar um novo terminal. Você também pode usar Ctrl + Shift + C para abrir um novo terminal.

3e52a362475553dc.jpeg

  1. Depois disso, verifique se você está no diretório de trabalho gemini-multimodal-chat-assistant e execute o seguinte comando:
uv run backend.py
  1. Se for bem-sucedido, a saída será exibida assim:
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)

Explicação do código

Como definir a rota HTTP para receber solicitações de chat

No FastAPI, definimos a rota usando o decorador app. Também usamos o Pydantic para definir o contrato da API. Especificamos que a rota para gerar a resposta está na rota /chat com o método POST. Essas funcionalidades são declaradas no código a seguir

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

Preparar o formato do histórico de chat do SDK Gemini

Uma das coisas importantes que precisam ser entendidas é como reestruturar o histórico de bate-papo para que ele possa ser inserido como um valor de argumento history ao inicializarmos um cliente Gemini mais tarde. Confira o código abaixo.

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

Para fornecer o histórico de chat no SDK Gemini, precisamos formatar os dados no tipo de dados List[Content]. Cada Conteúdo precisa ter pelo menos um valor de papel e partes. Papel se refere à origem da mensagem, seja ela usuário ou modelo. Em que partes se referem ao comando em si, que pode ser apenas texto ou uma combinação de diferentes modalidades. Saiba como estruturar argumentos de conteúdo em detalhes nesta documentação.

Processar dados não textuais ( multimodais)

Como mencionado anteriormente na seção de front-end, uma das maneiras de enviar dados não textuais ou multimodais é enviá-los como string base64. Também precisamos especificar o tipo MIME dos dados para que eles sejam interpretados corretamente. Por exemplo, forneça o tipo MIME image/jpeg se enviarmos dados de imagem com um sufixo .jpg.

Esta parte do código converte os dados base64 no formato Part.from_bytes do SDK Gemini.

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. Teste de integração

Agora, você deve ter vários serviços executados em diferentes guias do console do Cloud:

  • Serviço de front-end executado na porta 8080
* Running on local URL:  http://0.0.0.0:8080

To create a public link, set `share=True` in `launch()`.
  • Serviço de back-end executado na porta 8081
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8081 (Press CTRL+C to quit)

No estado atual, você pode enviar seus documentos no chat com o assistente do aplicativo da Web na porta 8080. Comece a fazer experimentos enviando arquivos e fazendo perguntas. Alguns tipos de arquivos ainda não têm suporte e vão gerar erros.

Você também pode editar as instruções do sistema no campo Entradas adicionais abaixo da caixa de texto.

ee9c849a276d378.png

6. Como implantar no Cloud Run

Agora, é claro que queremos mostrar esse app incrível para os outros. Para isso, podemos empacotar esse aplicativo e implantá-lo no Cloud Run como um serviço público que pode ser acessado por outras pessoas. Para isso, vamos analisar a arquitetura novamente.

b102df2c3f1adabf.jpeg

Neste codelab, vamos colocar o serviço de front-end e de back-end em um contêiner. Vamos precisar da ajuda do supervisord para gerenciar os dois serviços.

Crie um novo arquivo, clique em Arquivo->Novo arquivo de texto e copie e cole o código a seguir. Em seguida,salve como supervisord.conf.

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

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

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

Em seguida, vamos precisar do Dockerfile. Clique em File->New Text File e copie e cole o código abaixo. Em seguida, salve como Dockerfile.

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

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

ADD . /app
WORKDIR /app

RUN uv sync --frozen

EXPOSE 8080

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

ENV PYTHONUNBUFFERED=1

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

Neste ponto, já temos todos os arquivos necessários para implantar nossos aplicativos no Cloud Run. Vamos implantar. Navegue até o terminal do Cloud Shell e verifique se o projeto atual está configurado como seu projeto ativo. Caso contrário, use o comando gcloud configure para definir o ID do projeto:

gcloud config set project [PROJECT_ID]

Em seguida, execute o comando abaixo para implantar no Cloud Run.

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

Você vai precisar inserir um nome para o serviço, por exemplo, gemini-multimodal-chat-assistant. Como temos o Dockerfile no diretório de trabalho do aplicativo, ele vai criar o contêiner do Docker e enviá-lo para o Artifact Registry. Ele também vai informar que vai criar o repositório do Artifact Registry na região. Responda "Y". Diga y quando for perguntado se você quer permitir invocações não autenticadas. Estamos permitindo o acesso não autenticado porque este é um aplicativo de demonstração. Recomendamos usar a autenticação adequada para seus aplicativos corporativos e de produção.

Quando a implantação for concluída, você receberá um link semelhante ao abaixo:

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

Use o aplicativo na janela anônima ou no dispositivo móvel. Ele já deve estar ativo.

7. Desafio

Agora é sua vez de brilhar e aperfeiçoar suas habilidades de exploração. Você tem o que é necessário para mudar o código para que o assistente possa ler arquivos de áudio ou vídeo?

8. Limpar

Para evitar cobranças na sua conta do Google Cloud pelos recursos usados neste codelab, siga estas etapas:

  1. No console do Google Cloud, acesse a página Gerenciar recursos.
  2. Na lista de projetos, selecione o projeto que você quer excluir e clique em Excluir.
  3. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.
  4. Também é possível acessar o Cloud Run no console, selecionar o serviço que você acabou de implantar e excluir.