1. Criar um app Flutter com o 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 do Flutter. Já quis permitir que os usuários controlassem seu app usando 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 "a cor laranja de um pôr do sol" ou "azul-marinho profundo"), e o app:
- Processa essas descrições usando a API Gemini do Google
- Interpreta as descrições em valores de cor 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
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 registro detalhado mostrando as interações brutas do LLM do outro lado. Esse registro permite entender melhor como uma integração de LLM realmente funciona.
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:
- Configuração do projeto: você vai começar com uma estrutura básica de app do Flutter e o pacote
colorist_ui
. - Integração básica do Gemini: conecte seu app à lógica de IA do Firebase e implemente a comunicação LLM.
- Comandos eficazes: crie um comando do sistema que oriente o LLM a entender descrições de cores.
- Declarações de função: defina ferramentas que o LLM pode usar para definir cores no aplicativo.
- Processamento de ferramentas: processe chamadas de função do LLM e conecte-as ao estado do app.
- Respostas em streaming: melhore a experiência do usuário com respostas de LLM em streaming em tempo real.
- 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 a lógica de IA do Firebase para apps Flutter
- Crie comandos eficazes do sistema para orientar o comportamento do LLM.
- Implementar declarações de função que conectem a linguagem natural e os recursos do app
- Processar respostas de streaming para uma experiência do usuário responsiva
- Sincronize o estado entre os eventos da interface e o LLM.
- Gerenciar o estado da conversa de LLM usando o Riverpod
- Lidar com erros de forma adequada em aplicativos com tecnologia de LLM
Prévia do código: um exemplo 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 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 com uma 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 no Flutter: familiaridade com os conceitos básicos do Flutter e a sintaxe do Dart.
- Conhecimento de programação assíncrona: entender futuros, async/await e streams.
- Conta do Firebase: você vai precisar 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 pela integração da API Gemini. Isso estabelece a arquitetura do aplicativo e garante que a interface funcione 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 IU - Implementar um serviço de mensagem de eco e conectá-lo à interface
Criar um novo projeto do Flutter
Comece criando um novo 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 de chaves:
colorist_ui
: um pacote personalizado que fornece os componentes da interface do app Coloristflutter_riverpod
eriverpod_annotation
: para gerenciamento de estadologging
: para geração de registros estruturados- Desenvolvimento de dependências para geração de código e linting
O pubspec.yaml
vai ficar assim:
pubspec.yaml
name: colorist
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
colorist_ui: ^0.2.4
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
build_runner: ^2.4.15
riverpod_generator: ^2.6.5
riverpod_lint: ^2.6.5
json_serializable: ^6.9.5
custom_lint: ^0.7.5
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(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
chatStateNotifier.addLlmMessage(message, MessageState.complete);
logStateNotifier.logLlmText(message);
}
}
Isso configura um app do 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:
- MainScreen: o componente principal da interface que mostra:
- Layout de tela dividida em computadores (área de interação e painel de registro)
- Uma interface com guias em dispositivos móveis
- Tela colorida, interface de chat e miniaturas do histórico
- Gerenciamento de estado: o app usa vários notificadores de estado:
- ChatStateNotifier: gerencia as mensagens de chat.
- ColorStateNotifier: gerencia a cor e o histórico atuais.
- LogStateNotifier: gerencia as entradas de registro para depuração.
- Gerenciamento de mensagens: o app usa um modelo de mensagem com estados diferentes:
- Mensagens do usuário: inseridas pelo usuário
- Mensagens de LLM: geradas pelo LLM (ou pelo seu serviço de eco por enquanto)
- MessageState: rastreia se as mensagens de LLM estão completas ou ainda estão sendo transmitidas.
Arquitetura do aplicativo
O app segue a seguinte arquitetura:
- Camada da interface: fornecida pelo pacote
colorist_ui
. - Gerenciamento de estado: usa o Riverpod para gerenciamento de estado reativo.
- Camada de serviço: atualmente contém seu serviço de eco simples, que será substituído pelo serviço do Gemini Chat.
- 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.
O app Colorist vai aparecer com:
- Uma área de exibição de cores com uma cor padrão
- Uma interface de chat em que você pode digitar mensagens
- Painel de registro mostrando as interações do chat
Tente digitar uma mensagem como "Quero uma cor azul escuro" e pressione Enviar. O serviço de eco simplesmente repete sua mensagem. Nas próximas etapas, 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 forneça respostas inteligentes.
Solução de problemas
Problemas com o pacote 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
- Verificar se há versões de pacotes em conflito
Erros de build
Se você encontrar erros de build:
- Verifique se você tem o SDK do Flutter do canal estável mais recente instalado
- Executar
flutter clean
seguido porflutter pub get
- Verifique se há mensagens de erro específicas na saída do console.
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 imite 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 a lógica de IA do Firebase. Você vai configurar o Firebase, configurar 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 a lógica de IA do Firebase 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 de API assíncronas e estados de erro
Configurar o Firebase
Primeiro, você precisa configurar o Firebase para seu projeto do Flutter. Isso envolve criar um projeto do Firebase, adicionar seu app a ele e configurar as configurações necessárias da lógica de IA do Firebase.
Criar um projeto do Firebase
- Acesse o Console do Firebase e faça login com sua Conta do Google.
- Clique em Criar um projeto do Firebase ou selecione um projeto atual.
- Siga o assistente de configuração para criar seu projeto.
Configurar a lógica de IA do Firebase no seu projeto
- No Console do Firebase, acesse seu projeto.
- Na barra lateral à esquerda, selecione AI.
- No menu suspenso "IA", selecione Lógica de IA.
- No card "Lógica de IA do Firebase", selecione Começar.
- 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 do Flutter:
dart pub global activate flutterfire_cli
Adicionar o Firebase ao seu app Flutter
- Adicione os pacotes do núcleo do Firebase e da lógica de IA do Firebase ao seu projeto:
flutter pub add firebase_core firebase_ai
- Execute o comando de configuração do FlutterFire:
flutterfire configure
Esse comando vai:
- Pedir para você selecionar o projeto do Firebase que você acabou de criar
- Registrar seus apps do Flutter com o Firebase
- Gerar um arquivo
firebase_options.dart
com a configuração do projeto
O comando vai detectar automaticamente as plataformas selecionadas (iOS, Android, macOS, Windows, Web) e configurá-las adequadamente.
Configuração específica da plataforma
O Firebase exige versões mínimas mais recentes do que o padrão do Flutter. Ele também requer acesso à rede para se comunicar com os servidores da lógica de IA do Firebase.
Configurar permissões do macOS
Para o macOS, é necessário ativar o acesso à rede nos direitos do app:
- Abra
macos/Runner/DebugProfile.entitlements
e adicione:
macos/Runner/DebugProfile.entitlements (link em inglês)
<key>com.apple.security.network.client</key>
<true/>
- Abra também
macos/Runner/Release.entitlements
e adicione a mesma entrada. - Atualize a versão mínima do macOS na parte de cima de
macos/Podfile
:
macos/Podfile
# Firebase requires at least macOS 10.15
platform :osx, '10.15'
Configurar permissões do iOS
No iOS, atualize a versão mínima na parte de cima de ios/Podfile
:
ios/Podfile
# Firebase requires at least iOS 13.0
platform :ios, '13.0'
Configurar as configurações do Android
Para Android, atualize android/app/build.gradle.kts
:
android/app/build.gradle.kts
android {
// ...
ndkVersion = "27.0.12077973"
defaultConfig {
// ...
minSdk = 23
// ...
}
}
Criar provedores de modelos do Gemini
Agora, você vai criar os provedores do Riverpod para o Firebase e o Gemini. Crie um novo 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
part 'gemini.g.dart';
@riverpod
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@riverpod
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.
firebaseAppProvider
: inicializa o Firebase com a configuração do projetogeminiModelProvider
: cria uma instância de modelo generativo do GeminichatSessionProvider
: 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:flutter_riverpod/flutter_riverpod.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(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.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
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
Esse serviço:
- Aceita mensagens do usuário e as envia para a API Gemini
- Atualiza a interface de chat com as respostas do modelo
- Registra todas as comunicações para facilitar a compreensão do fluxo real do LLM
- Processa erros com feedback adequado do usuário
Observação:a janela "Log" vai parecer quase idêntica à janela de chat. O registro vai ficar mais interessante quando você introduzir chamadas de função e, em seguida, respostas de streaming.
Gerar código do Riverpod
Execute o comando do build runner para gerar o código necessário do Riverpod:
dart run build_runner build --delete-conflicting-outputs
Isso vai criar os arquivos .g.dart
de que o Riverpod precisa para funcionar.
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 desta atualização são:
- Como substituir o serviço de eco pelo serviço de chat baseado na API Gemini
- Como adicionar telas de carregamento e de erro usando o padrão
AsyncValue
do Riverpod com o métodowhen
- 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.
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 registro vai mostrar as interações com a API.
Noções básicas sobre a comunicação de LLM
Vamos entender o que acontece quando você se comunica com a API Gemini:
Fluxo de comunicação
- Entrada do usuário: o usuário insere texto na interface de chat.
- Formatação de solicitação: o app formata o texto como um objeto
Content
para a API Gemini. - Comunicação de API: o texto é enviado à API Gemini pela lógica de IA do Firebase.
- Processamento de LLM: o modelo Gemini processa o texto e gera uma resposta.
- Processamento de respostas: o app recebe a resposta e atualiza a interface.
- 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 de conversa. 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
Nesse ponto, você pode fazer qualquer pergunta à API Gemini, já que não há restrições sobre o que ela vai responder. Por exemplo, você pode pedir um resumo das Guerras das Rosas, que não está relacionado ao propósito do seu app de cores.
Na próxima etapa, você vai criar um comando do sistema para orientar o Gemini a interpretar as 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 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 à lógica de IA do Firebase
Erros de acesso à API
Se você receber erros ao acessar a API Gemini:
- Confirmar se o faturamento está configurado corretamente no projeto do Firebase
- Verifique se a lógica de IA do Firebase e a API Cloud AI estão ativadas no seu projeto do Firebase
- Verificar a conectividade de rede e as configurações do firewall
- Verifique se o nome do modelo (
gemini-2.0-flash
) está correto e disponível.
Problemas com o 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 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
- Como configurar a lógica de IA do Firebase 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 de API assíncronos (carregamento, erro, dados)
- Noções básicas sobre o fluxo de comunicação e as sessões de chat do LLM
4. Comandos eficazes para descrições de cores
Nesta etapa, você vai criar e implementar uma solicitação do sistema que orienta o Gemini a interpretar descrições de cores. As solicitações 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
- Como carregar e usar comandos do sistema em um app do Flutter
- Orientar um LLM a fornecer respostas formatadas de forma consistente
- Como testar como os comandos do sistema afetam o comportamento do LLM
Entender os comandos do sistema
Antes de entrar na implementação, vamos entender o que são solicitações do sistema e por que elas 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. Ao contrário das mensagens do usuário, os comandos do sistema:
- Estabelecer 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 uma "descrição de trabalho" do LLM. Ele informa ao modelo como se comportar ao longo da 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 o LLM porque:
- Garantir a consistência: oriente o modelo a fornecer respostas em um formato consistente.
- Melhorar a relevância: concentre o modelo no seu domínio específico (no seu caso, cores).
- Estabelecer limites: defina o que o modelo deve e não deve fazer.
- Melhorar a experiência do usuário: crie um padrão de interação mais natural e útil.
- Reduzir o pós-processamento: receba respostas em formatos mais fáceis de analisar ou exibir.
Para o app Colorist, você precisa que o LLM interprete de forma consistente as descrições de cores e forneça valores RGB em um formato específico.
Criar um recurso de comando do sistema
Primeiro, você vai criar um arquivo de solicitação do sistema que será carregado no momento da execução. Essa abordagem permite modificar a solicitação 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
Noções básicas sobre a estrutura de comando do sistema
Vamos detalhar o que esse comando faz:
- Definição de função: estabelece o LLM como um "assistente de especialistas em cores"
- Explicação da tarefa: define a tarefa principal como a interpretação de descrições de cores em valores RGB
- Formato da resposta: especifica exatamente como os valores RGB devem ser formatados para garantir a consistência.
- Exemplo de troca: fornece um exemplo concreto do padrão de interação esperado.
- Como lidar com casos extremos: instruções sobre como lidar com descrições pouco claras
- Restrições e diretrizes: define limites, como manter 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 comando do sistema
Crie um novo arquivo lib/providers/system_prompt.dart
para carregar a solicitação do sistema:
lib/providers/system_prompt.dart
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'system_prompt.g.dart';
@riverpod
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 no momento da execução.
Atualizar o provedor de modelo do Gemini
Agora, modifique o arquivo lib/providers/gemini.dart
para incluir a solicitação 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:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
import 'system_prompt.dart'; // Add this import
part 'gemini.g.dart';
@riverpod
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@riverpod
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 mudança principal é a adição de systemInstruction: Content.system(systemPrompt)
ao criar o modelo generativo. Isso informa ao Gemini que suas instruções são o comando do sistema para todas as interações nessa sessão de chat.
Gerar código do Riverpod
Execute o comando do build runner para gerar o código do Riverpod necessário:
dart run build_runner build --delete-conflicting-outputs
Executar e testar o aplicativo
Agora execute seu aplicativo:
flutter run -d DEVICE
Teste com várias descrições de cores:
- "Quero um azul claro"
- "Dê-me uma cor verde floresta"
- "Criar um laranja vibrante de pôr do sol"
- "Quero a cor de lavanda fresca"
- "Mostre algo como um azul-escuro de oceano"
O Gemini agora responde com explicações coloquiais sobre as cores e valores RGB formatados de forma consistente. O comando do sistema orientou o LLM a fornecer o tipo de resposta que você precisa.
Também tente pedir conteúdo fora do contexto das 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 comando para tarefas especializadas
As solicitações do sistema são arte e ciência. Elas são uma parte essencial da integração do LLM e podem afetar drasticamente a utilidade do modelo para seu aplicativo específico. O que você fez aqui é uma forma de engenharia de comandos: personalizar instruções para fazer o modelo se comportar de acordo com as necessidades do aplicativo.
A engenharia de comando eficaz envolve:
- Definição clara de papéis: estabelecer a finalidade do LLM.
- Instruções explícitas: detalham exatamente como o LLM deve responder.
- Exemplos concretos: mostrar, em vez de apenas dizer como são as respostas boas
- Processamento de casos extremos: instruir o LLM sobre como lidar com cenários ambíguos.
- 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 de interpretação de cores que oferece respostas formatadas especificamente para as necessidades do seu app. Esse é um padrão poderoso que pode ser aplicado a muitos domínios e tarefas diferentes.
A seguir
Na próxima etapa, você vai usar essa base para adicionar declarações de função, que permitem que o LLM não apenas sugira valores RGB, mas também chame funções no app para definir a cor diretamente. Isso demonstra como os LLMs podem preencher a lacuna entre a linguagem natural e os recursos concretos do aplicativo.
Solução de problemas
Problemas de 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 porflutter pub get
para atualizar o pacote de recursos.
Respostas inconsistentes
Se o LLM não estiver seguindo consistentemente suas instruções de formatação:
- Tente tornar os requisitos de formato mais explícitos na solicitação do sistema.
- Adicione mais exemplos para demonstrar o padrão esperado
- Verifique se o formato solicitado é razoável para o modelo.
Limitação de taxa da API
Se você encontrar erros relacionados à limitação de taxa:
- O serviço de lógica de IA do Firebase tem limites de uso
- Considerar 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 das solicitações do sistema em aplicativos de LLM
- Criar comandos eficazes com instruções, exemplos e restrições claras
- Como carregar e usar comandos do sistema em um aplicativo do Flutter
- Como orientar o comportamento do LLM para tarefas específicas do domínio
- Usar a engenharia de comando para moldar as respostas do LLM
Esta etapa demonstra como você pode personalizar significativamente o comportamento do LLM sem mudar o código, simplesmente fornecendo instruções claras no prompt do sistema.
5. Declarações de função para ferramentas de LLM
Nesta etapa, você vai começar a permitir que o Gemini tome medidas no seu app implementando declarações de função. Esse recurso poderoso permite que o LLM não apenas sugira valores RGB, mas também os defina na interface do app com chamadas de ferramentas especializadas. No entanto, será necessário seguir a próxima etapa para conferir 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 apps do Flutter
- Como definir declarações de função baseadas em esquema para o Gemini
- Como integrar declarações de função ao modelo do Gemini
- Como atualizar o comando do sistema para usar os recursos da ferramenta
Noções básicas sobre chamadas de função
Antes de implementar declarações de função, vamos entender o que elas são e por que são valiosas:
O que é uma chamada de função?
A chamada de função (às vezes chamada de "uso de ferramenta") é um recurso que permite que um LLM:
- Reconhecer quando uma solicitação do usuário se beneficiaria com a invocação de uma função específica
- Gerar um objeto JSON estruturado com os parâmetros necessários para essa função
- Permita que o aplicativo execute a função com esses parâmetros
- 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 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:
- Ação direta: os usuários podem descrever o que querem em linguagem natural, e o app responde com ações concretas.
- Saída estruturada: o LLM produz dados limpos e estruturados, em vez de texto que precisa ser analisado
- Operações complexas: permite que o LLM acesse dados externos, realize cálculos ou modifique o estado do aplicativo.
- Melhor experiência do usuário: cria uma integração perfeita entre a conversa e a funcionalidade.
No app Colorist, a chamada de função permite que os usuários digam "Quero uma cor verde floresta" e que a interface seja atualizada imediatamente com essa cor, sem precisar analisar os valores RGB do texto.
Definir declarações de função
Crie um novo 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:flutter_riverpod/flutter_riverpod.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
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);
Noções básicas sobre declarações de função
Vamos detalhar o que esse código faz:
- Nome da função: você nomeia a função
set_color
para indicar claramente a finalidade dela. - Descrição da função: você fornece uma descrição clara que ajuda a LLM a entender quando usá-la.
- Definições de parâmetro: você define parâmetros estruturados com as próprias descrições:
red
: o componente vermelho do RGB, especificado como um número entre 0,0 e 1,0green
: o componente verde de RGB, especificado como um número entre 0,0 e 1,0.blue
: o componente azul de RGB, especificado como um número entre 0,0 e 1,0
- Tipos de esquema: use
Schema.number()
para indicar que esses são valores numéricos. - Coleção de ferramentas: você cria uma lista de ferramentas que contém a declaração da função.
Essa abordagem estruturada ajuda a LLM do Gemini a entender:
- Quando chamar essa função
- Quais parâmetros precisam ser fornecidos
- Quais restrições se aplicam a esses parâmetros (como o intervalo de valores)
Atualizar o provedor de modelo do Gemini
Agora, modifique o arquivo lib/providers/gemini.dart
para incluir as declarações de função ao inicializar o modelo Gemini:
lib/providers/gemini.dart
import 'dart:async';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_riverpod/flutter_riverpod.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
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@riverpod
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 é a adição do 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 ele chamar.
Atualizar o comando do sistema
Agora, você precisa modificar a solicitação do sistema para instruir o LLM sobre o uso da 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:
- Introdução à ferramenta: em vez de pedir valores RGB formatados, agora você informa ao LLM sobre a ferramenta
set_color
. - Processo modificado: você muda a etapa 3 de "formatar valores na resposta" para "usar a ferramenta para definir valores".
- Exemplo atualizado: você mostra como a resposta deve incluir uma chamada de ferramenta em vez de texto formatado
- Requisito de formatação removido: como você está usando chamadas de função estruturadas, não é mais necessário usar um formato de texto específico.
Essa solicitação atualizada 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 do Riverpod necessário:
dart run build_runner build --delete-conflicting-outputs
Execute o aplicativo
Nesse ponto, o Gemini vai gerar conteúdo que tenta usar a chamada de função, mas você ainda não implementou 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 não vai haver mudanças de cor na interface até a próxima etapa.
Execute o app:
flutter run -d DEVICE
Tente descrever uma cor como "azul-mar profundo" 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 as 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:
- Seleção de função: o LLM decide se uma chamada de função seria útil com base na solicitação do usuário.
- Geração de parâmetros: o LLM gera valores de parâmetros que se encaixam no esquema da função.
- Formato de chamada de função: o LLM envia um objeto de chamada de função estruturado na resposta.
- Processamento do aplicativo: o app recebe essa chamada e executa a função relevante (implementada na próxima etapa).
- 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 a etapa 4 ou 5 (processamento das chamadas de função), que será feita 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:
- Intent do usuário: se a solicitação do usuário seria melhor atendida por uma função
- Relevância da função: quão bem as funções disponíveis correspondem à tarefa.
- Disponibilidade do parâmetro: se ele pode determinar com segurança os valores dos parâmetros.
- Instruções do sistema: orientações do comando do sistema sobre o uso da função
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 para 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 círculo, permitindo que as descrições do usuário acionem mudanças reais de cor na interface usando as 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:
- Confira se os nomes e tipos dos parâmetros correspondem ao esperado
- Verifique se o nome da função é claro e descritivo
- Verifique se a descrição da função explica com precisão o propósito dela.
Problemas com o comando do sistema
Se o LLM não estiver tentando usar a função:
- Verifique se a solicitação do sistema instrui claramente o LLM a usar a ferramenta
set_color
. - Confira se o exemplo no comando do sistema demonstra o uso da função.
- Tente tornar a instrução para usar a ferramenta mais explícita
Problemas gerais
Se você tiver outros problemas:
- Verifique se há erros relacionados às declarações de função no console.
- Verifique se as ferramentas são transmitidas corretamente para o modelo
- Verifique se todo o código gerado pelo Riverpod está atualizado
Principais conceitos aprendidos
- Como definir declarações de função para ampliar os recursos de LLM em apps Flutter
- Como criar esquemas de parâmetros para a coleta de dados estruturados
- Integrar declarações de função ao modelo Gemini
- Como atualizar solicitações do sistema para incentivar o uso de funções
- Como as 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 os recursos de conversa e de aplicativo.
6. Como implementar o processamento de ferramentas
Nesta etapa, você vai implementar manipuladores para as chamadas de função do Gemini. Isso completa o círculo 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
- Noções básicas sobre o pipeline completo de chamadas de função em aplicativos de LLM
- Processar chamadas de função do Gemini em um app Flutter
- Implementar manipuladores de função que modificam o estado do app
- Como processar respostas de função e retornar resultados ao LLM
- Como criar um fluxo de comunicação completo entre o LLM e a interface
- Gerar registros de chamadas de função e respostas para transparência
Noções básicas sobre o pipeline de chamada de função
Antes de mergulhar na implementação, vamos entender o pipeline de chamada de função completo:
O fluxo completo
- Entrada do usuário: o usuário descreve uma cor em linguagem natural (por exemplo, "verde floresta")
- Processamento de LLM: o Gemini analisa a descrição e decide chamar a função
set_color
. - Geração de chamadas de função: o Gemini cria um JSON estruturado com parâmetros (valores vermelho, verde e azul).
- Função de recebimento de chamada: o app recebe esses dados estruturados do Gemini.
- Execução da função: o app executa a função com os parâmetros fornecidos.
- Atualização de estado: a função atualiza o estado do app (mudando a cor exibida).
- Geração de resposta: sua função retorna resultados ao LLM.
- Incorporação de respostas: o LLM incorpora esses resultados na resposta final.
- 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 da LLM. Quando um LLM faz uma chamada de função, ele não simplesmente envia a solicitação e segue em frente. Em vez disso, ele aguarda que o aplicativo execute a função e retorne os resultados. 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ção
Vamos atualizar o arquivo lib/services/gemini_tools.dart
para adicionar gerenciadores a 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:flutter_riverpod/flutter_riverpod.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(logStateNotifierProvider.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(colorStateNotifierProvider.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(logStateNotifierProvider.notifier);
logStateNotifier.logFunctionResults(functionResults);
return functionResults;
}
Map<String, Object?> handleUnknownFunction(String functionName) {
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
logStateNotifier.logWarning('Unsupported function call $functionName');
return {
'success': false,
'reason': 'Unsupported function call $functionName',
};
} // To here.
}
@riverpod
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);
Noções básicas sobre os manipuladores de função
Vamos detalhar o que esses gerenciadores de função fazem:
handleFunctionCall
: um despachante central que:- Registra a chamada de função para transparência no painel de registro
- Roteia para o gerenciador apropriado com base no nome da função
- Retorna uma resposta estruturada que será enviada de volta ao LLM
handleSetColor
: o gerenciador específico da funçãoset_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 cor atuais
- Registra os resultados da função para depuração.
handleUnknownFunction
: um gerenciador substituto para funções desconhecidas que:- Registra um aviso sobre a função sem suporte
- Retorna uma resposta de erro para o 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 ao 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:flutter_riverpod/flutter_riverpod.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(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.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
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:
- Verifica se a resposta do LLM contém chamadas de função
- Para cada chamada de função, invoca o método
handleFunctionCall
com o nome e os argumentos da função - Coleta os resultados de cada chamada de função
- Envia esses resultados de volta ao LLM usando
Content.functionResponses
- Processa a resposta do LLM para os resultados da função
- Atualiza a interface com o texto da resposta final
Isso cria um fluxo de ida e volta:
- Usuário → LLM: solicita 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 que incorpora os resultados da função
Gerar código do Riverpod
Execute o comando do build runner para gerar o código do 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
Tente inserir várias descrições de cores:
- "Quero um vermelho escuro"
- "Mostre um azul céu calmante"
- "Diga a cor das folhas frescas de hortelã"
- "Quero ver um pôr do sol laranja quente"
- "Use um roxo real rico"
Agora você vai ver:
- Sua mensagem aparecendo na interface do chat
- Resposta do Gemini aparecendo no chat
- Chamadas de função registradas no painel de registro
- Resultados da função sendo registrados imediatamente após
- O retângulo de cor é atualizado para mostrar a cor descrita
- Atualização dos valores RGB para mostrar os componentes da nova cor
- A resposta final do Gemini aparece, muitas vezes comentando sobre a cor que foi definida
O painel de registro 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 a função está retornando
- As respostas de acompanhamento 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 da interface
Quando você chama updateColor
com novos valores RGB, ele:
- Cria um novo objeto
ColorData
com os valores fornecidos. - Atualiza a cor atual no estado do app
- Adiciona a cor ao histórico
- Aciona atualizações da interface pelo gerenciamento de estado do Riverpod
Os componentes da interface 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 um tratamento de erros robusto:
- Bloco try-catch: agrupa todas as interações de LLM para detectar exceções.
- Registro de erros: registra erros no painel de registro com stack traces.
- Feedback do usuário: mostra uma mensagem de erro amigável no chat.
- Limpeza de estado: finaliza o estado da mensagem mesmo que ocorra um erro.
Isso garante que o app permaneça estável e ofereça feedback adequado, mesmo quando ocorrerem problemas com a execução da função ou do serviço LLM.
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 poderosas:
- Interface de linguagem natural: os usuários expressam a intenção em linguagem cotidiana
- Interpretação inteligente: o LLM traduz descrições vagas em valores precisos.
- Manipulação direta: a interface é atualizada em resposta à linguagem natural.
- Respostas contextuais: o LLM fornece contexto de conversa sobre as mudanças.
- Baixa carga cognitiva: os usuários não precisam entender os valores RGB ou a teoria das cores.
Esse padrão de uso de chamadas de função de LLM para conectar a linguagem natural e as ações da interface pode ser estendido para vários 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 conforme elas são recebidas, 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.
- Verificar se os nomes e tipos dos 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 seu gerenciador corresponde exatamente ao que está na declaração.
- Examine o painel de registro para conferir informações detalhadas sobre as chamadas de função.
Problemas de resposta da função
Se os resultados da função não estiverem sendo enviados corretamente ao LLM:
- Verifique se a função retorna um mapa formatado corretamente
- Verifique se o Content.functionResponses está sendo criado corretamente
- Procure erros no registro relacionados às respostas da função.
- Verifique se você está usando a mesma sessão de chat para a resposta
Problemas na tela colorida
Se as cores não estiverem aparecendo corretamente:
- Os valores RGB são convertidos corretamente em números duplos (o 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 notificador de estado de cor está sendo chamado corretamente
- Examine o registro para encontrar os valores exatos transmitidos para a função.
Problemas gerais
Para problemas gerais:
- Analisar os registros para verificar se há erros ou avisos
- Verificar a conectividade da lógica de IA do Firebase
- Verifique 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 implementar um pipeline de chamada de função completo no Flutter
- Como criar uma comunicação completa entre um LLM e seu aplicativo
- Processamento de dados estruturados de respostas de LLM
- Enviar os resultados da função de volta ao LLM para incorporação às respostas
- Como usar o painel de registro para ter visibilidade sobre as interações entre o LLM e o aplicativo
- 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 poderosos para integração de LLM: 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 de conversa que parece mágica para os usuários.
7. Streaming de respostas para uma melhor UX
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ê vai processar blocos de texto e chamadas de função conforme elas forem recebidas, criando um aplicativo mais responsivo e envolvente.
O que você vai aprender nesta etapa
- A importância do streaming para aplicativos com tecnologia de LLM
- Como implementar respostas de LLM de streaming em um app Flutter
- Processar partes de texto parciais à medida que chegam da API
- Como 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 essenciais para criar ótimas experiências do usuário com LLMs:
Experiência do usuário aprimorada
As respostas em streaming oferecem vários benefícios importantes para a experiência do usuário:
- Latência percebida reduzida: o texto aparece imediatamente para os usuários (normalmente em 100 a 300 ms), em vez de esperar vários segundos por uma resposta completa. Essa percepção de imediatismo melhora drasticamente a satisfação do usuário.
- Ritmo de conversa natural: a exibição gradual do texto imita a forma como os humanos se comunicam, criando uma experiência de diálogo mais natural.
- Processamento progressivo de informações: os usuários podem começar a processar as informações conforme elas chegam, em vez de serem sobrecarregados por um grande bloco de texto de uma só vez.
- 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.
- 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 de UX, o streaming oferece benefícios técnicos:
- Execução antecipada de funções: as chamadas de função podem ser detectadas e executadas assim que aparecem no stream, sem esperar a resposta completa.
- Atualizações incrementais da interface: é possível atualizar a interface progressivamente à medida que novas informações chegam, criando uma experiência mais dinâmica.
- Gerenciamento de estado da conversa: o streaming fornece indicadores claros sobre quando as respostas estão concluídas ou ainda em andamento, permitindo um melhor gerenciamento de estado.
- Riscos de tempo limite reduzidos: com respostas sem streaming, gerações de longa duração correm o risco de tempo limite de conexão. O streaming estabelece a conexão com antecedência e a mantém.
No app Colorist, implementar o streaming significa que as respostas de texto e as mudanças de cor vão aparecer mais rapidamente para os usuários, 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';
final conversationStateProvider = StateProvider( // Add from here...
(ref) => ConversationState.idle,
); // 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(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.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.state = ConversationState.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.state = ConversationState.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(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.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
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
Entender a implementação de streaming
Vamos detalhar o que esse código faz:
- Rastreamento do estado da conversa:
- Um
conversationStateProvider
rastreia se o app está processando uma resposta - O estado muda de
idle
parabusy
durante o processamento e depois volta paraidle
. - Isso evita várias solicitações simultâneas que podem entrar em conflito.
- Um
- Inicialização do stream:
sendMessageStream()
retorna um fluxo de blocos de resposta em vez de umFuture
com a resposta completa.- Cada bloco pode conter texto, chamadas de função ou ambos.
- Processamento progressivo:
- O
await for
processa cada bloco à medida que ele 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
- O
- 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.
- Tratamento e limpeza de erros:
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 de conversa adequado.
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 mudança principal 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 de 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 do Riverpod necessário:
dart run build_runner build --delete-conflicting-outputs
Executar e testar respostas de streaming
Execute o aplicativo:
flutter run -d DEVICE
Agora, teste o comportamento de streaming com várias descrições de cores. Tente descrições como:
- "Mostrar a cor azul-esverdeado-escuro do oceano ao entardecer"
- "Gostaria de ver um coral vibrante que lembra flores tropicais"
- "Criar um verde-oliva suave como os uniformes do exército"
Fluxo técnico de streaming em detalhes
Vamos examinar exatamente o que acontece ao transmitir uma resposta:
Estabelecimento de conexão
Quando você chama sendMessageStream()
, o seguinte acontece:
- O app estabelece uma conexão com o serviço de lógica de IA do Firebase
- A solicitação do usuário é enviada ao serviço
- O servidor começa a processar a solicitação
- A conexão de transmissão permanece aberta, pronta para transmitir blocos
Transmissão de blocos
À medida que o Gemini gera conteúdo, os blocos são enviados pelo stream:
- O servidor envia blocos de texto conforme eles são gerados (geralmente algumas palavras ou frases)
- Quando o Gemini decide fazer uma chamada de função, ele envia as informações da chamada de função
- Outros blocos de texto podem seguir as chamadas de função
- A transmissão continua até que a geração seja concluída
Processamento progressivo
O app processa cada bloco de forma incremental:
- Cada bloco de texto é anexado à resposta existente
- As chamadas de função são executadas assim que são detectadas
- A interface é atualizada em tempo real com resultados de texto e função
- O estado é rastreado para mostrar que a resposta ainda está sendo transmitida
Conclusão da transmissão
Quando a geração for concluída:
- O stream é fechado pelo servidor
- O loop
await for
é encerrado naturalmente - A mensagem é marcada como concluída
- O estado da conversa é definido como inativo
- 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 as abordagens de streaming e não 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 | Espera longa seguida pela aparição repentina de texto | Aparência natural e progressiva do texto |
Gerenciamento de estado | Mais simples (as mensagens são pendentes ou concluídas) | Mais complexas (as mensagens podem estar em um estado de streaming) |
Execução de função | Ocorre apenas 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 complexa devido ao processamento de fluxos |
Para um aplicativo como o Colorist, os benefícios de UX do streaming superam a complexidade de 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:
- Indicações visuais claras: sempre ofereça indicações visuais claras que distingam as mensagens de streaming das completas.
- Bloqueio de entrada: desative a entrada do usuário durante o streaming para evitar várias solicitações sobrepostas.
- Recuperação de erros: projete a interface para lidar com a recuperação suave se o streaming for interrompido.
- Transições de estado: garanta transições suaves entre os estados inativo, de streaming e completo.
- Visualização de progresso: considere animações ou indicadores sutis que mostrem o processamento ativo.
- Opções de cancelamento: em um app completo, ofereça maneiras de cancelar gerações em andamento.
- Integração de resultados de função: projete sua interface para processar os resultados de função que aparecem durante a transmissão
- Otimização de performance: minimize a reconstrução da interface durante atualizações rápidas de streaming.
O pacote colorist_ui
implementa muitas dessas práticas recomendadas, 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 vai estar ciente das 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 fluxos:
- Sintomas: respostas parciais, texto ausente ou encerramento abrupto do stream
- Solução: verifique a conectividade de rede e garanta padrões assíncronos/de espera adequados no código.
- Diagnóstico: examine o painel de registro em busca de mensagens de erro ou avisos relacionados ao processamento de fluxos.
- Correção: todo o processamento de fluxo usa o tratamento de erros adequado com blocos
try
/catch
.
Chamadas de função ausentes
Se as chamadas de função não estiverem sendo 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 registro para saber se as chamadas de função estão sendo recebidas.
- Correção: ajuste o comando do sistema para instruir o LLM de forma mais explícita a usar a ferramenta
set_color
.
Tratamento de erros gerais
Para outros problemas:
- Etapa 1: verificar se há mensagens de erro no painel de registro
- Etapa 2: verificar a conectividade da lógica de IA do Firebase
- Etapa 3: verificar se todo o código gerado pelo Riverpod está atualizado
- Etapa 4: analise a implementação de streaming para verificar se há instruções de espera ausentes
Principais conceitos aprendidos
- Como implementar respostas de streaming com a API Gemini para uma UX mais responsiva
- Como gerenciar o estado da conversa para processar corretamente as interações de streaming
- Processar textos e chamadas de função em tempo real à medida que chegam
- Como criar interfaces responsivas que são atualizadas de forma incremental durante o streaming
- Como processar transmissões simultâneas 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 uma conversa.
8. Sincronização de contexto do LLM
Nesta etapa extra, você vai implementar a sincronização de contexto do 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 aprender nesta etapa
- Como criar a sincronização de contexto do LLM entre a interface e o LLM
- Serializar eventos da interface em um contexto que o LLM possa entender
- Como 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
- Como melhorar a consciência do contexto do LLM além das mensagens explícitas do chat
Noções básicas sobre a sincronização de contexto do 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 aborda essa limitação:
Por que a sincronização de contexto do LLM é importante
Quando os usuários interagem com o 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ê informe isso explicitamente. Sincronização de contexto do LLM:
- Mantém o contexto: mantém o LLM informado sobre todas as ações relevantes do usuário.
- Cria coerência: produz uma experiência coesa em que o LLM reconhece as interações da interface.
- Melhora a inteligência: permite que o LLM responda adequadamente a todas as ações do usuário.
- Melhora a experiência do usuário: deixa todo o aplicativo mais integrado e responsivo.
- 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 perfeito e consciente.
Atualize o serviço de chat do Gemini para receber notificações de seleção de cores
Primeiro, você vai adicionar um método ao GeminiChatService
para notificar o LLM quando um usuário selecionar uma cor do 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';
final conversationStateProvider = StateProvider(
(ref) => ConversationState.idle,
);
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(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.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.state = ConversationState.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.state = ConversationState.idle;
}
}
Future<void> _processBlock(
GenerateContentResponse block,
String llmMessageId,
) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
final logStateNotifier = ref.read(logStateNotifierProvider.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
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
A adição principal é o método notifyColorSelection
, que:
- Recebe um objeto
ColorData
que representa a cor selecionada - Codifica em um formato JSON que pode ser incluído em uma mensagem
- Envia uma mensagem formatada especialmente para o LLM indicando uma seleção do usuário
- Reutiliza o método
sendMessage
atual para processar a notificação
Essa abordagem evita a duplicação usando sua infraestrutura de processamento de mensagens atual.
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 para a 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 é a adição do callback notifyColorSelection
, que conecta o evento da interface (selecionar uma cor no histórico) ao sistema de notificação de 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:
- Explica o conceito de notificações de seleção de histórico para o LLM
- Exemplo de como essas notificações são
- Mostra um exemplo de resposta adequada
- Define as expectativas para reconhecer a seleção e comentar sobre a cor
Isso ajuda a 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 do 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
O teste da sincronização de contexto do LLM envolve:
- Primeiro, gere algumas cores descrevendo-as no chat
- "Mostre um roxo vibrante"
- "Quero uma cor verde floresta"
- "Dê-me um vermelho vivo"
- Em seguida, clique em uma das miniaturas coloridas na faixa de histórico.
Observe o seguinte:
- A cor selecionada aparece na tela principal
- Uma mensagem do usuário aparece no chat indicando a seleção de cores
- O LLM responde confirmando a seleção e comentando sobre a cor.
- Toda a interação parece natural e coesa
Isso cria uma experiência perfeita em que o LLM identifica e responde de forma adequada às mensagens diretas e às interações da interface.
Como a sincronização de contexto de LLM funciona
Vamos conferir os detalhes técnicos de como essa sincronização funciona:
Fluxo de dados
- Ação do usuário: o usuário clica em uma cor na faixa de histórico
- Evento da interface: o widget
MainScreen
detecta essa seleção. - Execução do callback: o callback
notifyColorSelection
é acionado. - Criação de mensagens: uma mensagem formatada de maneira especial é criada com os dados de cor.
- Processamento de LLM: a mensagem é enviada para o Gemini, que reconhece o formato.
- Resposta contextual: o Gemini responde de forma adequada com base no comando do sistema.
- 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 cores:
'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 as principais propriedades 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 forma 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 de interface tem várias aplicações além da seleção de cores:
Outros casos de uso
- Mudanças de filtro: notifique o LLM quando os usuários aplicarem filtros aos dados.
- Eventos de navegação: informe o LLM quando os usuários navegam para seções diferentes.
- Mudanças de seleção: atualize o LLM quando os usuários selecionarem itens de listas ou grids.
- Atualizações de preferência: informe ao LLM quando os usuários mudarem as configurações ou preferências.
- Manipulação de dados: notificar o LLM quando os usuários adicionarem, editarem ou excluirem dados.
Em cada caso, o padrão permanece o mesmo:
- Detectar o evento da interface
- Serializar dados relevantes
- Enviar uma notificação formatada especialmente para o LLM
- Orientar o LLM a responder adequadamente pelo comando do sistema
Práticas recomendadas para a 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 as 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 processar notificações, de preferência com exemplos.
4. Integração natural
Projete 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 responder corretamente às seleções de cores:
- Confira se o formato da mensagem de notificação corresponde ao descrito no comando do sistema.
- Verifique se os dados de cores estão sendo serializados corretamente
- Verifique se o comando do sistema tem instruções claras para processar 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
- Verificar se os estados da conversa são transferidos corretamente
- Verifique se as notificações estão sendo enviadas na mesma sessão de chat
Problemas gerais
Para problemas gerais:
- Analisar os registros para verificar se há erros ou avisos
- Verificar a conectividade da lógica de IA do Firebase
- Verifique 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 do LLM entre a interface e o LLM
- Serializar eventos de interface em um contexto compatível com LLM
- Como orientar o comportamento do LLM para diferentes padrões de interação
- Criar uma experiência coesa entre interações com e sem mensagens
- Melhorar a consciência do LLM sobre o estado mais amplo 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, e não apenas um gerador de texto. Esse padrão pode ser aplicado a incontáveis outros aplicativos para criar interfaces mais naturais e intuitivas com tecnologia de IA.
9. Parabéns!
Você concluiu o codelab "Colorista". 🎉
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-do-pôr-do-sol" ou "azul-mar-profundo"
- Use o Gemini para traduzir essas descrições de forma inteligente em valores RGB
- Mostrar as cores interpretadas em tempo real com respostas de streaming
- Processar interações do usuário com elementos de chat e da interface
- Manter a consciência contextual em diferentes métodos de interação
O que fazer depois disso
Agora que você aprendeu os conceitos básicos da integração do Gemini com o Flutter, confira algumas maneiras de continuar sua jornada:
Melhorar o app Colorist
- Paleta de cores: adicione funcionalidade para gerar esquemas de cores complementares ou correspondentes.
- Entrada de voz: integre o reconhecimento de fala para descrições verbais de cores.
- Gerenciamento de histórico: adicione opções para nomear, organizar e exportar conjuntos de cores.
- Solicitação personalizada: crie uma interface para que os usuários personalizem solicitações do sistema.
- Análises avançadas: acompanhe quais descrições funcionam melhor ou causam dificuldades
Conheça mais 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 possam entender e analisar documentos.
- Assistência para escrita criativa: crie ferramentas de escrita com sugestões com tecnologia de LLM.
- Automação de tarefas: crie apps que traduzem a linguagem natural em tarefas automatizadas.
- Aplicativos baseados em conhecimento: crie sistemas de especialistas em domínios específicos.
Recursos
Confira alguns recursos valiosos para continuar aprendendo:
Documentação oficial
Curso e guia de comandos
Comunidade
- Comunidade do Flutter
- Comunidade do Firebase
- Comunidade de desenvolvedores do Gemini
- Stack Overflow (em inglês)
Série de agentes do Observable Flutter
No episódio 59, Craig Labenz e Andrew Brogden exploram este codelab, destacando partes interessantes do build do app.
No episódio 60, Craig e Andrew voltam para ampliar o app do codelab com novos recursos e tentar fazer com que os LLMs façam o que são mandados.
No episódio 61, Craig é acompanhado por Chris Sells para analisar as manchetes de notícias e gerar as imagens correspondentes.
Feedback
Queremos saber sobre sua experiência com este codelab. Envie seu feedback:
Agradecemos por concluir este codelab. Esperamos que você continue explorando as possibilidades incríveis na interseção do Flutter e da IA.