Compila una app de Flutter potenciada por Gemini

1. Crea una app de Flutter potenciada por Gemini

Qué compilarás

En este codelab, compilarás Colorist, una aplicación interactiva de Flutter que incorpora la potencia de la API de Gemini directamente en tu app de Flutter. ¿Alguna vez quisiste permitir que los usuarios controlen tu app a través del lenguaje natural, pero no sabías por dónde empezar? En este codelab, se muestra cómo hacerlo.

Colorist permite a los usuarios describir colores en lenguaje natural (como "el naranja de un atardecer" o "azul profundo del océano"), y la app hace lo siguiente:

  • Procesa estas descripciones con la API de Gemini de Google
  • Interpreta las descripciones en valores de color RGB precisos
  • Muestra el color en la pantalla en tiempo real
  • Proporciona detalles técnicos sobre el color y un contexto interesante sobre él.
  • Mantiene un historial de los colores generados recientemente

Captura de pantalla de la app de Colorist que muestra la pantalla de color y la interfaz de chat

La app cuenta con una interfaz de pantalla dividida con un área de visualización a color y un sistema de chat interactivo en un lado, y un panel de registro detallado que muestra las interacciones sin procesar del LLM en el otro lado. Este registro te permite comprender mejor cómo funciona realmente una integración de LLM de forma interna.

Por qué es importante para los desarrolladores de Flutter

Los LLM están revolucionando la forma en que los usuarios interactúan con las aplicaciones, pero integrarlos de manera eficaz en las apps para dispositivos móviles y computadoras presenta desafíos únicos. En este codelab, aprenderás patrones prácticos que van más allá de las llamadas a la API sin procesar.

Tu proceso de aprendizaje

En este codelab, se te guía por el proceso de compilación de Colorist paso a paso:

  1. Configuración del proyecto: Comenzarás con una estructura básica de la app de Flutter y el paquete colorist_ui.
  2. Integración básica de Gemini: Conecta tu app a Firebase AI Logic y, luego, implementa la comunicación con LLM
  3. Instrucciones eficaces: Crea una instrucción del sistema que guíe al LLM para que comprenda las descripciones de color.
  4. Declaraciones de funciones: Definen herramientas que el LLM puede usar para establecer colores en tu aplicación.
  5. Control de herramientas: Procesa las llamadas a funciones del LLM y conéctalas al estado de tu app.
  6. Respuestas de transmisión: Mejora la experiencia del usuario con respuestas de LLM de transmisión en tiempo real
  7. Sincronización del contexto del LLM: Crea una experiencia coherente informando al LLM sobre las acciones del usuario.

Qué aprenderás

  • Configura Firebase AI Logic para aplicaciones de Flutter
  • Crea instrucciones del sistema eficaces para guiar el comportamiento del LLM
  • Implementa declaraciones de funciones que conecten el lenguaje natural y las funciones de la app.
  • Procesa respuestas de transmisión para una experiencia del usuario responsiva
  • Sincroniza el estado entre los eventos de la IU y el LLM
  • Administra el estado de la conversación del LLM con Riverpod
  • Aborda los errores con facilidad en las aplicaciones potenciadas por LLMs

Vista previa del código: Una muestra de lo que implementarás

A continuación, se muestra un ejemplo de la declaración de la función que crearás para permitir que el LLM establezca colores en tu 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)'),
  },
);

Un resumen en video de este codelab

Mira a Craig Labenz y Andrew Brogdon hablar sobre este codelab en el episodio 59 de Observable Flutter:

Requisitos previos

Para aprovechar al máximo este codelab, debes tener lo siguiente:

  • Experiencia en el desarrollo de Flutter: Familiaridad con los conceptos básicos de Flutter y la sintaxis de Dart
  • Conocimiento de programación asíncrona: Comprensión de Futures, async/await y Streams
  • Cuenta de Firebase: Necesitarás una Cuenta de Google para configurar Firebase.

Comencemos a compilar tu primera app de Flutter potenciada por LLM.

2. Configuración del proyecto y servicio de eco

En este primer paso, configurarás la estructura del proyecto y, luego, implementarás un servicio de eco que se reemplazará más adelante por la integración de la API de Gemini. Esto establece la arquitectura de la aplicación y garantiza que la IU funcione correctamente antes de agregar la complejidad de las llamadas al LLM.

Qué aprenderás en este paso

  • Cómo configurar un proyecto de Flutter con las dependencias requeridas
  • Trabaja con el paquete colorist_ui para los componentes de la IU
  • Implementa un servicio de mensajes de eco y conéctalo a la IU

Crea un proyecto de Flutter nuevo

Para comenzar, crea un proyecto nuevo de Flutter con el siguiente comando:

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

La marca -e indica que deseas un proyecto vacío sin la app counter predeterminada, que está diseñada para funcionar en computadoras, dispositivos móviles y la Web. Sin embargo, flutterfire no es compatible con Linux en este momento.

Cómo agregar dependencias

Navega al directorio del proyecto y agrega las dependencias necesarias:

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

Se agregarán los siguientes paquetes clave:

  • colorist_ui: Es un paquete personalizado que proporciona los componentes de la IU para la app de Colorist.
  • flutter_riverpod y riverpod_annotation: Para la administración del estado
  • logging: Para el registro estructurado
  • Dependencias de desarrollo para la generación y el análisis de código

Tu pubspec.yaml se verá de la siguiente manera:

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

Implementa el archivo main.dart

Reemplaza el contenido de lib/main.dart con la siguiente información:

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

Esto configura una app de Flutter que implementa un servicio de eco que imita el comportamiento de un LLM devolviendo el mensaje del usuario.

Información sobre la arquitectura

Dediquemos un minuto a comprender la arquitectura de la app de colorist:

El paquete colorist_ui

El paquete colorist_ui proporciona componentes de IU compilados previamente y herramientas de administración del estado:

  1. MainScreen: Es el componente principal de la IU que muestra lo siguiente:
    • Diseño de pantalla dividida en computadoras de escritorio (área de interacción y panel de registro)
    • Una interfaz con pestañas en dispositivos móviles
    • Pantalla a color, interfaz de chat y miniaturas del historial
  2. Administración del estado: La app usa varios notificadores de estado:
    • ChatStateNotifier: Administra los mensajes de chat
    • ColorStateNotifier: Administra el color actual y el historial
    • LogStateNotifier: Administra las entradas de registro para la depuración
  3. Control de mensajes: La app usa un modelo de mensajes con diferentes estados:
    • Mensajes del usuario: Ingresados por el usuario
    • Mensajes del LLM: Generados por el LLM (o tu servicio de eco por ahora)
    • MessageState: Realiza un seguimiento para saber si los mensajes del LLM se completaron o si aún se están transmitiendo.

Arquitectura de aplicaciones

La app sigue la siguiente arquitectura:

  1. Capa de la IU: Proporcionada por el paquete colorist_ui
  2. Administración de estado: Usa Riverpod para la administración de estado reactiva.
  3. Capa de servicio: Actualmente contiene tu servicio de eco simple, que se reemplazará por el servicio de Gemini Chat
  4. Integración de LLM: Se agregará en pasos posteriores

Esta separación te permite concentrarte en implementar la integración del LLM, ya que los componentes de la IU ya están listos.

Ejecuta la app

Ejecuta la app con el siguiente comando:

flutter run -d DEVICE

Reemplaza DEVICE por tu dispositivo de destino, como macos, windows, chrome o un ID de dispositivo.

Captura de pantalla de la app de Colorist que muestra el servicio de eco que renderiza Markdown

Ahora deberías ver la app de Colorist con lo siguiente:

  1. Un área de visualización de color con un color predeterminado
  2. Una interfaz de chat en la que puedes escribir mensajes
  3. Un panel de registro que muestra las interacciones del chat

Intenta escribir un mensaje como "Me gustaría un color azul oscuro" y presiona Enviar. El servicio de eco simplemente repetirá tu mensaje. En pasos posteriores, reemplazarás esto por una interpretación de color real con Firebase AI Logic.

Próximos pasos

En el siguiente paso, configurarás Firebase y, luego, implementarás la integración básica de la API de Gemini para reemplazar tu servicio de eco por el servicio de chat de Gemini. Esto permitirá que la app interprete las descripciones de color y proporcione respuestas inteligentes.

Solución de problemas

Problemas con el paquete de IU

Si tienes problemas con el paquete colorist_ui, haz lo siguiente:

  • Asegúrate de usar la versión más reciente
  • Verifica que hayas agregado la dependencia correctamente
  • Verifica si hay versiones de paquetes en conflicto

Errores de compilación

Si ves errores de compilación, haz lo siguiente:

  • Asegúrate de tener instalada la versión más reciente del SDK de Flutter del canal estable.
  • Ejecuta flutter clean seguido de flutter pub get
  • Verifica la salida de la consola para ver si hay mensajes de error específicos

Conceptos clave aprendidos

  • Cómo configurar un proyecto de Flutter con las dependencias necesarias
  • Comprender la arquitectura de la aplicación y las responsabilidades de los componentes
  • Implementa un servicio simple que imite el comportamiento de un LLM
  • Cómo conectar el servicio a los componentes de la IU
  • Cómo usar Riverpod para la administración del estado

3. Integración básica de Gemini Chat

En este paso, reemplazarás el servicio de eco del paso anterior por la integración de la API de Gemini con Firebase AI Logic. Configurarás Firebase, establecerás los proveedores necesarios y, luego, implementarás un servicio de chat básico que se comunique con la API de Gemini.

