Creare un'app Flutter basata su Gemini

1. Crea un'app Flutter basata su Gemini

Cosa creerai

In questo codelab creerai Colorist, un'applicazione Flutter interattiva che porta la potenza dell'API Gemini direttamente nella tua app Flutter. Hai mai voluto consentire agli utenti di controllare la tua app tramite il linguaggio naturale, ma non sapevi da dove iniziare? Questo codelab ti mostra come fare.

Colorist consente agli utenti di descrivere i colori in linguaggio naturale (ad esempio "l'arancione di un tramonto" o "blu oceano profondo") e l'app:

  • Elabora queste descrizioni utilizzando l'API Gemini di Google
  • Interpreta le descrizioni in valori di colore RGB precisi
  • Visualizza il colore sullo schermo in tempo reale
  • Fornisce dettagli tecnici sul colore e un contesto interessante
  • Conserva una cronologia dei colori generati di recente

Screenshot dell'app Colorist che mostra la visualizzazione dei colori e l'interfaccia della chat

L'app presenta un'interfaccia a schermo diviso con un'area di visualizzazione a colori e un sistema di chat interattivo da un lato e un pannello di log dettagliato che mostra le interazioni LLM non elaborate dall'altro. Questo log ti consente di comprendere meglio come funziona realmente un'integrazione LLM dietro le quinte.

Perché è importante per gli sviluppatori Flutter

Gli LLM stanno rivoluzionando il modo in cui gli utenti interagiscono con le applicazioni, ma la loro integrazione efficace nelle app mobile e desktop presenta sfide uniche. Questo codelab ti insegna pattern pratici che vanno oltre le semplici chiamate API non elaborate.

Il tuo percorso di apprendimento

Questo codelab ti guida passo passo nella procedura di creazione di Colorist:

  1. Configurazione del progetto: inizierai con una struttura di base dell'app Flutter e il pacchetto colorist_ui
  2. Integrazione di base di Gemini: collega la tua app a Firebase AI Logic e implementa la comunicazione LLM
  3. Prompt efficaci: crea un prompt di sistema che guidi l'LLM a comprendere le descrizioni dei colori
  4. Dichiarazioni di funzioni: definisci gli strumenti che l'LLM può utilizzare per impostare i colori nella tua applicazione
  5. Gestione degli strumenti: elabora le chiamate di funzione dall'LLM e collegale allo stato della tua app
  6. Risposte in streaming: migliora l'esperienza utente con risposte LLM in streaming in tempo reale
  7. Sincronizzazione del contesto LLM: crea un'esperienza coesa informando l'LLM delle azioni dell'utente

Obiettivi didattici

  • Configura Firebase AI Logic per le applicazioni Flutter
  • Creare prompt di sistema efficaci per guidare il comportamento degli LLM
  • Implementa dichiarazioni di funzioni che collegano il linguaggio naturale e le funzionalità dell'app
  • Elabora le risposte in streaming per un'esperienza utente reattiva
  • Sincronizzare lo stato tra gli eventi dell'interfaccia utente e il LLM
  • Gestire lo stato della conversazione LLM utilizzando Riverpod
  • Gestire gli errori in modo controllato nelle applicazioni basate su LLM

Anteprima del codice: un assaggio di ciò che implementerai

Ecco un'anteprima della dichiarazione di funzione che creerai per consentire all'LLM di impostare i colori nella tua 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)'),
  },
);

Una panoramica video di questo codelab

Guarda Craig Labenz e Andrew Brogdon discutere di questo codelab nell'episodio n. 59 di Observable Flutter:

Prerequisiti

Per sfruttare al meglio questo codelab, devi avere:

  • Esperienza di sviluppo Flutter: familiarità con le nozioni di base di Flutter e la sintassi di Dart
  • Conoscenza della programmazione asincrona: comprensione di Futures, async/await e stream
  • Account Firebase: per configurare Firebase è necessario un Account Google

Iniziamo a creare la tua prima app Flutter basata su LLM.

2. Configurazione del progetto e servizio di test

In questo primo passaggio, configurerai la struttura del progetto e implementerai un servizio di echo che verrà sostituito in un secondo momento con l'integrazione dell'API Gemini. In questo modo viene stabilita l'architettura dell'applicazione e si garantisce che la UI funzioni correttamente prima di aggiungere la complessità delle chiamate LLM.

Cosa imparerai in questo passaggio

  • Configurazione di un progetto Flutter con le dipendenze richieste
  • Utilizzo del pacchetto colorist_ui per i componenti UI
  • Implementazione di un servizio di messaggi di echo e connessione all'interfaccia utente

Crea un nuovo progetto Flutter

Inizia creando un nuovo progetto Flutter con il seguente comando:

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

Il flag -e indica che vuoi un progetto vuoto senza l'app counter predefinita. L'app è progettata per funzionare su computer, dispositivi mobili e web. Tuttavia, al momento flutterfire non supporta Linux.

Aggiungi dipendenze

Naviga alla directory del progetto e aggiungi le dipendenze richieste:

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

Verranno aggiunti i seguenti pacchetti di chiavi:

  • colorist_ui: Un pacchetto personalizzato che fornisce i componenti UI per l'app Colorist
  • flutter_riverpod e riverpod_annotation: per la gestione dello stato
  • logging: Per il logging strutturato
  • Dipendenze di sviluppo per la generazione e il controllo del codice

Il tuo pubspec.yaml sarà simile a questo:

pubspec.yaml

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

environment:
  sdk: ^3.9.2

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

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

flutter:
  uses-material-design: true

Configurare le opzioni di analisi

Aggiungi custom_lint al file analysis_options.yaml nella root del progetto:

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

Questa configurazione consente di utilizzare lint specifici di Riverpod per contribuire a mantenere la qualità del codice.

Implementa il file main.dart

Sostituisci i contenuti di lib/main.dart con quanto segue:

lib/main.dart

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

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

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

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

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

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

In questo modo, viene configurata un'app Flutter che implementa un servizio di echo che imita il comportamento di un LLM restituendo il messaggio dell'utente.

Comprendere l'architettura

Dedichiamo un minuto a capire l'architettura dell'app colorist:

Il pacchetto colorist_ui

Il pacchetto colorist_ui fornisce componenti UI predefiniti e strumenti di gestione dello stato:

  1. MainScreen: il componente principale dell'interfaccia utente che mostra:
    • Un layout a schermo diviso sul computer (area di interazione e pannello dei log)
    • Un'interfaccia a schede su dispositivo mobile
    • Display a colori, interfaccia della chat e miniature della cronologia
  2. Gestione dello stato: l'app utilizza diversi notificatori di stato:
    • ChatStateNotifier: gestisce i messaggi di chat
    • ColorStateNotifier: gestisce il colore attuale e la cronologia
    • LogStateNotifier: gestisce le voci di log per il debug
  3. Gestione dei messaggi: l'app utilizza un modello di messaggio con stati diversi:
    • Messaggi per gli utenti: inseriti dall'utente
    • Messaggi LLM: generati dall'LLM (o dal tuo servizio di echo per ora)
    • MessageState: tiene traccia se i messaggi LLM sono completi o ancora in streaming

Architettura dell'applicazione

L'app segue la seguente architettura:

  1. Livello UI: fornito dal pacchetto colorist_ui
  2. Gestione dello stato: utilizza Riverpod per la gestione reattiva dello stato
  3. Livello di servizio: attualmente contiene il servizio di semplice echo, che verrà sostituito dal servizio Gemini Chat
  4. Integrazione LLM: verrà aggiunta nei passaggi successivi

Questa separazione ti consente di concentrarti sull'implementazione dell'integrazione LLM, mentre i componenti UI sono già gestiti.

Esegui l'app

Esegui l'app con questo comando:

flutter run -d DEVICE

Sostituisci DEVICE con il dispositivo di destinazione, ad esempio macos, windows, chrome o un ID dispositivo.

Screenshot dell'app Colorist che mostra il rendering del markdown del servizio di eco

Ora dovresti vedere l'app Colorist con:

  1. Un'area di visualizzazione dei colori con un colore predefinito
  2. Un'interfaccia di chat in cui puoi digitare messaggi
  3. Un riquadro dei log che mostra le interazioni della chat

Prova a digitare un messaggio come "Vorrei un colore blu intenso" e premi Invia. Il servizio di eco ripeterà semplicemente il tuo messaggio. Nei passaggi successivi, sostituirai questo valore con l'interpretazione effettiva del colore utilizzando Firebase AI Logic.

Passaggi successivi

Nel passaggio successivo, configurerai Firebase e implementerai l'integrazione di base dell'API Gemini per sostituire il servizio di echo con il servizio di chat Gemini. In questo modo, l'app potrà interpretare le descrizioni dei colori e fornire risposte intelligenti.

Risoluzione dei problemi

Problemi relativi al pacchetto UI

Se riscontri problemi con il pacchetto colorist_ui:

  • Assicurati di utilizzare l'ultima versione
  • Verifica di aver aggiunto correttamente la dipendenza
  • Controlla la presenza di versioni di pacchetti in conflitto

Errori di compilazione

Se visualizzi errori di compilazione:

  • Assicurati di aver installato l'ultima versione dell'SDK Flutter del canale stabile
  • Esegui flutter clean seguito da flutter pub get
  • Controlla l'output della console per messaggi di errore specifici

