Criar um app Flutter com o Gemini

1. Criar um app do Flutter com tecnologia do Gemini

O que você vai criar

Neste codelab, você vai criar o Colorist, um aplicativo interativo do Flutter que traz o poder da API Gemini diretamente para seu app Flutter. Já quis permitir que os usuários controlassem seu app por linguagem natural, mas não sabia por onde começar? Este codelab mostra como fazer isso.

O Colorist permite que os usuários descrevam cores em linguagem natural (como "o laranja de um pôr do sol" ou "azul profundo do oceano"), e o app:

  • Processa essas descrições usando a API Gemini do Google
  • Interpreta as descrições em valores de cores RGB precisos
  • Mostra a cor na tela em tempo real
  • Fornece detalhes técnicos e contexto interessante sobre a cor.
  • Mantém um histórico de cores geradas recentemente.

Captura de tela do app Colorist mostrando a tela de cores e a interface de chat

O app tem uma interface de tela dividida com uma área de exibição colorida e um sistema de chat interativo de um lado, e um painel de registros detalhado mostrando as interações brutas do LLM do outro lado. Com esse registro, você entende melhor como uma integração de LLM funciona de verdade.

Por que isso é importante para desenvolvedores do Flutter

Os LLMs estão revolucionando a forma como os usuários interagem com os aplicativos, mas a integração deles em apps para dispositivos móveis e computadores apresenta desafios únicos. Este codelab ensina padrões práticos que vão além das chamadas de API brutas.

Sua jornada de aprendizado

Este codelab orienta você no processo de criação do Colorist etapa por etapa:

  1. Configuração do projeto: você vai começar com uma estrutura básica de app do Flutter e o pacote colorist_ui.
  2. Integração básica do Gemini: conecte seu app ao Firebase AI Logic e implemente a comunicação LLM
  3. Comandos eficazes: crie um comando do sistema que oriente o LLM a entender descrições de cores.
  4. Declarações de função: definem ferramentas que o LLM pode usar para definir cores no seu aplicativo.
  5. Processamento de ferramentas: processa chamadas de função do LLM e as conecta ao estado do seu app.
  6. Respostas de streaming: melhore a experiência do usuário com respostas de LLM de streaming em tempo real.
  7. Sincronização de contexto do LLM: crie uma experiência coesa informando o LLM sobre as ações do usuário.

O que você vai aprender

  • Configurar o Firebase AI Logic para aplicativos Flutter
  • Criar comandos de sistema eficazes para orientar o comportamento do LLM
  • Implemente declarações de função que conectam a linguagem natural e os recursos do app
  • Processar respostas de streaming para uma experiência do usuário responsiva
  • Sincronizar o estado entre eventos da interface e o LLM
  • Gerenciar o estado da conversa do LLM usando o Riverpod
  • Lidar com erros de maneira adequada em aplicativos com tecnologia de LLM

Prévia do código: uma amostra do que você vai implementar

Confira um exemplo da declaração de função que você vai criar para permitir que o LLM defina cores no seu app:

FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
  'set_color',
  'Set the color of the display square based on red, green, and blue values.',
  parameters: {
    'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
    'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
    'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
  },
);

Um vídeo de visão geral deste codelab

Assista Craig Labenz e Andrew Brogdon discutirem este codelab no episódio 59 do Observable Flutter:

Pré-requisitos

Para aproveitar ao máximo este codelab, você precisa ter:

  • Experiência em desenvolvimento com Flutter: familiaridade com os conceitos básicos do Flutter e a sintaxe do Dart
  • Conhecimento de programação assíncrona: compreensão de Futures, async/await e streams.
  • Conta do Firebase: você precisa de uma Conta do Google para configurar o Firebase.

Vamos começar a criar seu primeiro app do Flutter com tecnologia de LLM!

2. Configuração do projeto e serviço de eco

Nesta primeira etapa, você vai configurar a estrutura do projeto e implementar um serviço de eco que será substituído mais tarde pela integração da API Gemini. Isso estabelece a arquitetura do aplicativo e garante que a interface esteja funcionando corretamente antes de adicionar a complexidade das chamadas de LLM.

O que você vai aprender nesta etapa

  • Configurar um projeto do Flutter com as dependências necessárias
  • Como trabalhar com o pacote colorist_ui para componentes de UI
  • Implementar um serviço de mensagens de eco e conectá-lo à interface

Criar um projeto do Flutter

Comece criando um projeto do Flutter com o seguinte comando:

flutter create -e colorist --platforms=android,ios,macos,web,windows

A flag -e indica que você quer um projeto vazio sem o app counter padrão. O app foi projetado para funcionar em computadores, dispositivos móveis e na Web. No entanto, o flutterfire não oferece suporte para Linux no momento.

Adicionar dependências

Navegue até o diretório do projeto e adicione as dependências necessárias:

cd colorist
flutter pub add colorist_ui flutter_riverpod riverpod_annotation
flutter pub add --dev build_runner riverpod_generator riverpod_lint json_serializable custom_lint

Isso vai adicionar os seguintes pacotes principais:

  • colorist_ui: um pacote personalizado que fornece os componentes de UI para o app Colorist.
  • flutter_riverpod e riverpod_annotation: para gerenciamento de estado
  • logging: para geração de registros estruturados
  • Dependências de desenvolvimento para geração de código e linting

Seu pubspec.yaml vai ficar parecido com este:

pubspec.yaml

name: colorist
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.9.2

dependencies:
  flutter:
    sdk: flutter
  colorist_ui: ^0.3.0
  flutter_riverpod: ^3.0.0
  riverpod_annotation: ^3.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0
  build_runner: ^2.7.1
  riverpod_generator: ^3.0.0
  riverpod_lint: ^3.0.0
  json_serializable: ^6.11.1

flutter:
  uses-material-design: true

Configurar opções de análise

Adicione custom_lint ao arquivo analysis_options.yaml na raiz do projeto:

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

Essa configuração ativa lints específicos do Riverpod para ajudar a manter a qualidade do código.

Implementar o arquivo main.dart

Substitua o conteúdo de lib/main.dart pelo seguinte:

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() async {
  runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: MainScreen(
        sendMessage: (message) {
          sendMessage(message, ref);
        },
      ),
    );
  }

  // A fake LLM that just echoes back what it receives.
  void sendMessage(String message, WidgetRef ref) {
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    chatStateNotifier.addLlmMessage(message, MessageState.complete);
    logStateNotifier.logLlmText(message);
  }
}

Isso configura um app Flutter que implementa um serviço de eco que imita o comportamento de um LLM retornando a mensagem do usuário.

Noções básicas sobre a arquitetura

Vamos entender a arquitetura do app colorist:

O pacote colorist_ui

O pacote colorist_ui oferece componentes de interface pré-criados e ferramentas de gerenciamento de estado:

  1. MainScreen: o principal componente da interface que mostra:
    • Um layout de tela dividida no computador (área de interação e painel de registros)
    • Uma interface com guias em dispositivos móveis
    • Tela colorida, interface de chat e miniaturas do histórico
  2. Gerenciamento de estado: o app usa vários notificadores de estado:
    • ChatStateNotifier: gerencia as mensagens do chat.
    • ColorStateNotifier: gerencia a cor atual e o histórico.
    • LogStateNotifier: gerencia as entradas de registro para depuração.
  3. Gerenciamento de mensagens: o app usa um modelo de mensagens com diferentes estados:
    • Mensagens do usuário: inseridas pelo usuário
    • Mensagens do LLM: geradas pelo LLM (ou pelo seu serviço de eco, por enquanto)
    • MessageState: rastreia se as mensagens do LLM estão completas ou ainda sendo transmitidas.

Arquitetura do aplicativo

O app segue a seguinte arquitetura:

  1. Camada de interface: fornecida pelo pacote colorist_ui
  2. Gerenciamento de estado: usa o Riverpod para gerenciamento de estado reativo.
  3. Camada de serviço: atualmente contém seu serviço de eco simples, que será substituído pelo serviço do Gemini Chat.
  4. Integração de LLM: será adicionada em etapas posteriores

Essa separação permite que você se concentre na implementação da integração do LLM enquanto os componentes da interface já estão prontos.

Executar o app

Execute o app com o seguinte comando:

flutter run -d DEVICE

Substitua DEVICE pelo dispositivo de destino, como macos, windows, chrome ou um ID do dispositivo.

Captura de tela do app Colorist mostrando o serviço de eco renderizando markdown

Agora você vai ver o app Colorist com:

  1. Uma área de exibição de cores com uma cor padrão
  2. Uma interface de chat em que você pode digitar mensagens
  3. Um painel de registros mostrando as interações de chat

Tente digitar uma mensagem como "Quero uma cor azul escura" e pressione Enviar. O serviço de eco vai apenas repetir sua mensagem. Nas etapas posteriores, você vai substituir isso pela interpretação de cores real usando a lógica de IA do Firebase.

A seguir

Na próxima etapa, você vai configurar o Firebase e implementar a integração básica da API Gemini para substituir o serviço de eco pelo serviço de chat do Gemini. Isso permite que o app interprete descrições de cores e dê respostas inteligentes.

Solução de problemas

Problemas com pacotes da interface

Se você encontrar problemas com o pacote colorist_ui:

  • Verifique se você está usando a versão mais recente
  • Verifique se você adicionou a dependência corretamente
  • Verifique se há versões de pacotes conflitantes

Erros de build

Se você encontrar erros de build:

  • Verifique se você tem a versão mais recente do SDK do Flutter no canal estável instalada
  • Execute flutter clean seguido de flutter pub get
  • Verifique a saída do console para mensagens de erro específicas.

Principais conceitos aprendidos

  • Configurar um projeto do Flutter com as dependências necessárias
  • Entender a arquitetura do aplicativo e as responsabilidades dos componentes
  • Implementar um serviço simples que imita o comportamento de um LLM
  • Como conectar o serviço aos componentes da interface
  • Como usar o Riverpod para gerenciamento de estado

3. Integração básica do Gemini Chat

Nesta etapa, você vai substituir o serviço de eco da etapa anterior pela integração da API Gemini usando o Firebase AI Logic. Você vai configurar o Firebase, definir os provedores necessários e implementar um serviço de chat básico que se comunica com a API Gemini.

O que você vai aprender nesta etapa

  • Configurar o Firebase em um aplicativo Flutter
  • Como configurar o Firebase AI Logic para acesso ao Gemini
  • Como criar provedores do Riverpod para serviços do Firebase e do Gemini
  • Implementar um serviço de chat básico com a API Gemini
  • Como processar respostas assíncronas da API e estados de erro

Configurar o Firebase