Qué aprenderás en este paso

  • Cómo configurar Firebase en una aplicación de Flutter
  • Cómo configurar Firebase AI Logic para acceder a Gemini
  • Cómo crear proveedores de Riverpod para los servicios de Firebase y Gemini
  • Implementa un servicio de chat básico con la API de Gemini
  • Cómo controlar las respuestas de la API asíncrona y los estados de error

Configura Firebase

Primero, debes configurar Firebase para tu proyecto de Flutter. Esto implica crear un proyecto de Firebase, agregar tu app a él y configurar los parámetros necesarios de Firebase AI Logic.

Crea un proyecto de Firebase

  1. Ve a la consola de Firebase y accede con tu Cuenta de Google.
  2. Haz clic en Crear un proyecto de Firebase o selecciona uno existente.
  3. Sigue el asistente de configuración para crear tu proyecto.

Configura Firebase AI Logic en tu proyecto de Firebase

  1. En Firebase console, navega a tu proyecto.
  2. En la barra lateral izquierda, selecciona IA.
  3. En el menú desplegable de IA, selecciona Lógica de IA.
  4. En la tarjeta de Firebase AI Logic, selecciona Comenzar.
  5. Sigue las indicaciones para habilitar la API de Gemini Developer en tu proyecto.

Instala la CLI de FlutterFire

La CLI de FlutterFire simplifica la configuración de Firebase en las apps de Flutter:

dart pub global activate flutterfire_cli

Agrega Firebase a tu app de Flutter

  1. Agrega los paquetes de Firebase Core y Firebase AI Logic a tu proyecto:
flutter pub add firebase_core firebase_ai
  1. Ejecuta el comando de configuración de FlutterFire:
flutterfire configure

Este comando hará lo siguiente:

  • Pedirte que selecciones el proyecto de Firebase que acabas de crear
  • Registra tus apps de Flutter en Firebase
  • Genera un archivo firebase_options.dart con la configuración de tu proyecto

El comando detectará automáticamente las plataformas seleccionadas (iOS, Android, macOS, Windows y la Web) y las configurará de forma adecuada.

Configuración específica de la plataforma

Firebase requiere versiones mínimas más altas que las predeterminadas para Flutter. También requiere acceso a la red para comunicarse con los servidores de Firebase AI Logic.

Configura los permisos de macOS

En macOS, debes habilitar el acceso a la red en los derechos de tu app:

  1. Abre macos/Runner/DebugProfile.entitlements y agrega lo siguiente:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. También abre macos/Runner/Release.entitlements y agrega la misma entrada.

Cómo configurar los parámetros de iOS

En iOS, actualiza la versión mínima en la parte superior de ios/Podfile:

ios/Podfile

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

Crea proveedores de modelos de Gemini

Ahora crearás los proveedores de Riverpod para Firebase y Gemini. Crea un archivo lib/providers/gemini.dart nuevo:

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
Future<FirebaseApp> firebaseApp(Ref ref) =>
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

@riverpod
Future<GenerativeModel> geminiModel(Ref ref) async {
  await ref.watch(firebaseAppProvider.future);

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

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

Este archivo define la base de tres proveedores de claves. Estos proveedores se generan cuando ejecutas dart run build_runner con los generadores de código de Riverpod.

  1. firebaseAppProvider: Inicializa Firebase con la configuración de tu proyecto
  2. geminiModelProvider: Crea una instancia del modelo generativo de Gemini
  3. chatSessionProvider: Crea y mantiene una sesión de chat con el modelo de Gemini.

La anotación keepAlive: true en la sesión de chat garantiza que persista durante todo el ciclo de vida de la app, lo que mantiene el contexto de la conversación.

Implementa el servicio de chat de Gemini

Crea un archivo nuevo lib/services/gemini_chat_service.dart para implementar el servicio de chat:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

@riverpod
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

Este servicio:

  1. Acepta mensajes del usuario y los envía a la API de Gemini
  2. Actualiza la interfaz de chat con las respuestas del modelo.
  3. Registra todas las comunicaciones para facilitar la comprensión del flujo real del LLM
  4. Controla los errores con comentarios adecuados para el usuario

Nota: En este punto, la ventana de registro se verá casi idéntica a la ventana de chat. El registro se volverá más interesante una vez que introduzcas las llamadas a funciones y, luego, las respuestas de transmisión.

Genera código de Riverpod

Ejecuta el comando de compilación para generar el código de Riverpod necesario:

dart run build_runner build --delete-conflicting-outputs

Esto creará los archivos .g.dart que Riverpod necesita para funcionar.

Actualiza el archivo main.dart

Actualiza tu archivo lib/main.dart para usar el nuevo servicio de chat de 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),
      ),
    );
  }
}

Los cambios clave de esta actualización son los siguientes:

  1. Reemplaza el servicio de eco por el servicio de chat basado en la API de Gemini
  2. Cómo agregar pantallas de carga y error con el patrón AsyncValue de Riverpod y el método when
  3. Conecta la IU a tu nuevo servicio de chat a través de la devolución de llamada sendMessage

Ejecuta la app

Ejecuta la app con el siguiente comando:

flutter run -d DEVICE

Reemplaza DEVICE por tu dispositivo de destino, como macos, windows, chrome o un ID de dispositivo.

Captura de pantalla de la app de Colorist en la que se muestra el LLM de Gemini respondiendo a una solicitud de un color amarillo soleado

Ahora, cuando escribas un mensaje, se enviará a la API de Gemini y recibirás una respuesta del LLM en lugar de un eco. En el panel de registro, se mostrarán las interacciones con la API.

Cómo comprender la comunicación de los LLM

Dediquemos un momento a comprender qué sucede cuando te comunicas con la API de Gemini:

El flujo de comunicación

  1. Entrada del usuario: El usuario ingresa texto en la interfaz de chat.
  2. Formato de la solicitud: La app da formato al texto como un objeto Content para la API de Gemini.
  3. Comunicación con la API: El texto se envía a la API de Gemini a través de Firebase AI Logic
  4. Procesamiento del LLM: El modelo de Gemini procesa el texto y genera una respuesta.
  5. Control de respuestas: La app recibe la respuesta y actualiza la IU.
  6. Registro: Se registra toda la comunicación para garantizar la transparencia.

Sesiones de chat y contexto de la conversación

La sesión de chat de Gemini mantiene el contexto entre los mensajes, lo que permite interacciones conversacionales. Esto significa que el LLM "recuerda" los intercambios anteriores en la sesión actual, lo que permite conversaciones más coherentes.

La anotación keepAlive: true en tu proveedor de sesiones de chat garantiza que este contexto persista durante todo el ciclo de vida de la app. Este contexto persistente es fundamental para mantener un flujo de conversación natural con el LLM.

Próximos pasos

En este punto, puedes preguntarle cualquier cosa a la API de Gemini, ya que no hay restricciones sobre lo que responderá. Por ejemplo, podrías pedirle un resumen de las Guerras de las Rosas, que no se relaciona con el propósito de tu app de colores.

En el siguiente paso, crearás una instrucción del sistema para guiar a Gemini a interpretar las descripciones de color de manera más eficaz. Esto demostrará cómo personalizar el comportamiento de un LLM para las necesidades específicas de la aplicación y enfocar sus capacidades en el dominio de tu app.

Solución de problemas

Problemas de configuración de Firebase

Si tienes problemas con la inicialización de Firebase, haz lo siguiente:

  • Asegúrate de que tu archivo firebase_options.dart se haya generado correctamente
  • Verifica que hayas actualizado al plan Blaze para acceder a Firebase AI Logic

Errores de acceso a la API

Si recibes errores al acceder a la API de Gemini, haz lo siguiente:

  • Confirma que la facturación esté configurada correctamente en tu proyecto de Firebase
  • Verifica que Firebase AI Logic y la API de Cloud AI estén habilitadas en tu proyecto de Firebase
  • Verifica la conectividad de red y la configuración del firewall
  • Verifica que el nombre del modelo (gemini-2.0-flash) sea correcto y esté disponible

Problemas de contexto de la conversación

Si notas que Gemini no recuerda el contexto anterior del chat, haz lo siguiente:

  • Confirma que la función chatSession esté anotada con @Riverpod(keepAlive: true).
  • Verifica que estés reutilizando la misma sesión de chat para todos los intercambios de mensajes.
  • Verifica que la sesión de chat se inicialice correctamente antes de enviar mensajes

Problemas específicos de la plataforma

Para problemas específicos de la plataforma, haz lo siguiente:

  • iOS/macOS: Asegúrate de que los derechos adecuados estén establecidos y de que las versiones mínimas estén configuradas
  • Android: Verifica que la versión mínima del SDK esté configurada correctamente
  • Verifica los mensajes de error específicos de la plataforma en la consola

Conceptos clave aprendidos

  • Cómo configurar Firebase en una aplicación de Flutter
  • Cómo configurar Firebase AI Logic para acceder a Gemini
  • Cómo crear proveedores de Riverpod para servicios asíncronos
  • Implementa un servicio de chat que se comunica con un LLM
  • Cómo controlar los estados de la API asíncrona (carga, error, datos)
  • Comprende el flujo de comunicación y las sesiones de chat de los LLM

4. Instrucciones eficaces para descripciones de colores

En este paso, crearás e implementarás una instrucción del sistema que guíe a Gemini para interpretar las descripciones de color. Las instrucciones del sistema son una forma eficaz de personalizar el comportamiento de los LLM para tareas específicas sin cambiar el código.