Concetti chiave appresi

  • Configurazione di un progetto Flutter con le dipendenze necessarie
  • Comprendere l'architettura dell'applicazione e le responsabilità dei componenti
  • Implementazione di un servizio semplice che imita il comportamento di un LLM
  • Connessione del servizio ai componenti della UI
  • Utilizzare Riverpod per la gestione dello stato

3. Integrazione di base di Gemini Chat

In questo passaggio, sostituirai il servizio di echo del passaggio precedente con l'integrazione dell'API Gemini utilizzando Firebase AI Logic. Configurerai Firebase, imposterai i provider necessari e implementerai un servizio di chat di base che comunica con l'API Gemini.

Cosa imparerai in questo passaggio

  • Configurazione di Firebase in un'applicazione Flutter
  • Configurazione di Firebase AI Logic per l'accesso a Gemini
  • Creazione di provider Riverpod per i servizi Firebase e Gemini
  • Implementazione di un servizio di chat di base con l'API Gemini
  • Gestione delle risposte API asincrone e degli stati di errore

Configura Firebase

Innanzitutto, devi configurare Firebase per il tuo progetto Flutter. Ciò comporta la creazione di un progetto Firebase, l'aggiunta dell'app e la configurazione delle impostazioni necessarie di Firebase AI Logic.

Crea un progetto Firebase

  1. Vai alla console Firebase e accedi con il tuo Account Google.
  2. Fai clic su Crea un progetto Firebase o seleziona un progetto esistente.
  3. Segui la procedura guidata di configurazione per creare il progetto.

Configurare Firebase AI Logic nel tuo progetto Firebase

  1. Nella console Firebase, vai al tuo progetto.
  2. Nella barra laterale a sinistra, seleziona AI.
  3. Nel menu a discesa AI, seleziona Logica AI.
  4. Nella scheda Firebase AI Logic, seleziona Inizia.
  5. Segui le istruzioni per abilitare l'API Gemini Developer per il tuo progetto.

Installa l'interfaccia a riga di comando FlutterFire

L'interfaccia a riga di comando FlutterFire semplifica la configurazione di Firebase nelle app Flutter:

dart pub global activate flutterfire_cli

Aggiungi Firebase alla tua app Flutter

  1. Aggiungi i pacchetti Firebase Core e Firebase AI Logic al tuo progetto:
flutter pub add firebase_core firebase_ai
  1. Esegui il comando di configurazione FlutterFire:
flutterfire configure

Questo comando:

  • Ti chiede di selezionare il progetto Firebase che hai appena creato
  • Registra le tue app Flutter con Firebase
  • Genera un file firebase_options.dart con la configurazione del progetto

Il comando rileva automaticamente le piattaforme selezionate (iOS, Android, macOS, Windows, web) e le configura in modo appropriato.

Configurazione specifica della piattaforma

Firebase richiede versioni minime superiori a quelle predefinite per Flutter. Richiede anche l'accesso alla rete per comunicare con i server Firebase AI Logic.

Configurare le autorizzazioni di macOS

Per macOS, devi attivare l'accesso alla rete nei diritti dell'app:

  1. Apri macos/Runner/DebugProfile.entitlements e aggiungi:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. Apri anche macos/Runner/Release.entitlements e aggiungi la stessa voce.

Configurare le impostazioni di iOS

Per iOS, aggiorna la versione minima nella parte superiore di ios/Podfile:

ios/Podfile

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

Crea fornitori di modelli Gemini

Ora creerai i provider Riverpod per Firebase e Gemini. Crea un nuovo file lib/providers/gemini.dart:

lib/providers/gemini.dart

import 'dart:async';

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

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

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

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

Questo file definisce la base per tre fornitori chiave. Questi provider vengono generati quando esegui dart run build_runner dai generatori di codice Riverpod. Questo codice utilizza l'approccio basato sulle annotazioni di Riverpod 3 con pattern di provider aggiornati.

  1. firebaseAppProvider: inizializza Firebase con la configurazione del progetto
  2. geminiModelProvider: Crea un'istanza del modello generativo Gemini
  3. chatSessionProvider: crea e gestisce una sessione di chat con il modello Gemini

L'annotazione keepAlive: true nella sessione di chat garantisce che venga mantenuta per tutto il ciclo di vita dell'app, mantenendo il contesto della conversazione.

Implementare il servizio Gemini Chat

Crea un nuovo file lib/services/gemini_chat_service.dart per implementare il servizio di chat:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

Questo servizio:

  1. Accetta i messaggi degli utenti e li invia all'API Gemini
  2. Aggiorna l'interfaccia della chat con le risposte del modello
  3. Registra tutte le comunicazioni per facilitare la comprensione del flusso effettivo dell'LLM
  4. Gestisce gli errori con un feedback utente appropriato

Nota: a questo punto, la finestra di log avrà un aspetto quasi identico a quello della finestra di chat. Il log diventerà più interessante quando introdurrai le chiamate di funzioni e poi le risposte in streaming.

Genera codice Riverpod

Esegui il comando build runner per generare il codice Riverpod necessario:

dart run build_runner build --delete-conflicting-outputs

Verranno creati i file .g.dart necessari per il funzionamento di Riverpod.

Aggiorna il file main.dart

Aggiorna il file lib/main.dart per utilizzare il nuovo servizio di chat 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),
      ),
    );
  }
}

Le modifiche principali di questo aggiornamento sono:

  1. Sostituzione del servizio di echo con il servizio di chat basato sull'API Gemini
  2. Aggiungere schermate di caricamento e di errore utilizzando il pattern AsyncValue di Riverpod con il metodo when
  3. Connessione della UI al nuovo servizio di chat tramite il callback sendMessage

Esegui l'app

Esegui l'app con questo comando:

flutter run -d DEVICE

Sostituisci DEVICE con il dispositivo di destinazione, ad esempio macos, windows, chrome o un ID dispositivo.

Screenshot dell&#39;app Colorist che mostra la risposta del modello LLM Gemini a una richiesta di un colore giallo soleggiato

Ora, quando digiti un messaggio, questo verrà inviato all'API Gemini e riceverai una risposta dal LLM anziché un'eco. Il riquadro dei log mostra le interazioni con l'API.

Comprendere la comunicazione LLM

Vediamo cosa succede quando comunichi con l'API Gemini:

Il flusso di comunicazione

  1. Input dell'utente: l'utente inserisce del testo nell'interfaccia di chat
  2. Formattazione della richiesta: l'app formatta il testo come oggetto Content per l'API Gemini
  3. Comunicazione API: il testo viene inviato all'API Gemini tramite Firebase AI Logic
  4. Elaborazione LLM: il modello Gemini elabora il testo e genera una risposta
  5. Gestione della risposta: l'app riceve la risposta e aggiorna l'interfaccia utente
  6. Logging: tutte le comunicazioni vengono registrate per garantire la trasparenza

Sessioni di chat e contesto della conversazione

La sessione di chat di Gemini mantiene il contesto tra i messaggi, consentendo interazioni conversazionali. Ciò significa che l'LLM "ricorda" gli scambi precedenti nella sessione corrente, consentendo conversazioni più coerenti.

L'annotazione keepAlive: true sul tuo fornitore di sessioni di chat garantisce che questo contesto persista per tutto il ciclo di vita dell'app. Questo contesto persistente è fondamentale per mantenere un flusso di conversazione naturale con l'LLM.

Passaggi successivi

A questo punto, puoi chiedere qualsiasi cosa all'API Gemini, in quanto non sono in vigore restrizioni su ciò a cui risponderà. Ad esempio, potresti chiedere un riepilogo della Guerra delle due rose, che non è correlata allo scopo della tua app di colori.

Nel passaggio successivo, creerai un prompt di sistema per guidare Gemini nell'interpretazione delle descrizioni dei colori in modo più efficace. In questo modo, mostreremo come personalizzare il comportamento di un LLM per esigenze specifiche dell'applicazione e concentrare le sue funzionalità sul dominio della tua app.

Risoluzione dei problemi

Problemi di configurazione di Firebase

Se riscontri errori durante l'inizializzazione di Firebase:

  • Assicurati che il file firebase_options.dart sia stato generato correttamente
  • Verifica di aver eseguito l'upgrade al piano Blaze per l'accesso a Firebase AI Logic

Errori di accesso all'API

Se ricevi errori di accesso all'API Gemini:

  • Verifica che la fatturazione sia configurata correttamente nel tuo progetto Firebase
  • Verifica che Firebase AI Logic e l'API Cloud AI siano abilitate nel progetto Firebase
  • Controlla la connettività di rete e le impostazioni del firewall
  • Verifica che il nome del modello (gemini-2.0-flash) sia corretto e disponibile

Problemi di contesto della conversazione

Se noti che Gemini non ricorda il contesto precedente della chat:

  • Verifica che la funzione chatSession sia annotata con @Riverpod(keepAlive: true)
  • Verifica di riutilizzare la stessa sessione di chat per tutti gli scambi di messaggi
  • Verifica che la sessione di chat sia inizializzata correttamente prima di inviare messaggi

Problemi specifici della piattaforma

Per problemi specifici della piattaforma:

  • iOS/macOS: assicurati che i diritti appropriati siano impostati e che le versioni minime siano configurate
  • Android: verifica che la versione minima dell'SDK sia impostata correttamente
  • Controllare i messaggi di errore specifici della piattaforma nella console

Concetti chiave appresi

  • Configurazione di Firebase in un'applicazione Flutter
  • Configurazione di Firebase AI Logic per l'accesso a Gemini
  • Creazione di provider Riverpod per servizi asincroni
  • Implementazione di un servizio di chat che comunica con un LLM
  • Gestione degli stati asincroni dell'API (caricamento, errore, dati)
  • Comprendere il flusso di comunicazione e le sessioni di chat degli LLM