Primeiro, configure o Firebase para seu projeto Flutter. Isso envolve criar um projeto do Firebase, adicionar seu app a ele e configurar as opções necessárias da API Firebase AI Logic.

Criar um projeto do Firebase

  1. Acesse o Console do Firebase e faça login com sua Conta do Google.
  2. Clique em Criar um projeto do Firebase ou selecione um projeto atual.
  3. Siga as etapas do assistente de configuração para criar seu projeto.

Configurar o Firebase AI Logic no seu projeto do Firebase

  1. No console do Firebase, acesse seu projeto.
  2. Na barra lateral à esquerda, selecione IA.
  3. No menu suspenso de IA, selecione Lógica de IA.
  4. No card do Firebase AI Logic, selecione Começar.
  5. Siga as instruções para ativar a API Gemini Developer no seu projeto.

Instalar a CLI do FlutterFire

A CLI do FlutterFire simplifica a configuração do Firebase em apps Flutter:

dart pub global activate flutterfire_cli

Adicionar o Firebase ao seu app Flutter

  1. Adicione os pacotes do Firebase Core e da Lógica de IA do Firebase ao seu projeto:
flutter pub add firebase_core firebase_ai
  1. Execute o comando de configuração do FlutterFire:
flutterfire configure

Esse comando vai:

  • Pedir para selecionar o projeto do Firebase que você acabou de criar
  • Registre seus apps do Flutter com o Firebase
  • Gere um arquivo firebase_options.dart com a configuração do projeto.

O comando detecta automaticamente as plataformas selecionadas (iOS, Android, macOS, Windows, Web) e as configura adequadamente.

Configuração específica da plataforma

O Firebase exige versões mínimas mais recentes do que as padrão do Flutter. Ele também exige acesso à rede para se comunicar com os servidores do Firebase AI Logic.

Configurar permissões do macOS

Para macOS, é necessário ativar o acesso à rede nos direitos do app:

  1. Abra macos/Runner/DebugProfile.entitlements e adicione:

macos/Runner/DebugProfile.entitlements (link em inglês)

<key>com.apple.security.network.client</key>
<true/>
  1. Abra também macos/Runner/Release.entitlements e adicione a mesma entrada.

Configurar os ajustes do iOS

No iOS, atualize a versão mínima na parte de cima de ios/Podfile:

ios/Podfile

# Firebase requires at least iOS 15.0
platform :ios, '15.0'

Criar provedores de modelos do Gemini

Agora você vai criar os provedores do Riverpod para Firebase e Gemini. Crie um arquivo lib/providers/gemini.dart:

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';

part 'gemini.g.dart';

@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
  await ref.watch(firebaseAppProvider.future);

  final model = FirebaseAI.googleAI().generativeModel(
    model: 'gemini-2.0-flash',
  );
  return model;
}

@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
  final model = await ref.watch(geminiModelProvider.future);
  return model.startChat();
}

Esse arquivo define a base para três provedores de chaves. Esses provedores são gerados quando você executa dart run build_runner pelos geradores de código do Riverpod. Esse código usa a abordagem baseada em anotações do Riverpod 3 com padrões de provedor atualizados.

  1. firebaseAppProvider: inicializa o Firebase com a configuração do seu projeto.
  2. geminiModelProvider: cria uma instância de modelo generativo do Gemini
  3. chatSessionProvider: cria e mantém uma sessão de chat com o modelo do Gemini.

A anotação keepAlive: true na sessão de chat garante que ela persista durante todo o ciclo de vida do app, mantendo o contexto da conversa.

Implementar o serviço de chat do Gemini

Crie um novo arquivo lib/services/gemini_chat_service.dart para implementar o serviço de chat:

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';

part 'gemini_chat_service.g.dart';

class GeminiChatService {
  GeminiChatService(this.ref);
  final Ref ref;

  Future<void> sendMessage(String message) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    final llmMessage = chatStateNotifier.createLlmMessage();
    try {
      final response = await chatSession.sendMessage(Content.text(message));

      final responseText = response.text;
      if (responseText != null) {
        logStateNotifier.logLlmText(responseText);
        chatStateNotifier.appendToMessage(llmMessage.id, responseText);
      }
    } catch (e, st) {
      logStateNotifier.logError(e, st: st);
      chatStateNotifier.appendToMessage(
        llmMessage.id,
        "\nI'm sorry, I encountered an error processing your request. "
        "Please try again.",
      );
    } finally {
      chatStateNotifier.finalizeMessage(llmMessage.id);
    }
  }
}

@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

Este serviço:

  1. Aceita mensagens do usuário e as envia para a API Gemini.
  2. Atualiza a interface de chat com respostas do modelo
  3. Registra todas as comunicações para facilitar a compreensão do fluxo real do LLM
  4. Gerencia erros com feedback adequado para o usuário

Observação:a janela de registro vai parecer quase idêntica à janela de chat neste momento. O registro vai ficar mais interessante quando você introduzir chamadas de função e respostas de streaming.

Gerar código do Riverpod

Execute o comando do build runner para gerar o código Riverpod necessário:

dart run build_runner build --delete-conflicting-outputs

Isso vai criar os arquivos .g.dart necessários para o funcionamento do Riverpod.

Atualizar o arquivo main.dart

Atualize o arquivo lib/main.dart para usar o novo serviço de chat do Gemini:

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';

void main() async {
  runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final model = ref.watch(geminiModelProvider);

    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: model.when(
        data: (data) => MainScreen(
          sendMessage: (text) {
            ref.read(geminiChatServiceProvider).sendMessage(text);
          },
        ),
        loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
        error: (err, st) => ErrorScreen(error: err),
      ),
    );
  }
}

As principais mudanças nesta atualização são:

  1. Substituir o serviço de eco pelo serviço de chat baseado na API Gemini
  2. Adicionar telas de carregamento e erro usando o padrão AsyncValue do Riverpod com o método when
  3. Como conectar a interface ao novo serviço de chat usando o callback sendMessage

Executar o app

Execute o app com o seguinte comando:

flutter run -d DEVICE

Substitua DEVICE pelo dispositivo de destino, como macos, windows, chrome ou um ID do dispositivo.

Captura de tela do app Colorist mostrando o LLM do Gemini respondendo a uma solicitação de uma cor amarela ensolarada

Agora, quando você digitar uma mensagem, ela será enviada para a API Gemini, e você vai receber uma resposta do LLM em vez de um eco. O painel de registros vai mostrar as interações com a API.

Como entender a comunicação com LLMs

Vamos entender o que acontece quando você se comunica com a API Gemini:

O fluxo de comunicação

  1. Entrada do usuário: o usuário insere texto na interface de chat
  2. Formatação de solicitação: o app formata o texto como um objeto Content para a API Gemini.
  3. Comunicação da API: o texto é enviado para a API Gemini pela Firebase AI Logic.
  4. Processamento de LLM: o modelo Gemini processa o texto e gera uma resposta.
  5. Processamento de respostas: o app recebe a resposta e atualiza a interface.
  6. Registro: toda a comunicação é registrada para garantir a transparência.

Sessões de chat e contexto da conversa

A sessão de chat do Gemini mantém o contexto entre as mensagens, permitindo interações conversacionais. Isso significa que o LLM "lembra" das trocas anteriores na sessão atual, permitindo conversas mais coerentes.

A anotação keepAlive: true no provedor de sessão de chat garante que esse contexto persista durante todo o ciclo de vida do app. Esse contexto persistente é crucial para manter um fluxo de conversa natural com o LLM.

A seguir

Neste ponto, você pode perguntar qualquer coisa à API Gemini, já que não há restrições sobre o que ela pode responder. Por exemplo, você pode pedir um resumo das Guerras das Rosas, que não está relacionado ao propósito do app de cores.

Na próxima etapa, você vai criar um comando do sistema para orientar o Gemini a interpretar descrições de cores de maneira mais eficaz. Isso vai demonstrar como personalizar o comportamento de um LLM para necessidades específicas do aplicativo e concentrar os recursos no domínio do seu app.

Solução de problemas

Problemas de configuração do Firebase

Se você encontrar erros na inicialização do Firebase:

  • Verifique se o arquivo firebase_options.dart foi gerado corretamente
  • Verifique se você fez upgrade para o plano Blaze para ter acesso à Firebase AI Logic

Erros de acesso à API

Se você receber erros ao acessar a API Gemini:

  • Confirme se o faturamento está configurado corretamente no seu projeto do Firebase.
  • Verifique se o Firebase AI Logic e a API Cloud AI estão ativados no seu projeto do Firebase
  • Verificar a conectividade de rede e as configurações de firewall
  • Verifique se o nome do modelo (gemini-2.0-flash) está correto e disponível.

Problemas de contexto da conversa

Se você notar que o Gemini não se lembra do contexto anterior da conversa:

  • Confirme se a função chatSession está anotada com @Riverpod(keepAlive: true)
  • Verifique se você está reutilizando a mesma sessão de chat para todas as trocas de mensagens.
  • Verifique se a sessão de chat foi inicializada corretamente antes de enviar mensagens.

Problemas específicos da plataforma

Para problemas específicos da plataforma:

  • iOS/macOS: verifique se os direitos adequados estão definidos e se as versões mínimas estão configuradas.
  • Android: verificar se a versão mínima do SDK está definida corretamente
  • Verificar mensagens de erro específicas da plataforma no console

Principais conceitos aprendidos

  • Configurar o Firebase em um aplicativo Flutter
  • Configurar o Firebase AI Logic para acessar o Gemini
  • Como criar provedores do Riverpod para serviços assíncronos
  • Implementar um serviço de chat que se comunica com um LLM
  • Como processar estados assíncronos da API (carregamento, erro, dados)
  • Entender o fluxo de comunicação e as sessões de chat dos LLMs

4. Comandos eficazes para descrições de cores

Nesta etapa, você vai criar e implementar um comando do sistema que orienta o Gemini na interpretação de descrições de cores. Os comandos do sistema são uma maneira eficiente de personalizar o comportamento do LLM para tarefas específicas sem mudar o código.

O que você vai aprender nesta etapa

  • Entender os comandos do sistema e a importância deles em aplicativos de LLM
  • Como criar comandos eficazes para tarefas específicas do domínio
  • Carregar e usar comandos do sistema em um app do Flutter
  • Orientar um LLM para fornecer respostas com formatação consistente
  • Testar como os comandos do sistema afetam o comportamento do LLM

Entender os comandos do sistema

Antes de começar a implementação, vamos entender o que são comandos do sistema e por que eles são importantes:

O que são comandos do sistema?