Qué aprenderás en este paso

  • Comprende las instrucciones del sistema y su importancia en las aplicaciones de LLM
  • Cómo crear instrucciones eficaces para tareas específicas del dominio
  • Cómo cargar y usar instrucciones del sistema en una app de Flutter
  • Cómo guiar un LLM para que proporcione respuestas con formato coherente
  • Prueba cómo las instrucciones del sistema afectan el comportamiento del LLM

Comprende los mensajes del sistema

Antes de profundizar en la implementación, veamos qué son las instrucciones del sistema y por qué son importantes:

¿Qué son las instrucciones del sistema?

Una instrucción del sistema es un tipo especial de instrucción que se le da a un LLM y que establece el contexto, los lineamientos de comportamiento y las expectativas para sus respuestas. A diferencia de los mensajes del usuario, las instrucciones del sistema tienen las siguientes características:

  • Establece el rol y el arquetipo del LLM
  • Definir conocimientos o capacidades especializados
  • Proporciona instrucciones de formato
  • Establece restricciones en las respuestas
  • Describe cómo controlar diferentes situaciones

Piensa en la instrucción del sistema como la "descripción del trabajo" del LLM: le indica al modelo cómo comportarse a lo largo de la conversación.

Por qué son importantes las instrucciones del sistema

Las instrucciones del sistema son fundamentales para crear interacciones coherentes y útiles con los LLM porque hacen lo siguiente:

  1. Garantiza la coherencia: Guía al modelo para que proporcione respuestas en un formato coherente.
  2. Mejora la relevancia: Enfoca el modelo en tu dominio específico (en tu caso, los colores).
  3. Establece límites: Define lo que el modelo debe y no debe hacer
  4. Mejorar la experiencia del usuario: Crear un patrón de interacción más natural y útil
  5. Reducir el procesamiento posterior: Obtén respuestas en formatos que sean más fáciles de analizar o mostrar.

En el caso de tu app de Colorist, necesitas que el LLM interprete de forma coherente las descripciones de color y proporcione valores RGB en un formato específico.

Crea un recurso de instrucción del sistema

Primero, crearás un archivo de instrucciones del sistema que se cargará en el tiempo de ejecución. Este enfoque te permite modificar la instrucción sin volver a compilar tu app.

Crea un archivo nuevo assets/system_prompt.md con el siguiente contenido:

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

Cómo comprender la estructura de la instrucción del sistema

Veamos en detalle qué hace esta instrucción:

  1. Definición del rol: Establece el LLM como un "asistente experto en color".
  2. Explicación de la tarea: Define la tarea principal como la interpretación de descripciones de colores en valores RGB
  3. Formato de respuesta: Especifica exactamente cómo se deben formatear los valores RGB para garantizar la coherencia.
  4. Ejemplo de intercambio: Proporciona un ejemplo concreto del patrón de interacción esperado.
  5. Manejo de casos extremos: Indica cómo manejar descripciones poco claras
  6. Restricciones y lineamientos: Establece límites, como mantener los valores RGB entre 0.0 y 1.0.

Este enfoque estructurado garantiza que las respuestas del LLM sean coherentes, informativas y estén formateadas de una manera que sería fácil de analizar si quisieras extraer los valores RGB de forma programática.

Actualiza pubspec.yaml

Ahora, actualiza la parte inferior de tu pubspec.yaml para incluir el directorio de recursos:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

Ejecuta flutter pub get para actualizar el paquete de recursos.

Crea un proveedor de instrucciones del sistema

Crea un archivo nuevo lib/providers/system_prompt.dart para cargar la instrucción del sistema:

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

Este proveedor usa el sistema de carga de recursos de Flutter para leer el archivo de instrucciones en el tiempo de ejecución.

Actualiza el proveedor del modelo de Gemini

Ahora, modifica tu archivo lib/providers/gemini.dart para incluir la instrucción del 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
Future<FirebaseApp> firebaseApp(Ref ref) =>
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

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

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

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

El cambio clave es agregar systemInstruction: Content.system(systemPrompt) cuando se crea el modelo generativo. Esto le indica a Gemini que use tus instrucciones como la instrucción del sistema para todas las interacciones en esta sesión de chat.

Genera código de Riverpod

Ejecuta el comando del ejecutor de compilación para generar el código de Riverpod necesario:

dart run build_runner build --delete-conflicting-outputs

Ejecuta y prueba la aplicación

Ahora ejecuta tu aplicación:

flutter run -d DEVICE

Captura de pantalla de la app de Colorist en la que se muestra el LLM de Gemini respondiendo con una respuesta en carácter para una app de selección de color

Intenta probarlo con varias descripciones de color:

  • “Quisiera un azul cielo”.
  • "Quiero un verde bosque".
  • "Haz un naranja atardecer vibrante".
  • "Quiero el color de la lavanda fresca".
  • "Muéstrame algo como un azul profundo del océano".

Deberías notar que Gemini ahora responde con explicaciones conversacionales sobre los colores junto con valores RGB con formato coherente. La instrucción del sistema guió de manera eficaz al LLM para que proporcione el tipo de respuestas que necesitas.

También puedes pedirle contenido que no esté relacionado con los colores. Por ejemplo, las principales causas de las Guerras de las Rosas. Deberías notar una diferencia con respecto al paso anterior.

La importancia de la ingeniería de instrucciones para tareas especializadas

Las instrucciones del sistema son tanto arte como ciencia. Son una parte fundamental de la integración de LLM que puede afectar drásticamente la utilidad del modelo para tu aplicación específica. Lo que hiciste aquí es una forma de ingeniería de instrucciones: adaptar las instrucciones para que el modelo se comporte de maneras que se adapten a las necesidades de tu aplicación.

La ingeniería de instrucciones eficaz implica lo siguiente:

  1. Definición clara del rol: Establecer el propósito del LLM
  2. Instrucciones explícitas: Detallan exactamente cómo debe responder el LLM.
  3. Ejemplos concretos: Mostrar, en lugar de solo decir, cómo son las respuestas óptimas
  4. Manejo de casos extremos: Darle instrucciones al LLM sobre cómo abordar situaciones ambiguas
  5. Especificaciones de formato: Garantizar que las respuestas estén estructuradas de una manera coherente y útil

La instrucción del sistema que creaste transforma las capacidades genéricas de Gemini en un asistente especializado en la interpretación de colores que proporciona respuestas con un formato específico para las necesidades de tu aplicación. Este es un patrón potente que puedes aplicar a muchos dominios y tareas diferentes.

Próximos pasos

En el siguiente paso, agregarás declaraciones de funciones, lo que permitirá que el LLM no solo sugiera valores RGB, sino que también llame a funciones en tu app para establecer el color directamente. Esto demuestra cómo los LLM pueden cerrar la brecha entre el lenguaje natural y las funciones concretas de las aplicaciones.

Solución de problemas

Problemas de carga de recursos

Si tienes problemas para cargar la instrucción del sistema, haz lo siguiente:

  • Verifica que tu pubspec.yaml muestre correctamente el directorio de recursos
  • Verifica que la ruta de acceso en rootBundle.loadString() coincida con la ubicación del archivo.
  • Ejecuta flutter clean seguido de flutter pub get para actualizar el paquete de recursos.

Respuestas incongruentes

Si el LLM no sigue tus instrucciones de formato de manera coherente, haz lo siguiente:

  • Intenta hacer que los requisitos de formato sean más explícitos en la instrucción del sistema.
  • Agrega más ejemplos para demostrar el patrón esperado
  • Asegúrate de que el formato que solicitas sea razonable para el modelo.

Limitación de frecuencia de la API

Si encuentras errores relacionados con la limitación de frecuencia, haz lo siguiente:

  • Ten en cuenta que el servicio de Firebase AI Logic tiene límites de uso.
  • Considera implementar una lógica de reintentos con retirada exponencial
  • Verifica si hay problemas de cuota en Firebase console

Conceptos clave aprendidos

  • Comprende el rol y la importancia de las instrucciones del sistema en las aplicaciones de LLM
  • Cómo crear instrucciones eficaces con indicaciones, ejemplos y restricciones claros
  • Cómo cargar y usar instrucciones del sistema en una aplicación de Flutter
  • Cómo guiar el comportamiento de los LLM para tareas específicas del dominio
  • Cómo usar la ingeniería de instrucciones para dar forma a las respuestas de los LLM

En este paso, se demuestra cómo puedes lograr una personalización significativa del comportamiento del LLM sin cambiar tu código, simplemente proporcionando instrucciones claras en la instrucción del sistema.

5. Declaraciones de funciones para herramientas de LLM

En este paso, comenzarás a trabajar para permitir que Gemini realice acciones en tu app implementando declaraciones de funciones. Esta potente función permite que el LLM no solo sugiera valores RGB, sino que también los establezca en la IU de tu app a través de llamadas a herramientas especializadas. Sin embargo, se requerirá el siguiente paso para ver las solicitudes del LLM ejecutadas en la app de Flutter.

Qué aprenderás en este paso

  • Comprensión del llamado a funciones de LLM y sus beneficios para las aplicaciones de Flutter
  • Cómo definir declaraciones de funciones basadas en esquemas para Gemini
  • Cómo integrar declaraciones de funciones con tu modelo de Gemini
  • Actualización de la instrucción del sistema para utilizar las capacidades de la herramienta

Información sobre las llamadas a funciones

Antes de implementar declaraciones de funciones, comprendamos qué son y por qué son valiosas:

¿Qué es la llamada a función?