4. Prompt efficaci per le descrizioni dei colori

In questo passaggio, creerai e implementerai un prompt di sistema che guida Gemini nell'interpretazione delle descrizioni dei colori. I prompt di sistema sono un modo efficace per personalizzare il comportamento degli LLM per attività specifiche senza modificare il codice.

Cosa imparerai in questo passaggio

  • Comprendere i prompt di sistema e la loro importanza nelle applicazioni LLM
  • Creare prompt efficaci per attività specifiche del dominio
  • Caricare e utilizzare i prompt di sistema in un'app Flutter
  • Guidare un LLM a fornire risposte con una formattazione coerente
  • Test di come i prompt di sistema influiscono sul comportamento degli LLM

Informazioni sui prompt di sistema

Prima di entrare nei dettagli dell'implementazione, vediamo cosa sono i prompt di sistema e perché sono importanti:

Che cosa sono i prompt di sistema?

Un prompt di sistema è un tipo speciale di istruzione fornita a un LLM che imposta il contesto, le linee guida sul comportamento e le aspettative per le sue risposte. A differenza dei messaggi degli utenti, i prompt di sistema:

  • Stabilire il ruolo e la personalità del LLM
  • Definisci conoscenze o capacità specializzate
  • Fornire istruzioni di formattazione
  • Impostare vincoli sulle risposte
  • Descrivere come gestire vari scenari

Pensa a un prompt di sistema come alla "descrizione del lavoro" dell'LLM: indica al modello come comportarsi durante la conversazione.

Perché i prompt di sistema sono importanti

I prompt di sistema sono fondamentali per creare interazioni LLM coerenti e utili perché:

  1. Garantire la coerenza: guida il modello a fornire risposte in un formato coerente
  2. Migliorare la pertinenza: concentra il modello sul tuo dominio specifico (nel tuo caso, i colori)
  3. Stabilire dei limiti: definisci cosa deve e non deve fare il modello
  4. Migliorare l'esperienza utente: crea un pattern di interazione più naturale e utile
  5. Ridurre la post-elaborazione: ricevi risposte in formati più facili da analizzare o visualizzare

Per l'app Colorist, devi fare in modo che il LLM interpreti in modo coerente le descrizioni dei colori e fornisca i valori RGB in un formato specifico.

Creare un asset di prompt di sistema

Per prima cosa, creerai un file di prompt di sistema che verrà caricato in fase di runtime. Questo approccio ti consente di modificare il prompt senza ricompilare l'app.

Crea un nuovo file assets/system_prompt.md con i seguenti contenuti:

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

Informazioni sulla struttura del prompt di sistema

Analizziamo nel dettaglio cosa fa questo prompt:

  1. Definizione del ruolo: definisce l'LLM come "assistente esperto di colori"
  2. Spiegazione della richiesta: definisce la richiesta principale come l'interpretazione delle descrizioni dei colori in valori RGB
  3. Formato della risposta: specifica esattamente come devono essere formattati i valori RGB per garantire la coerenza
  4. Esempio di scambio: fornisce un esempio concreto del pattern di interazione previsto
  5. Gestione dei casi limite: indica come gestire le descrizioni poco chiare
  6. Vincoli e linee guida: imposta limiti come il mantenimento dei valori RGB tra 0,0 e 1,0

Questo approccio strutturato garantisce che le risposte dell'LLM siano coerenti, informative e formattate in modo da essere facilmente analizzabili se volessi estrarre i valori RGB in modo programmatico.

Aggiorna pubspec.yaml

Ora, aggiorna la parte inferiore del file pubspec.yaml per includere la directory degli asset:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

Esegui flutter pub get per aggiornare il bundle di asset.

Crea un provider di prompt di sistema

Crea un nuovo file lib/providers/system_prompt.dart per caricare il prompt di sistema:

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

Questo fornitore utilizza il sistema di caricamento degli asset di Flutter per leggere il file di prompt in fase di runtime.

Aggiorna il provider del modello Gemini

Ora modifica il file lib/providers/gemini.dart per includere il prompt di sistema:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

La modifica principale è l'aggiunta di systemInstruction: Content.system(systemPrompt) durante la creazione del modello generativo. In questo modo, Gemini utilizzerà le tue istruzioni come prompt di sistema per tutte le interazioni in questa sessione di chat.

Genera codice Riverpod

Esegui il comando di build runner per generare il codice Riverpod necessario:

dart run build_runner build --delete-conflicting-outputs

Esegui e testa l'applicazione

Ora esegui l'applicazione:

flutter run -d DEVICE

Screenshot dell&#39;app Colorist che mostra la risposta dell&#39;LLM Gemini con una risposta in linea con il personaggio per un&#39;app di selezione dei colori

Prova a testarlo con varie descrizioni di colore:

  • "Vorrei un azzurro cielo"
  • "Give me a forest green" (dammi un verde foresta)
  • "Crea un arancione tramonto intenso"
  • "Voglio il colore della lavanda fresca"
  • "Show me something like a deep ocean blue" (fammi vedere qualcosa come un blu oceano profondo)

Noterai che Gemini ora risponde con spiegazioni conversazionali sui colori, oltre a valori RGB formattati in modo coerente. Il prompt di sistema ha guidato efficacemente l'LLM a fornire il tipo di risposte di cui hai bisogno.

Prova anche a chiedere contenuti al di fuori del contesto dei colori. Ad esempio, le cause principali della Guerra delle due rose. Dovresti notare una differenza rispetto al passaggio precedente.

L'importanza dell'ingegneria dei prompt per attività specializzate

I prompt di sistema sono sia arte che scienza. Sono una parte fondamentale dell'integrazione di LLM che può influire notevolmente sull'utilità del modello per la tua applicazione specifica. Quello che hai fatto qui è una forma di ingegneria dei prompt: adattare le istruzioni per far sì che il modello si comporti in modo da soddisfare le esigenze della tua applicazione.

Il prompt engineering efficace prevede:

  1. Definizione chiara del ruolo: stabilire lo scopo del LLM
  2. Istruzioni esplicite: descrivono in dettaglio come deve rispondere l'LLM
  3. Esempi concreti: mostrare anziché dire come sono le risposte valide
  4. Gestione dei casi limite: istruire l'LLM su come gestire scenari ambigui
  5. Specifiche di formattazione: assicurarsi che le risposte siano strutturate in modo coerente e utilizzabile

Il prompt di sistema che hai creato trasforma le funzionalità generiche di Gemini in un assistente specializzato nell'interpretazione dei colori che fornisce risposte formattate appositamente per le esigenze della tua applicazione. Si tratta di un pattern potente che puoi applicare a molti domini e attività diversi.

Passaggi successivi

Nel passaggio successivo, creerai questa base aggiungendo dichiarazioni di funzioni, che consentono all'LLM non solo di suggerire valori RGB, ma anche di chiamare funzioni nella tua app per impostare direttamente il colore. Ciò dimostra come i LLM possono colmare il divario tra il linguaggio naturale e le funzionalità concrete delle applicazioni.

Risoluzione dei problemi

Problemi di caricamento degli asset

Se si verificano errori durante il caricamento del prompt di sistema:

  • Verifica che pubspec.yaml elenchi correttamente la directory degli asset
  • Verifica che il percorso in rootBundle.loadString() corrisponda alla posizione del file
  • Esegui flutter clean seguito da flutter pub get per aggiornare il bundle di asset

Risposte incoerenti

Se il LLM non segue costantemente le istruzioni di formattazione:

  • Prova a rendere più espliciti i requisiti di formato nel prompt di sistema
  • Aggiungi altri esempi per dimostrare il pattern previsto
  • Assicurati che il formato che richiedi sia ragionevole per il modello

Limitazione della frequenza delle richieste API

Se riscontri errori relativi alla limitazione della frequenza:

  • Tieni presente che il servizio Firebase AI Logic ha limiti di utilizzo
  • Valuta la possibilità di implementare la logica di nuovi tentativi con backoff esponenziale
  • Controlla la console Firebase per eventuali problemi relativi alla quota

Concetti chiave appresi

  • Comprendere il ruolo e l'importanza dei prompt di sistema nelle applicazioni LLM
  • Creare prompt efficaci con istruzioni, esempi e vincoli chiari
  • Caricare e utilizzare i prompt di sistema in un'applicazione Flutter
  • Guida il comportamento dell'LLM per attività specifiche del dominio
  • Utilizzo del prompt engineering per modellare le risposte degli LLM

Questo passaggio mostra come ottenere una personalizzazione significativa del comportamento del modello LLM senza modificare il codice, semplicemente fornendo istruzioni chiare nel prompt di sistema.

5. Dichiarazioni di funzione per gli strumenti LLM

In questo passaggio, inizierai a consentire a Gemini di eseguire azioni nella tua app implementando le dichiarazioni di funzioni. Questa potente funzionalità consente all'LLM non solo di suggerire valori RGB, ma anche di impostarli nell'interfaccia utente dell'app tramite chiamate di strumenti specializzati. Tuttavia, sarà necessario il passaggio successivo per visualizzare le richieste LLM eseguite nell'app Flutter.