Um comando do sistema é um tipo especial de instrução dada a um LLM que define o contexto, as diretrizes de comportamento e as expectativas para as respostas dele. Ao contrário das mensagens do usuário, os comandos do sistema:

  • Definir a função e o perfil do LLM
  • Definir conhecimentos ou recursos especializados
  • Fornecer instruções de formatação
  • Definir restrições nas respostas
  • Descrever como lidar com vários cenários

Pense em um comando do sistema como se você estivesse ao LLM a "descrição do trabalho" dele. Ele informa ao modelo como se comportar durante a conversa.

Por que os comandos do sistema são importantes

Os comandos do sistema são essenciais para criar interações consistentes e úteis com LLMs porque:

  1. Garantir a consistência: oriente o modelo a fornecer respostas em um formato consistente.
  2. Melhorar a relevância: concentre o modelo no seu domínio específico (no seu caso, cores).
  3. Estabeleça limites: defina o que o modelo pode e não pode fazer
  4. Melhorar a experiência do usuário: crie um padrão de interação mais natural e útil.
  5. Reduzir o pós-processamento: receba respostas em formatos mais fáceis de analisar ou mostrar.

Para o app Colorist, você precisa que o LLM interprete as descrições de cores de maneira consistente e forneça valores RGB em um formato específico.

Criar um recurso de comando do sistema

Primeiro, crie um arquivo de solicitação do sistema que será carregado durante a execução. Essa abordagem permite modificar o aviso sem recompilar o app.

Crie um novo arquivo assets/system_prompt.md com o seguinte conteúdo:

assets/system_prompt.md

# Colorist System Prompt

You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and provide the appropriate RGB values that best represent that description.

## Your Capabilities

You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. When users describe a color, you should:

1. Analyze their description to understand the color they are trying to convey
2. Determine the appropriate RGB values (values should be between 0.0 and 1.0)
3. Respond with a conversational explanation and explicitly state the RGB values

## How to Respond to User Inputs

When users describe a color:

1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Always include the RGB values clearly in your response, formatted as: `RGB: (red=X.X, green=X.X, blue=X.X)`
4. Provide a brief explanation of your interpretation

Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones.

RGB: (red=1.0, green=0.5, blue=0.25)

I've selected values with high red, moderate green, and low blue to capture that beautiful sunset glow. This creates a warm orange with a slightly reddish tint, reminiscent of the sun low on the horizon."

## When Descriptions are Unclear

If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.

## Important Guidelines

- Always keep RGB values between 0.0 and 1.0
- Always format RGB values as: `RGB: (red=X.X, green=X.X, blue=X.X)` for easy parsing
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations

Entender a estrutura do comando do sistema

Vamos detalhar o que esse comando faz:

  1. Definição da função: estabelece o LLM como um "assistente especialista em cores"
  2. Explicação da tarefa: define a tarefa principal como interpretar descrições de cores em valores RGB.
  3. Formato da resposta: especifica exatamente como os valores RGB devem ser formatados para consistência.
  4. Exemplo de troca: fornece um exemplo concreto do padrão de interação esperado.
  5. Tratamento de casos extremos: instrui como lidar com descrições pouco claras
  6. Restrições e diretrizes: define limites, como manter os valores RGB entre 0,0 e 1,0.

Essa abordagem estruturada garante que as respostas do LLM sejam consistentes, informativas e formatadas de uma maneira fácil de analisar se você quiser extrair os valores RGB de forma programática.

Atualizar o pubspec.yaml

Agora, atualize a parte de baixo do pubspec.yaml para incluir o diretório de recursos:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

Execute flutter pub get para atualizar o pacote de recursos.

Criar um provedor de comandos do sistema

Crie um arquivo lib/providers/system_prompt.dart para carregar o comando do sistema:

lib/providers/system_prompt.dart

import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'system_prompt.g.dart';

@Riverpod(keepAlive: true)
Future<String> systemPrompt(Ref ref) =>
    rootBundle.loadString('assets/system_prompt.md');

Esse provedor usa o sistema de carregamento de recursos do Flutter para ler o arquivo de solicitação durante a execução.

Atualizar o provedor do modelo Gemini

Agora modifique o arquivo lib/providers/gemini.dart para incluir o comando do sistema:

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';
import 'system_prompt.dart';                                          // Add this import

part 'gemini.g.dart';

@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
  await ref.watch(firebaseAppProvider.future);
  final systemPrompt = await ref.watch(systemPromptProvider.future);  // Add this line

  final model = FirebaseAI.googleAI().generativeModel(
    model: 'gemini-2.0-flash',
    systemInstruction: Content.system(systemPrompt),                  // And this line
  );
  return model;
}

@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
  final model = await ref.watch(geminiModelProvider.future);
  return model.startChat();
}

A principal mudança é adicionar systemInstruction: Content.system(systemPrompt) ao criar o modelo generativo. Isso informa ao Gemini para usar suas instruções como o comando do sistema em todas as interações nesta sessão de chat.

Gerar código do Riverpod

Execute o comando do build runner para gerar o código Riverpod necessário:

dart run build_runner build --delete-conflicting-outputs

executar e testar o aplicativo

Agora execute seu aplicativo:

flutter run -d DEVICE

Captura de tela do app Colorist mostrando o LLM do Gemini respondendo com uma resposta no personagem para um app de seleção de cores

Teste com várias descrições de cores:

  • "Quero um azul-celeste"
  • "Me dê um verde floresta"
  • "Crie um laranja vibrante de pôr do sol"
  • "Quero a cor da lavanda fresca"
  • "Mostre algo como um azul profundo do oceano"

O Gemini agora responde com explicações conversacionais sobre as cores e valores RGB formatados de maneira consistente. O comando do sistema orientou o LLM a fornecer o tipo de respostas que você precisa.

Você também pode pedir conteúdo fora do contexto de cores. Por exemplo, as principais causas das Guerras das Rosas. Você vai notar uma diferença em relação à etapa anterior.

A importância da engenharia de comandos para tarefas especializadas

Os comandos do sistema são uma arte e uma ciência. Eles são uma parte essencial da integração de LLMs que pode afetar muito a utilidade do modelo para seu aplicativo específico. O que você fez aqui é uma forma de engenharia de comandos: adaptar instruções para que o modelo se comporte de acordo com as necessidades do seu aplicativo.

A engenharia de comando eficaz envolve:

  1. Definição clara da função: estabelecer a finalidade do LLM
  2. Instruções explícitas: detalham exatamente como o LLM deve responder.
  3. Exemplos concretos: mostrar em vez de apenas dizer como são as boas respostas
  4. Tratamento de casos extremos: instruir o LLM sobre como lidar com cenários ambíguos
  5. Especificações de formatação: garantir que as respostas sejam estruturadas de maneira consistente e utilizável

O comando do sistema que você criou transforma os recursos genéricos do Gemini em um assistente especializado em interpretação de cores que oferece respostas formatadas especificamente para as necessidades do seu aplicativo. Esse é um padrão eficiente que pode ser aplicado a vários domínios e tarefas.

A seguir

Na próxima etapa, você vai criar essa base adicionando declarações de função, que permitem que o LLM não apenas sugira valores RGB, mas também chame funções no seu app para definir a cor diretamente. Isso demonstra como os LLMs podem reduzir a distância entre a linguagem natural e os recursos concretos de aplicativos.

Solução de problemas

Problemas no carregamento de recursos

Se você encontrar erros ao carregar o comando do sistema:

  • Verifique se o pubspec.yaml lista corretamente o diretório de recursos
  • Verifique se o caminho em rootBundle.loadString() corresponde ao local do arquivo.
  • Execute flutter clean seguido de flutter pub get para atualizar o pacote de recursos.

Respostas inconsistentes

Se o LLM não seguir suas instruções de formato de maneira consistente:

  • Tente deixar os requisitos de formato mais explícitos no comando do sistema
  • Adicione mais exemplos para demonstrar o padrão esperado
  • Verifique se o formato solicitado é adequado para o modelo.

Limitação de taxa de API

Se você encontrar erros relacionados à limitação de taxa:

  • O serviço Firebase AI Logic tem limites de uso.
  • Implementar a lógica de repetição com espera exponencial
  • Verifique se há problemas de cota no console do Firebase.

Principais conceitos aprendidos

  • Entender a função e a importância dos comandos do sistema em aplicativos de LLM
  • Como criar comandos eficazes com instruções, exemplos e restrições claras
  • Carregar e usar comandos do sistema em um aplicativo Flutter
  • Orientar o comportamento de LLMs para tarefas específicas do domínio
  • Como usar a engenharia de comandos para moldar as respostas dos LLMs

Esta etapa demonstra como personalizar significativamente o comportamento do LLM sem mudar o código. Basta fornecer instruções claras no comando do sistema.

5. Declarações de função para ferramentas de LLM

Nesta etapa, você vai começar a permitir que o Gemini realize ações no seu app implementando declarações de função. Com esse recurso avançado, o LLM não apenas sugere valores RGB, mas também os define na interface do seu app usando chamadas de ferramentas especializadas. No entanto, será necessário seguir para a próxima etapa para ver as solicitações de LLM executadas no app Flutter.

O que você vai aprender nesta etapa

  • Entender a chamada de função da LLM e os benefícios dela para aplicativos Flutter
  • Como definir declarações de função baseadas em esquema para o Gemini
  • Como integrar declarações de função ao seu modelo do Gemini
  • Atualizar o comando do sistema para usar os recursos da ferramenta

Noções básicas sobre as chamadas de função

Antes de implementar declarações de função, vamos entender o que elas são e por que são importantes:

O que é a chamada de função?

A chamada de função (às vezes chamada de "uso de ferramentas") é um recurso que permite que um LLM:

  1. Reconhecer quando uma solicitação do usuário se beneficiaria da invocação de uma função específica
  2. Gerar um objeto JSON estruturado com os parâmetros necessários para essa função
  3. Permita que o aplicativo execute a função com esses parâmetros
  4. Receber o resultado da função e incorporá-lo à resposta

Em vez de apenas descrever o que fazer, a chamada de função permite que a LLM acione ações concretas no seu aplicativo.

Por que a chamada de função é importante para apps Flutter

A chamada de função cria uma ponte poderosa entre a linguagem natural e os recursos do aplicativo:

  1. Ação direta: os usuários podem descrever o que querem em linguagem natural, e o app responde com ações concretas.
  2. Saída estruturada: o LLM gera dados limpos e estruturados em vez de texto que precisa ser analisado.
  3. Operações complexas: permite que o LLM acesse dados externos, faça cálculos ou modifique o estado do aplicativo.
  4. Melhor experiência do usuário: cria uma integração perfeita entre conversa e funcionalidade

No app Colorist, a chamada de função permite que os usuários digam "Quero um verde-floresta" e a interface seja atualizada imediatamente com essa cor, sem precisar analisar valores RGB do texto.