La llamada a función (a veces denominada "uso de herramientas") es una capacidad que permite que un LLM haga lo siguiente:

  1. Reconocer cuándo una solicitud del usuario se beneficiaría de la invocación de una función específica
  2. Genera un objeto JSON estructurado con los parámetros necesarios para esa función.
  3. Permite que tu aplicación ejecute la función con esos parámetros.
  4. Recibir el resultado de la función e incorporarlo a su respuesta

En lugar de que el LLM solo describa qué hacer, la llamada a funciones le permite activar acciones concretas en tu aplicación.

Por qué es importante la llamada a funciones para las apps de Flutter

La llamada a funciones crea un puente potente entre el lenguaje natural y las funciones de la aplicación:

  1. Acción directa: Los usuarios pueden describir lo que quieren en lenguaje natural, y la app responde con acciones concretas.
  2. Salida estructurada: El LLM produce datos estructurados y limpios en lugar de texto que necesita análisis.
  3. Operaciones complejas: Permite que el LLM acceda a datos externos, realice cálculos o modifique el estado de la aplicación
  4. Mejor experiencia del usuario: Crea una integración perfecta entre la conversación y la funcionalidad.

En tu app de Colorist, la llamada a funciones permite que los usuarios digan "Quiero un verde bosque" y que la IU se actualice de inmediato con ese color, sin tener que analizar los valores RGB del texto.

Cómo definir declaraciones de funciones

Crea un archivo lib/services/gemini_tools.dart nuevo para definir las declaraciones de tu función:

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
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

Información sobre las declaraciones de funciones

Desglosemos lo que hace este código:

  1. Nombres de funciones: Le asignas a tu función el nombre set_color para indicar claramente su propósito.
  2. Descripción de la función: Proporcionas una descripción clara que ayuda al LLM a comprender cuándo usarla.
  3. Definiciones de parámetros: Defines parámetros estructurados con sus propias descripciones:
    • red: Componente rojo del RGB, especificado como un número entre 0.0 y 1.0
    • green: Componente verde del RGB, especificado como un número entre 0.0 y 1.0
    • blue: Es el componente azul del RGB, especificado como un número entre 0.0 y 1.0.
  4. Tipos de esquema: Usas Schema.number() para indicar que estos son valores numéricos.
  5. Colección de herramientas: Creas una lista de herramientas que contiene la declaración de tu función.

Este enfoque estructurado ayuda al LLM de Gemini a comprender lo siguiente:

  • Cuándo debería llamar a esta función
  • Qué parámetros debe proporcionar
  • Qué restricciones se aplican a esos parámetros (como el rango de valores)

Actualiza el proveedor del modelo de Gemini

Ahora, modifica tu archivo lib/providers/gemini.dart para incluir las declaraciones de funciones cuando inicialices el modelo de 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
Future<FirebaseApp> firebaseApp(Ref ref) =>
    Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

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

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

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

El cambio clave es agregar el parámetro tools: geminiTools.tools cuando se crea el modelo generativo. Esto hace que Gemini conozca las funciones a las que puede llamar.

Actualiza la instrucción del sistema

Ahora debes modificar la instrucción del sistema para indicarle al LLM que use la nueva herramienta set_color. Actualiza 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

Los cambios clave en la instrucción del sistema son los siguientes:

  1. Introducción a la herramienta: En lugar de solicitar valores RGB con formato, ahora le indicas al LLM sobre la herramienta set_color.
  2. Proceso modificado: Cambias el paso 3 de "dar formato a los valores en la respuesta" a "usar la herramienta para establecer valores".
  3. Ejemplo actualizado: Muestras cómo la respuesta debe incluir una llamada a herramienta en lugar de texto con formato
  4. Se quitó el requisito de formato: Como usas llamadas a funciones estructuradas, ya no necesitas un formato de texto específico.

Esta instrucción actualizada dirige al LLM para que use la llamada a funciones en lugar de solo proporcionar valores RGB en formato de texto.

Genera código de Riverpod

Ejecuta el comando del ejecutor de compilación para generar el código de Riverpod necesario:

dart run build_runner build --delete-conflicting-outputs

Ejecuta la aplicación

En este punto, Gemini generará contenido que intentará usar la llamada a función, pero aún no implementaste controladores para las llamadas a función. Cuando ejecutes la app y describas un color, verás que Gemini responde como si hubiera invocado una herramienta, pero no verás ningún cambio de color en la IU hasta el siguiente paso.

Ejecuta tu app:

flutter run -d DEVICE

Captura de pantalla de la app de Colorist que muestra el LLM de Gemini respondiendo con una respuesta parcial

Intenta describir un color, como “azul océano profundo” o “verde bosque”, y observa las respuestas. El LLM intenta llamar a las funciones definidas anteriormente, pero tu código aún no detecta las llamadas a funciones.

El proceso de llamada a función

Veamos qué sucede cuando Gemini usa la llamada a funciones:

  1. Selección de la función: El LLM decide si una llamada a función sería útil según la solicitud del usuario.
  2. Generación de parámetros: El LLM genera valores de parámetros que se ajustan al esquema de la función.
  3. Formato de llamada a función: El LLM envía un objeto de llamada a función estructurado en su respuesta.
  4. Control de la aplicación: Tu app recibiría esta llamada y ejecutaría la función pertinente (implementada en el siguiente paso).
  5. Integración de respuestas: En las conversaciones de varios turnos, el LLM espera que se devuelva el resultado de la función.

En el estado actual de tu app, se producen los tres primeros pasos, pero aún no implementaste los pasos 4 ni 5 (control de las llamadas a funciones), lo que harás en el siguiente paso.

Detalles técnicos: Cómo decide Gemini cuándo usar funciones

Gemini toma decisiones inteligentes sobre cuándo usar funciones según lo siguiente:

  1. Intención del usuario: Indica si una función sería la mejor opción para satisfacer la solicitud del usuario.
  2. Relevancia de la función: Qué tan bien coinciden las funciones disponibles con la tarea
  3. Disponibilidad del parámetro: Indica si puede determinar con certeza los valores de los parámetros.
  4. Instrucciones del sistema: Orientación de la instrucción del sistema sobre el uso de funciones

Al proporcionar declaraciones de funciones claras y las instrucciones del sistema, configuraste Gemini para que reconozca las solicitudes de descripción de color como oportunidades para llamar a la función set_color.

Próximos pasos

En el siguiente paso, implementarás controladores para las llamadas a funciones provenientes de Gemini. Esto completará el círculo y permitirá que las descripciones del usuario activen cambios de color reales en la IU a través de las llamadas a funciones del LLM.

Solución de problemas

Problemas con la declaración de funciones

Si encuentras errores con las declaraciones de funciones, haz lo siguiente:

  • Verifica que los nombres y los tipos de los parámetros coincidan con lo esperado
  • Verifica que el nombre de la función sea claro y descriptivo
  • Asegúrate de que la descripción de la función explique con precisión su propósito.

Problemas con las instrucciones del sistema

Si el LLM no intenta usar la función, haz lo siguiente:

  • Verifica que la instrucción del sistema indique claramente al LLM que use la herramienta de set_color.
  • Verifica que el ejemplo de la instrucción del sistema demuestre el uso de la función.
  • Intenta que la instrucción para usar la herramienta sea más explícita.

Problemas generales

Si tienes otros problemas, sigue estos pasos:

  • Revisa la consola en busca de errores relacionados con las declaraciones de funciones
  • Verifica que las herramientas se pasen correctamente al modelo
  • Asegúrate de que todo el código generado por Riverpod esté actualizado

Conceptos clave aprendidos

  • Cómo definir declaraciones de funciones para extender las capacidades de los LLM en apps de Flutter
  • Cómo crear esquemas de parámetros para la recopilación de datos estructurados
  • Integración de declaraciones de funciones con el modelo de Gemini
  • Actualización de las instrucciones del sistema para fomentar el uso de funciones
  • Cómo los LLM seleccionan y llaman funciones

En este paso, se demuestra cómo los LLMs pueden cerrar la brecha entre la entrada de lenguaje natural y las llamadas a funciones estructuradas, lo que sienta las bases para una integración perfecta entre las funciones de conversación y las de la aplicación.

6. Implementa el control de herramientas

En este paso, implementarás controladores para las llamadas a funciones provenientes de Gemini. Esto completa el círculo de comunicación entre las entradas de lenguaje natural y las funciones concretas de la aplicación, lo que permite que el LLM manipule directamente tu IU en función de las descripciones del usuario.

Qué aprenderás en este paso

  • Comprende la canalización completa de llamadas a funciones en aplicaciones basadas en LLMs
  • Cómo procesar llamadas a funciones de Gemini en una aplicación de Flutter
  • Implementa controladores de funciones que modifican el estado de la aplicación
  • Cómo controlar las respuestas de las funciones y devolver los resultados al LLM
  • Cómo crear un flujo de comunicación completo entre el LLM y la IU
  • Registro de llamadas y respuestas de funciones para mayor transparencia

Información sobre la canalización de llamadas a funciones

Antes de comenzar con la implementación, comprendamos la canalización completa de la llamada a funciones:

El flujo de extremo a extremo

  1. Entrada del usuario: El usuario describe un color en lenguaje natural (p.ej., "verde bosque")
  2. Procesamiento del LLM: Gemini analiza la descripción y decide llamar a la función set_color.
  3. Generación de llamadas a funciones: Gemini crea un JSON estructurado con parámetros (valores de rojo, verde y azul).
  4. Recepción de la llamada a la función: Tu app recibe estos datos estructurados de Gemini.
  5. Ejecución de la función: Tu app ejecuta la función con los parámetros proporcionados.
  6. Actualización de estado: La función actualiza el estado de tu app (cambia el color que se muestra).
  7. Generación de respuestas: Tu función devuelve resultados al LLM
  8. Incorporación de la respuesta: El LLM incorpora estos resultados en su respuesta final.
  9. Actualización de la IU: La IU reacciona al cambio de estado y muestra el color nuevo.