Cosa imparerai in questo passaggio

  • Informazioni sulla chiamata di funzione LLM e sui suoi vantaggi per le applicazioni Flutter
  • Definizione di dichiarazioni di funzioni basate su schema per Gemini
  • Integrare le dichiarazioni di funzioni con il modello Gemini
  • Aggiornamento del prompt di sistema per utilizzare le funzionalità dello strumento

Informazioni sulla chiamata di funzione

Prima di implementare le dichiarazioni di funzione, vediamo cosa sono e perché sono utili:

Che cos'è la chiamata di funzione?

La chiamata di funzione (a volte chiamata "utilizzo di strumenti") è una funzionalità che consente a un LLM di:

  1. Riconoscere quando una richiesta utente trarrebbe vantaggio dall'invocazione di una funzione specifica
  2. Genera un oggetto JSON strutturato con i parametri necessari per la funzione.
  3. Consenti all'applicazione di eseguire la funzione con questi parametri
  4. Ricevere il risultato della funzione e incorporarlo nella risposta

Anziché limitarsi a descrivere cosa fare, la chiamata di funzione consente all'LLM di attivare azioni concrete nella tua applicazione.

Perché la chiamata di funzioni è importante per le app Flutter

La chiamata di funzioni crea un ponte potente tra il linguaggio naturale e le funzionalità dell'applicazione:

  1. Azione diretta: gli utenti possono descrivere ciò che vogliono in linguaggio naturale e l'app risponde con azioni concrete
  2. Output strutturato: l'LLM produce dati puliti e strutturati anziché testo che deve essere analizzato
  3. Operazioni complesse: consente all'LLM di accedere a dati esterni, eseguire calcoli o modificare lo stato dell'applicazione
  4. Migliore esperienza utente: crea un'integrazione perfetta tra conversazione e funzionalità

Nell'app Colorist, la chiamata di funzione consente agli utenti di dire "Voglio un verde foresta" e di aggiornare immediatamente la UI con quel colore, senza dover analizzare i valori RGB dal testo.

Definisci dichiarazioni di funzione

Crea un nuovo file lib/services/gemini_tools.dart per definire le dichiarazioni delle funzioni:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

Informazioni sulle dichiarazioni di funzione

Analizziamo nel dettaglio cosa fa questo codice:

  1. Denominazione della funzione: assegna alla funzione il nome set_color per indicarne chiaramente lo scopo
  2. Descrizione della funzione: fornisci una descrizione chiara che aiuti l'LLM a capire quando utilizzarla
  3. Definizioni dei parametri: definisci i parametri strutturati con le relative descrizioni:
    • red: il componente rosso di RGB, specificato come numero compreso tra 0.0 e 1.0
    • green: il componente verde di RGB, specificato come numero compreso tra 0,0 e 1,0
    • blue: il componente blu di RGB, specificato come numero compreso tra 0,0 e 1,0
  4. Tipi di schema: utilizzi Schema.number() per indicare che si tratta di valori numerici
  5. Raccolta di strumenti: crei un elenco di strumenti contenenti la dichiarazione della funzione

Questo approccio strutturato aiuta il modello LLM Gemini a comprendere:

  • Quando deve chiamare questa funzione
  • Quali parametri deve fornire
  • Quali vincoli si applicano a questi parametri (ad esempio l'intervallo di valori)

Aggiorna il provider del modello Gemini

Ora, modifica il file lib/providers/gemini.dart per includere le dichiarazioni di funzione durante l'inizializzazione del modello Gemini:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

La modifica principale consiste nell'aggiunta del parametro tools: geminiTools.tools durante la creazione del modello generativo. In questo modo, Gemini è a conoscenza delle funzioni che può chiamare.

Aggiornare il prompt di sistema

Ora devi modificare il prompt di sistema per istruire il LLM sull'utilizzo del nuovo strumento set_color. Aggiornamento 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

Le modifiche principali al prompt di sistema sono:

  1. Introduzione dello strumento: anziché chiedere valori RGB formattati, ora comunichi all'LLM lo strumento set_color
  2. Procedura modificata: modifichi il passaggio 3 da "Formatta i valori nella risposta" a "Utilizza lo strumento per impostare i valori".
  3. Esempio aggiornato: mostri come la risposta deve includere una chiamata di strumento anziché testo formattato
  4. Requisito di formattazione rimosso: poiché utilizzi chiamate di funzioni strutturate, non hai più bisogno di un formato di testo specifico

Questo prompt aggiornato indica all'LLM di utilizzare la chiamata di funzione anziché fornire solo valori RGB in formato di testo.

Genera codice Riverpod

Esegui il comando di build runner per generare il codice Riverpod necessario:

dart run build_runner build --delete-conflicting-outputs

Eseguire l'applicazione

A questo punto, Gemini genererà contenuti che tentano di utilizzare le chiamate di funzione, ma non hai ancora implementato i gestori per le chiamate di funzione. Quando esegui l'app e descrivi un colore, vedrai Gemini rispondere come se avesse richiamato uno strumento, ma non vedrai alcuna modifica del colore nell'interfaccia utente fino al passaggio successivo.

Esegui l'app:

flutter run -d DEVICE

Screenshot dell&#39;app Colorist che mostra la risposta parziale del modello LLM Gemini

Prova a descrivere un colore come "blu oceano profondo" o "verde foresta" e osserva le risposte. L'LLM sta tentando di chiamare le funzioni definite sopra, ma il tuo codice non rileva ancora le chiamate di funzione.

La procedura di chiamata di funzione

Vediamo cosa succede quando Gemini utilizza la chiamata di funzione:

  1. Selezione della funzione: l'LLM decide se una chiamata di funzione sarebbe utile in base alla richiesta dell'utente
  2. Generazione di parametri: il LLM genera valori di parametri che corrispondono allo schema della funzione
  3. Formato della chiamata di funzione: l'LLM invia un oggetto di chiamata di funzione strutturato nella sua risposta
  4. Gestione dell'applicazione: la tua app riceverà questa chiamata ed eseguirà la funzione pertinente (implementata nel passaggio successivo)
  5. Integrazione della risposta: nelle conversazioni in più turni, l'LLM si aspetta che venga restituito il risultato della funzione

Nello stato attuale della tua app, si verificano i primi tre passaggi, ma non hai ancora implementato il passaggio 4 o 5 (gestione delle chiamate di funzioni), che eseguirai nel passaggio successivo.

Dettagli tecnici: come Gemini decide quando utilizzare le funzioni

Gemini prende decisioni intelligenti su quando utilizzare le funzioni in base a:

  1. Intento dell'utente: se la richiesta dell'utente può essere soddisfatta al meglio da una funzione
  2. Pertinenza della funzione: quanto le funzioni disponibili corrispondono all'attività
  3. Disponibilità dei parametri: indica se è possibile determinare con certezza i valori dei parametri
  4. Istruzioni di sistema: indicazioni del prompt di sistema sull'utilizzo delle funzioni

Fornendo dichiarazioni di funzioni e istruzioni di sistema chiare, hai configurato Gemini in modo che riconosca le richieste di descrizione del colore come opportunità per chiamare la funzione set_color.

Passaggi successivi

Nel passaggio successivo, implementerai i gestori per le chiamate di funzione provenienti da Gemini. In questo modo, il cerchio si chiude e le descrizioni degli utenti possono attivare modifiche effettive del colore nell'interfaccia utente tramite le chiamate di funzione dell'LLM.

Risoluzione dei problemi

Problemi con la dichiarazione di funzione

Se riscontri errori con le dichiarazioni di funzioni:

  • Verifica che i nomi e i tipi di parametri corrispondano a quanto previsto
  • Verifica che il nome della funzione sia chiaro e descrittivo
  • Assicurati che la descrizione della funzione ne spieghi accuratamente lo scopo

Problemi con i prompt di sistema

Se l'LLM non tenta di utilizzare la funzione:

  • Verifica che il prompt di sistema istruisca chiaramente il LLM a utilizzare lo strumento set_color
  • Controlla che l'esempio nel prompt di sistema mostri l'utilizzo della funzione
  • Prova a rendere più esplicita l'istruzione per utilizzare lo strumento

Problemi generici

Se riscontri altri problemi:

  • Controlla la console per verificare la presenza di errori relativi alle dichiarazioni di funzioni
  • Verifica che gli strumenti vengano passati correttamente al modello
  • Assicurati che tutto il codice generato da Riverpod sia aggiornato

Concetti chiave appresi

  • Definizione delle dichiarazioni di funzioni per estendere le funzionalità LLM nelle app Flutter
  • Creazione di schemi di parametri per la raccolta di dati strutturati
  • Integrazione delle dichiarazioni di funzioni con il modello Gemini
  • Aggiornamento dei prompt di sistema per incoraggiare l'utilizzo delle funzioni
  • Come gli LLM selezionano e chiamano le funzioni

Questo passaggio mostra come gli LLM possono colmare il divario tra l'input in linguaggio naturale e le chiamate di funzione strutturate, creando le basi per un'integrazione perfetta tra le funzionalità di conversazione e dell'applicazione.

6. Implementazione della gestione degli strumenti

In questo passaggio, implementerai i gestori per le chiamate di funzione provenienti da Gemini. In questo modo si completa il ciclo di comunicazione tra gli input in linguaggio naturale e le funzionalità concrete dell'applicazione, consentendo all'LLM di manipolare direttamente la tua UI in base alle descrizioni degli utenti.

Cosa imparerai in questo passaggio

  • Comprendere la pipeline completa di chiamata di funzione nelle applicazioni LLM
  • Elaborazione delle chiamate di funzione da Gemini in un'applicazione Flutter
  • Implementazione di gestori di funzioni che modificano lo stato dell'applicazione
  • Gestione delle risposte delle funzioni e restituzione dei risultati all'LLM
  • Creazione di un flusso di comunicazione completo tra LLM e UI
  • Registrazione delle chiamate di funzioni e delle risposte per la trasparenza

Informazioni sulla pipeline di chiamata di funzione

Prima di entrare nel dettaglio dell'implementazione, vediamo la pipeline completa di chiamata di funzione:

Il flusso end-to-end

  1. Input dell'utente: l'utente descrive un colore in linguaggio naturale (ad es. "verde foresta")
  2. Elaborazione LLM: Gemini analizza la descrizione e decide di chiamare la funzione set_color
  3. Generazione di chiamate di funzione: Gemini crea un JSON strutturato con parametri (valori rosso, verde e blu)
  4. Ricezione della chiamata di funzione: la tua app riceve questi dati strutturati da Gemini
  5. Esecuzione della funzione: la tua app esegue la funzione con i parametri forniti
  6. Aggiornamento dello stato: la funzione aggiorna lo stato dell'app (modificando il colore visualizzato)
  7. Generazione della risposta: la funzione restituisce i risultati all'LLM
  8. Incorporamento della risposta: l'LLM incorpora questi risultati nella risposta finale
  9. Aggiornamento della UI: la UI reagisce alla modifica dello stato, visualizzando il nuovo colore

Il ciclo di comunicazione completo è essenziale per una corretta integrazione di LLM. Quando un LLM esegue una chiamata di funzione, non si limita a inviare la richiesta e a proseguire. Attende invece che l'applicazione esegua la funzione e restituisca i risultati. L'LLM utilizza quindi questi risultati per formulare la risposta finale, creando un flusso di conversazione naturale che riconosce le azioni intraprese.

Implementa i gestori di funzioni

Aggiorniamo il file lib/services/gemini_tools.dart per aggiungere i gestori per le chiamate di funzioni:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

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

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

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

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

Informazioni sui gestori di funzioni

Vediamo cosa fanno questi gestori di funzioni:

  1. handleFunctionCall: un dispatcher centrale che:
    • Registra la chiamata di funzione per la trasparenza nel riquadro dei log
    • Percorsi al gestore appropriato in base al nome della funzione
    • Restituisce una risposta strutturata che verrà inviata all'LLM
  2. handleSetColor: il gestore specifico per la tua funzione set_color che:
    • Estrae i valori RGB dalla mappa degli argomenti
    • Li converte nei tipi previsti (numeri reali)
    • Aggiorna lo stato del colore dell'applicazione utilizzando colorStateNotifier
    • Crea una risposta strutturata con lo stato di riuscita e le informazioni sul colore corrente
    • Registra i risultati della funzione per il debug
  3. handleUnknownFunction: un gestore di riserva per le funzioni sconosciute che:
    • Registra un avviso relativo alla funzione non supportata
    • Restituisce una risposta di errore all'LLM

La funzione handleSetColor è particolarmente importante in quanto colma il divario tra la comprensione del linguaggio naturale del LLM e le modifiche concrete dell'interfaccia utente.

Aggiorna il servizio Gemini Chat per elaborare chiamate di funzioni e risposte

Ora aggiorniamo il file lib/services/gemini_chat_service.dart per elaborare le chiamate di funzione dalle risposte dell'LLM e inviare i risultati all'LLM:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

Comprendere il flusso di comunicazione

L'aggiunta principale è la gestione completa delle chiamate di funzione e delle risposte:

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);
  }
}