Definir declarações de função

Crie um arquivo lib/services/gemini_tools.dart para definir as declarações de função:

lib/services/gemini_tools.dart

import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

  FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
    'set_color',
    'Set the color of the display square based on red, green, and blue values.',
    parameters: {
      'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
      'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
      'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
    },
  );

  List<Tool> get tools => [
    Tool.functionDeclarations([setColorFuncDecl]),
  ];
}

@Riverpod(keepAlive: true)
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

Noções básicas sobre declarações de função

Vamos detalhar o que esse código faz:

  1. Nome da função: você nomeia a função set_color para indicar claramente a finalidade dela.
  2. Descrição da função: você fornece uma descrição clara que ajuda a LLM a entender quando usar a função.
  3. Definições de parâmetros: você define parâmetros estruturados com descrições próprias:
    • red: o componente vermelho do RGB, especificado como um número entre 0,0 e 1,0.
    • green: o componente verde do RGB, especificado como um número entre 0,0 e 1,0.
    • blue: o componente azul do RGB, especificado como um número entre 0,0 e 1,0.
  4. Tipos de esquema: use Schema.number() para indicar que são valores numéricos.
  5. Coleção de ferramentas: você cria uma lista de ferramentas que contém a declaração da função.

Essa abordagem estruturada ajuda o LLM do Gemini a entender:

  • Quando essa função deve ser chamada
  • Quais parâmetros ele precisa fornecer
  • Quais restrições se aplicam a esses parâmetros (como o intervalo de valores)

Atualizar o provedor do modelo Gemini

Agora, modifique o arquivo lib/providers/gemini.dart para incluir as declarações de função ao inicializar o modelo do Gemini:

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';
import '../services/gemini_tools.dart';                              // Add this import
import 'system_prompt.dart';

part 'gemini.g.dart';

@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
  await ref.watch(firebaseAppProvider.future);
  final systemPrompt = await ref.watch(systemPromptProvider.future);
  final geminiTools = ref.watch(geminiToolsProvider);                // Add this line

  final model = FirebaseAI.googleAI().generativeModel(
    model: 'gemini-2.0-flash',
    systemInstruction: Content.system(systemPrompt),
    tools: geminiTools.tools,                                        // And this line
  );
  return model;
}

@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
  final model = await ref.watch(geminiModelProvider.future);
  return model.startChat();
}

A principal mudança é adicionar o parâmetro tools: geminiTools.tools ao criar o modelo generativo. Isso faz com que o Gemini saiba quais funções estão disponíveis para serem chamadas.

Atualizar o comando do sistema

Agora você precisa modificar o comando do sistema para instruir o LLM sobre como usar a nova ferramenta set_color. Atualize assets/system_prompt.md:

assets/system_prompt.md

# Colorist System Prompt

You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.

## Your Capabilities

You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:

`set_color` - Sets the RGB values for the color display based on a description

## How to Respond to User Inputs

When users describe a color:

1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation

Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."

[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]

After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."

## When Descriptions are Unclear

If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.

## Important Guidelines

- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations

As principais mudanças no comando do sistema são:

  1. Introdução da ferramenta: em vez de pedir valores RGB formatados, agora você informa ao LLM sobre a ferramenta set_color.
  2. Processo modificado: você muda a etapa 3 de "formatar valores na resposta" para "usar a ferramenta para definir valores"
  3. Exemplo atualizado: você mostra como a resposta deve incluir uma chamada de função em vez de texto formatado
  4. Requisito de formatação removido: como você está usando chamadas de função estruturadas, não é mais necessário um formato de texto específico.

Esse comando atualizado direciona a LLM a usar a chamada de função em vez de apenas fornecer valores RGB em formato de texto.

Gerar código do Riverpod

Execute o comando do build runner para gerar o código Riverpod necessário:

dart run build_runner build --delete-conflicting-outputs

Execute o aplicativo

Neste ponto, o Gemini vai gerar conteúdo que tenta usar a chamada de função, mas você ainda não implementou os manipuladores para as chamadas de função. Quando você executar o app e descrever uma cor, o Gemini vai responder como se tivesse invocado uma ferramenta, mas nenhuma mudança de cor vai aparecer na interface até a próxima etapa.

Execute o app:

flutter run -d DEVICE

Captura de tela do app Colorist mostrando o LLM do Gemini respondendo com uma resposta parcial

Descreva uma cor, como "azul-marinho" ou "verde-floresta", e observe as respostas. O LLM está tentando chamar as funções definidas acima, mas seu código ainda não está detectando chamadas de função.

O processo de chamada de função

Vamos entender o que acontece quando o Gemini usa a chamada de função:

  1. Seleção de função: o LLM decide se uma chamada de função seria útil com base na solicitação do usuário.
  2. Geração de parâmetros: o LLM gera valores de parâmetros que se ajustam ao esquema da função.
  3. Formato de chamada de função: o LLM envia um objeto de chamada de função estruturado na resposta.
  4. Processamento de aplicativos: seu app receberia essa chamada e executaria a função relevante (implementada na próxima etapa).
  5. Integração de respostas: em conversas de várias interações, o LLM espera que o resultado da função seja retornado.

No estado atual do app, as três primeiras etapas estão ocorrendo, mas você ainda não implementou as etapas 4 ou 5 (processamento das chamadas de função), o que será feito na próxima etapa.

Detalhes técnicos: como o Gemini decide quando usar funções

O Gemini toma decisões inteligentes sobre quando usar funções com base em:

  1. Objetivo do usuário: se o pedido do usuário seria melhor atendido por uma função
  2. Relevância da função: o quanto as funções disponíveis correspondem à tarefa
  3. Disponibilidade de parâmetros: se é possível determinar valores de parâmetros com confiança
  4. Instruções do sistema: orientações do comando do sistema sobre o uso de funções

Ao fornecer declarações de função e instruções do sistema claras, você configurou o Gemini para reconhecer solicitações de descrição de cores como oportunidades de chamar a função set_color.

A seguir

Na próxima etapa, você vai implementar manipuladores para as chamadas de função do Gemini. Isso vai completar o ciclo, permitindo que as descrições do usuário acionem mudanças de cor reais na interface por meio das chamadas de função da LLM.

Solução de problemas

Problemas de declaração de função

Se você encontrar erros com declarações de função:

  • Verifique se os nomes e tipos de parâmetros correspondem ao que é esperado
  • Verifique se o nome da função é claro e descritivo
  • Verifique se a descrição da função explica corretamente a finalidade dela.

Problemas com comandos do sistema

Se o LLM não estiver tentando usar a função:

  • Verifique se o comando do sistema instrui claramente o LLM a usar a ferramenta set_color.
  • Verifique se o exemplo no comando do sistema demonstra o uso da função
  • Tente deixar mais explícita a instrução para usar a ferramenta

Problemas gerais

Se você tiver outros problemas:

  • Verifique se há erros relacionados a declarações de função no console.
  • Verifique se as ferramentas foram transmitidas corretamente ao modelo
  • Verifique se todo o código gerado pelo Riverpod está atualizado

Principais conceitos aprendidos

  • Definir declarações de função para ampliar os recursos de LLM em apps Flutter
  • Como criar esquemas de parâmetros para coleta de dados estruturados
  • Integrar declarações de função com o modelo Gemini
  • Atualização dos comandos do sistema para incentivar o uso de funções
  • Entender como os LLMs selecionam e chamam funções

Esta etapa demonstra como os LLMs podem preencher a lacuna entre a entrada de linguagem natural e as chamadas de função estruturadas, preparando o terreno para uma integração perfeita entre a conversa e os recursos do aplicativo.

6. Implementar o processamento de ferramentas

Nesta etapa, você vai implementar manipuladores para as chamadas de função do Gemini. Isso completa o ciclo de comunicação entre entradas de linguagem natural e recursos concretos do aplicativo, permitindo que o LLM manipule diretamente a interface com base nas descrições do usuário.

O que você vai aprender nesta etapa

  • Entender o pipeline completo de chamada de função em aplicativos de LLM
  • Processar chamadas de função do Gemini em um aplicativo Flutter
  • Implementar manipuladores de funções que modificam o estado do aplicativo
  • Como processar respostas de funções e retornar resultados ao LLM
  • Como criar um fluxo de comunicação completo entre o LLM e a interface
  • Como gerar registros de chamadas e respostas de funções para ter mais transparência

Noções básicas sobre o pipeline de chamada de função

Antes de entrar na implementação, vamos entender o pipeline completo de chamadas de função:

O fluxo de ponta a ponta

  1. Entrada do usuário: o usuário descreve uma cor em linguagem natural (por exemplo, "verde floresta")
  2. Processamento de LLM: o Gemini analisa a descrição e decide chamar a função set_color.
  3. Geração de chamada de função: o Gemini cria um JSON estruturado com parâmetros (valores vermelho, verde e azul).
  4. Recepção de chamada de função: seu app recebe esses dados estruturados do Gemini.
  5. Execução da função: seu app executa a função com os parâmetros fornecidos.
  6. Atualização de estado: a função atualiza o estado do app (mudando a cor exibida).
  7. Geração de respostas: sua função retorna resultados ao LLM
  8. Incorporação de respostas: o LLM incorpora esses resultados à resposta final.
  9. Atualização da interface: a interface reage à mudança de estado, mostrando a nova cor.

O ciclo de comunicação completo é essencial para a integração adequada do LLM. Quando um LLM faz uma chamada de função, ele não apenas envia a solicitação e segue em frente. Em vez disso, ele aguarda o aplicativo executar a função e retornar resultados. Em seguida, o LLM usa esses resultados para formular a resposta final, criando um fluxo de conversa natural que reconhece as ações realizadas.

Implementar gerenciadores de funções

Vamos atualizar o arquivo lib/services/gemini_tools.dart para adicionar gerenciadores de chamadas de função:

lib/services/gemini_tools.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

  FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
    'set_color',
    'Set the color of the display square based on red, green, and blue values.',
    parameters: {
      'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
      'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
      'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
    },
  );

  List<Tool> get tools => [
    Tool.functionDeclarations([setColorFuncDecl]),
  ];

  Map<String, Object?> handleFunctionCall(                           // Add from here
    String functionName,
    Map<String, Object?> arguments,
  ) {
    final logStateNotifier = ref.read(logStateProvider.notifier);
    logStateNotifier.logFunctionCall(functionName, arguments);
    return switch (functionName) {
      'set_color' => handleSetColor(arguments),
      _ => handleUnknownFunction(functionName),
    };
  }

  Map<String, Object?> handleSetColor(Map<String, Object?> arguments) {
    final colorStateNotifier = ref.read(colorStateProvider.notifier);
    final red = (arguments['red'] as num).toDouble();
    final green = (arguments['green'] as num).toDouble();
    final blue = (arguments['blue'] as num).toDouble();
    final functionResults = {
      'success': true,
      'current_color': colorStateNotifier
          .updateColor(red: red, green: green, blue: blue)
          .toLLMContextMap(),
    };

    final logStateNotifier = ref.read(logStateProvider.notifier);
    logStateNotifier.logFunctionResults(functionResults);
    return functionResults;
  }

  Map<String, Object?> handleUnknownFunction(String functionName) {
    final logStateNotifier = ref.read(logStateProvider.notifier);
    logStateNotifier.logWarning('Unsupported function call $functionName');
    return {
      'success': false,
      'reason': 'Unsupported function call $functionName',
    };
  }                                                                  // To here.
}

@Riverpod(keepAlive: true)
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

Noções básicas sobre os manipuladores de funções

Vamos detalhar o que esses gerenciadores de funções fazem:

  1. handleFunctionCall: um dispatcher central que:
    • Registra a chamada de função para transparência no painel de registros.
    • Faz o roteamento para o gerenciador adequado com base no nome da função
    • Retorna uma resposta estruturada que será enviada de volta ao LLM
  2. handleSetColor: o manipulador específico da sua função set_color que:
    • Extrai valores RGB do mapa de argumentos.
    • Converte-os nos tipos esperados (duplos)
    • Atualiza o estado de cor do aplicativo usando o colorStateNotifier.
    • Cria uma resposta estruturada com status de sucesso e informações de cores atuais.
    • Registra os resultados da função para depuração.
  3. handleUnknownFunction: um manipulador de substituição para funções desconhecidas que:
    • Registra um aviso sobre a função sem suporte.
    • Retorna uma resposta de erro ao LLM

A função handleSetColor é particularmente importante porque preenche a lacuna entre a compreensão da linguagem natural do LLM e as mudanças concretas na interface.

Atualizar o serviço de chat do Gemini para processar chamadas de função e respostas

Agora, vamos atualizar o arquivo lib/services/gemini_chat_service.dart para processar chamadas de função das respostas do LLM e enviar os resultados de volta para o LLM:

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';
import 'gemini_tools.dart';                                          // Add this import

part 'gemini_chat_service.g.dart';

class GeminiChatService {
  GeminiChatService(this.ref);
  final Ref ref;

  Future<void> sendMessage(String message) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    final llmMessage = chatStateNotifier.createLlmMessage();
    try {
      final response = await chatSession.sendMessage(Content.text(message));

      final responseText = response.text;
      if (responseText != null) {
        logStateNotifier.logLlmText(responseText);
        chatStateNotifier.appendToMessage(llmMessage.id, responseText);
      }

      if (response.functionCalls.isNotEmpty) {                       // Add from here
        final geminiTools = ref.read(geminiToolsProvider);
        final functionResultResponse = await chatSession.sendMessage(
          Content.functionResponses([
            for (final functionCall in response.functionCalls)
              FunctionResponse(
                functionCall.name,
                geminiTools.handleFunctionCall(
                  functionCall.name,
                  functionCall.args,
                ),
              ),
          ]),
        );
        final responseText = functionResultResponse.text;
        if (responseText != null) {
          logStateNotifier.logLlmText(responseText);
          chatStateNotifier.appendToMessage(llmMessage.id, responseText);
        }
      }                                                              // To here.
    } catch (e, st) {
      logStateNotifier.logError(e, st: st);
      chatStateNotifier.appendToMessage(
        llmMessage.id,
        "\nI'm sorry, I encountered an error processing your request. "
        "Please try again.",
      );
    } finally {
      chatStateNotifier.finalizeMessage(llmMessage.id);
    }
  }
}

@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

Como entender o fluxo de comunicação

A principal adição aqui é o processamento completo de chamadas de função e respostas:

if (response.functionCalls.isNotEmpty) {
  final geminiTools = ref.read(geminiToolsProvider);
  final functionResultResponse = await chatSession.sendMessage(
    Content.functionResponses([
      for (final functionCall in response.functionCalls)
        FunctionResponse(
          functionCall.name,
          geminiTools.handleFunctionCall(
            functionCall.name,
            functionCall.args,
          ),
        ),
    ]),
  );
  final responseText = functionResultResponse.text;
  if (responseText != null) {
    logStateNotifier.logLlmText(responseText);
    chatStateNotifier.appendToMessage(llmMessage.id, responseText);
  }
}

Este código:

  1. Verifica se a resposta do LLM contém alguma chamada de função
  2. Para cada chamada de função, invoca o método handleFunctionCall com o nome e os argumentos da função.
  3. Coleta os resultados de cada chamada de função.
  4. Envia esses resultados de volta ao LLM usando Content.functionResponses
  5. Processa a resposta do LLM aos resultados da função
  6. Atualiza a interface com o texto da resposta final

Isso cria um fluxo de ida e volta:

  • Usuário → LLM: pede uma cor
  • LLM → app: chamadas de função com parâmetros
  • App → Usuário: nova cor exibida
  • App → LLM: resultados da função
  • LLM → Usuário: resposta final incorporando resultados da função

Gerar código do Riverpod

Execute o comando do build runner para gerar o código Riverpod necessário:

dart run build_runner build --delete-conflicting-outputs

Executar e testar o fluxo completo

Agora execute seu aplicativo:

flutter run -d DEVICE

Captura de tela do app Colorist mostrando o LLM do Gemini respondendo com uma chamada de função

Tente inserir várias descrições de cores:

  • "Quero um vermelho carmesim intenso"
  • "Mostre um azul-celeste relaxante"
  • "Qual é a cor das folhas de hortelã fresca?"
  • "Quero ver um laranja quente de pôr do sol"
  • "Deixe um roxo real intenso"

Agora você vai ver:

  1. Sua mensagem aparecendo na interface de chat
  2. A resposta do Gemini aparecendo no chat
  3. Chamadas de função sendo registradas no painel de registros
  4. Os resultados da função são registrados imediatamente após
  5. O retângulo de cor atualizando para mostrar a cor descrita
  6. Valores RGB sendo atualizados para mostrar os componentes da nova cor
  7. A resposta final do Gemini aparece, muitas vezes comentando sobre a cor definida.

O painel de registros fornece insights sobre o que está acontecendo nos bastidores. Você vai ver:

  • As chamadas de função exatas que o Gemini está fazendo
  • Os parâmetros escolhidos para cada valor RGB
  • Os resultados que sua função está retornando
  • As respostas complementares do Gemini

O notificador de estado de cor

O colorStateNotifier que você está usando para atualizar as cores faz parte do pacote colorist_ui. Ele gerencia:

  • A cor atual exibida na interface
  • O histórico de cores (últimas 10 cores)
  • Notificação de mudanças de estado para componentes de UI

Quando você chama updateColor com novos valores RGB, ele:

  1. Cria um novo objeto ColorData com os valores fornecidos.
  2. Atualiza a cor atual no estado do app.
  3. Adiciona a cor ao histórico
  4. Aciona atualizações da interface usando o gerenciamento de estado do Riverpod.

Os componentes de UI no pacote colorist_ui monitoram esse estado e são atualizados automaticamente quando ele muda, criando uma experiência reativa.

Entender o tratamento de erros

Sua implementação inclui tratamento de erros robusto:

  1. Bloco try-catch: envolve todas as interações de LLM para detectar exceções.
  2. Registro de erros: registra erros no painel de registros com rastreamentos de pilha.
  3. Feedback do usuário: fornece uma mensagem de erro amigável no chat.
  4. Limpeza de estado: finaliza o estado da mensagem mesmo que ocorra um erro.

Isso garante que o app permaneça estável e forneça feedback adequado, mesmo quando ocorrem problemas com o serviço de LLM ou a execução da função.

O poder da chamada de função para a experiência do usuário

O que você fez aqui demonstra como os LLMs podem criar interfaces naturais eficientes:

  1. Interface de linguagem natural: os usuários expressam a intenção em linguagem cotidiana
  2. Interpretação inteligente: o LLM traduz descrições vagas em valores precisos.
  3. Manipulação direta: a interface é atualizada em resposta à linguagem natural.
  4. Respostas contextuais: o LLM fornece contexto de conversa sobre as mudanças
  5. Baixa carga cognitiva: os usuários não precisam entender os valores RGB ou a teoria das cores.

Esse padrão de uso da chamada de função do LLM para unir linguagem natural e ações da interface pode ser estendido a inúmeros outros domínios além da seleção de cores.

A seguir

Na próxima etapa, você vai melhorar a experiência do usuário implementando respostas de streaming. Em vez de esperar a resposta completa, você processa blocos de texto e chamadas de função à medida que são recebidos, criando um aplicativo mais responsivo e envolvente.

Solução de problemas

Problemas com chamadas de função

Se o Gemini não estiver chamando suas funções ou os parâmetros estiverem incorretos:

  • Verifique se a declaração da função corresponde ao que está descrito no comando do sistema
  • Verifique se os nomes e tipos de parâmetros são consistentes
  • Verifique se o comando do sistema instrui explicitamente o LLM a usar a ferramenta
  • Verifique se o nome da função no manipulador corresponde exatamente ao que está na declaração.
  • Examine o painel de registros para informações detalhadas sobre as chamadas de função.

Problemas com respostas de funções

Se os resultados da função não estiverem sendo enviados corretamente de volta para o LLM:

  • Verifique se a função retorna um mapa formatado corretamente.
  • Verifique se o Content.functionResponses está sendo construído corretamente.
  • Procure erros no registro relacionados a respostas de função.
  • Verifique se você está usando a mesma sessão de chat para a resposta.

Problemas de exibição de cores

Se as cores não estiverem aparecendo corretamente:

  • Verifique se os valores RGB foram convertidos corretamente em números de ponto flutuante de precisão dupla (a LLM pode enviá-los como números inteiros).
  • Verifique se os valores estão no intervalo esperado (0,0 a 1,0).
  • Verifique se o indicador de estado de cor está sendo chamado corretamente
  • Examine o registro para ver os valores exatos transmitidos à função.

Problemas gerais

Para problemas gerais:

  • Examine os registros em busca de erros ou avisos
  • Verificar a conectividade do Firebase AI Logic
  • Verificar se há incompatibilidades de tipo nos parâmetros de função
  • Verifique se todo o código gerado pelo Riverpod está atualizado