El ciclo de comunicación completo es esencial para la integración adecuada del LLM. Cuando un LLM realiza una llamada a una función, no solo envía la solicitud y continúa. En cambio, espera a que tu aplicación ejecute la función y muestre los resultados. Luego, el LLM usa estos resultados para formular su respuesta final, lo que crea un flujo de conversación natural que reconoce las acciones realizadas.

Implementa controladores de funciones

Actualicemos tu archivo lib/services/gemini_tools.dart para agregar controladores para las llamadas a funciones:

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
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

Información sobre los controladores de funciones

Veamos qué hacen estos controladores de funciones:

  1. handleFunctionCall: Es un distribuidor central que hace lo siguiente:
    • Registra la llamada a la función para brindar transparencia en el panel de registro.
    • Enruta al controlador adecuado según el nombre de la función
    • Devuelve una respuesta estructurada que se enviará al LLM
  2. handleSetColor: Es el controlador específico de tu función set_color que hace lo siguiente:
    • Extrae valores RGB del mapa de argumentos.
    • Los convierte en los tipos esperados (números de punto flotante de doble precisión)
    • Actualiza el estado de color de la aplicación con colorStateNotifier
    • Crea una respuesta estructurada con el estado de éxito y la información de color actual.
    • Registra los resultados de la función para la depuración.
  3. handleUnknownFunction: Es un controlador de resguardo para funciones desconocidas que hace lo siguiente:
    • Registra una advertencia sobre la función no compatible.
    • Devuelve una respuesta de error al LLM

La función handleSetColor es particularmente importante, ya que une la brecha entre la comprensión del lenguaje natural del LLM y los cambios concretos en la IU.

Actualiza el servicio de Gemini Chat para procesar llamadas y respuestas de funciones

Ahora, actualicemos el archivo lib/services/gemini_chat_service.dart para procesar las llamadas a funciones de las respuestas del LLM y enviar los resultados al 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
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

Información sobre el flujo de comunicación

La principal incorporación aquí es el control completo de las llamadas y respuestas de funciones:

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

Este código:

  1. Verifica si la respuesta del LLM contiene alguna llamada a función
  2. Para cada llamada a función, invoca tu método handleFunctionCall con el nombre y los argumentos de la función.
  3. Recopila los resultados de cada llamada a la función.
  4. Envía estos resultados de vuelta al LLM con Content.functionResponses.
  5. Procesa la respuesta del LLM a los resultados de la función
  6. Actualiza la IU con el texto de la respuesta final

Esto crea un flujo de viaje de ida y vuelta:

  • Usuario → LLM: Solicita un color
  • LLM → App: Llamadas a funciones con parámetros
  • App → Usuario: Se muestra un color nuevo
  • App → LLM: Resultados de la función
  • LLM → Usuario: Respuesta final que incorpora los resultados de la función

Genera código de Riverpod

Ejecuta el comando del ejecutor de compilación para generar el código de Riverpod necesario:

dart run build_runner build --delete-conflicting-outputs

Ejecuta y prueba el flujo completo

Ahora ejecuta tu aplicación:

flutter run -d DEVICE

Captura de pantalla de la app de Colorist en la que se muestra el LLM de Gemini respondiendo con una llamada a función

Prueba ingresar varias descripciones de colores:

  • “Quisiera un rojo carmesí intenso”.
  • "Muéstrame un azul celeste relajante".
  • "Dime el color de las hojas de menta fresca".
  • "Quiero ver un naranja cálido del atardecer".
  • "Que sea un púrpura real intenso".

Ahora deberías ver lo siguiente:

  1. Tu mensaje aparece en la interfaz de chat
  2. La respuesta de Gemini que aparece en el chat
  3. Llamadas a funciones que se registran en el panel de registros
  4. Los resultados de la función se registran inmediatamente después.
  5. El rectángulo de color se actualiza para mostrar el color descrito.
  6. Valores RGB que se actualizan para mostrar los componentes del color nuevo
  7. Aparece la respuesta final de Gemini, que a menudo comenta el color que se estableció.

El panel de registro proporciona información sobre lo que sucede tras bambalinas. En esta página verá lo siguiente:

  • Las llamadas a funciones exactas que realiza Gemini
  • Los parámetros que elige para cada valor RGB
  • Los resultados que devuelve tu función
  • Las respuestas de seguimiento de Gemini

El notificador de estado de color

El colorStateNotifier que usas para actualizar los colores forma parte del paquete colorist_ui. Administra lo siguiente:

  • El color actual que se muestra en la IU
  • El historial de colores (últimos 10 colores)
  • Notificación de cambios de estado a los componentes de la IU

Cuando llamas a updateColor con valores RGB nuevos, sucede lo siguiente:

  1. Crea un objeto ColorData nuevo con los valores proporcionados.
  2. Actualiza el color actual en el estado de la app.
  3. Agrega el color al historial.
  4. Activa actualizaciones de la IU a través de la administración de estados de Riverpod

Los componentes de la IU en el paquete colorist_ui observan este estado y se actualizan automáticamente cuando cambia, lo que crea una experiencia reactiva.

Información sobre el manejo de errores

Tu implementación incluye un manejo de errores sólido:

  1. Bloque try-catch: Envuelve todas las interacciones con el LLM para detectar cualquier excepción
  2. Registro de errores: Registra errores en el panel de registro con seguimientos de pila
  3. Comentarios del usuario: Proporciona un mensaje de error amigable en el chat.
  4. Limpieza del estado: Finaliza el estado del mensaje incluso si se produce un error

Esto garantiza que la app siga siendo estable y proporcione comentarios adecuados, incluso cuando se produzcan problemas con el servicio de LLM o la ejecución de funciones.

El poder de la llamada a funciones para la experiencia del usuario

Lo que lograste aquí demuestra cómo los LLM pueden crear interfaces naturales potentes:

  1. Interfaz de lenguaje natural: Los usuarios expresan su intención en lenguaje cotidiano.
  2. Interpretación inteligente: El LLM traduce descripciones vagas en valores precisos.
  3. Manipulación directa: La IU se actualiza en respuesta al lenguaje natural.
  4. Respuestas contextuales: El LLM proporciona contexto conversacional sobre los cambios.
  5. Carga cognitiva baja: Los usuarios no necesitan comprender los valores RGB ni la teoría del color.

Este patrón de uso de llamadas a funciones de LLM para conectar el lenguaje natural y las acciones de la IU se puede extender a muchos otros dominios más allá de la selección de color.

Próximos pasos

En el siguiente paso, mejorarás la experiencia del usuario implementando respuestas de transmisión. En lugar de esperar la respuesta completa, procesarás fragmentos de texto y llamadas a funciones a medida que se reciban, lo que creará una aplicación más atractiva y con mayor capacidad de respuesta.

Solución de problemas

Problemas con las llamadas a funciones

Si Gemini no llama a tus funciones o los parámetros son incorrectos, haz lo siguiente:

  • Verifica que la declaración de tu función coincida con lo que se describe en la instrucción del sistema
  • Verifica que los nombres y los tipos de parámetros sean coherentes
  • Asegúrate de que la instrucción del sistema indique explícitamente al LLM que use la herramienta.
  • Verifica que el nombre de la función en tu controlador coincida exactamente con el que aparece en la declaración.
  • Examina el panel de registro para obtener información detallada sobre las llamadas a funciones

Problemas con la respuesta de la función

Si los resultados de la función no se envían correctamente al LLM, haz lo siguiente:

  • Verifica que tu función devuelva un mapa con el formato correcto
  • Verifica que Content.functionResponses se esté construyendo correctamente
  • Busca errores en el registro relacionados con las respuestas de la función.
  • Asegúrate de usar la misma sesión de chat para la respuesta.

Problemas de visualización de color

Si los colores no se muestran correctamente, haz lo siguiente:

  • Asegúrate de que los valores RGB se conviertan correctamente en números de doble precisión (el LLM podría enviarlos como números enteros).
  • Verifica que los valores estén dentro del rango esperado (de 0.0 a 1.0).
  • Comprueba que el notificador de estado de color se llame correctamente
  • Examina el registro para ver los valores exactos que se pasan a la función.

Problemas generales

Para problemas generales:

  • Examina los registros en busca de errores o advertencias
  • Verifica la conectividad de Firebase AI Logic
  • Verifica si hay discrepancias de tipos en los parámetros de las funciones
  • Asegúrate de que todo el código generado por Riverpod esté actualizado

Conceptos clave aprendidos

  • Implementa una canalización completa de llamadas a funciones en Flutter
  • Cómo crear una comunicación completa entre un LLM y tu aplicación
  • Procesamiento de datos estructurados a partir de respuestas de LLM
  • Envío de los resultados de la función al LLM para su incorporación en las respuestas
  • Cómo usar el panel de registros para obtener visibilidad de las interacciones entre la aplicación y el LLM
  • Cómo conectar entradas de lenguaje natural con cambios concretos en la IU

Con este paso completo, tu app ahora demuestra uno de los patrones más potentes para la integración de LLM: traducir entradas de lenguaje natural en acciones concretas de la IU, a la vez que se mantiene una conversación coherente que reconoce estas acciones. Esto crea una interfaz intuitiva y conversacional que los usuarios perciben como mágica.