Questo codice:

  1. Controlla se la risposta LLM contiene chiamate di funzioni
  2. Per ogni chiamata di funzione, richiama il metodo handleFunctionCall con il nome e gli argomenti della funzione
  3. Raccoglie i risultati di ogni chiamata di funzione
  4. Invia questi risultati all'LLM utilizzando Content.functionResponses
  5. Elabora la risposta dell'LLM ai risultati della funzione
  6. Aggiorna la UI con il testo della risposta finale

In questo modo viene creato un flusso di andata e ritorno:

  • Utente → LLM: richiede un colore
  • LLM → App: chiamate di funzione con parametri
  • App → Utente: nuovo colore visualizzato
  • App → LLM: risultati della funzione
  • LLM → Utente: risposta finale che incorpora i risultati della funzione

Genera codice Riverpod

Esegui il comando di build runner per generare il codice Riverpod necessario:

dart run build_runner build --delete-conflicting-outputs

Esegui e testa il flusso completo

Ora esegui l'applicazione:

flutter run -d DEVICE

Screenshot dell&#39;app Colorist che mostra la risposta del modello LLM Gemini con una chiamata di funzione

Prova a inserire varie descrizioni di colori:

  • "Vorrei un rosso cremisi intenso"
  • "Show me a calming sky blue" (mostrami un azzurro cielo rilassante)
  • "Dammi il colore delle foglie di menta fresca"
  • "Voglio vedere un arancione caldo del tramonto"
  • "Rendi il colore un viola reale intenso"

Ora dovresti vedere:

  1. Il tuo messaggio visualizzato nell'interfaccia di chat
  2. La risposta di Gemini che appare nella chat
  3. Chiamate di funzione registrate nel riquadro dei log
  4. Risultati della funzione registrati immediatamente dopo
  5. Il rettangolo di colore che si aggiorna per visualizzare il colore descritto
  6. Aggiornamento dei valori RGB per mostrare i componenti del nuovo colore
  7. La risposta finale di Gemini, che spesso commenta il colore impostato

Il pannello dei log fornisce informazioni su cosa succede dietro le quinte. Visualizzerai:

  • Le chiamate di funzione esatte che Gemini sta effettuando
  • I parametri che sceglie per ogni valore RGB
  • I risultati restituiti dalla funzione
  • Le risposte successive di Gemini

Indicatore dello stato del colore

Il colorStateNotifier che stai utilizzando per aggiornare i colori fa parte del pacchetto colorist_ui. Gestisce:

  • Il colore corrente visualizzato nell'interfaccia utente
  • La cronologia dei colori (ultimi 10 colori)
  • Notifica delle modifiche di stato ai componenti UI

Quando chiami updateColor con nuovi valori RGB:

  1. Crea un nuovo oggetto ColorData con i valori forniti
  2. Aggiorna il colore corrente nello stato dell'app
  3. Aggiunge il colore alla cronologia
  4. Attiva gli aggiornamenti della UI tramite la gestione dello stato di Riverpod

I componenti dell'interfaccia utente nel pacchetto colorist_ui osservano questo stato e si aggiornano automaticamente quando cambia, creando un'esperienza reattiva.

Informazioni sulla gestione degli errori

La tua implementazione include una gestione degli errori efficace:

  1. Blocco try-catch: racchiude tutte le interazioni LLM per rilevare eventuali eccezioni
  2. Registrazione degli errori: registra gli errori nel riquadro dei log con le tracce dello stack
  3. Feedback utente: fornisce un messaggio di errore amichevole nella chat
  4. Pulizia dello stato: finalizza lo stato del messaggio anche se si verifica un errore

In questo modo, l'app rimane stabile e fornisce un feedback appropriato anche quando si verificano problemi con l'esecuzione del servizio o della funzione LLM.

Il potere della chiamata di funzioni per l'esperienza utente

Ciò che hai realizzato qui dimostra come i modelli LLM possono creare interfacce naturali efficaci:

  1. Interfaccia in linguaggio naturale: gli utenti esprimono l'intent nel linguaggio di tutti i giorni
  2. Interpretazione intelligente: il LLM traduce le descrizioni vaghe in valori precisi
  3. Manipolazione diretta: l'interfaccia utente si aggiorna in risposta al linguaggio naturale
  4. Risposte contestuali: l'LLM fornisce il contesto conversazionale delle modifiche
  5. Basso carico cognitivo: gli utenti non devono comprendere i valori RGB o la teoria del colore

Questo pattern di utilizzo delle chiamate di funzione LLM per colmare il divario tra il linguaggio naturale e le azioni dell'interfaccia utente può essere esteso a innumerevoli altri domini oltre alla selezione del colore.

Passaggi successivi

Nel passaggio successivo, migliorerai l'esperienza utente implementando le risposte in streaming. Anziché attendere la risposta completa, elaborerai i blocchi di testo e le chiamate di funzioni man mano che vengono ricevuti, creando un'applicazione più reattiva e coinvolgente.

Risoluzione dei problemi

Problemi con le chiamate di funzione

Se Gemini non chiama le tue funzioni o i parametri non sono corretti:

  • Verifica che la dichiarazione della funzione corrisponda a quanto descritto nel prompt di sistema
  • Verifica che i nomi e i tipi di parametri siano coerenti
  • Assicurati che il prompt di sistema istruisca esplicitamente l'LLM a utilizzare lo strumento
  • Verifica che il nome della funzione nel gestore corrisponda esattamente a quello nella dichiarazione.
  • Esamina il riquadro dei log per informazioni dettagliate sulle chiamate di funzioni

Problemi con la risposta della funzione

Se i risultati della funzione non vengono inviati correttamente al LLM:

  • Verifica che la funzione restituisca una mappa formattata correttamente
  • Verifica che Content.functionResponses sia costruito correttamente
  • Cerca eventuali errori nel log relativi alle risposte delle funzioni
  • Assicurati di utilizzare la stessa sessione di chat per la risposta

Problemi di visualizzazione dei colori