Principais conceitos aprendidos

  • Implementar um pipeline completo de chamada de função no Flutter
  • Criar comunicação completa entre um LLM e seu aplicativo
  • Processamento de dados estruturados de respostas do LLM
  • Enviar os resultados da função de volta ao LLM para incorporação nas respostas
  • Usar o painel de registros para ter visibilidade das interações entre o aplicativo e o LLM
  • Como conectar entradas de linguagem natural a mudanças concretas na interface

Com essa etapa concluída, seu app demonstra um dos padrões mais eficientes para integração de LLMs: traduzir entradas de linguagem natural em ações concretas da interface, mantendo uma conversa coerente que reconhece essas ações. Isso cria uma interface intuitiva e conversacional que parece mágica para os usuários.

7. Respostas de streaming para uma melhor experiência do usuário

Nesta etapa, você vai melhorar a experiência do usuário implementando respostas de streaming do Gemini. Em vez de esperar que toda a resposta seja gerada, você processa partes de texto e chamadas de função à medida que são recebidas, criando um aplicativo mais responsivo e envolvente.

O que você vai fazer nesta etapa

  • A importância do streaming para aplicativos com tecnologia de LLM
  • Implementar respostas de LLM de streaming em um aplicativo Flutter
  • Processamento de partes de texto parciais à medida que chegam da API
  • Gerenciar o estado da conversa para evitar conflitos de mensagens
  • Como processar chamadas de função em respostas de streaming
  • Criar indicadores visuais para respostas em andamento

Por que o streaming é importante para aplicativos de LLM

Antes de implementar, vamos entender por que as respostas de streaming são cruciais para criar excelentes experiências do usuário com LLMs:

Melhor experiência do usuário

As respostas de streaming oferecem vários benefícios significativos para a experiência do usuário:

  1. Latência percebida reduzida: os usuários veem o texto começar a aparecer imediatamente (normalmente em 100 a 300 ms), em vez de esperar vários segundos por uma resposta completa. Essa percepção de imediatismo melhora muito a satisfação do usuário.
  2. Ritmo de conversa natural: o aparecimento gradual do texto imita a forma como os humanos se comunicam, criando uma experiência de diálogo mais natural.
  3. Processamento progressivo de informações: os usuários podem começar a processar as informações à medida que elas chegam, em vez de serem sobrecarregados por um grande bloco de texto de uma só vez.
  4. Oportunidade de interrupção antecipada: em um aplicativo completo, os usuários podem interromper ou redirecionar o LLM se perceberem que ele está indo em uma direção inadequada.
  5. Confirmação visual da atividade: o texto de streaming fornece feedback imediato de que o sistema está funcionando, reduzindo a incerteza.

Vantagens técnicas

Além das melhorias na experiência do usuário, o streaming oferece benefícios técnicos:

  1. Execução antecipada de funções: as chamadas de função podem ser detectadas e executadas assim que aparecem no fluxo, sem esperar a resposta completa.
  2. Atualizações incrementais da interface: é possível atualizar a interface progressivamente à medida que novas informações chegam, criando uma experiência mais dinâmica.
  3. Gerenciamento do estado da conversa: o streaming fornece sinais claros sobre quando as respostas estão concluídas ou ainda em andamento, permitindo um melhor gerenciamento do estado.
  4. Riscos de tempo limite reduzidos: com respostas sem streaming, gerações de longa duração correm o risco de atingir o tempo limite da conexão. O streaming estabelece a conexão no início e a mantém.

No app Colorist, a implementação do streaming significa que os usuários vão ver as respostas de texto e as mudanças de cor aparecendo mais rapidamente, criando uma experiência muito mais responsiva.

Adicionar gerenciamento de estado da conversa

Primeiro, vamos adicionar um provedor de estado para acompanhar se o app está processando uma resposta de streaming. Atualize o arquivo lib/services/gemini_chat_service.dart:

lib/services/gemini_chat_service.dart

import 'dart:async';

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';
import 'gemini_tools.dart';

part 'gemini_chat_service.g.dart';

class ConversationStateNotifier extends Notifier<ConversationState> {  // Add from here...
  @override
  ConversationState build() => ConversationState.idle;

  void busy() {
    state = ConversationState.busy;
  }

  void idle() {
    state = ConversationState.idle;
  }
}

final conversationStateProvider =
    NotifierProvider<ConversationStateNotifier, ConversationState>(
      ConversationStateNotifier.new,
    );                                                                 // To here.

class GeminiChatService {
  GeminiChatService(this.ref);
  final Ref ref;

  Future<void> sendMessage(String message) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final conversationState = ref.read(conversationStateProvider);   // Add this line
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

    if (conversationState == ConversationState.busy) {               // Add from here...
      logStateNotifier.logWarning(
        "Can't send a message while a conversation is in progress",
      );
      throw Exception(
        "Can't send a message while a conversation is in progress",
      );
    }
    final conversationStateNotifier = ref.read(
      conversationStateProvider.notifier,
    );
    conversationStateNotifier.busy();                                // To here.
    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    final llmMessage = chatStateNotifier.createLlmMessage();
    try {                                                            // Modify from here...
      final responseStream = chatSession.sendMessageStream(
        Content.text(message),
      );
      await for (final block in responseStream) {
        await _processBlock(block, llmMessage.id);
      }                                                              // To here.
    } catch (e, st) {
      logStateNotifier.logError(e, st: st);
      chatStateNotifier.appendToMessage(
        llmMessage.id,
        "\nI'm sorry, I encountered an error processing your request. "
        "Please try again.",
      );
    } finally {
      chatStateNotifier.finalizeMessage(llmMessage.id);
      conversationStateNotifier.idle();                              // Add this line.
    }
  }

  Future<void> _processBlock(                                        // Add from here...
    GenerateContentResponse block,
    String llmMessageId,
  ) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);
    final blockText = block.text;
    if (blockText != null) {
      logStateNotifier.logLlmText(blockText);
      chatStateNotifier.appendToMessage(llmMessageId, blockText);
    }

    if (block.functionCalls.isNotEmpty) {
      final geminiTools = ref.read(geminiToolsProvider);
      final responseStream = chatSession.sendMessageStream(
        Content.functionResponses([
          for (final functionCall in block.functionCalls)
            FunctionResponse(
              functionCall.name,
              geminiTools.handleFunctionCall(
                functionCall.name,
                functionCall.args,
              ),
            ),
        ]),
      );
      await for (final response in responseStream) {
        final responseText = response.text;
        if (responseText != null) {
          logStateNotifier.logLlmText(responseText);
          chatStateNotifier.appendToMessage(llmMessageId, responseText);
        }
      }
    }
  }                                                                  // To here.
}

@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

Entender a implementação de streaming

Vamos detalhar o que esse código faz:

  1. Rastreamento do estado da conversa:
    • Um conversationStateProvider rastreia se o app está processando uma resposta no momento.
    • O estado muda de idlebusy durante o processamento e volta para idle.
    • Isso evita várias solicitações simultâneas que podem entrar em conflito.
  2. Inicialização do stream:
    • sendMessageStream() retorna um fluxo de partes da resposta em vez de um Future com a resposta completa.
    • Cada parte pode conter texto, chamadas de função ou ambos.
  3. Processamento progressivo:
    • O await for processa cada bloco à medida que chega em tempo real.
    • O texto é anexado à interface imediatamente, criando o efeito de streaming.
    • As chamadas de função são executadas assim que são detectadas
  4. Processamento de chamadas de função:
    • Quando uma chamada de função é detectada em um bloco, ela é executada imediatamente.
    • Os resultados são enviados de volta ao LLM por outra chamada de streaming.
    • A resposta do LLM a esses resultados também é processada em streaming.
  5. Tratamento de erros e limpeza:
    • try/catch oferece um tratamento de erros robusto
    • O bloco finally garante que o estado da conversa seja redefinido corretamente.
    • A mensagem é sempre finalizada, mesmo que ocorram erros.

Essa implementação cria uma experiência de streaming responsiva e confiável, mantendo o estado adequado da conversa.

Atualizar a tela principal para conectar o estado da conversa

Modifique o arquivo lib/main.dart para transmitir o estado da conversa à tela principal:

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';

void main() async {
  runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final model = ref.watch(geminiModelProvider);
    final conversationState = ref.watch(conversationStateProvider);  // Add this line

    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: model.when(
        data: (data) => MainScreen(
          conversationState: conversationState,                      // And this line
          sendMessage: (text) {
            ref.read(geminiChatServiceProvider).sendMessage(text);
          },
        ),
        loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
        error: (err, st) => ErrorScreen(error: err),
      ),
    );
  }
}

A principal mudança aqui é transmitir o conversationState para o widget MainScreen. O MainScreen (fornecido pelo pacote colorist_ui) vai usar esse estado para desativar a entrada de texto enquanto uma resposta está sendo processada.

Isso cria uma experiência do usuário coesa em que a interface reflete o estado atual da conversa.

Gerar código do Riverpod

Execute o comando do build runner para gerar o código Riverpod necessário:

dart run build_runner build --delete-conflicting-outputs

Executar e testar respostas de streaming

Execute o aplicativo:

flutter run -d DEVICE

Captura de tela do app Colorist mostrando o LLM do Gemini respondendo em streaming

Agora teste o comportamento de streaming com várias descrições de cores. Tente descrições como:

  • "Mostre a cor azul-petróleo escura do oceano ao anoitecer"
  • "Quero ver um coral vibrante que me lembre flores tropicais"
  • "Crie um verde-oliva discreto, como um uniforme militar antigo"

O fluxo técnico de streaming em detalhes

Vamos examinar exatamente o que acontece ao transmitir uma resposta:

Estabelecimento da conexão

Quando você chama sendMessageStream(), acontece o seguinte:

  1. O app estabelece uma conexão com o serviço do Firebase AI Logic.
  2. A solicitação do usuário é enviada ao serviço.
  3. O servidor começa a processar a solicitação.
  4. A conexão de stream permanece aberta, pronta para transmitir partes

Transmissão de fragmentos

À medida que o Gemini gera conteúdo, os blocos são enviados pelo stream:

  1. O servidor envia partes de texto à medida que são geradas (normalmente algumas palavras ou frases).
  2. Quando o Gemini decide fazer uma chamada de função, ele envia as informações da chamada de função
  3. Outros trechos de texto podem seguir as chamadas de função
  4. O stream continua até que a geração seja concluída

Processamento progressivo

O app processa cada parte de forma incremental:

  1. Cada parte de texto é anexada à resposta atual
  2. As chamadas de função são executadas assim que são detectadas
  3. A interface é atualizada em tempo real com texto e resultados de funções.
  4. O estado é rastreado para mostrar que a resposta ainda está sendo transmitida

Conclusão da transmissão