7. Respuestas de transmisión para mejorar la UX

En este paso, mejorarás la experiencia del usuario implementando respuestas de transmisión de Gemini. En lugar de esperar a que se genere toda la respuesta, procesarás fragmentos de texto y llamadas a funciones a medida que se reciban, lo que creará una aplicación más atractiva y con mayor capacidad de respuesta.

Qué se abordará en este paso

  • La importancia de la transmisión para las aplicaciones potenciadas por LLMs
  • Cómo implementar respuestas de LLM de transmisión en una aplicación de Flutter
  • Procesamiento de fragmentos de texto parciales a medida que llegan de la API
  • Administración del estado de la conversación para evitar conflictos de mensajes
  • Cómo controlar las llamadas a funciones en las respuestas de transmisión
  • Cómo crear indicadores visuales para las respuestas en curso

Por qué el procesamiento de transmisiones es importante para las aplicaciones basadas en LLMs

Antes de implementar, comprendamos por qué las respuestas de transmisión son fundamentales para crear experiencias de usuario excelentes con LLMs:

Experiencia del usuario mejorada

Las respuestas de transmisión proporcionan varios beneficios importantes para la experiencia del usuario:

  1. Latencia percibida reducida: Los usuarios ven que el texto comienza a aparecer de inmediato (por lo general, entre 100 y 300 ms), en lugar de esperar varios segundos para obtener una respuesta completa. Esta percepción de inmediatez mejora drásticamente la satisfacción del usuario.
  2. Ritmo de conversación natural: La aparición gradual del texto imita la forma en que se comunican los humanos, lo que crea una experiencia de diálogo más natural.
  3. Procesamiento de información progresivo: Los usuarios pueden comenzar a procesar la información a medida que llega, en lugar de sentirse abrumados por un gran bloque de texto de una sola vez.
  4. Oportunidad de interrupción anticipada: En una aplicación completa, los usuarios podrían interrumpir o redireccionar el LLM si ven que va en una dirección poco útil.
  5. Confirmación visual de la actividad: El texto de transmisión proporciona comentarios inmediatos que indican que el sistema funciona, lo que reduce la incertidumbre.

Ventajas técnicas

Además de las mejoras en la UX, la transmisión ofrece beneficios técnicos:

  1. Ejecución anticipada de funciones: Las llamadas a funciones se pueden detectar y ejecutar en cuanto aparecen en la transmisión, sin esperar la respuesta completa.
  2. Actualizaciones incrementales de la IU: Puedes actualizar tu IU de forma progresiva a medida que llega información nueva, lo que crea una experiencia más dinámica.
  3. Administración del estado de la conversación: La transmisión proporciona indicadores claros sobre cuándo se completan las respuestas y cuándo aún están en curso, lo que permite una mejor administración del estado.
  4. Menor riesgo de agotamiento del tiempo de espera: Con las respuestas sin transmisión, las generaciones de larga duración corren el riesgo de que se agote el tiempo de espera de la conexión. La transmisión establece la conexión con anticipación y la mantiene.

En el caso de tu app de Colorist, implementar la transmisión significa que los usuarios verán las respuestas de texto y los cambios de color con mayor rapidez, lo que creará una experiencia mucho más receptiva.

Cómo agregar administración de estado de conversación

Primero, agreguemos un proveedor de estado para hacer un seguimiento de si la app está controlando una respuesta de transmisión. Actualiza el archivo 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/legacy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