Se i colori non vengono visualizzati correttamente:

  • Assicurati che i valori RGB vengano convertiti correttamente in numeri reali (il modello LLM potrebbe inviarli come numeri interi)
  • Verifica che i valori rientrino nell'intervallo previsto (da 0,0 a 1,0).
  • Verifica che la notifica dello stato del colore venga chiamata correttamente
  • Esamina il log per i valori esatti passati alla funzione

Problemi generali

Per problemi generali:

  • Esamina i log per verificare la presenza di errori o avvisi
  • Verifica la connettività di Firebase AI Logic
  • Verifica la presenza di mancate corrispondenze dei tipi nei parametri delle funzioni
  • Assicurati che tutto il codice generato da Riverpod sia aggiornato

Concetti chiave appresi

  • Implementazione di una pipeline completa di chiamate di funzione in Flutter
  • Creare una comunicazione completa tra un LLM e la tua applicazione
  • Elaborazione di dati strutturati dalle risposte LLM
  • Invio dei risultati della funzione all'LLM per l'incorporamento nelle risposte
  • Utilizzo del pannello dei log per ottenere visibilità sulle interazioni tra LLM e applicazioni
  • Collegamento degli input in linguaggio naturale a modifiche concrete dell'interfaccia utente

Una volta completato questo passaggio, la tua app mostra uno dei pattern più efficaci per l'integrazione degli LLM: la traduzione degli input in linguaggio naturale in azioni concrete dell'interfaccia utente, mantenendo al contempo una conversazione coerente che riconosce queste azioni. In questo modo si crea un'interfaccia intuitiva e conversazionale che gli utenti percepiscono come magica.

7. Risposte dinamiche per una migliore esperienza utente

In questo passaggio, migliorerai l'esperienza utente implementando le risposte in streaming di Gemini. Anziché attendere la generazione dell'intera risposta, elaborerai i blocchi di testo e le chiamate di funzioni man mano che vengono ricevuti, creando un'applicazione più reattiva e coinvolgente.

Argomenti trattati in questo passaggio

  • L'importanza dello streaming per le applicazioni basate su LLM
  • Implementazione di risposte LLM in modalità flusso in un'applicazione Flutter
  • Elaborazione di blocchi di testo parziali man mano che arrivano dall'API
  • Gestire lo stato della conversazione per evitare conflitti tra i messaggi
  • Gestione delle chiamate di funzione nelle risposte dinamiche
  • Creare indicatori visivi per le risposte in corso

Perché lo streaming è importante per le applicazioni LLM

Prima dell'implementazione, vediamo perché le risposte in streaming sono fondamentali per creare esperienze utente eccellenti con gli LLM:

Esperienza utente migliorata

Le risposte in streaming offrono diversi vantaggi significativi per l'esperienza utente:

  1. Latenza percepita ridotta: gli utenti vedono il testo iniziare a comparire immediatamente (in genere entro 100-300 ms), anziché aspettare diversi secondi per una risposta completa. Questa percezione di immediatezza migliora notevolmente la soddisfazione degli utenti.
  2. Ritmo conversazionale naturale: la comparsa graduale del testo imita il modo in cui comunicano gli esseri umani, creando un'esperienza di dialogo più naturale.
  3. Elaborazione progressiva delle informazioni: gli utenti possono iniziare a elaborare le informazioni man mano che arrivano, anziché essere sopraffatti da un blocco di testo di grandi dimensioni tutto in una volta.
  4. Possibilità di interruzione anticipata: in un'applicazione completa, gli utenti potrebbero interrompere o reindirizzare il LLM se vedono che sta andando in una direzione non utile.
  5. Conferma visiva dell'attività: il testo in streaming fornisce un feedback immediato sul funzionamento del sistema, riducendo l'incertezza.

Vantaggi tecnici

Oltre ai miglioramenti dell'esperienza utente, lo streaming offre vantaggi tecnici:

  1. Esecuzione anticipata delle funzioni: le chiamate di funzioni possono essere rilevate ed eseguite non appena vengono visualizzate nel flusso, senza attendere la risposta completa.
  2. Aggiornamenti incrementali della UI: puoi aggiornare la UI in modo progressivo man mano che arrivano nuove informazioni, creando un'esperienza più dinamica.
  3. Gestione dello stato della conversazione: lo streaming fornisce segnali chiari su quando le risposte sono complete e quando sono ancora in corso, consentendo una migliore gestione dello stato.
  4. Riduzione dei rischi di timeout: con le risposte non in streaming, le generazioni a lunga esecuzione rischiano timeout della connessione. Lo streaming stabilisce la connessione in anticipo e la mantiene.

Per l'app Colorist, l'implementazione dello streaming significa che gli utenti vedranno le risposte di testo e le modifiche di colore apparire più rapidamente, creando un'esperienza molto più reattiva.

Aggiungere la gestione dello stato della conversazione

Innanzitutto, aggiungiamo un fornitore di stato per monitorare se l'app sta attualmente gestendo una risposta di streaming. Aggiorna il file lib/services/gemini_chat_service.dart:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

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

Informazioni sull'implementazione dello streaming

Analizziamo nel dettaglio cosa fa questo codice:

  1. Monitoraggio dello stato della conversazione:
    • Un conversationStateProvider tiene traccia se l'app sta attualmente elaborando una risposta
    • Durante l'elaborazione, lo stato passa da idlebusy e poi torna a idle
    • In questo modo si evitano più richieste simultanee che potrebbero entrare in conflitto
  2. Inizializzazione dello stream:
    • sendMessageStream() restituisce un flusso di blocchi di risposta anziché un Future con la risposta completa
    • Ogni blocco può contenere testo, chiamate di funzioni o entrambi.
  3. Elaborazione progressiva:
    • await for elabora ogni blocco in tempo reale man mano che arriva
    • Il testo viene aggiunto immediatamente all'interfaccia utente, creando l'effetto di streaming
    • Le chiamate di funzione vengono eseguite non appena vengono rilevate
  4. Gestione delle chiamate di funzione:
    • Quando viene rilevata una chiamata di funzione in un blocco, viene eseguita immediatamente
    • I risultati vengono inviati nuovamente all'LLM tramite un'altra chiamata di streaming
    • Anche la risposta dell'LLM a questi risultati viene elaborata in streaming.
  5. Gestione degli errori e pulizia:
    • try/catch fornisce una gestione degli errori efficace
    • Il blocco finally garantisce che lo stato della conversazione venga reimpostato correttamente
    • Il messaggio viene sempre finalizzato, anche se si verificano errori

Questa implementazione crea un'esperienza di streaming reattiva e affidabile, mantenendo al contempo lo stato corretto della conversazione.

Aggiorna la schermata principale per connettere lo stato della conversazione

Modifica il file lib/main.dart per trasferire lo stato della conversazione alla schermata principale:

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

La modifica principale consiste nel passare conversationState al widget MainScreen. MainScreen (fornito dal pacchetto colorist_ui) utilizzerà questo stato per disattivare l'inserimento di testo durante l'elaborazione di una risposta.

In questo modo si crea un'esperienza utente coerente in cui la UI riflette lo stato attuale della conversazione.

Genera codice Riverpod

Esegui il comando di build runner per generare il codice Riverpod necessario:

dart run build_runner build --delete-conflicting-outputs

Eseguire e testare le risposte dinamiche

Esegui l'applicazione:

flutter run -d DEVICE

Screenshot dell&#39;app Colorist che mostra la risposta del modello LLM Gemini in modalità streaming

Ora prova a testare il comportamento dello streaming con varie descrizioni dei colori. Prova descrizioni come:

  • "Fammi vedere il colore blu intenso dell'oceano al crepuscolo"
  • "Vorrei vedere un corallo dai colori vivaci che mi ricordi i fiori tropicali"
  • "Crea un verde oliva opaco come le vecchie uniformi dell'esercito"

Il flusso tecnico dello streaming in dettaglio

Esaminiamo cosa succede esattamente quando viene riprodotta in streaming una risposta:

Creazione della connessione

Quando chiami sendMessageStream(), si verifica quanto segue:

  1. L'app stabilisce una connessione al servizio Firebase AI Logic
  2. La richiesta dell'utente viene inviata al servizio
  3. Il server inizia a elaborare la richiesta
  4. La connessione di streaming rimane aperta, pronta a trasmettere i chunk

Trasmissione a blocchi

Man mano che Gemini genera i contenuti, i blocchi vengono inviati tramite lo stream:

  1. Il server invia blocchi di testo man mano che vengono generati (in genere poche parole o frasi)
  2. Quando Gemini decide di effettuare una chiamata di funzione, invia le informazioni della chiamata di funzione
  3. Dopo le chiamate di funzione potrebbero seguire altri blocchi di testo
  4. Lo stream continua fino al completamento della generazione

Elaborazione progressiva

La tua app elabora ogni blocco in modo incrementale:

  1. Ogni blocco di testo viene aggiunto alla risposta esistente
  2. Le chiamate di funzione vengono eseguite non appena vengono rilevate
  3. L'interfaccia utente si aggiorna in tempo reale con i risultati del testo e della funzione
  4. Lo stato viene monitorato per mostrare che la risposta è ancora in streaming

Completamento dello stream

Al termine della generazione:

  1. Lo stream viene chiuso dal server
  2. Il tuo loop await for termina in modo naturale
  3. Il messaggio è contrassegnato come completato
  4. Lo stato della conversazione viene ripristinato su Inattiva
  5. L'interfaccia utente viene aggiornata per riflettere lo stato di completamento