Quando a geração for concluída:

  1. O stream é fechado pelo servidor.
  2. O loop await for sai naturalmente
  3. A mensagem é marcada como concluída
  4. O estado da conversa é redefinido como inativo
  5. A interface é atualizada para refletir o estado concluído.

Comparação entre streaming e não streaming

Para entender melhor os benefícios do streaming, vamos comparar abordagens com e sem streaming:

Aspecto

Não streaming

Streaming

Latência percebida

O usuário não vê nada até que a resposta completa esteja pronta

O usuário vê as primeiras palavras em milissegundos

Experiência do usuário

Longa espera seguida de aparição repentina de texto

Aparência de texto natural e progressiva

Gerenciamento de estado

Mais simples (as mensagens estão pendentes ou concluídas)

Mais complexo (as mensagens podem estar em um estado de streaming)

Execução de função

Ocorre somente após a resposta completa

Ocorre durante a geração da resposta

Complexidade da implementação

Mais simples de implementar

Requer gerenciamento de estado adicional

Recuperação de erros

Resposta tudo ou nada

Respostas parciais ainda podem ser úteis

Complexidade do código

Menos complexo

Mais complexo devido ao processamento de fluxos

Para um aplicativo como o Colorist, os benefícios de UX do streaming superam a complexidade da implementação, especialmente para interpretações de cores que podem levar vários segundos para serem geradas.

Práticas recomendadas para a UX de streaming

Ao implementar o streaming nos seus próprios aplicativos de LLM, considere estas práticas recomendadas:

  1. Indicadores visuais claros: sempre forneça indicações visuais claras que diferenciem mensagens completas e em streaming.
  2. Bloqueio de entrada: desative a entrada do usuário durante o streaming para evitar várias solicitações sobrepostas.
  3. Recuperação de erros: crie sua interface para lidar com uma recuperação simples se o streaming for interrompido.
  4. Transições de estado: garanta transições suaves entre os estados ocioso, de streaming e concluído.
  5. Visualização do progresso: considere animações ou indicadores sutis que mostrem o processamento ativo.
  6. Opções de cancelamento: em um app completo, ofereça maneiras para os usuários cancelarem as gerações em andamento.
  7. Integração de resultados de função: crie sua interface para processar resultados de função que aparecem no meio do fluxo.
  8. Otimização de performance: minimize as recriações da interface durante atualizações rápidas de stream.

O pacote colorist_ui implementa muitas dessas práticas recomendadas para você, mas elas são considerações importantes para qualquer implementação de LLM de streaming.

A seguir

Na próxima etapa, você vai implementar a sincronização de LLM notificando o Gemini quando os usuários selecionarem cores no histórico. Isso vai criar uma experiência mais coesa em que o LLM reconhece as mudanças iniciadas pelo usuário no estado do aplicativo.

Solução de problemas

Problemas de processamento de stream

Se você tiver problemas com o processamento de stream:

  • Sintomas: respostas parciais, texto ausente ou encerramento abrupto do stream
  • Solução: verifique a conectividade de rede e garanta padrões async/await adequados no seu código.
  • Diagnóstico: examine o painel de registros em busca de mensagens de erro ou avisos relacionados ao processamento de stream.
  • Correção: garanta que todo o processamento de fluxos use o tratamento de erros adequado com blocos try/catch.

Chamadas de função ausentes

Se as chamadas de função não forem detectadas no fluxo:

  • Sintomas: o texto aparece, mas as cores não são atualizadas, ou o registro não mostra chamadas de função.
  • Solução: verifique as instruções do comando do sistema sobre o uso de chamadas de função.
  • Diagnóstico: verifique o painel de registros para saber se as chamadas de função estão sendo recebidas.
  • Correção: ajuste o comando do sistema para instruir de forma mais explícita o LLM a usar a ferramenta set_color.

Tratamento de erros gerais

Para outros problemas:

  • Etapa 1: verificar se há mensagens de erro no painel de registros
  • Etapa 2: verificar a conectividade do Firebase AI Logic
  • Etapa 3: garantir que todo o código gerado pelo Riverpod esteja atualizado
  • Etapa 4: revise a implementação de streaming para verificar se há instruções "await" ausentes

Principais conceitos aprendidos

  • Implementar respostas de streaming com a API Gemini para uma UX mais responsiva
  • Gerenciar o estado da conversa para processar interações de streaming corretamente
  • Processamento de texto em tempo real e chamadas de função à medida que chegam
  • Criar interfaces responsivas que são atualizadas de forma incremental durante o streaming
  • Como processar streams simultâneos com padrões assíncronos adequados
  • Fornecer feedback visual adequado durante as respostas de streaming

Ao implementar o streaming, você melhorou significativamente a experiência do usuário do app Colorist, criando uma interface mais responsiva e envolvente que parece realmente conversacional.

8. Sincronização de contexto do LLM

Nesta etapa extra, você vai implementar a sincronização de contexto de LLM notificando o Gemini quando os usuários selecionarem cores no histórico. Isso cria uma experiência mais coesa em que o LLM está ciente das ações do usuário na interface, não apenas das mensagens explícitas.

O que você vai fazer nesta etapa

  • Criar sincronização de contexto de LLM entre a interface e o LLM
  • Serializar eventos da interface em um contexto que o LLM possa entender
  • Atualizar o contexto da conversa com base nas ações do usuário
  • Criar uma experiência coerente em diferentes métodos de interação
  • Melhorar a percepção de contexto do LLM além das mensagens de chat explícitas

Noções básicas sobre a sincronização de contexto de LLM

Os chatbots tradicionais só respondem a mensagens explícitas do usuário, criando uma desconexão quando os usuários interagem com o app por outros meios. A sincronização de contexto do LLM resolve essa limitação:

Por que a sincronização de contexto do LLM é importante

Quando os usuários interagem com seu app usando elementos da interface (como selecionar uma cor no histórico), o LLM não tem como saber o que aconteceu, a menos que você diga explicitamente. Sincronização de contexto do LLM:

  1. Mantém o contexto: mantém o LLM informado sobre todas as ações relevantes do usuário.
  2. Cria coerência: produz uma experiência coesa em que o LLM reconhece as interações da interface.
  3. Melhora a inteligência: permite que o LLM responda adequadamente a todas as ações do usuário.
  4. Melhora a experiência do usuário: faz com que todo o aplicativo pareça mais integrado e responsivo.
  5. Reduz o esforço do usuário: elimina a necessidade de os usuários explicarem manualmente as ações da interface.

No app Colorist, quando um usuário seleciona uma cor no histórico, você quer que o Gemini reconheça essa ação e comente de forma inteligente sobre a cor selecionada, mantendo a ilusão de um assistente integrado e atento.

Atualização do serviço de chat do Gemini para notificações de seleção de cores

Primeiro, adicione um método ao GeminiChatService para notificar o LLM quando um usuário selecionar uma cor no histórico. Atualize o arquivo lib/services/gemini_chat_service.dart:

lib/services/gemini_chat_service.dart

import 'dart:async';
import 'dart:convert';                                               // Add this import

import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../providers/gemini.dart';
import 'gemini_tools.dart';

part 'gemini_chat_service.g.dart';

class ConversationStateNotifier extends Notifier<ConversationState> {
  @override
  ConversationState build() => ConversationState.idle;

  void busy() {
    state = ConversationState.busy;
  }

  void idle() {
    state = ConversationState.idle;
  }
}

final conversationStateProvider =
    NotifierProvider<ConversationStateNotifier, ConversationState>(
      ConversationStateNotifier.new,
    );

class GeminiChatService {
  GeminiChatService(this.ref);
  final Ref ref;

  Future<void> notifyColorSelection(ColorData color) => sendMessage(  // Add from here...
    'User selected color from history: ${json.encode(color.toLLMContextMap())}',
  );                                                                  // To here.

  Future<void> sendMessage(String message) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final conversationState = ref.read(conversationStateProvider);
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

    if (conversationState == ConversationState.busy) {
      logStateNotifier.logWarning(
        "Can't send a message while a conversation is in progress",
      );
      throw Exception(
        "Can't send a message while a conversation is in progress",
      );
    }
    final conversationStateNotifier = ref.read(
      conversationStateProvider.notifier,
    );
    conversationStateNotifier.busy();
    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    final llmMessage = chatStateNotifier.createLlmMessage();
    try {
      final responseStream = chatSession.sendMessageStream(
        Content.text(message),
      );
      await for (final block in responseStream) {
        await _processBlock(block, llmMessage.id);
      }
    } catch (e, st) {
      logStateNotifier.logError(e, st: st);
      chatStateNotifier.appendToMessage(
        llmMessage.id,
        "\nI'm sorry, I encountered an error processing your request. "
        "Please try again.",
      );
    } finally {
      chatStateNotifier.finalizeMessage(llmMessage.id);
      conversationStateNotifier.idle();
    }
  }

  Future<void> _processBlock(
    GenerateContentResponse block,
    String llmMessageId,
  ) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final chatStateNotifier = ref.read(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);
    final blockText = block.text;
    if (blockText != null) {
      logStateNotifier.logLlmText(blockText);
      chatStateNotifier.appendToMessage(llmMessageId, blockText);
    }

    if (block.functionCalls.isNotEmpty) {
      final geminiTools = ref.read(geminiToolsProvider);
      final responseStream = chatSession.sendMessageStream(
        Content.functionResponses([
          for (final functionCall in block.functionCalls)
            FunctionResponse(
              functionCall.name,
              geminiTools.handleFunctionCall(
                functionCall.name,
                functionCall.args,
              ),
            ),
        ]),
      );
      await for (final response in responseStream) {
        final responseText = response.text;
        if (responseText != null) {
          logStateNotifier.logLlmText(responseText);
          chatStateNotifier.appendToMessage(llmMessageId, responseText);
        }
      }
    }
  }
}

@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

A principal adição é o método notifyColorSelection, que:

  1. Usa um objeto ColorData que representa a cor selecionada.
  2. Codifica para um formato JSON que pode ser incluído em uma mensagem
  3. Envia uma mensagem formatada especialmente para o LLM indicando uma seleção do usuário.
  4. Reutiliza o método sendMessage atual para processar a notificação.

Essa abordagem evita a duplicação usando sua infraestrutura atual de processamento de mensagens.

Atualizar o app principal para conectar notificações de seleção de cores

Agora, modifique o arquivo lib/main.dart para transmitir a função de notificação de seleção de cores à tela principal:

lib/main.dart

import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';

void main() async {
  runApp(ProviderScope(child: MainApp()));
}