final conversationStateProvider = StateProvider(                     // Add from here...
  (ref) => ConversationState.idle,
);                                                                   // To here.

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

  Future<void> sendMessage(String message) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final conversationState = ref.read(conversationStateProvider);   // Add this line
    final chatStateNotifier = ref.read(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.state = ConversationState.busy;        // To here.
    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    final llmMessage = chatStateNotifier.createLlmMessage();
    try {                                                            // Modify from here...
      final responseStream = chatSession.sendMessageStream(
        Content.text(message),
      );
      await for (final block in responseStream) {
        await _processBlock(block, llmMessage.id);
      }                                                              // To here.
    } catch (e, st) {
      logStateNotifier.logError(e, st: st);
      chatStateNotifier.appendToMessage(
        llmMessage.id,
        "\nI'm sorry, I encountered an error processing your request. "
        "Please try again.",
      );
    } finally {
      chatStateNotifier.finalizeMessage(llmMessage.id);
      conversationStateNotifier.state = ConversationState.idle;      // Add this line.
    }
  }

  Future<void> _processBlock(                                        // Add from here...
    GenerateContentResponse block,
    String llmMessageId,
  ) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final chatStateNotifier = ref.read(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
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

Información sobre la implementación de la transmisión

Desglosemos lo que hace este código:

  1. Seguimiento del estado de la conversación:
    • Un conversationStateProvider hace un seguimiento de si la app está procesando una respuesta
    • El estado pasa de idlebusy durante el procesamiento y, luego, vuelve a idle.
    • Esto evita varias solicitudes simultáneas que podrían entrar en conflicto.
  2. Inicialización de la transmisión:
    • sendMessageStream() devuelve un flujo de fragmentos de respuesta en lugar de un Future con la respuesta completa.
    • Cada fragmento puede contener texto, llamadas a funciones o ambos.
  3. Procesamiento progresivo:
    • await for procesa cada fragmento a medida que llega en tiempo real
    • El texto se agrega a la IU de inmediato, lo que crea el efecto de transmisión.
    • Las llamadas a funciones se ejecutan en cuanto se detectan.
  4. Control de llamadas a funciones:
    • Cuando se detecta una llamada a una función en un fragmento, se ejecuta de inmediato.
    • Los resultados se envían de vuelta al LLM a través de otra llamada de transmisión.
    • La respuesta del LLM a estos resultados también se procesa de forma continua.
  5. Manejo de errores y limpieza:
    • try/catch proporciona un manejo de errores sólido
    • El bloque finally garantiza que el estado de la conversación se restablezca correctamente
    • El mensaje siempre se finaliza, incluso si se producen errores.

Esta implementación crea una experiencia de transmisión confiable y responsiva, a la vez que mantiene el estado de conversación adecuado.

Actualiza la pantalla principal para conectar el estado de la conversación

Modifica tu archivo lib/main.dart para pasar el estado de la conversación a la pantalla principal:

lib/main.dart

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

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

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

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

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

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

El cambio clave aquí es pasar el conversationState al widget MainScreen. El MainScreen (proporcionado por el paquete colorist_ui) usará este estado para inhabilitar la entrada de texto mientras se procesa una respuesta.

Esto crea una experiencia del usuario coherente en la que la IU refleja el estado actual de la conversación.

Genera código de Riverpod

Ejecuta el comando del ejecutor de compilación para generar el código de Riverpod necesario:

dart run build_runner build --delete-conflicting-outputs

Ejecuta y prueba respuestas de transmisión

Ejecuta tu aplicación:

flutter run -d DEVICE

Captura de pantalla de la app de Colorist en la que se muestra el LLM de Gemini respondiendo de forma continua

Ahora, prueba el comportamiento de transmisión con varias descripciones de color. Prueba con descripciones como las siguientes:

  • "Muéstrame el color azul verdoso oscuro del océano al atardecer".
  • "Me gustaría ver un coral vibrante que me recuerde a las flores tropicales".
  • "Crea un verde oliva apagado, como el de los uniformes militares antiguos".

El flujo técnico de la transmisión en detalle

Veamos exactamente qué sucede cuando se transmite una respuesta:

Establecimiento de la conexión

Cuando llamas a sendMessageStream(), sucede lo siguiente:

  1. La app establece una conexión con el servicio de Firebase AI Logic
  2. La solicitud del usuario se envía al servicio
  3. El servidor comienza a procesar la solicitud.
  4. La conexión de transmisión permanece abierta y lista para transmitir fragmentos.

Transmisión de fragmentos

A medida que Gemini genera contenido, los fragmentos se envían a través de la transmisión:

  1. El servidor envía fragmentos de texto a medida que se generan (por lo general, algunas palabras u oraciones).
  2. Cuando Gemini decide hacer una llamada a función, envía la información de la llamada a función
  3. Es posible que sigan fragmentos de texto adicionales a las llamadas a funciones
  4. La transmisión continúa hasta que se completa la generación.

Procesamiento progresivo

Tu app procesa cada fragmento de forma incremental:

  1. Cada fragmento de texto se agrega a la respuesta existente.
  2. Las llamadas a funciones se ejecutan en cuanto se detectan.
  3. La IU se actualiza en tiempo real con los resultados de texto y de funciones.
  4. Se hace un seguimiento del estado para mostrar que la respuesta aún se está transmitiendo.

Finalización de la transmisión

Cuando se complete la generación, ocurrirá lo siguiente:

  1. El servidor cierra la transmisión
  2. Tu bucle de await for sale de forma natural
  3. El mensaje se marca como completado
  4. El estado de la conversación vuelve a inactivo
  5. La IU se actualiza para reflejar el estado completado.

Comparación entre la transmisión y la no transmisión

Para comprender mejor los beneficios de la transmisión, comparemos los enfoques de transmisión y los que no son de transmisión:

Aspecto

Sin transmisión

Transmisión

Latencia percibida

El usuario no ve nada hasta que la respuesta completa está lista

El usuario ve las primeras palabras en milisegundos

Experiencia del usuario

Larga espera seguida de la aparición repentina del texto

Aparición de texto natural y progresiva

Administración de estados

Más simple (los mensajes están pendientes o completos)

Más complejo (los mensajes pueden estar en un estado de transmisión)

Ejecución de la función

Ocurre solo después de una respuesta completa

Se produce durante la generación de respuestas.

Complejidad de la implementación

Implementación más sencilla

Requiere administración de estados adicional

Recuperación de errores

Respuesta del tipo todo o nada

Las respuestas parciales aún pueden ser útiles

Complejidad del código

Menos complejo

Más complejo debido al control de transmisiones

En el caso de una aplicación como Colorist, los beneficios de UX de la transmisión superan la complejidad de la implementación, en especial para las interpretaciones de color que pueden tardar varios segundos en generarse.

Prácticas recomendadas para la UX de transmisión

Cuando implementes la transmisión en tus propias aplicaciones de LLM, ten en cuenta estas prácticas recomendadas:

  1. Indicadores visuales claros: Siempre proporciona indicadores visuales claros que distingan los mensajes de transmisión de los mensajes completos.
  2. Bloqueo de entrada: Inhabilita la entrada del usuario durante la transmisión para evitar varias solicitudes superpuestas.
  3. Recuperación de errores: Diseña tu IU para que se recupere correctamente si se interrumpe la transmisión.
  4. Transiciones de estado: Asegúrate de que las transiciones entre los estados de inactividad, transmisión y completado sean fluidas.
  5. Visualización del progreso: Considera usar animaciones o indicadores sutiles que muestren el procesamiento activo.
  6. Opciones de cancelación: En una app completa, proporciona formas para que los usuarios cancelen las generaciones en curso.
  7. Integración de resultados de funciones: Diseña tu IU para controlar los resultados de funciones que aparecen a mitad de la transmisión.
  8. Optimización del rendimiento: Se minimizan las recompilaciones de la IU durante las actualizaciones rápidas de la transmisión.

El paquete colorist_ui implementa muchas de estas prácticas recomendadas por ti, pero son consideraciones importantes para cualquier implementación de LLM de transmisión.

Próximos pasos

En el siguiente paso, implementarás la sincronización del LLM notificándole a Gemini cuando los usuarios seleccionen colores del historial. Esto creará una experiencia más cohesiva en la que el LLM tendrá en cuenta los cambios iniciados por el usuario en el estado de la aplicación.

Solución de problemas

Problemas de procesamiento de transmisiones

Si tienes problemas con el procesamiento de transmisión, haz lo siguiente:

  • Síntomas: Respuestas parciales, falta de texto o finalización abrupta de la transmisión
  • Solución: Comprueba la conectividad de red y asegúrate de que los patrones async/await sean correctos en tu código.
  • Diagnóstico: Examina el panel de registros en busca de mensajes de error o advertencias relacionados con el procesamiento de transmisiones.
  • Corrección: Asegúrate de que todo el procesamiento de transmisiones use el control de errores adecuado con bloques try/catch

Faltan llamadas a la función

Si no se detectan las llamadas a funciones en la transmisión, haz lo siguiente:

  • Síntomas: Aparece texto, pero los colores no se actualizan, o el registro no muestra llamadas a funciones
  • Solución: Verifica las instrucciones de la instrucción del sistema sobre el uso de llamadas a funciones
  • Diagnóstico: Verifica el panel de registros para ver si se reciben llamadas a funciones.
  • Corrección: Ajusta la instrucción del sistema para indicarle de forma más explícita al LLM que use la herramienta set_color.

Manejo general de errores

Si tienes algún otro problema, haz lo siguiente:

  • Paso 1: Comprueba si hay mensajes de error en el panel de registro
  • Paso 2: Verifica la conectividad de Firebase AI Logic
  • Paso 3: Asegúrate de que todo el código generado por Riverpod esté actualizado
  • Paso 4: Revisa la implementación de transmisión para detectar cualquier instrucción await faltante

Conceptos clave aprendidos

  • Implementa respuestas de transmisión con la API de Gemini para una UX más responsiva
  • Administrar el estado de la conversación para controlar las interacciones de transmisión de forma adecuada
  • Procesar texto en tiempo real y llamadas a funciones a medida que llegan
  • Creación de IU responsivas que se actualizan de forma incremental durante la transmisión
  • Cómo controlar transmisiones simultáneas con patrones asíncronos adecuados
  • Proporcionar comentarios visuales adecuados durante las respuestas de transmisión

Con la implementación de la transmisión, mejoraste significativamente la experiencia del usuario de tu app de Colorist, ya que creaste una interfaz más responsiva y atractiva que se siente verdaderamente conversacional.

8. Sincronización del contexto del LLM

En este paso adicional, implementarás la sincronización del contexto del LLM notificándole a Gemini cuando los usuarios seleccionen colores del historial. Esto crea una experiencia más cohesiva en la que el LLM conoce las acciones del usuario en la interfaz, no solo sus mensajes explícitos.

Qué se abordará en este paso

  • Cómo crear la sincronización del contexto del LLM entre tu IU y el LLM
  • Serialización de eventos de la IU en un contexto que el LLM puede comprender
  • Actualización del contexto de la conversación en función de las acciones del usuario
  • Crear una experiencia coherente en diferentes métodos de interacción
  • Mejoramos la capacidad de los LLM para comprender el contexto más allá de los mensajes de chat explícitos

Información sobre la sincronización del contexto de LLM

Los chatbots tradicionales solo responden a los mensajes explícitos de los usuarios, lo que genera una desconexión cuando los usuarios interactúan con la app por otros medios. La sincronización del contexto del LLM aborda esta limitación de la siguiente manera:

Por qué es importante la sincronización del contexto del LLM

Cuando los usuarios interactúan con tu app a través de elementos de la IU (como seleccionar un color del historial), el LLM no tiene forma de saber qué sucedió, a menos que se lo indiques de forma explícita. Sincronización del contexto del LLM:

  1. Mantiene el contexto: Mantiene al LLM informado sobre todas las acciones relevantes del usuario
  2. Crea coherencia: Produce una experiencia cohesiva en la que el LLM reconoce las interacciones de la IU.
  3. Mejora la inteligencia: Permite que el LLM responda de manera adecuada a todas las acciones del usuario
  4. Mejora la experiencia del usuario: Hace que toda la aplicación se sienta más integrada y con mayor capacidad de respuesta.
  5. Reduce el esfuerzo del usuario: Elimina la necesidad de que los usuarios expliquen manualmente sus acciones en la IU.

En tu app de Colorist, cuando un usuario selecciona un color del historial, quieres que Gemini reconozca esta acción y comente de forma inteligente sobre el color seleccionado, manteniendo la ilusión de un asistente fluido y consciente.

Actualiza el servicio de chat de Gemini para las notificaciones de selección de color

Primero, agregarás un método a GeminiChatService para notificar al LLM cuando un usuario seleccione un color del historial. Actualiza el archivo 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/legacy.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

final conversationStateProvider = StateProvider(
  (ref) => ConversationState.idle,
);

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

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

  Future<void> sendMessage(String message) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final conversationState = ref.read(conversationStateProvider);
    final chatStateNotifier = ref.read(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.state = ConversationState.busy;
    chatStateNotifier.addUserMessage(message);
    logStateNotifier.logUserText(message);
    final llmMessage = chatStateNotifier.createLlmMessage();
    try {
      final responseStream = chatSession.sendMessageStream(
        Content.text(message),
      );
      await for (final block in responseStream) {
        await _processBlock(block, llmMessage.id);
      }
    } catch (e, st) {
      logStateNotifier.logError(e, st: st);
      chatStateNotifier.appendToMessage(
        llmMessage.id,
        "\nI'm sorry, I encountered an error processing your request. "
        "Please try again.",
      );
    } finally {
      chatStateNotifier.finalizeMessage(llmMessage.id);
      conversationStateNotifier.state = ConversationState.idle;
    }
  }

  Future<void> _processBlock(
    GenerateContentResponse block,
    String llmMessageId,
  ) async {
    final chatSession = await ref.read(chatSessionProvider.future);
    final chatStateNotifier = ref.read(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
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);

La principal incorporación es el método notifyColorSelection, que hace lo siguiente:

  1. Toma un objeto ColorData que representa el color seleccionado.
  2. Codifica el objeto en un formato JSON que se puede incluir en un mensaje.
  3. Envía un mensaje con un formato especial al LLM que indica una selección del usuario.
  4. Reutiliza el método sendMessage existente para controlar la notificación

Este enfoque evita la duplicación, ya que utiliza tu infraestructura existente de procesamiento de mensajes.

Actualiza la app principal para conectar las notificaciones de selección de color

Ahora, modifica tu archivo lib/main.dart para pasar la función de notificación de selección de color a la pantalla principal:

lib/main.dart

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

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

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

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

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

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

El cambio clave es agregar la devolución de llamada notifyColorSelection, que conecta el evento de la IU (seleccionar un color del historial) al sistema de notificaciones del LLM.

Actualiza la instrucción del sistema

Ahora, debes actualizar la instrucción del sistema para indicarle al LLM cómo responder a las notificaciones de selección de color. Modifica tu archivo 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

La principal incorporación es la sección "When Users Select Historical Colors", que hace lo siguiente:

  1. Explica el concepto de notificaciones de selección del historial al LLM
  2. Proporciona un ejemplo de cómo se ven estas notificaciones
  3. Muestra un ejemplo de una respuesta adecuada.
  4. Establece expectativas para confirmar la selección y comentar el color.

Esto ayuda al LLM a comprender cómo responder de manera adecuada a estos mensajes especiales.

Genera código de Riverpod

Ejecuta el comando del ejecutor de compilación para generar el código de Riverpod necesario:

dart run build_runner build --delete-conflicting-outputs

Ejecuta y prueba la sincronización del contexto del LLM

Ejecuta tu aplicación:

flutter run -d DEVICE

Captura de pantalla de la app de Colorist en la que se muestra el LLM de Gemini respondiendo a una selección del historial de colores

Probar la sincronización del contexto del LLM implica lo siguiente:

  1. Primero, genera algunos colores describiéndolos en el chat.
    • "Muéstrame un color morado vibrante".
    • "Me gustaría un verde bosque".
    • "Dame un rojo brillante".
  2. Luego, haz clic en una de las miniaturas de color de la tira del historial.

Deberías observar lo siguiente:

  1. El color seleccionado aparece en la pantalla principal.
  2. Aparece un mensaje del usuario en el chat que indica la selección de color.
  3. El LLM responde confirmando la selección y comentando el color.
  4. Toda la interacción se siente natural y coherente.

Esto crea una experiencia fluida en la que el LLM conoce los mensajes directos y las interacciones de la IU, y responde de manera adecuada a ambos.

Cómo funciona la sincronización del contexto del LLM

Exploremos los detalles técnicos de cómo funciona esta sincronización:

Flujo de datos

  1. Acción del usuario: El usuario hace clic en un color de la tira del historial.
  2. Evento de IU: El widget MainScreen detecta esta selección.
  3. Ejecución de la devolución de llamada: Se activa la devolución de llamada notifyColorSelection.
  4. Creación del mensaje: Se crea un mensaje con formato especial con los datos de color.
  5. Procesamiento del LLM: El mensaje se envía a Gemini, que reconoce el formato.
  6. Respuesta contextual: Gemini responde de forma adecuada según la instrucción del sistema.
  7. Actualización de la IU: La respuesta aparece en el chat, lo que crea una experiencia cohesiva.

Serialización de datos

Un aspecto clave de este enfoque es cómo serializas los datos de color:

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

El método toLLMContextMap() (proporcionado por el paquete colorist_ui) convierte un objeto ColorData en un mapa con propiedades clave que el LLM puede comprender. Por lo general, esto incluye lo siguiente:

  • Valores RGB (rojo, verde y azul)
  • Representación del código hexadecimal
  • Cualquier nombre o descripción asociado con el color

Si le das formato a estos datos de manera coherente y los incluyes en el mensaje, te aseguras de que el LLM tenga toda la información que necesita para responder de manera adecuada.

Aplicaciones más amplias de la sincronización del contexto de LLM

Este patrón de notificar al LLM sobre los eventos de la IU tiene numerosas aplicaciones más allá de la selección de color:

Otros casos prácticos

  1. Cambios en el filtro: Notifica al LLM cuando los usuarios aplican filtros a los datos.
  2. Eventos de navegación: Informan al LLM cuando los usuarios navegan a diferentes secciones.
  3. Cambios en la selección: Actualiza el LLM cuando los usuarios seleccionan elementos de listas o cuadrículas.
  4. Actualizaciones de preferencias: Indica al LLM cuándo los usuarios cambian la configuración o las preferencias.
  5. Manipulación de datos: Notifica al LLM cuando los usuarios agregan, editan o borran datos.

En cada caso, el patrón sigue siendo el mismo:

  1. Detecta el evento de la IU
  2. Serializa los datos pertinentes
  3. Envía una notificación con un formato especial al LLM
  4. Guía al LLM para que responda de manera adecuada a través de la instrucción del sistema

Prácticas recomendadas para la sincronización del contexto de LLM

Según tu implementación, estas son algunas prácticas recomendadas para lograr una sincronización de contexto eficaz del LLM:

1. Formato coherente

Usa un formato coherente para las notificaciones, de modo que el LLM pueda identificarlas fácilmente:

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

2. Contexto enriquecido

Incluye suficientes detalles en las notificaciones para que el LLM responda de forma inteligente. En el caso de los colores, esto significa valores RGB, códigos hexadecimales y cualquier otra propiedad pertinente.

3. Instrucciones claras

Proporciona instrucciones explícitas en la instrucción del sistema sobre cómo controlar las notificaciones, idealmente con ejemplos.

4. Integración natural

Diseña las notificaciones para que fluyan de forma natural en la conversación, no como interrupciones técnicas.

5. Notificación selectiva

Solo notifica al LLM sobre las acciones que son relevantes para la conversación. No es necesario comunicar todos los eventos de la IU.

Solución de problemas

Problemas con notificaciones

Si el LLM no responde correctamente a las selecciones de color, haz lo siguiente:

  • Verifica que el formato del mensaje de notificación coincida con lo que se describe en la instrucción del sistema.
  • Verifica que los datos de color se serialicen correctamente
  • Asegúrate de que la instrucción del sistema tenga instrucciones claras para controlar las selecciones.
  • Busca errores en el servicio de chat cuando se envían notificaciones

Administración del contexto

Si el LLM parece perder el contexto, haz lo siguiente:

  • Verifica que la sesión de chat se mantenga correctamente
  • Verifica que los estados de la conversación cambien correctamente
  • Asegúrate de que las notificaciones se envíen a través de la misma sesión de chat.

Problemas generales

Para problemas generales:

  • Examina los registros en busca de errores o advertencias
  • Verifica la conectividad de Firebase AI Logic
  • Verifica si hay discrepancias de tipos en los parámetros de las funciones
  • Asegúrate de que todo el código generado por Riverpod esté actualizado

Conceptos clave aprendidos

  • Cómo crear la sincronización del contexto del LLM entre la IU y el LLM
  • Serialización de eventos de la IU en un contexto apto para LLM
  • Cómo guiar el comportamiento del LLM para diferentes patrones de interacción
  • Crear una experiencia coherente en las interacciones de mensajes y las que no son de mensajes
  • Mejora el conocimiento del LLM sobre el estado general de la aplicación

Con la implementación de la sincronización del contexto del LLM, creaste una experiencia verdaderamente integrada en la que el LLM se siente como un asistente consciente y responsivo, en lugar de solo un generador de texto. Este patrón se puede aplicar a muchas otras aplicaciones para crear interfaces más naturales e intuitivas potenciadas por IA.

9. ¡Felicitaciones!

Completaste el codelab de Colorist correctamente. 🎉

Qué compilaste

Creaste una aplicación de Flutter completamente funcional que integra la API de Gemini de Google para interpretar descripciones de colores en lenguaje natural. Ahora, tu app puede hacer lo siguiente:

  • Procesar descripciones en lenguaje natural, como "naranja atardecer" o "azul océano profundo"
  • Usar Gemini para traducir de forma inteligente estas descripciones en valores RGB
  • Mostrar los colores interpretados en tiempo real con respuestas de transmisión
  • Controla las interacciones del usuario a través de elementos de chat y de la IU
  • Mantener la conciencia contextual en diferentes métodos de interacción

Lo que vendrá

Ahora que dominas los conceptos básicos de la integración de Gemini con Flutter, te mostramos algunas formas de continuar tu recorrido:

Mejora tu app de Colorist

  • Paletas de colores: Agrega la función para generar esquemas de colores complementarios o coincidentes.
  • Entrada de voz: Integra el reconocimiento de voz para descripciones verbales de colores
  • Administración del historial: Se agregaron opciones para nombrar, organizar y exportar conjuntos de colores.
  • Instrucciones personalizadas: Crea una interfaz para que los usuarios personalicen las instrucciones del sistema.
  • Estadísticas avanzadas: Realiza un seguimiento de las descripciones que funcionan mejor o causan dificultades.

Explora más funciones de Gemini

  • Entradas multimodales: Agrega entradas de imágenes para extraer colores de las fotos
  • Generación de contenido: Usa Gemini para generar contenido relacionado con el color, como descripciones o historias.
  • Mejoras en las llamadas a funciones: Crea integraciones de herramientas más complejas con varias funciones
  • Parámetros de configuración de seguridad: Explora los diferentes parámetros de configuración de seguridad y su impacto en las respuestas.

Aplicar estos patrones a otros dominios

  • Análisis de documentos: Crea apps que puedan comprender y analizar documentos
  • Asistencia para la escritura creativa: Crea herramientas de escritura con sugerencias potenciadas por LLMs
  • Automatización de tareas: Diseña apps que traduzcan el lenguaje natural en tareas automatizadas.
  • Aplicaciones basadas en el conocimiento: Crea sistemas expertos en dominios específicos

Recursos

Estos son algunos recursos valiosos para continuar tu aprendizaje:

Documentación oficial

Curso y guía de instrucciones

Comunidad

Serie de Observable Flutter Agentic

En el episodio núm. 59, Craig Labenz y Andrew Brogden exploran este codelab y destacan las partes interesantes de la compilación de la app.

En el episodio núm. 60, vuelve a acompañar a Craig y Andrew mientras extienden la app del codelab con nuevas capacidades y luchan para que los LLMs hagan lo que se les indica.

En el episodio núm. 61, Craig se une a Chris Sells para analizar los titulares de noticias desde una perspectiva nueva y generar imágenes correspondientes.

Comentarios

Nos encantaría conocer tu experiencia con este codelab. Considera enviar tus comentarios a través de los siguientes medios:

Gracias por completar este codelab. Esperamos que sigas explorando las emocionantes posibilidades en la intersección de Flutter y la IA.