Confronto tra streaming e non streaming

Per comprendere meglio i vantaggi dello streaming, confrontiamo gli approcci di streaming e non streaming:

Aspetto

Non in streaming

Streaming

Latenza percepita

L'utente non vede nulla finché la risposta completa non è pronta

L'utente vede le prime parole in pochi millisecondi

Esperienza utente

Lunga attesa seguita dalla comparsa improvvisa del testo

Aspetto naturale e progressivo del testo

Gestione dello stato

Più semplice (i messaggi sono in attesa o completi)

Più complesso (i messaggi possono essere in stato di streaming)

Esecuzione della funzione

Si verifica solo dopo la risposta completa

Si verifica durante la generazione della risposta

Complessità dell'implementazione

Più semplice da implementare

Richiede una gestione aggiuntiva dello stato

Recupero dagli errori

Risposta tutto o niente

Le risposte parziali possono comunque essere utili

Complessità del codice

Meno complesso

Più complesso a causa della gestione degli stream

Per un'applicazione come Colorist, i vantaggi dell'esperienza utente dello streaming superano la complessità dell'implementazione, soprattutto per le interpretazioni del colore che potrebbero richiedere diversi secondi per essere generate.

Best practice per l'esperienza utente di streaming

Quando implementi lo streaming nelle tue applicazioni LLM, tieni presente queste best practice:

  1. Indicatori visivi chiari: fornisci sempre indicatori visivi chiari che distinguano i messaggi in streaming da quelli completi.
  2. Blocco input: disattiva l'input dell'utente durante lo streaming per evitare più richieste sovrapposte
  3. Recupero errori: progetta la tua UI in modo che gestisca il recupero controllato se lo streaming viene interrotto
  4. Transizioni di stato: garantisci transizioni fluide tra gli stati di inattività, streaming e completamento
  5. Visualizzazione dell'avanzamento: valuta la possibilità di utilizzare animazioni o indicatori discreti che mostrino l'elaborazione attiva
  6. Opzioni di annullamento: in un'app completa, fornisci agli utenti modi per annullare le generazioni in corso
  7. Integrazione dei risultati delle funzioni: progetta la tua UI in modo che gestisca i risultati delle funzioni che compaiono a metà stream
  8. Ottimizzazione delle prestazioni: riduci al minimo le ricostruzioni della UI durante gli aggiornamenti rapidi dello stream

Il pacchetto colorist_ui implementa molte di queste best practice, ma sono considerazioni importanti per qualsiasi implementazione di LLM di streaming.

Passaggi successivi

Nel passaggio successivo, implementerai la sincronizzazione LLM notificando a Gemini quando gli utenti selezionano i colori dalla cronologia. In questo modo si crea un'esperienza più coesa in cui il LLM è a conoscenza delle modifiche allo stato dell'applicazione avviate dall'utente.

Risoluzione dei problemi

Problemi di elaborazione dei flussi

Se riscontri problemi con l'elaborazione dello stream:

  • Sintomi: risposte parziali, testo mancante o interruzione improvvisa dello stream
  • Soluzione: controlla la connettività di rete e assicurati che i pattern async/await siano corretti nel codice
  • Diagnosi: esamina il riquadro dei log per individuare messaggi di errore o avvisi relativi all'elaborazione dello stream
  • Correzione: assicurati che tutta l'elaborazione dello stream utilizzi una gestione degli errori corretta con i blocchi try/catch

Chiamate delle funzioni mancanti

Se le chiamate di funzioni non vengono rilevate nello stream:

  • Sintomi: viene visualizzato il testo, ma i colori non vengono aggiornati o il log non mostra chiamate di funzioni
  • Soluzione: verifica le istruzioni del prompt di sistema sull'utilizzo delle chiamate di funzioni
  • Diagnosi: controlla il riquadro dei log per vedere se vengono ricevute chiamate di funzioni
  • Soluzione: modifica il prompt di sistema per indicare in modo più esplicito all'LLM di utilizzare lo strumento set_color.

Gestione degli errori generali

Per qualsiasi altro problema:

  • Passaggio 1: controlla il riquadro dei log per individuare messaggi di errore
  • Passaggio 2: verifica la connettività di Firebase AI Logic
  • Passaggio 3: assicurati che tutto il codice generato da Riverpod sia aggiornato
  • Passaggio 4: esamina l'implementazione dello streaming per verificare la presenza di istruzioni await mancanti

Concetti chiave appresi

  • Implementazione di risposte in streaming con l'API Gemini per un'esperienza utente più reattiva
  • Gestione dello stato della conversazione per gestire correttamente le interazioni di streaming
  • Elaborazione di chiamate Real-Time Text e di funzioni man mano che arrivano
  • Creazione di UI reattive che vengono aggiornate in modo incrementale durante lo streaming
  • Gestione di stream simultanei con pattern asincroni appropriati
  • Fornire un feedback visivo appropriato durante le risposte in streaming

Implementando lo streaming, hai migliorato significativamente l'esperienza utente della tua app Colorist, creando un'interfaccia più reattiva e coinvolgente che sembra davvero conversazionale.

8. Sincronizzazione del contesto LLM

In questo passaggio bonus, implementerai la sincronizzazione del contesto LLM notificando a Gemini quando gli utenti selezionano i colori dalla cronologia. In questo modo si crea un'esperienza più coesa in cui il LLM è consapevole delle azioni dell'utente nell'interfaccia, non solo dei suoi messaggi espliciti.

Argomenti trattati in questo passaggio

  • Creazione della sincronizzazione del contesto LLM tra la tua UI e l'LLM
  • Serializzazione degli eventi UI nel contesto che il modello LLM può comprendere
  • Aggiornamento del contesto della conversazione in base alle azioni dell'utente
  • Creare un'esperienza coerente con diversi metodi di interazione
  • Migliorare la consapevolezza del contesto degli LLM al di là dei messaggi di chat espliciti

Comprendere la sincronizzazione del contesto LLM

I chatbot tradizionali rispondono solo a messaggi espliciti degli utenti, creando una disconnessione quando gli utenti interagiscono con l'app in altri modi. La sincronizzazione del contesto dell'LLM risolve questa limitazione:

Perché la sincronizzazione del contesto dei modelli LLM è importante

Quando gli utenti interagiscono con la tua app tramite elementi dell'interfaccia utente (ad esempio selezionando un colore dalla cronologia), il LLM non ha modo di sapere cosa è successo, a meno che tu non lo comunichi esplicitamente. Sincronizzazione del contesto LLM:

  1. Mantiene il contesto: mantiene il modello LLM informato di tutte le azioni pertinenti dell'utente
  2. Crea coerenza: produce un'esperienza coesa in cui l'LLM riconosce le interazioni con la UI
  3. Migliora l'intelligenza: consente all'LLM di rispondere in modo appropriato a tutte le azioni dell'utente
  4. Migliora l'esperienza utente: rende l'intera applicazione più integrata e reattiva
  5. Riduce lo sforzo dell'utente: elimina la necessità per gli utenti di spiegare manualmente le loro azioni dell'interfaccia utente

Nella tua app Colorist, quando un utente seleziona un colore dalla cronologia, vuoi che Gemini riconosca questa azione e commenti in modo intelligente il colore selezionato, mantenendo l'illusione di un assistente consapevole e senza interruzioni.

Aggiornare il servizio di chat Gemini per le notifiche di selezione del colore

Innanzitutto, aggiungi un metodo a GeminiChatService per notificare al modello linguistico di grandi dimensioni quando un utente seleziona un colore dalla cronologia. Aggiorna il file lib/services/gemini_chat_service.dart:

lib/services/gemini_chat_service.dart

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

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

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

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

L'aggiunta principale è il metodo notifyColorSelection, che:

  1. Accetta un oggetto ColorData che rappresenta il colore selezionato
  2. Lo codifica in un formato JSON che può essere incluso in un messaggio
  3. Invia un messaggio con una formattazione speciale al LLM che indica una selezione dell'utente
  4. Riutilizza il metodo sendMessage esistente per gestire la notifica

Questo approccio evita la duplicazione utilizzando l'infrastruttura di gestione dei messaggi esistente.

Aggiorna l'app principale per connettere le notifiche di selezione del colore

Ora, modifica il file lib/main.dart per passare la funzione di notifica della selezione del colore alla schermata principale:

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

La modifica principale consiste nell'aggiunta del callback notifyColorSelection, che collega l'evento dell'interfaccia utente (selezione di un colore dalla cronologia) al sistema di notifiche del modello linguistico di grandi dimensioni.

Aggiornare il prompt di sistema

Ora devi aggiornare il prompt di sistema per indicare al modello LLM come rispondere alle notifiche di selezione del colore. Modifica il file 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

L'aggiunta principale è la sezione "Quando gli utenti selezionano colori storici", che:

  1. Spiega all'LLM il concetto di notifiche di selezione della cronologia
  2. Fornisce un esempio di come appaiono queste notifiche
  3. Mostra un esempio di risposta appropriata
  4. Definisce le aspettative per il riconoscimento della selezione e il commento sul colore

In questo modo, l'LLM può capire come rispondere in modo appropriato a questi messaggi speciali.

Genera codice Riverpod

Esegui il comando di build runner per generare il codice Riverpod necessario:

dart run build_runner build --delete-conflicting-outputs

Esegui e testa la sincronizzazione del contesto LLM

Esegui l'applicazione:

flutter run -d DEVICE