class MainApp extends ConsumerWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final model = ref.watch(geminiModelProvider);
    final conversationState = ref.watch(conversationStateProvider);

    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: model.when(
        data: (data) => MainScreen(
          conversationState: conversationState,
          notifyColorSelection: (color) {                            // Add from here...
            ref.read(geminiChatServiceProvider).notifyColorSelection(color);
          },                                                         // To here.
          sendMessage: (text) {
            ref.read(geminiChatServiceProvider).sendMessage(text);
          },
        ),
        loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
        error: (err, st) => ErrorScreen(error: err),
      ),
    );
  }
}

A principal mudança é adicionar o callback notifyColorSelection, que conecta o evento da interface (selecionar uma cor no histórico) ao sistema de notificação do LLM.

Atualizar o comando do sistema

Agora, você precisa atualizar o comando do sistema para instruir o LLM sobre como responder às notificações de seleção de cores. Modifique o arquivo assets/system_prompt.md:

assets/system_prompt.md

# Colorist System Prompt

You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.

## Your Capabilities

You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:

`set_color` - Sets the RGB values for the color display based on a description

## How to Respond to User Inputs

When users describe a color:

1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation

Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."

[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]

After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."

## When Descriptions are Unclear

If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.

## When Users Select Historical Colors

Sometimes, the user will manually select a color from the history panel. When this happens, you'll receive a notification about this selection that includes details about the color. Acknowledge this selection with a brief response that recognizes what they've done and comments on the selected color.

Example notification:
User: "User selected color from history: {red: 0.2, green: 0.5, blue: 0.8, hexCode: #3380CC}"
You: "I see you've selected an ocean blue from your history. This tranquil blue with a moderate intensity has a calming, professional quality to it. Would you like to explore similar shades or create a contrasting color?"

## Important Guidelines

- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations

A principal adição é a seção "Quando os usuários selecionam cores históricas", que:

  1. Explica o conceito de notificações de seleção de histórico para o LLM
  2. Exemplo de como essas notificações aparecem
  3. Mostra um exemplo de resposta adequada
  4. Define expectativas para reconhecer a seleção e comentar sobre a cor

Isso ajuda o LLM a entender como responder adequadamente a essas mensagens especiais.

Gerar código do Riverpod

Execute o comando do build runner para gerar o código Riverpod necessário:

dart run build_runner build --delete-conflicting-outputs

Executar e testar a sincronização de contexto do LLM

Execute o aplicativo:

flutter run -d DEVICE

Captura de tela do app Colorist mostrando o LLM do Gemini respondendo a uma seleção do histórico de cores

Para testar a sincronização de contexto do LLM, faça o seguinte:

  1. Primeiro, gere algumas cores descrevendo-as no chat
    • "Mostre um roxo vibrante"
    • "Quero um verde-floresta"
    • "Me dê um vermelho brilhante"
  2. Em seguida, clique em uma das miniaturas de cor na faixa do histórico.

Você vai observar:

  1. A cor selecionada aparece na tela principal.
  2. Uma mensagem do usuário aparece no chat indicando a seleção de cores
  3. O LLM responde reconhecendo a seleção e comentando sobre a cor.
  4. Toda a interação parece natural e coesa

Isso cria uma experiência integrada em que o LLM reconhece e responde adequadamente a mensagens diretas e interações da interface.

Como funciona a sincronização de contexto de LLM

Vamos conhecer os detalhes técnicos de como essa sincronização funciona:

Fluxo de dados

  1. Ação do usuário: o usuário clica em uma cor na faixa do histórico
  2. Evento da interface: o widget MainScreen detecta essa seleção.
  3. Execução de callback: o callback notifyColorSelection é acionado.
  4. Criação de mensagens: uma mensagem formatada especialmente é criada com os dados de cor.
  5. Processamento de LLM: a mensagem é enviada ao Gemini, que reconhece o formato.
  6. Resposta contextual: o Gemini responde de forma adequada com base no comando do sistema
  7. Atualização da interface: a resposta aparece no chat, criando uma experiência coesa

Serialização de dados

Um aspecto importante dessa abordagem é como você serializa os dados de cor:

'User selected color from history: ${json.encode(color.toLLMContextMap())}'

O método toLLMContextMap() (fornecido pelo pacote colorist_ui) converte um objeto ColorData em um mapa com propriedades principais que o LLM pode entender. Isso geralmente inclui:

  • Valores RGB (vermelho, verde, azul)
  • Representação do código hexadecimal
  • Qualquer nome ou descrição associada à cor

Ao formatar esses dados de maneira consistente e incluí-los na mensagem, você garante que o LLM tenha todas as informações necessárias para responder adequadamente.

Aplicações mais amplas da sincronização de contexto de LLM

Esse padrão de notificação do LLM sobre eventos da interface tem várias aplicações além da seleção de cores:

Outros casos de uso

  1. Filtrar mudanças: notifica o LLM quando os usuários aplicam filtros aos dados
  2. Eventos de navegação: informam o LLM quando os usuários navegam para seções diferentes.
  3. Mudanças na seleção: atualize o LLM quando os usuários selecionarem itens em listas ou grades.
  4. Atualizações de preferências: informe ao LLM quando os usuários mudarem configurações ou preferências
  5. Manipulação de dados: notifique o LLM quando os usuários adicionarem, editarem ou excluírem dados.

Em cada caso, o padrão permanece o mesmo:

  1. Detectar o evento da interface
  2. Serializar dados relevantes
  3. Enviar uma notificação formatada especialmente para o LLM
  4. Guiar o LLM para responder adequadamente usando o comando do sistema

Práticas recomendadas para sincronização de contexto de LLM

Com base na sua implementação, confira algumas práticas recomendadas para uma sincronização de contexto de LLM eficaz:

1. Consistência no formato

Use um formato consistente para notificações para que o LLM possa identificá-las facilmente:

"User [action] [object]: [structured data]"

2. Contexto avançado

Inclua detalhes suficientes nas notificações para que o LLM responda de forma inteligente. Para cores, isso significa valores RGB, códigos hexadecimais e outras propriedades relevantes.

3. Instruções claras

Dê instruções explícitas no comando do sistema sobre como lidar com notificações, de preferência com exemplos.

4. Integração natural

Crie notificações para fluir naturalmente na conversa, não como interrupções técnicas.

5. Notificação seletiva

Notifique o LLM apenas sobre ações relevantes para a conversa. Nem todos os eventos da interface precisam ser comunicados.

Solução de problemas

Problemas com as notificações

Se o LLM não estiver respondendo corretamente às seleções de cores:

  • Verifique se o formato da mensagem de notificação corresponde ao que está descrito no comando do sistema.
  • Verifique se os dados de cor estão sendo serializados corretamente.
  • Verifique se o comando do sistema tem instruções claras para lidar com as seleções
  • Procure erros no serviço de chat ao enviar notificações

Gerenciamento de contexto

Se o LLM parecer perder o contexto:

  • Verifique se a sessão de chat está sendo mantida corretamente
  • Verifique se os estados de conversa fazem a transição corretamente
  • Verifique se as notificações estão sendo enviadas na mesma sessão de chat

Problemas gerais

Para problemas gerais:

  • Examine os registros em busca de erros ou avisos
  • Verificar a conectividade do Firebase AI Logic
  • Verificar se há incompatibilidades de tipo nos parâmetros de função
  • Verifique se todo o código gerado pelo Riverpod está atualizado

Principais conceitos aprendidos

  • Como criar a sincronização de contexto de LLM entre a interface e o LLM
  • Serialização de eventos de interface em um contexto compatível com LLM
  • Orientar o comportamento do LLM para diferentes padrões de interação
  • Criar uma experiência coesa em interações de mensagens e não mensagens
  • Melhorar a compreensão do LLM sobre o estado geral do aplicativo

Ao implementar a sincronização de contexto do LLM, você criou uma experiência verdadeiramente integrada em que o LLM parece um assistente consciente e responsivo, em vez de apenas um gerador de texto. Esse padrão pode ser aplicado a inúmeras outras aplicações para criar interfaces mais naturais e intuitivas com tecnologia de IA.

9. Parabéns!

Você concluiu o codelab Colorist. 🎉

O que você criou

Você criou um aplicativo Flutter totalmente funcional que integra a API Gemini do Google para interpretar descrições de cores em linguagem natural. Agora, o app pode:

  • Processar descrições em linguagem natural, como "laranja pôr do sol" ou "azul profundo do oceano"
  • Usar o Gemini para traduzir de forma inteligente essas descrições em valores RGB
  • Mostrar as cores interpretadas em tempo real com respostas de streaming
  • Processar interações do usuário por chat e elementos da interface
  • Manter a percepção contextual em diferentes métodos de interação

O que fazer depois disso

Agora que você aprendeu o básico da integração do Gemini com o Flutter, confira algumas maneiras de continuar sua jornada:

Melhorar o app Colorist

  • Paletas de cores: adicione funcionalidade para gerar esquemas de cores complementares ou correspondentes.
  • Entrada de texto por voz: integre o reconhecimento de fala para descrições verbais de cores.
  • Gerenciamento do histórico: adicione opções para nomear, organizar e exportar conjuntos de cores.
  • Comandos personalizados: crie uma interface para que os usuários personalizem os comandos do sistema.
  • Análises avançadas: acompanhe quais descrições funcionam melhor ou causam dificuldades

Conheça outros recursos do Gemini

  • Entradas multimodais: adicione entradas de imagem para extrair cores de fotos
  • Geração de conteúdo: use o Gemini para gerar conteúdo relacionado a cores, como descrições ou histórias.
  • Melhorias na chamada de função: crie integrações de ferramentas mais complexas com várias funções.
  • Configurações de segurança: conheça as diferentes configurações de segurança e o impacto delas nas respostas.

Aplicar esses padrões a outros domínios

  • Análise de documentos: crie apps que podem entender e analisar documentos
  • Assistência para escrita criativa: crie ferramentas de escrita com sugestões baseadas em LLMs
  • Automação de tarefas: crie apps que traduzem linguagem natural em tarefas automatizadas.
  • Aplicativos baseados em conhecimento: crie sistemas especializados em domínios específicos.

Recursos

Confira alguns recursos valiosos para continuar aprendendo:

Documentação oficial

Curso e guia de comandos

Comunidade

Série de agentes do Flutter observáveis

No episódio 59, Craig Labenz e Andrew Brogden exploram este codelab, destacando partes interessantes da criação do app.

No episódio 60, Craig e Andrew se juntam novamente para estender o app do codelab com novos recursos e lutar para que os LLMs façam o que é pedido.

No episódio 61, Craig se junta a Chris Sells para analisar manchetes de notícias e gerar imagens correspondentes.

Feedback

Queremos saber como foi sua experiência com este codelab. Envie seu feedback por:

Agradecemos por concluir este codelab e esperamos que você continue explorando as possibilidades incríveis na interseção entre o Flutter e a IA.