Screenshot dell&#39;app Colorist che mostra il modello linguistico di grandi dimensioni Gemini che risponde a una selezione dalla cronologia dei colori

Il test della sincronizzazione del contesto LLM prevede:

  1. Innanzitutto, genera alcuni colori descrivendoli nella chat
    • "Show me a vibrant purple" (fammi vedere un viola vibrante)
    • "Vorrei un verde foresta"
    • "Give me a bright red" (dammi un rosso vivo)
  2. Poi, fai clic su una delle miniature dei colori nella striscia della cronologia.

Dovresti osservare:

  1. Il colore selezionato viene visualizzato nel display principale
  2. Nella chat viene visualizzato un messaggio dell'utente che indica la selezione del colore
  3. Il modello LLM risponde confermando la selezione e commentando il colore.
  4. L'intera interazione risulta naturale e coesa

In questo modo si crea un'esperienza fluida in cui l'LLM è consapevole e risponde in modo appropriato sia ai messaggi diretti sia alle interazioni con la UI.

Come funziona la sincronizzazione del contesto LLM

Vediamo i dettagli tecnici di come funziona questa sincronizzazione:

Data Flow

  1. Azione utente: l'utente fa clic su un colore nella striscia della cronologia
  2. Evento UI: il widget MainScreen rileva questa selezione
  3. Esecuzione del callback: viene attivato il callback notifyColorSelection
  4. Creazione del messaggio: viene creato un messaggio con una formattazione speciale con i dati sul colore
  5. Elaborazione LLM: il messaggio viene inviato a Gemini, che riconosce il formato
  6. Risposta contestuale: Gemini risponde in modo appropriato in base al prompt di sistema
  7. Aggiornamento della UI: la risposta viene visualizzata nella chat, creando un'esperienza coesa

Serializzazione dei dati

Un aspetto fondamentale di questo approccio è il modo in cui serializzi i dati di colore:

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

Il metodo toLLMContextMap() (fornito dal pacchetto colorist_ui) converte un oggetto ColorData in una mappa con proprietà chiave che l'LLM può comprendere. In genere, sono inclusi:

  • Valori RGB (rosso, verde, blu)
  • Rappresentazione del codice esadecimale
  • Qualsiasi nome o descrizione associati al colore

Se formatti questi dati in modo coerente e li includi nel messaggio, ti assicuri che l'LLM disponga di tutte le informazioni necessarie per rispondere in modo appropriato.

Applicazioni più ampie della sincronizzazione del contesto LLM

Questo pattern di notifica del modello linguistico di grandi dimensioni sugli eventi dell'interfaccia utente ha numerose applicazioni oltre alla selezione del colore:

Altri casi d'uso

  1. Modifiche ai filtri: notifica al LLM quando gli utenti applicano filtri ai dati
  2. Eventi di navigazione: comunicano al LLM quando gli utenti navigano in sezioni diverse
  3. Modifiche alla selezione: aggiorna il LLM quando gli utenti selezionano elementi da elenchi o griglie
  4. Aggiornamenti delle preferenze: indica all'LLM quando gli utenti modificano le impostazioni o le preferenze
  5. Manipolazione dei dati: notifica all'LLM quando gli utenti aggiungono, modificano o eliminano dati

In ogni caso, il pattern rimane lo stesso:

  1. Rilevare l'evento UI
  2. Serializzare i dati pertinenti
  3. Invia una notifica con una formattazione speciale all'LLM
  4. Guidare l'LLM a rispondere in modo appropriato tramite il prompt di sistema

Best practice per la sincronizzazione del contesto LLM

A seconda dell'implementazione, ecco alcune best practice per una sincronizzazione efficace del contesto LLM:

1. Formato coerente

Utilizza un formato coerente per le notifiche in modo che il LLM possa identificarle facilmente:

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

2. Contesto avanzato

Includi dettagli sufficienti nelle notifiche per consentire all'LLM di rispondere in modo intelligente. Per i colori, si intendono i valori RGB, i codici esadecimali e qualsiasi altra proprietà pertinente.

3. Istruzioni chiare

Fornisci istruzioni esplicite nel prompt di sistema su come gestire le notifiche, idealmente con esempi.

4. Integrazione naturale

Progetta le notifiche in modo che fluiscano naturalmente nella conversazione, non come interruzioni tecniche.

5. Notifica selettiva

Notifica al LLM solo le azioni pertinenti alla conversazione. Non è necessario comunicare ogni evento dell'interfaccia utente.

Risoluzione dei problemi

Problemi relativi alle notifiche

Se il LLM non risponde correttamente alle selezioni di colore:

  • Verifica che il formato del messaggio di notifica corrisponda a quanto descritto nel prompt di sistema
  • Verifica che i dati sul colore vengano serializzati correttamente
  • Assicurati che il prompt di sistema contenga istruzioni chiare per la gestione delle selezioni
  • Cerca eventuali errori nel servizio di chat durante l'invio delle notifiche

Gestione del contesto

Se l'LLM sembra perdere il contesto:

  • Controlla che la sessione di chat venga gestita correttamente
  • Verificare che gli stati della conversazione cambino correttamente
  • Assicurati che le notifiche vengano inviate tramite la stessa sessione di chat

Problemi generali

Per problemi generali:

  • Esamina i log per verificare la presenza di errori o avvisi
  • Verifica la connettività di Firebase AI Logic
  • Verifica la presenza di mancate corrispondenze dei tipi nei parametri delle funzioni
  • Assicurati che tutto il codice generato da Riverpod sia aggiornato

Concetti chiave appresi

  • Creazione della sincronizzazione del contesto LLM tra l'interfaccia utente e l'LLM
  • Serializzazione degli eventi dell'interfaccia utente in un contesto compatibile con i modelli LLM
  • Guidare il comportamento del LLM per diversi pattern di interazione
  • Creare un'esperienza coerente tra le interazioni con i messaggi e quelle senza messaggi
  • Migliorare la consapevolezza dell'LLM dello stato dell'applicazione più ampio

Implementando la sincronizzazione del contesto dell'LLM, hai creato un'esperienza davvero integrata in cui l'LLM sembra un assistente consapevole e reattivo, anziché un semplice generatore di testo. Questo pattern può essere applicato a innumerevoli altre applicazioni per creare interfacce più naturali e intuitive basate sull'AI.

9. Complimenti!

Hai completato correttamente il codelab Colorist. 🎉

Cosa hai creato

Hai creato un'applicazione Flutter completamente funzionale che integra l'API Gemini di Google per interpretare le descrizioni dei colori in linguaggio naturale. Ora la tua app può:

  • Elaborare descrizioni in linguaggio naturale come "arancione tramonto" o "blu oceano profondo"
  • Utilizza Gemini per tradurre in modo intelligente queste descrizioni in valori RGB
  • Visualizzare i colori interpretati in tempo reale con le risposte in streaming
  • Gestire le interazioni degli utenti tramite chat ed elementi dell'interfaccia utente
  • Mantenere la consapevolezza contestuale in diversi metodi di interazione

Come procedere

Ora che hai acquisito le nozioni di base sull'integrazione di Gemini con Flutter, ecco alcuni modi per continuare il tuo percorso:

Migliorare l'app Colorist

  • Tavolozze di colori: aggiungi la funzionalità per generare combinazioni di colori complementari o corrispondenti
  • Input vocale: integra il riconoscimento vocale per le descrizioni verbali dei colori
  • Gestione della cronologia: aggiungi opzioni per denominare, organizzare ed esportare i set di colori
  • Prompt personalizzati: crea un'interfaccia per consentire agli utenti di personalizzare i prompt di sistema
  • Analisi avanzate: monitora quali descrizioni funzionano meglio o causano difficoltà

Esplorare altre funzionalità di Gemini

  • Input multimodali: aggiungi input di immagini per estrarre i colori dalle foto
  • Generazione di contenuti: utilizza Gemini per generare contenuti correlati ai colori, come descrizioni o storie
  • Miglioramenti della chiamata di funzione: crea integrazioni di strumenti più complesse con più funzioni
  • Impostazioni di sicurezza: esplora le diverse impostazioni di sicurezza e il loro impatto sulle risposte

Applica questi pattern ad altri domini

  • Analisi dei documenti: crea app in grado di comprendere e analizzare i documenti
  • Assistenza per la scrittura creativa: crea strumenti di scrittura con suggerimenti basati su LLM
  • Automazione delle attività: progetta app che traducono il linguaggio naturale in attività automatizzate
  • Applicazioni basate sulla conoscenza: crea sistemi esperti in domini specifici

Risorse

Ecco alcune risorse preziose per continuare a imparare:

Documentazione ufficiale

Corso e guida sui prompt

Community

Observable Flutter Agentic series

Nell'episodio n. 59, Craig Labenz e Andrew Brogden esplorano questo codelab, mettendo in evidenza le parti interessanti della creazione dell'app.

Nell'episodio n. 60, Craig e Andrew estendono l'app codelab con nuove funzionalità e lottano per far fare agli LLM ciò che viene detto loro.

Nell'episodio n. 61, Craig è affiancato da Chris Sells per analizzare in modo nuovo i titoli delle notizie e generare le immagini corrispondenti.

Feedback

Ci piacerebbe conoscere la tua esperienza con questo codelab. Valuta la possibilità di fornire un feedback tramite:

Grazie per aver completato questo codelab. Ci auguriamo che continuerai a esplorare le entusiasmanti possibilità all'incrocio tra Flutter e AI.