Создайте приложение Flutter на базе Gemini

Создайте приложение Flutter на базе Gemini

О практической работе

subjectПоследнее обновление: мая 19, 2025
account_circleАвторы: Brett Morgan

1. Создайте приложение Flutter на базе Gemini

Что вы построите

В этой лабораторной работе вы создадите Colorist — интерактивное приложение Flutter, которое переносит мощь API Gemini прямо в ваше приложение Flutter. Вы когда-нибудь хотели позволить пользователям управлять вашим приложением с помощью естественного языка, но не знали, с чего начать? Эта лабораторная работа покажет вам, как это сделать.

Colorist позволяет пользователям описывать цвета на естественном языке (например, «оранжевый цвет заката» или «глубокий синий океан»), а приложение:

  • Обрабатывает эти описания с помощью API Gemini от Google.
  • Интерпретирует описания в точные значения цветов RGB.
  • Отображает цвет на экране в режиме реального времени
  • Предоставляет технические сведения о цвете и интересный контекст о цвете.
  • Сохраняет историю недавно созданных цветов.

Скриншот приложения Colorist, демонстрирующий цветной дисплей и интерфейс чата

Приложение имеет интерфейс с разделенным экраном с областью цветного дисплея и интерактивной системой чата с одной стороны, и подробную панель журнала, показывающую необработанные взаимодействия LLM с другой стороны. Этот журнал позволяет вам лучше понять, как на самом деле работает интеграция LLM под капотом.

Почему это важно для разработчиков Flutter

LLM революционизируют взаимодействие пользователей с приложениями, но эффективная интеграция их в мобильные и настольные приложения представляет собой уникальные проблемы. Эта кодовая лаборатория научит вас практическим шаблонам, которые выходят за рамки простых вызовов API.

Ваш путь обучения

Эта лабораторная работа шаг за шагом проведет вас через процесс создания Colorist:

  1. Настройка проекта . Вы начнете с базовой структуры приложения Flutter и пакета colorist_ui .
  2. Базовая интеграция Gemini — подключите свое приложение к Vertex AI в Firebase и реализуйте простую коммуникацию LLM
  3. Эффективные подсказки — создайте системные подсказки, которые помогут LLM понять описания цветов.
  4. Объявления функций — определение инструментов, которые LLM может использовать для установки цветов в вашем приложении.
  5. Обработка инструментов — обработка вызовов функций из LLM и подключение их к состоянию вашего приложения.
  6. Потоковые ответы — Улучшите пользовательский опыт с помощью потоковых ответов LLM в режиме реального времени.
  7. Синхронизация контекста LLM — создание целостного опыта путем информирования LLM о действиях пользователя.

Чему вы научитесь

  • Настройка Vertex AI в Firebase для приложений Flutter
  • Разрабатывайте эффективные системные подсказки для управления поведением LLM
  • Реализуйте объявления функций , которые связывают естественный язык и функции приложения.
  • Обработка потоковых ответов для обеспечения отзывчивого пользовательского опыта
  • Синхронизировать состояние между событиями пользовательского интерфейса и LLM
  • Управление состоянием разговора LLM с помощью Riverpod
  • Изящная обработка ошибок в приложениях на базе LLM

Предварительный просмотр кода: пример того, что вы будете реализовывать

Вот краткий обзор объявления функции, которую вы создадите, чтобы позволить LLM устанавливать цвета в вашем приложении:

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

Видеообзор этой кодовой лаборатории

Посмотрите, как Крейг Лабенц и Эндрю Брогдон обсуждают эту лабораторную работу в выпуске Observable Flutter #59:

Предпосылки

Чтобы извлечь максимальную пользу из этой лабораторной работы, вам необходимо:

  • Опыт разработки Flutter — знакомство с основами Flutter и синтаксисом Dart
  • Знание асинхронного программирования — понимание Futures, async/await и потоков
  • Учетная запись Firebase . Для настройки Firebase вам понадобится учетная запись Google.
  • Проект Firebase с включенным выставлением счетов — Vertex AI в Firebase требует наличия учетной записи для выставления счетов

Давайте начнем создавать ваше первое приложение Flutter на базе LLM!

2. Настройка проекта и эхо-сервис

На этом первом этапе вы настроите структуру проекта и реализуете простую службу эха, которая позже будет заменена интеграцией API Gemini. Это устанавливает архитектуру приложения и гарантирует, что ваш пользовательский интерфейс работает правильно, прежде чем добавлять сложность вызовов LLM.

Чему вы научитесь на этом этапе

  • Настройка проекта Flutter с необходимыми зависимостями
  • Работа с пакетом colorist_ui для компонентов пользовательского интерфейса
  • Реализация службы эхо-сообщений и подключение ее к пользовательскому интерфейсу

Важное замечание по ценообразованию

Создать новый проект Flutter

Начните с создания нового проекта Flutter с помощью следующей команды:

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

Флаг -e указывает, что вам нужен пустой проект без приложения- counter по умолчанию. Приложение предназначено для работы на настольных компьютерах, мобильных устройствах и в Интернете. Однако в настоящее время flutterfire не поддерживает Linux.

Добавить зависимости

Перейдите в каталог вашего проекта и добавьте необходимые зависимости:

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

Это добавит следующие ключевые пакеты:

  • colorist_ui : Пользовательский пакет, предоставляющий компоненты пользовательского интерфейса для приложения Colorist.
  • flutter_riverpod и riverpod_annotation : Для управления состоянием
  • logging : для структурированного ведения журнала
  • Зависимости разработки для генерации кода и линтинга

Ваш pubspec.yaml будет выглядеть примерно так:

pubspec.yaml

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

environment:
  sdk: ^3.8.0

dependencies:
  flutter:
    sdk: flutter
  colorist_ui: ^0.2.3
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  build_runner: ^2.4.15
  riverpod_generator: ^2.6.5
  riverpod_lint: ^2.6.5
  json_serializable: ^6.9.5
  custom_lint: ^0.7.5

flutter:
  uses-material-design: true

Настройте параметры анализа

Добавьте custom_lint в файл analysis_options.yaml в корне вашего проекта:

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

Эта конфигурация позволяет использовать специфичные для Riverpod линты для поддержания качества кода.

Реализовать файл main.dart

Замените содержимое lib/main.dart следующим:

lib/main.dart

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

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

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

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

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

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

Это настраивает приложение Flutter, реализующее простую службу эха, которая имитирует поведение LLM, просто возвращая сообщение пользователя.

Понимание архитектуры

Давайте уделим минуту изучению архитектуры приложения colorist :

Пакет colorist_ui

Пакет colorist_ui предоставляет готовые компоненты пользовательского интерфейса и инструменты управления состоянием:

  1. MainScreen : основной компонент пользовательского интерфейса, отображающий:
    • Разделенный экран на рабочем столе (область взаимодействия и панель журнала)
    • Интерфейс с вкладками на мобильном устройстве
    • Цветной дисплей, интерфейс чата и миниатюры истории
  2. Управление состоянием : приложение использует несколько уведомлений о состоянии:
    • ChatStateNotifier : управляет сообщениями чата.
    • ColorStateNotifier : управляет текущим цветом и историей
    • LogStateNotifier : управляет записями журнала для отладки.
  3. Обработка сообщений : приложение использует модель сообщений с различными состояниями:
    • Сообщения пользователя : Введено пользователем
    • Сообщения LLM : генерируются LLM (или вашей службой эха на данный момент)
    • MessageState : отслеживает, завершены ли сообщения LLM или все еще передаются

Архитектура приложения

Приложение имеет следующую архитектуру:

  1. Слой пользовательского интерфейса : Предоставляется пакетом colorist_ui
  2. Управление состоянием : использует Riverpod для реактивного управления состоянием.
  3. Уровень обслуживания : в настоящее время содержит простую службу эха, которая будет заменена службой чата Gemini.
  4. Интеграция LLM : будет добавлена ​​на последующих этапах

Такое разделение позволяет вам сосредоточиться на реализации интеграции LLM, в то время как компоненты пользовательского интерфейса уже реализованы.

Запустите приложение

Запустите приложение с помощью следующей команды:

flutter run -d DEVICE

Замените DEVICE на целевое устройство, например macos , windows , chrome или идентификатор устройства.

Скриншот приложения Colorist, демонстрирующий работу службы Echo, отображающей разметку

Теперь вы должны увидеть приложение Colorist с:

  1. Цветная область отображения с цветом по умолчанию
  2. Интерфейс чата, в котором вы можете печатать сообщения
  3. Панель журнала, отображающая взаимодействие в чате

Попробуйте ввести сообщение типа «Мне нужен глубокий синий цвет» и нажмите «Отправить». Служба эха просто повторит ваше сообщение. На последующих этапах вы замените это фактической интерпретацией цвета с помощью API Gemini через Vertex AI в Firebase.

Что дальше?

На следующем этапе вы настроите Firebase и реализуете базовую интеграцию Gemini API, чтобы заменить ваш эхо-сервис на чат-сервис Gemini. Это позволит приложению интерпретировать цветовые описания и предоставлять интеллектуальные ответы.

Поиск неисправностей

Проблемы с пакетом пользовательского интерфейса

Если у вас возникли проблемы с пакетом colorist_ui :

  • Убедитесь, что вы используете последнюю версию.
  • Убедитесь, что вы правильно добавили зависимость.
  • Проверьте наличие конфликтующих версий пакетов.

Ошибки сборки

Если вы видите ошибки сборки:

  • Убедитесь, что у вас установлена ​​последняя стабильная версия Flutter SDK.
  • Запустите flutter clean а затем flutter pub get
  • Проверьте вывод консоли на наличие конкретных сообщений об ошибках.

Ключевые изученные концепции

  • Настройка проекта Flutter с необходимыми зависимостями
  • Понимание архитектуры приложения и обязанностей компонентов
  • Реализация простого сервиса, имитирующего поведение LLM
  • Подключение сервиса к компонентам пользовательского интерфейса
  • Использование Riverpod для управления состоянием

3. Базовая интеграция чата Gemini

На этом этапе вы замените службу echo из предыдущего шага на интеграцию Gemini API с использованием Vertex AI в Firebase. Вы настроите Firebase, установите необходимых поставщиков и реализуете базовую службу чата, которая взаимодействует с Gemini API.

Чему вы научитесь на этом этапе

  • Настройка Firebase в приложении Flutter
  • Настройка Vertex AI в Firebase для доступа Gemini
  • Создание поставщиков Riverpod для сервисов Firebase и Gemini
  • Реализация базового чат-сервиса с использованием API Gemini
  • Обработка асинхронных ответов API и состояний ошибок

Настройте Firebase

Сначала вам нужно настроить Firebase для вашего проекта Flutter. Это включает в себя создание проекта Firebase, добавление в него вашего приложения и настройку необходимых параметров Vertex AI.

Создать проект Firebase

  1. Перейдите в консоль Firebase и войдите в свою учетную запись Google.
  2. Нажмите «Создать проект Firebase» или выберите существующий проект.
  3. Следуйте указаниям мастера настройки, чтобы создать свой проект.
  4. После создания проекта вам нужно будет перейти на план Blaze (оплата по мере использования) для доступа к сервисам Vertex AI. Нажмите кнопку Upgrade в левом нижнем углу консоли Firebase.

Настройте Vertex AI в своем проекте Firebase

  1. В консоли Firebase перейдите к своему проекту.
  2. На левой боковой панели выберите AI .
  3. На карточке Vertex AI в Firebase выберите Get Started .
  4. Следуйте инструкциям, чтобы включить Vertex AI в API Firebase для вашего проекта.

Установите FlutterFire CLI

Интерфейс командной строки FlutterFire упрощает настройку Firebase в приложениях Flutter:

dart pub global activate flutterfire_cli

Добавьте Firebase в свое приложение Flutter

  1. Добавьте в свой проект пакеты Firebase core и Vertex AI:
flutter pub add firebase_core firebase_vertexai
  1. Запустите команду конфигурации FlutterFire:
flutterfire configure

Эта команда:

  • Попросите вас выбрать проект Firebase, который вы только что создали.
  • Зарегистрируйте свои приложения Flutter в Firebase
  • Создайте файл firebase_options.dart с конфигурацией вашего проекта.

Команда автоматически определит выбранные вами платформы (iOS, Android, macOS, Windows, веб) и настроит их соответствующим образом.

Конфигурация, зависящая от платформы

Firebase требует минимальных версий выше, чем те, что по умолчанию для Flutter. Также требуется сетевой доступ для взаимодействия с Vertex AI на серверах Firebase.

Настройте разрешения macOS

Для macOS вам необходимо включить сетевой доступ в правах вашего приложения:

  1. Откройте macos/Runner/DebugProfile.entitlements и добавьте:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. Также откройте macos/Runner/Release.entitlements и добавьте ту же запись.
  2. Обновите минимальную версию macOS в верхней части macos/Podfile :

macos/Podfile

# Firebase requires at least macOS 10.15
platform
:osx, '10.15'

Настройте разрешения iOS

Для iOS обновите минимальную версию в верхней части ios/Podfile :

ios/Подфайл

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

Настройте параметры Android

Для Android обновите android/app/build.gradle.kts :

android/app/build.gradle.kts

android {
   
// ...
    ndkVersion
= "27.0.12077973"

    defaultConfig
{
       
// ...
        minSdk
= 23
       
// ...
   
}
}

Создайте поставщиков моделей Gemini

Теперь вы создадите поставщиков Riverpod для Firebase и Gemini. Создайте новый файл lib/providers/gemini.dart :

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

 
final model = FirebaseVertexAI.instance.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();
}

Этот файл определяет основу для трех ключевых поставщиков. Эти поставщики генерируются при запуске dart run build_runner генераторами кода Riverpod.

  1. firebaseAppProvider : инициализирует Firebase с конфигурацией вашего проекта.
  2. geminiModelProvider : создает экземпляр генеративной модели Gemini.
  3. chatSessionProvider : создает и поддерживает сеанс чата с моделью Gemini.

Аннотация keepAlive: true в сеансе чата гарантирует его сохранение на протяжении всего жизненного цикла приложения, поддерживая контекст разговора.

Внедрить чат-сервис Gemini

Создайте новый файл 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

Эта услуга:

  1. Принимает сообщения пользователей и отправляет их в API Gemini
  2. Обновляет интерфейс чата ответами модели
  3. Регистрирует все коммуникации для простоты понимания реального потока LLM
  4. Обрабатывает ошибки с соответствующей обратной связью с пользователем.

Примечание: Окно журнала на этом этапе будет выглядеть почти идентично окну чата. Журнал станет интереснее, как только вы введете вызовы функций, а затем потоковые ответы.

Сгенерировать код Riverpod

Запустите команду build runner, чтобы сгенерировать необходимый код Riverpod:

dart run build_runner build --delete-conflicting-outputs

Это создаст файлы .g.dart , необходимые для работы Riverpod.

Обновите файл main.dart

Обновите файл lib/main.dart , чтобы использовать новую службу чата 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),
     
),
   
);
 
}
}

Ключевые изменения в этом обновлении:

  1. Замена службы эха на службу чата на базе API Gemini
  2. Добавление экранов загрузки и ошибок с использованием шаблона Riverpod AsyncValue с методом when
  3. Подключение пользовательского интерфейса к новому сервису чата через обратный вызов sendMessage

Запустите приложение

Запустите приложение с помощью следующей команды:

flutter run -d DEVICE

Замените DEVICE на целевое устройство, например macos , windows , chrome или идентификатор устройства.

Скриншот приложения Colorist, на котором показан ответ Gemini LLM на запрос солнечно-желтого цвета

Теперь, когда вы печатаете сообщение, оно будет отправлено в API Gemini, и вы получите ответ от LLM, а не эхо. Панель журнала покажет взаимодействие с API.

Понимание коммуникации LLM

Давайте на минутку разберемся, что происходит при взаимодействии с API Gemini:

Поток коммуникации

  1. Пользовательский ввод : пользователь вводит текст в интерфейс чата.
  2. Форматирование запроса : приложение форматирует текст как объект Content для API Gemini.
  3. Связь через API : текст отправляется в API Gemini через Vertex AI в Firebase.
  4. Обработка LLM : модель Gemini обрабатывает текст и генерирует ответ.
  5. Обработка ответа : приложение получает ответ и обновляет пользовательский интерфейс.
  6. Ведение журнала : все сообщения регистрируются для обеспечения прозрачности.

Сеансы чата и контекст разговора

Сеанс чата Gemini сохраняет контекст между сообщениями, позволяя вести диалоговое взаимодействие. Это означает, что LLM «запоминает» предыдущие обмены в текущем сеансе, что позволяет вести более связные разговоры.

Аннотация keepAlive: true на вашем провайдере сеанса чата гарантирует, что этот контекст сохраняется на протяжении всего жизненного цикла приложения. Этот постоянный контекст имеет решающее значение для поддержания естественного потока разговора с LLM.

Что дальше?

На этом этапе вы можете задавать Gemini API любые вопросы, поскольку нет никаких ограничений на то, на что он будет отвечать. Например, вы можете попросить его дать краткое содержание Войны Алой и Белой розы, что не связано с целью вашего цветового приложения.

На следующем этапе вы создадите системную подсказку, которая поможет Gemini более эффективно интерпретировать описания цветов. Это продемонстрирует, как настроить поведение LLM для нужд конкретного приложения и сосредоточить его возможности на домене вашего приложения.

Поиск неисправностей

Проблемы с конфигурацией Firebase

Если у вас возникли ошибки при инициализации Firebase:

  • Убедитесь, что ваш файл firebase_options.dart был правильно сгенерирован.
  • Убедитесь, что вы перешли на тарифный план Blaze для доступа к Vertex AI

Ошибки доступа к API

Если при доступе к API Gemini возникают ошибки:

  • Убедитесь, что выставление счетов в вашем проекте Firebase настроено правильно.
  • Проверьте, включены ли Vertex AI и Cloud AI API в вашем проекте Firebase.
  • Проверьте сетевое подключение и настройки брандмауэра.
  • Убедитесь, что название модели ( gemini-2.0-flash ) указано правильно и доступно.

Проблемы с контекстом разговора

Если вы заметили, что Gemini не помнит предыдущий контекст чата:

  • Убедитесь, что функция chatSession аннотирована @Riverpod(keepAlive: true)
  • Убедитесь, что вы повторно используете один и тот же сеанс чата для всех обменов сообщениями.
  • Перед отправкой сообщений убедитесь, что сеанс чата правильно инициализирован.

Проблемы, связанные с платформой

Для проблем, связанных с платформой:

  • iOS/macOS: убедитесь, что установлены правильные права и настроены минимальные версии.
  • Android: убедитесь, что минимальная версия SDK установлена ​​правильно
  • Проверьте сообщения об ошибках, специфичные для платформы, в консоли.

Ключевые изученные концепции

  • Настройка Firebase в приложении Flutter
  • Настройка Vertex AI в Firebase для доступа к Gemini
  • Создание поставщиков Riverpod для асинхронных сервисов
  • Реализация чат-сервиса для общения с LLM
  • Обработка асинхронных состояний API (загрузка, ошибка, данные)
  • Понимание потока коммуникации LLM и сеансов чата

4. Эффективное побуждение к описанию цвета

На этом этапе вы создадите и реализуете системную подсказку, которая поможет Gemini интерпретировать цветовые описания. Системные подсказки — это мощный способ настройки поведения LLM для конкретных задач без изменения кода.

Чему вы научитесь на этом этапе

  • Понимание системных подсказок и их важности в заявках на получение степени магистра права (LLM)
  • Создание эффективных подсказок для задач, специфичных для предметной области
  • Загрузка и использование системных подсказок в приложении Flutter
  • Помощь LLM в предоставлении единообразных ответов
  • Тестирование того, как системные подсказки влияют на поведение LLM

Понимание системных подсказок

Прежде чем углубляться в реализацию, давайте разберемся, что такое системные подсказки и почему они важны:

Что такое системные подсказки?

Системная подсказка — это особый тип инструкции, предоставляемой LLM, которая устанавливает контекст, правила поведения и ожидания для его ответов. В отличие от пользовательских сообщений, системные подсказки:

  • Определить роль и личность LLM
  • Определить специализированные знания или возможности
  • Предоставьте инструкции по форматированию
  • Установить ограничения на ответы
  • Опишите, как справляться с различными сценариями

Представьте, что системная подсказка — это «должностная инструкция» магистра права, сообщающая модели, как вести себя во время разговора.

Почему системные подсказки имеют значение

Системные подсказки имеют решающее значение для создания последовательных и полезных взаимодействий LLM, поскольку они:

  1. Обеспечьте согласованность : направьте модель на предоставление ответов в согласованном формате.
  2. Повышение релевантности : сфокусируйте модель на вашей конкретной области (в вашем случае — на цветах).
  3. Установите границы : определите, что модель должна и чего не должна делать.
  4. Улучшите пользовательский опыт : создайте более естественную и полезную модель взаимодействия.
  5. Уменьшите постобработку : получайте ответы в форматах, которые легче анализировать и отображать.

Для вашего приложения Colorist вам понадобится степень магистра права, чтобы последовательно интерпретировать описания цветов и предоставлять значения RGB в определенном формате.

Создать системный запрос актива

Сначала вы создадите системный файл приглашения, который будет загружен во время выполнения. Этот подход позволяет вам изменять приглашение без перекомпиляции вашего приложения.

Создайте новый файл assets/system_prompt.md со следующим содержимым:

активы/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

Понимание структуры подсказок системы

Давайте разберем, что делает эта подсказка:

  1. Определение роли : определяет LLM как «ассистента эксперта по цвету».
  2. Пояснение задачи : определяет основную задачу как интерпретацию описаний цветов в значения RGB.
  3. Формат ответа : точно указывает, как должны быть отформатированы значения RGB для обеспечения согласованности.
  4. Пример обмена : дает конкретный пример ожидаемой модели взаимодействия.
  5. Обработка пограничных случаев : Инструкции по обработке неясных описаний.
  6. Ограничения и рекомендации : устанавливает границы, например, поддерживая значения RGB в диапазоне от 0,0 до 1,0.

Такой структурированный подход гарантирует, что ответы LLM будут последовательными, информативными и отформатированными таким образом, чтобы их было легко анализировать, если вы захотите извлечь значения RGB программным способом.

Обновление pubspec.yaml

Теперь обновите нижнюю часть вашего pubspec.yaml , включив в нее каталог assets:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

Запустите flutter pub get , чтобы обновить набор ресурсов.

Создать поставщика системных подсказок

Создайте новый файл lib/providers/system_prompt.dart для загрузки системного приглашения:

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

Этот поставщик использует систему загрузки ресурсов Flutter для чтения файла подсказки во время выполнения.

Обновите поставщика модели Gemini

Теперь измените файл lib/providers/gemini.dart включив в него системную подсказку:

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

 
final model = FirebaseVertexAI.instance.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();
}

Ключевое изменение — добавление systemInstruction: Content.system(systemPrompt) при создании генеративной модели. Это говорит Gemini использовать ваши инструкции в качестве системного приглашения для всех взаимодействий в этом сеансе чата.

Сгенерировать код Riverpod

Запустите команду build runner, чтобы сгенерировать необходимый код Riverpod:

dart run build_runner build --delete-conflicting-outputs

Запустите и протестируйте приложение.

Теперь запустите ваше приложение:

flutter run -d DEVICE

Скриншот приложения Colorist, на котором показан ответ Gemini LLM в виде символа для приложения выбора цвета

Попробуйте протестировать его с различными цветовыми описаниями:

  • «Я бы хотел небесно-голубой»
  • «Дайте мне лесной зелёный»
  • «Сделайте закат ярким оранжевым»
  • «Я хочу цвет свежей лаванды»
  • «Покажите мне что-то похожее на глубокий синий океан»

Вы должны заметить, что Gemini теперь отвечает разговорными объяснениями о цветах вместе с последовательно отформатированными значениями RGB. Системная подсказка эффективно направила LLM на предоставление нужного вам типа ответов.

Также попробуйте спросить его о содержании вне контекста цветов. Скажем, основные причины Войны Алой и Белой розы. Вы должны заметить разницу с предыдущим шагом.

Важность оперативного проектирования для специализированных задач

Системные подсказки — это и искусство, и наука. Они являются важнейшей частью интеграции LLM, которая может существенно повлиять на полезность модели для вашего конкретного приложения. То, что вы сделали здесь, — это форма инженерии подсказок — адаптация инструкций, чтобы заставить модель вести себя так, как нужно вашему приложению.

Эффективное оперативное проектирование включает в себя:

  1. Четкое определение роли : установление цели получения степени магистра права (LLM)
  2. Четкие инструкции : подробное описание того, как должен реагировать LLM
  3. Конкретные примеры : демонстрация, а не просто рассказ о том, как выглядят хорошие ответы
  4. Обработка пограничных случаев : обучение LLM тому, как справляться с неоднозначными сценариями
  5. Спецификации форматирования : обеспечение единообразной и удобной структуры ответов.

Созданная вами системная подсказка преобразует общие возможности Gemini в специализированного помощника по интерпретации цветов, который предоставляет ответы, отформатированные специально для нужд вашего приложения. Это мощный шаблон, который можно применять во многих различных областях и задачах.

Что дальше?

На следующем этапе вы будете строить на этой основе, добавляя объявления функций, которые позволяют LLM не просто предлагать значения RGB, но и фактически вызывать функции в вашем приложении, чтобы напрямую устанавливать цвет. Это демонстрирует, как LLM могут преодолеть разрыв между естественным языком и конкретными функциями приложения.

Поиск неисправностей

Проблемы с загрузкой активов

Если при загрузке системы возникли ошибки, выполните следующие действия:

  • Убедитесь, что ваш pubspec.yaml правильно перечисляет каталог ресурсов.
  • Проверьте, что путь в rootBundle.loadString() соответствует расположению вашего файла.
  • Запустите flutter clean а затем flutter pub get , чтобы обновить пакет ресурсов.

Непоследовательные ответы

Если LLM не всегда следует вашим инструкциям по форматированию:

  • Попробуйте сделать требования к формату более явными в системном приглашении.
  • Добавьте больше примеров, чтобы продемонстрировать ожидаемую закономерность.
  • Убедитесь, что запрашиваемый вами формат соответствует модели.

Ограничение скорости API

Если вы столкнулись с ошибками, связанными с ограничением скорости:

  • Имейте в виду, что сервис Vertex AI имеет ограничения по использованию.
  • Рассмотрите возможность внедрения логики повторных попыток с экспоненциальной задержкой
  • Проверьте консоль Firebase на наличие проблем с квотами.

Ключевые изученные концепции

  • Понимание роли и важности системных подсказок в заявках на получение степени магистра права (LLM)
  • Создание эффективных подсказок с четкими инструкциями, примерами и ограничениями
  • Загрузка и использование системных подсказок в приложении Flutter
  • Руководство поведением LLM для задач, специфичных для предметной области
  • Использование оперативной инженерии для формирования ответов LLM

Этот шаг демонстрирует, как можно добиться значительной настройки поведения LLM, не изменяя код, — просто предоставив четкие инструкции в системном приглашении.

5. Декларации функций для инструментов LLM

На этом этапе вы начнете работу по включению Gemini в действие вашего приложения путем реализации деклараций функций. Эта мощная функция позволяет LLM не просто предлагать значения RGB, но и фактически устанавливать их в пользовательском интерфейсе вашего приложения с помощью специализированных вызовов инструментов. Однако потребуется следующий шаг, чтобы увидеть запросы LLM, выполненные в приложении Flutter.

Чему вы научитесь на этом этапе

  • Понимание вызова функций LLM и его преимуществ для приложений Flutter
  • Определение деклараций функций на основе схемы для Gemini
  • Интеграция деклараций функций с вашей моделью Gemini
  • Обновление системного запроса на использование возможностей инструмента

Понимание вызова функций

Прежде чем реализовывать объявления функций, давайте разберемся, что они собой представляют и в чем их ценность:

Что такое вызов функции?

Вызов функций (иногда называемый «использованием инструмента») — это возможность, которая позволяет LLM:

  1. Определите, когда запрос пользователя выиграет от вызова определенной функции
  2. Сгенерировать структурированный объект JSON с параметрами, необходимыми для этой функции.
  3. Позвольте вашему приложению выполнить функцию с этими параметрами.
  4. Получите результат функции и включите его в свой ответ.

Вместо того чтобы просто описывать, что делать, LLM вызывает функции, которые позволяют LLM инициировать конкретные действия в вашем приложении.

Почему вызов функций важен для приложений Flutter

Вызов функций создает мощный мост между естественным языком и функциями приложения:

  1. Прямое действие : пользователи могут описать свои желания на естественном языке, а приложение ответит конкретными действиями.
  2. Структурированный вывод : LLM выдает чистые, структурированные данные, а не текст, требующий анализа.
  3. Сложные операции : позволяет LLM получать доступ к внешним данным, выполнять вычисления или изменять состояние приложения.
  4. Лучший пользовательский опыт : обеспечивает бесшовную интеграцию между общением и функциональностью.

В приложении Colorist вызов функции позволяет пользователям сказать: «Я хочу цвет лесной зелени», и пользовательский интерфейс немедленно обновится с использованием этого цвета, без необходимости анализа значений RGB из текста.

Определить объявления функций

Создайте новый файл lib/services/gemini_tools.dart для определения объявлений ваших функций:

lib/services/gemini_tools.dart

import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

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

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

@riverpod
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

Понимание деклараций функций

Давайте разберем, что делает этот код:

  1. Именование функций : вы называете свою функцию set_color , чтобы четко обозначить ее назначение.
  2. Описание функции : вы предоставляете четкое описание, которое помогает LLM понять, когда ее использовать.
  3. Определения параметров : Вы определяете структурированные параметры с их собственными описаниями:
    • red : красный компонент RGB, указанный как число от 0,0 до 1,0
    • green : зеленый компонент RGB, указанный как число от 0,0 до 1,0
    • blue : синий компонент RGB, указанный как число от 0,0 до 1,0
  4. Типы схем : Schema.number() используется для указания того, что это числовые значения.
  5. Коллекция инструментов : вы создаете список инструментов, содержащий объявление вашей функции.

Такой структурированный подход помогает студентам программы Gemini LLM понять:

  • Когда следует вызывать эту функцию
  • Какие параметры он должен предоставить
  • Какие ограничения применяются к этим параметрам (например, диапазон значений)

Обновите поставщика модели Gemini

Теперь измените файл lib/providers/gemini.dart включив в него объявления функций при инициализации модели Gemini:

lib/providers/gemini.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini.g.dart';

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

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

 
final model = FirebaseVertexAI.instance.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();
}

Ключевое изменение — добавление параметра tools: geminiTools.tools при создании генеративной модели. Это позволяет Gemini осознать функции, которые доступны для вызова.

Обновите системную подсказку

Теперь вам нужно изменить системное приглашение, чтобы указать LLM об использовании нового инструмента set_color . Обновите assets/system_prompt.md :

активы/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

Ключевые изменения в системном приглашении:

  1. Знакомство с инструментом : вместо того, чтобы запрашивать отформатированные значения RGB, теперь вы сообщаете LLM об инструменте set_color
  2. Измененный процесс : вы меняете шаг 3 с «форматировать значения в ответе» на «использовать инструмент для установки значений».
  3. Обновленный пример : вы показываете, что ответ должен включать вызов инструмента вместо форматированного текста.
  4. Удалено требование к форматированию : поскольку вы используете структурированные вызовы функций, вам больше не нужен определенный текстовый формат.

Это обновленное приглашение предписывает LLM использовать вызов функций, а не просто предоставлять значения RGB в текстовой форме.

Сгенерировать код Riverpod

Запустите команду build runner, чтобы сгенерировать необходимый код Riverpod:

dart run build_runner build --delete-conflicting-outputs

Запустите приложение.

На этом этапе Gemini сгенерирует контент, который пытается использовать вызов функции, но вы еще не реализовали обработчики для вызовов функций. Когда вы запустите приложение и опишите цвет, вы увидите, что Gemini отреагирует так, как будто он вызвал инструмент, но вы не увидите никаких изменений цвета в пользовательском интерфейсе до следующего шага.

Запустите приложение:

flutter run -d DEVICE

Скриншот приложения Colorist, на котором показан частичный ответ Gemini LLM

Попробуйте описать цвет, например, «глубокий океанский синий» или «лесной зеленый» и понаблюдайте за ответами. LLM пытается вызвать функции, определенные выше, но ваш код пока не обнаруживает вызовы функций.

Процесс вызова функции

Давайте разберемся, что происходит, когда Gemini использует вызов функции:

  1. Выбор функции : LLM решает, будет ли полезен вызов функции, основываясь на запросе пользователя.
  2. Генерация параметров : LLM генерирует значения параметров, которые соответствуют схеме функции.
  3. Формат вызова функции : LLM отправляет в своем ответе структурированный объект вызова функции.
  4. Обработка приложения : Ваше приложение получит этот вызов и выполнит соответствующую функцию (реализованную на следующем шаге).
  5. Интеграция ответов : в многооборотных диалогах LLM ожидает возврата результата функции.

В текущем состоянии вашего приложения первые три шага выполняются, но вы еще не реализовали шаг 4 или 5 (обработку вызовов функций), что вы сделаете на следующем шаге.

Технические подробности: как Gemini решает, когда использовать функции

Близнецы принимают разумные решения о том, когда использовать функции, основываясь на:

  1. Намерение пользователя : будет ли запрос пользователя лучше всего удовлетворен функцией
  2. Релевантность функций : насколько хорошо доступные функции соответствуют задаче.
  3. Доступность параметров : может ли он уверенно определять значения параметров.
  4. Системные инструкции : Руководство от вашей системной подсказки по использованию функций

Предоставляя четкие объявления функций и системные инструкции, вы настроили Gemini на распознавание запросов описания цвета как возможностей для вызова функции set_color .

Что дальше?

На следующем этапе вы реализуете обработчики для вызовов функций, поступающих из Gemini. Это замкнет круг, позволяя описаниям пользователей вызывать реальные изменения цвета в пользовательском интерфейсе через вызовы функций LLM.

Поиск неисправностей

Проблемы с объявлением функций

Если вы столкнулись с ошибками при объявлении функций:

  • Проверьте, что имена и типы параметров соответствуют ожидаемым.
  • Убедитесь, что имя функции понятное и описательное.
  • Убедитесь, что описание функции точно объясняет ее назначение.

Проблемы с системными подсказками

Если LLM не пытается использовать функцию:

  • Убедитесь, что системное приглашение четко указывает LLM использовать инструмент set_color
  • Убедитесь, что пример в системном приглашении демонстрирует использование функции.
  • Попробуйте сделать инструкцию по использованию инструмента более точной.

Общие вопросы

Если у вас возникли другие проблемы:

  • Проверьте консоль на наличие ошибок, связанных с декларациями функций.
  • Убедитесь, что инструменты правильно переданы модели.
  • Убедитесь, что весь сгенерированный Riverpod код актуален

Ключевые изученные концепции

  • Определение деклараций функций для расширения возможностей LLM в приложениях Flutter
  • Создание схем параметров для структурированного сбора данных
  • Интеграция деклараций функций с моделью Gemini
  • Обновление подсказок системы для поощрения использования функций
  • Понимание того, как LLMs выбирают и вызовут функции

Этот шаг демонстрирует, как LLM могут преодолеть разрыв между вводом естественного языка и структурированными вызовами функций, закладывая основу для бесшовной интеграции между функциями разговора и применения.

6. Реализация обработки инструментов

На этом этапе вы реализуете обработчиков для вызовов функций, поступающих из Близнецов. Это завершает круг связи между входами естественного языка и конкретными функциями применения, что позволяет LLM напрямую манипулировать вашим пользовательским интерфейсом на основе описаний пользователей.

Чему вы узнаете на этом шаге

  • Понимание полной функции вызова конвейера в приложениях LLM
  • Вызовы функций обработки из Близнецов в приложении трепетания
  • Реализация обработчиков функций, которые изменяют состояние приложения
  • Обработка функций ответов и возврата результатов в LLM
  • Создание полного потока связи между LLM и пользовательским интерфейсом
  • Вызовы функций ведения журнала для прозрачности для прозрачности

Понимание функции вызывающего трубопровода

Прежде чем погрузиться в реализацию, давайте поймем полный трубопровод для вызова функции:

Сквозной поток

  1. Пользовательский ввод : пользователь описывает цвет на естественном языке (например, «Forest Green»)
  2. Обработка LLM : Gemini анализирует описание и решает вызовать функцию set_color
  3. Генерация вызовов функции : Близнецы создают структурированный json с параметрами (красный, зеленый, синий значения)
  4. Прием вызова функции : ваше приложение получает эти структурированные данные от Gemini
  5. Выполнение функции : ваше приложение выполняет функцию с предоставленными параметрами
  6. Обновление состояния : функция обновляет состояние вашего приложения (изменение отображаемого цвета)
  7. Генерация ответов : ваша функция возвращает результаты обратно в LLM
  8. Включение ответа : LLM включает эти результаты в свой окончательный ответ
  9. Обновление пользовательского интерфейса : ваш пользовательский интерфейс реагирует на изменение состояния, отображая новый цвет

Полный цикл связи необходим для правильной интеграции LLM. Когда LLM делает вызов функции, он не просто отправляет запрос и движется дальше. Вместо этого он ждет, пока ваше приложение выполнит функцию и возвращает результаты. Затем LLM использует эти результаты, чтобы сформулировать его окончательный ответ, создавая естественный поток разговоров, который признает предпринятые действия.

Реализовать обработчики функций

Давайте обновим ваш файл lib/services/gemini_tools.dart чтобы добавить обработчики для вызовов функций:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
 
GeminiTools(this.ref);

 
final Ref ref;

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

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

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

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

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

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

@riverpod
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);

Понимание обработчиков функций

Давайте разберем то, что делают эти обработчики функций:

  1. handleFunctionCall
    • Зарегистрирует вызов функции для прозрачности на панели журнала
    • Маршруты к соответствующему обработчику на основе имени функции
    • Возвращает структурированный ответ, который будет отправлен обратно в LLM
  2. handleSetColor : конкретный обработчик для вашей функции set_color , которая:
    • Извлекает значения RGB с карты аргументов
    • Преобразует их в ожидаемые типы (удваиваемые)
    • Обновляет состояние цвета приложения, используя colorStateNotifier
    • Создает структурированный ответ со статусом успеха и текущей информацией о цвете
    • Регистрирует результаты функции для отладки
  3. handleUnknownFunction : запасной обработчик для неизвестных функций, который:
    • Регистрирует предупреждение о неподдерживаемой функции
    • Возвращает ответ ошибки в LLM

Функция handleSetColor особенно важна, поскольку она преодолевает разрыв между естественным языком LLM и конкретными изменениями пользовательского интерфейса.

Обновить службу чата Gemini для обработки функций и ответов

Теперь давайте обновим файл lib/services/gemini_chat_service.dart для обработки вызовов функций из ответов LLM и отправить результаты обратно в LLM:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

Понимание потока общения

Ключевым дополнением здесь является полная обработка функций вызовов и ответов:

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

Этот код:

  1. Проверяет, содержит ли ответ LLM какие -либо вызовы функций
  2. Для каждого вызова функции вызывает метод handleFunctionCall с именем функции и аргументами
  3. Собирает результаты каждого вызова функции
  4. Отправляет эти результаты обратно в LLM с помощью Content.functionResponses
  5. Обрабатывает ответ LLM на результаты функции
  6. Обновляет пользовательский интерфейс с окончательным текстом ответа

Это создает поток поездки в оба конца:

  • Пользователь → LLM: запрашивает цвет
  • LLM → APP: вызовы функций с параметрами
  • Приложение → Пользователь: отображается новый цвет
  • Приложение → LLM: результаты функции
  • LLM → Пользователь: Окончательный ответ, включающий результаты функции

Генерировать код RiverPod

Запустите команду Build Runner, чтобы сгенерировать необходимый код RiverPod:

dart run build_runner build --delete-conflicting-outputs

Запустить и проверить полный поток

Теперь запустите свое приложение:

flutter run -d DEVICE

Скриншот приложения Colorist, показывающий, что Gemini LLM отвечает с помощью функционального вызова

Попробуйте ввести различные описания цвета:

  • "Я бы хотел глубокий малиновый красный"
  • "Покажи мне успокаивающий небо синий"
  • "Дай мне цвет свежих листьев мяты"
  • "Я хочу увидеть теплый закат апельсин"
  • "Сделай это богатым королевским фиолетовым"

Теперь вы должны увидеть:

  1. Ваше сообщение появляется в интерфейсе чата
  2. Ответ Близнецы появляется в чате
  3. Вызовы функций регистрируются на панели журнала
  4. Результаты функции регистрируются сразу после
  5. Цветный прямоугольник обновления для отображения описанного цвета
  6. RGB значения обновления, чтобы показать компоненты нового цвета
  7. Окончательный ответ Gemini, часто комментирующий цвет, который был установлен

Панель журнала дает представление о том, что происходит за кулисами. Вы увидите:

  • Точная функция вызовов Близнецов делает
  • Параметры, которые он выбирает для каждого значения RGB
  • Результаты возвращаются вашей функцией
  • Последующие ответы от Близнецов

Уведомление о состоянии цвета

colorStateNotifier который вы используете для обновления цветов, является частью пакета colorist_ui . Он управляет:

  • Текущий цвет отображается в пользовательском интерфейсе
  • История цвета (последние 10 цветов)
  • Уведомление об изменениях состояния в компонентах пользовательского интерфейса

Когда вы называете updateColor с новыми значениями RGB, это:

  1. Создает новый объект ColorData с предоставленными значениями
  2. Обновляет текущий цвет в состоянии приложения
  3. Добавляет цвет в историю
  4. Триггеры обновления пользовательского интерфейса через государственное управление Riverpod

Компоненты пользовательского интерфейса в пакете colorist_ui смотрят это состояние и автоматически обновляются при его изменении, создавая реактивный опыт.

Понимание обработки ошибок

Ваша реализация включает в себя надежную обработку ошибок:

  1. Блок Try-Catch : завершает все взаимодействия LLM, чтобы поймать любые исключения
  2. Журнализация ошибок : записывает ошибки на панели журнала со следами стека
  3. Отзывы пользователя : предоставляет дружественное сообщение об ошибке в чате
  4. Очистка состояния : завершает состояние сообщения, даже если возникает ошибка

Это гарантирует, что приложение остается стабильным и обеспечивает соответствующую обратную связь, даже когда возникают проблемы с службой LLM или выполнением функций.

Сила функции, призывающая к опыту пользователя

То, что вы достигли здесь, демонстрирует, как LLM могут создавать мощные природные интерфейсы:

  1. Интерфейс естественного языка : пользователи выражают намерение на повседневном языке
  2. Интеллектуальная интерпретация : LLM переводит смутные описания в точные значения
  3. Прямая манипуляция : обновления пользовательского интерфейса в ответ на естественный язык
  4. Контекстуальные ответы : LLM предоставляет разговорной контекст об изменениях
  5. Низкая когнитивная нагрузка : пользователям не нужно понимать значения RGB или теорию цвета

Этот шаблон использования функции LLM, призывая к мощению естественного языка и действий пользовательского интерфейса, может быть распространена на бесчисленные другие домены за пределами выбора цвета.

Что дальше?

На следующем шаге вы улучшите пользовательский опыт, внедряя потоковые ответы. Вместо того, чтобы ждать полного ответа, вы обрабатываете текстовые куски и функциональные вызовы по мере их получения, создавая более отзывчивое и привлекательное приложение.

Поиск неисправностей

Проблемы вызова функции

Если Близнецы не вызывают ваши функции, или параметры неверны:

  • Проверьте объявление о вашей функции, соответствует тому, что описано в системном приглашении
  • Проверьте, что имена и типы параметров согласуются
  • Убедитесь, что ваша система приглашается явно инструктирует LLM использовать инструмент
  • Проверьте имя функции в вашем обработчике, точно соответствует тому, что в объявлении
  • Изучите панель журнала для получения подробной информации о вызовах функций

Проблемы ответа функции

Если результаты функции не отправляются обратно в LLM:

  • Убедитесь, что ваша функция возвращает правильно отформатированную карту
  • Убедитесь, что content.functionResponses строится правильно
  • Ищите любые ошибки в журнале, связанных с ответы функции
  • Убедитесь, что вы используете тот же сеанс чата для ответа

Проблемы с цветным дисплеем

Если цвета не отображаются правильно:

  • Убедитесь, что значения RGB должным образом преобразованы в удвоение (LLM может отправить их в качестве целых чисел)
  • Убедитесь, что значения находятся в ожидаемом диапазоне (от 0,0 до 1,0)
  • Убедитесь, что уведомление о состоянии цвета называется правильно
  • Изучите журнал для точных значений, передаваемых функции

Общие проблемы

Для общих вопросов:

  • Изучите журналы на предмет ошибок или предупреждений
  • Проверьте AI Vertex AI в подключении Firebase
  • Проверьте наличие любых несоответствий типа в параметрах функции
  • Убедитесь, что весь код, сгенерированный Riverpod

Ключевые понятия изучены

  • Реализация полной функции вызова трубопровода в Flutter
  • Создание полной связи между LLM и вашим приложением
  • Обработка структурированных данных из ответов LLM
  • Отправка результатов функции обратно в LLM для включения в ответы
  • Использование панели журнала для получения видимости в взаимодействиях с применением LLM
  • Соединение входов естественного языка к конкретным изменениям пользовательского интерфейса

С завершением этого шага ваше приложение теперь демонстрирует один из самых мощных моделей для интеграции LLM: преобразование вводов естественного языка в конкретные действия пользовательского интерфейса, сохраняя при этом согласованный разговор, который признает эти действия. Это создает интуитивно понятный, разговорный интерфейс, который кажется волшебным для пользователей.

7. Потоковые ответы для лучшего UX

На этом этапе вы улучшите пользовательский опыт, внедряя потоковые ответы от Gemini. Вместо того, чтобы ждать, чтобы весь ответ был сгенерирован, вы обрабатываете текстовые куски и функциональные вызовы по мере их получения, создавая более отзывчивое и привлекательное приложение.

Что вы осветите на этом шаге

  • Важность потоковой передачи для приложений LLM-мощности
  • Внедрение ответов потоковой LLM в приложении Flutter
  • Обработка частичных текстовых кусков по мере их прибытия из API
  • Управление состоянием разговора для предотвращения конфликтов сообщений
  • Обработка вызовов функций в потоковых ответах
  • Создание визуальных индикаторов для противоречивых ответов

Почему потоковая передача имеет значение для приложений LLM

Перед реализацией давайте поймем, почему потоковые ответы имеют решающее значение для создания превосходного пользовательского опыта с LLMS:

Улучшенный пользовательский опыт

Потоковые ответы предоставляют несколько значительных преимуществ пользовательского опыта:

  1. Снижение воспринимаемой задержки . Пользователи видят начало текста сразу же (обычно в течение 100-300 мс), вместо того, чтобы ждать несколько секунд для полного ответа. Это восприятие непосредственности значительно улучшает удовлетворенность пользователями.
  2. Естественный разговорной ритм : постепенный внешний вид текста имитирует, как люди общаются, создавая более естественный опыт диалога.
  3. Прогрессивная обработка информации : пользователи могут начать обрабатывать информацию по мере ее появления, а не быть перегруженным большим блоком текста одновременно.
  4. Возможность раннего перерыва : в полном приложении пользователи могут потенциально прервать или перенаправить LLM, если они увидят, что он идет в бесполезном направлении.
  5. Визуальное подтверждение деятельности : текст потокового вещания обеспечивает немедленную обратную связь, которую работает система, снижая неопределенность.

Технические преимущества

Помимо улучшений UX, потоковая передача предлагает технические преимущества:

  1. Раннее выполнение функций : вызовы функций могут быть обнаружены и выполнены, как только они появятся в потоке, не ожидая полного ответа.
  2. Инопрентные обновления пользовательского интерфейса : вы можете постепенно обновлять свой пользовательский интерфейс по мере появления новой информации, создавая более динамичный опыт.
  3. Управление государством разговора : потоковая передача дает четкие сигналы о том, когда ответы заканчиваются по сравнению с тем, что они продолжаются, что позволяет лучше управлять государством.
  4. Снижение рисков тайм-аута : с нетронутыми реакциями долгосрочные поколения рискуют. Потоковая передача устанавливает связь на раннем этапе и поддерживает его.

Для вашего приложения The Colorist реализация потоковой передачи означает, что пользователи увидят как текстовые ответы, так и изменения цвета, которые появляются более быстро, создавая значительно более отзывчивый опыт.

Добавить управление государством разговора

Во -первых, давайте добавим государственного поставщика, чтобы отследить, обрабатывает ли приложение в настоящее время потоковое ответ. Обновите свой файл 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

 
Future<void> _processBlock(                                        // Add from here...
   
GenerateContentResponse block,
   
String llmMessageId,
 
) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
final blockText = block.text;

   
if (blockText != null) {
     
logStateNotifier.logLlmText(blockText);
     
chatStateNotifier.appendToMessage(llmMessageId, blockText);
   
}

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

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

Понимание потоковой реализации

Давайте разберемся, что делает этот код:

  1. Отслеживание состояния разговора :
    • conversationStateProvider отслеживает, является ли приложение в настоящее время обрабатывает ответ
    • Государство переходит от idlebusy во время обработки, а затем снова на idle
    • Это предотвращает несколько параллельных запросов, которые могут конфликтовать
  2. Инициализация потока :
    • sendMessageStream() возвращает поток кусков ответов вместо Future с полным ответом
    • Каждый кусок может содержать текст, вызовы функций или оба
  3. Прогрессивная обработка :
    • await for процессов каждый кусок, когда он прибывает в режиме реального времени
    • Текст добавляется в пользовательский интерфейс немедленно, создавая потоковой эффект
    • Функциональные вызовы выполняются, как только они обнаруживаются
  4. Обработка вызова функции :
    • Когда вызов функционального вызова обнаружена в куске, он немедленно выполняется
    • Результаты отправляются обратно в LLM через другой потоковой вызов
    • Ответ LLM на эти результаты также обрабатывается потоковым способом
  5. Обработка ошибок и очистка :
    • try / catch обеспечивает надежную обработку ошибок
    • finally блок гарантирует, что состояние разговора сбросится правильно
    • Сообщение всегда завершено, даже если возникают ошибки

Эта реализация создает отзывчивый, надежный потоковой опыт при сохранении правильного состояния разговора.

Обновите основной экран, чтобы подключить состояние разговора

Измените свой файл lib/main.dart , чтобы передать состояние разговора на основной экране:

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

Ключевым изменением здесь является передача conversationState в виджет MainScreen . MainScreen (предоставленный пакетом colorist_ui ) будет использовать это состояние для отключения ввода текста, когда ответ обрабатывается.

Это создает сплоченный пользовательский опыт, где пользовательский интерфейс отражает текущее состояние разговора.

Генерировать код RiverPod

Запустите команду Build Runner, чтобы сгенерировать необходимый код RiverPod:

dart run build_runner build --delete-conflicting-outputs

Запустить и проверить потоковые ответы

Запустите свое приложение:

flutter run -d DEVICE

Скриншот приложения Colorist, показывающий, что Gemini LLM отвечает потоковым способом

Теперь попробуйте проверить потоковое поведение с различными описаниями цветов. Попробуйте описания, такие как:

  • "Покажи мне глубокий цвет бирюзового цвета океана в Сумерках"
  • «Я хотел бы увидеть яркий коралл, который напоминает мне о тропических цветах»
  • «Создать приглушенный оливковый зеленый, как старая армейская усталость»

Потоковой технический поток подробно

Давайте рассмотрим, что именно происходит при потоковой передаче ответа:

Установление связи

Когда вы звоните sendMessageStream() , происходит следующее:

  1. Приложение устанавливает соединение с сервисом AI Vertex
  2. Запрос пользователя отправляется в службу
  3. Сервер начинает обрабатывать запрос
  4. Соединение потока остается открытым, готовым к передаче кусков

Чанк передачи

Поскольку Близнецы генерируют содержание, кусочки отправляются через поток:

  1. Сервер отправляет текстовые куски по мере их создания (обычно несколько слов или предложений)
  2. Когда Близнецы решают сделать вызов функции, он отправляет информацию о вызове функции
  3. Дополнительные текстовые куски могут следовать за вызовами функций
  4. Поток продолжается до завершения поколения

Прогрессивная обработка

Ваше приложение постепенно обрабатывает каждый кусок:

  1. Каждый текстовый кусок добавляется к существующему ответу
  2. Функциональные вызовы выполняются, как только они обнаруживаются
  3. Пользовательский интерфейс обновлений в режиме реального времени с результатами текста и функций
  4. Состояние отслеживается, чтобы показать, что ответ все еще транслируется

Потоловое завершение

Когда поколение завершено:

  1. Поток закрыт сервером
  2. Ваш await for петли, естественно
  3. Сообщение помечено как полное
  4. Состояние разговора возвращается на холостое время
  5. Обновления пользовательского интерфейса, чтобы отразить завершенное состояние

Потоковое сравнение по сравнению с нетронутым сравнением

Чтобы лучше понять преимущества потоковой передачи, давайте сравним потоковую передачу и не потоковые подходы:

Аспект

Не потоковой

Потоковое вещание

Воспринимаемая задержка

Пользователь ничего не видит, пока не будет готов полный ответ

Пользователь видит первые слова в миллисекундах

Пользовательский опыт

Длительное ожидание с последующим внезапным текстовым появлением

Натуральный, прогрессивный текст внешний вид

Государственное управление

Проще (сообщения либо ожидают, либо завершены)

Более сложные (сообщения могут быть в потоковом состоянии)

Выполнение функции

Происходит только после полного ответа

Происходит во время генерации ответов

Сложность реализации

Проще в реализации

Требуется дополнительное управление государством

Восстановление ошибок

Ответ все или ничего

Частичные ответы все еще могут быть полезны

Сложность кода

Менее сложный

Более сложный из -за обработки потока

Для такого приложения, как The Colorist, преимущества потоковой передачи UX перевешивают сложность реализации, особенно для цветных интерпретаций, которые могут занять несколько секунд для генерации.

Лучшие практики потоковой передачи UX

При внедрении потоковой передачи в ваших собственных приложениях LLM рассмотрите эти лучшие практики:

  1. Чистые визуальные индикаторы : всегда дайте четкие визуальные сигналы, которые различают потоковую передачу по сравнению с полными сообщениями
  2. Блокировка ввода : отключить ввод пользователя во время потоковой передачи, чтобы предотвратить несколько перекрывающихся запросов
  3. Восстановление ошибок : разработайте свой пользовательский интерфейс для обработки изящного восстановления, если трансляция прерывается
  4. Переходы состояния : убедитесь, что плавные переходы между холостом ходом, потоковой передачей и полными состояниями
  5. Визуализация прогресса : рассмотрим тонкие анимации или индикаторы, которые показывают активную обработку
  6. Параметры отмены : в полном приложении предоставьте пользователям способы отменить поколения в процессе
  7. Интеграция результатов функции : спроектируйте свой пользовательский интерфейс для обработки результатов функции, появляясь в середине потока
  8. Оптимизация производительности : минимизировать восстановление пользовательского интерфейса во время обновлений быстрого потока

Пакет colorist_ui реализует многие из этих лучших практик для вас, но это важные соображения для любой реализации потокового LLM.

Что дальше?

На следующем шаге вы будете реализовать синхронизацию LLM, уведомляя Gemini, когда пользователи выбирают цвета из истории. Это создаст более сплоченный опыт, когда LLM знает об инициированных пользователях изменения в состоянии приложения.

Поиск неисправностей

Проблемы обработки потоков

Если вы сталкиваетесь с проблемами с обработкой потока:

  • Симптомы : частичные ответы, отсутствующий текст или резкое прекращение потока
  • Решение : Проверьте сетевой подключение и убедитесь
  • Диагноз : осмотрите панель журнала на наличие сообщений об ошибках или предупреждениях, связанных с обработкой потока
  • Исправление : убедитесь, что вся обработка потока использует правильную обработку ошибок с блоками try / catch

Отсутствующие вызовы функций

Если вызовы функций не обнаруживаются в потоке:

  • Симптомы : текст появляется, но цвета не обновляются, или журнал не показывает вызовов функций
  • Решение : Проверьте инструкции подсказки системы об использовании вызовов функций
  • Диагноз : Проверьте панель журнала, чтобы увидеть, получены ли вызовы функций
  • Исправление : отрегулируйте подсказку системы, чтобы более четко указать LLM для использования инструмента set_color

Общая обработка ошибок

Для любых других вопросов:

  • Шаг 1 : Проверьте панель журнала для сообщений об ошибках
  • Шаг 2 : Проверьте AI Vertex AI в подключении Firebase
  • Шаг 3 : Убедитесь, что весь сгенерированный Riverpod код обновлен
  • Шаг 4 : Просмотрите реализацию потоковой передачи.

Ключевые понятия изучены

  • Реализация потоковых ответов с API Gemini для более отзывчивого UX
  • Управление состоянием разговора для правильного обращения с потоковыми взаимодействиями
  • Обработка текстовых и функциональных вызовов в реальном времени по мере их прибытия
  • Создание отзывчивых интерфейсов, которые обновляются постепенно во время потоковой передачи
  • Обработка одновременных потоков с правильными асинхровыми узорами
  • Обеспечение соответствующей визуальной обратной связи во время потоковых ответов

Внедряя потоковую передачу, вы значительно улучшили пользовательский опыт вашего приложения Colorist, создав более отзывчивый, привлекательный интерфейс, который кажется действительно разговорным.

8. Синхронизация контекста LLM

На этом бонусном шаге вы реализуете синхронизацию контекста LLM, уведомляя Gemini, когда пользователи выбирают цвета из истории. Это создает более сплоченный опыт, когда LLM знает о действиях пользователя в интерфейсе, а не только о их явных сообщениях.

Что вы осветите на этом шаге

  • Создание синхронизации контекста LLM между вашим пользовательским интерфейсом и LLM
  • Сериализация событий пользовательского интерфейса в контекст, который может понять LLM
  • Обновление контекста разговора на основе действий пользователя
  • Создание последовательного опыта в разных методах взаимодействия
  • Повышение осознания контекста LLM за пределами явных сообщений в чате

Понимание синхронизации контекста LLM

Традиционные чат -боты отвечают только на явные сообщения пользователей, создавая отключение, когда пользователи взаимодействуют с приложением с помощью других средств. Синхронизация контекста LLM рассматривает это ограничение:

Почему синхронизация контекста LLM имеет значение

Когда пользователи взаимодействуют с вашим приложением через элементы пользовательского интерфейса (например, выбор цвета из истории), LLM не может узнать, что произошло, если вы явно не скажете его. Синхронизация контекста LLM:

  1. Поддерживает контекст : Информирован LLM обо всех соответствующих действиях пользователя
  2. Создает когерентность : создает сплоченный опыт, когда LLM признает взаимодействие с пользовательским интерфейсом
  3. Увеличение интеллекта : позволяет LLM соответствующим образом реагировать на все действия пользователя
  4. Улучшает пользовательский опыт : делает все приложение более интегрированным и отзывчивым
  5. Уменьшает усилия пользователя : устраняет необходимость для пользователей вручную объяснить свои действия пользовательского интерфейса

В вашем приложении The Colorist, когда пользователь выбирает цвет из истории, вы хотите, чтобы Близнецы признали это действие и разумно комментируют выбранное цвет, сохраняя иллюзию бесшовного, осведомленного помощника.

Обновите службу чата Gemini для уведомлений о выборе цвета

Во -первых, вы добавите метод в GeminiChatService , чтобы уведомить LLM, когда пользователь выбирает цвет из истории. Обновите свой файл 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_vertexai/firebase_vertexai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

 
Future<void> _processBlock(
   
GenerateContentResponse block,
   
String llmMessageId,
 
) async {
   
final chatSession = await ref.read(chatSessionProvider.future);
   
final chatStateNotifier = ref.read(chatStateNotifierProvider.notifier);
   
final logStateNotifier = ref.read(logStateNotifierProvider.notifier);
   
final blockText = block.text;

   
if (blockText != null) {
     
logStateNotifier.logLlmText(blockText);
     
chatStateNotifier.appendToMessage(llmMessageId, blockText);
   
}

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

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

Ключевым дополнением является метод notifyColorSelection том, что:

  1. Берет объект ColorData представляющий выбранную цвет
  2. Кодирует его в формат JSON, который может быть включен в сообщение
  3. Отправляет специально отформатированное сообщение в LLM с указанием выбора пользователя
  4. Повторно использует существующий метод sendMessage для обработки уведомления

Этот подход избегает дублирования, используя вашу существующую инфраструктуру обработки сообщений.

Обновить основное приложение для подключения уведомлений о выборе цвета

Теперь измените свой файл lib/main.dart чтобы передать функцию уведомления о выборе цвета на главный экран:

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

Изменение ключа - это добавление обратного вызова notifyColorSelection , который соединяет событие пользовательского интерфейса (выбор цвета из истории) с системой уведомлений LLM.

Обновить системную подсказку

Теперь вам нужно обновить свою систему подсказки, чтобы обучить LLM о том, как реагировать на уведомления о выборе цвета. Измените свои 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

Ключевым дополнением является раздел «Когда пользователи выбирают исторические цвета», который:

  1. Объясняет концепцию уведомлений о выборе истории в LLM
  2. Дает пример того, как выглядят эти уведомления
  3. Показывает пример подходящего ответа
  4. Устанавливает ожидания для признания выбора и комментировать цвет

Это помогает LLM понять, как соответствующим образом реагировать на эти специальные сообщения.

Генерировать код RiverPod

Запустите команду Build Runner, чтобы сгенерировать необходимый код RiverPod:

dart run build_runner build --delete-conflicting-outputs

Запустить и тестировать контекст LLM Синхронизация

Запустите свое приложение:

flutter run -d DEVICE

Скриншот приложения Colorist, показывающий, что Gemini LLM отвечает на выбор из истории цвета

Тестирование синхронизации контекста LLM включает в себя:

  1. Во -первых, генерируйте несколько цветов, описывая их в чате
    • "Покажи мне яркий фиолетовый"
    • "Я бы хотел лесной зеленый"
    • "Дай мне ярко -красный"
  2. Затем нажмите на одну из цветных миниатюр в истории истории

Вы должны наблюдать:

  1. Выбранный цвет появляется на главном дисплее
  2. Пользовательский сообщение появляется в чате, указывающем на выбор цвета
  3. LLM отвечает, признавая выбор и комментируя цвет
  4. Все взаимодействие кажется естественным и сплоченным

Это создает бесшовный опыт, когда LLM знает и соответствующим образом реагирует как на прямые сообщения, так и взаимодействие пользовательского интерфейса.

Как работает синхронизация контекста LLM

Давайте рассмотрим технические детали того, как работает эта синхронизация:

Поток данных

  1. Действие пользователя : пользователь нажимает цвет в истории истории
  2. Событие пользовательского интерфейса : виджет MainScreen обнаруживает этот выбор
  3. Выполнение обратного вызова : запускается обратный вызов notifyColorSelection
  4. Создание сообщений : специально отформатированное сообщение создается с цветными данными
  5. Обработка LLM : сообщение отправляется Близнецам, что распознает формат
  6. Контекстуальный ответ : Близнецы отвечают соответствующим образом на основе системы системы
  7. Обновление пользовательского интерфейса : ответ появляется в чате, создавая сплоченный опыт

Сериализация данных

Ключевым аспектом этого подхода является то, как вы сериализуете цветные данные:

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

Метод toLLMContextMap() (предоставленный пакетом The colorist_ui ) преобразует объект ColorData в карту с ключевыми свойствами, которые LLM может понять. Обычно это включает в себя:

  • Значения RGB (красный, зеленый, синий)
  • Хекс -код представление
  • Любое имя или описание, связанное с цветом

Постоянно форматируя эти данные и включив их в сообщение, вы гарантируете, что LLM имеет всю информацию, необходимую для надлежащего ответа.

Более широкие приложения контекста LLM синхронизация

Этот шаблон уведомления LLM о событиях пользовательского интерфейса имеет многочисленные приложения за пределами выбора цвета:

Другие варианты использования

  1. Изменения фильтра : уведомить LLM, когда пользователи применяют фильтры к данным
  2. Навигационные события : информируйте LLM, когда пользователи перемещаются в разные разделы
  3. Изменения выбора : обновите LLM, когда пользователи выбирают элементы из списков или сетки
  4. Обновления предпочтений : Расскажите LLM, когда пользователи меняют настройки или настройки
  5. Манипулирование данными : уведомить LLM, когда пользователи добавляют, редактируют или удаляют данные

В каждом случае шаблон остается прежним:

  1. Обнаружить мероприятие пользовательского интерфейса
  2. Сериализовать соответствующие данные
  3. Отправить специально отформатированное уведомление в LLM
  4. Направляйте LLM, чтобы соответствующим образом реагировать через систему

Лучшие практики для синхронизации контекста LLM

Основываясь на вашей реализации, вот некоторые лучшие практики для эффективной синхронизации контекста LLM:

1. Последовательное форматирование

Используйте последовательный формат для уведомлений, чтобы LLM мог легко идентифицировать их:

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

2. Богатый контекст

Включите достаточно подробностей в уведомлениях для LLM, чтобы ответить разумно. Для цветов это означает значения RGB, шестигранные коды и любые другие соответствующие свойства.

3. Чистые инструкции

Предоставьте явные инструкции в системе, как о том, как обрабатывать уведомления, в идеале с примерами.

4. Естественная интеграция

Уведомления о дизайне, чтобы естественным образом проходить в разговоре, а не как технические перерывы.

5. Селективное уведомление

Уведомите LLM только о действиях, которые имеют отношение к разговору. Не каждое мероприятие пользовательского интерфейса должно быть передано.

Поиск неисправностей

Вопросы уведомления

Если LLM не отвечает должным образом на выбор цвета:

  • Убедитесь, что формат сообщений уведомлений соответствует тому, что описано в подсказке системы
  • Убедитесь, что данные цвета правильно сериализуются
  • Убедитесь, что в подсказке системы есть четкие инструкции по обработке выбора
  • Ищите любые ошибки в службе чата при отправке уведомлений

Контекстное управление

Если LLM, кажется, теряет контекст:

  • Убедитесь, что сеанс чата поддерживается должным образом
  • Убедитесь, что говорится о переходе правильного перехода
  • Убедитесь, что уведомления отправляются через один и тот же сеанс чата

Общие проблемы

Для общих вопросов:

  • Изучите журналы на предмет ошибок или предупреждений
  • Проверьте AI Vertex AI в подключении Firebase
  • Проверьте наличие любых несоответствий типа в параметрах функции
  • Убедитесь, что весь код, сгенерированный Riverpod

Ключевые понятия изучены

  • Создание синхронизации контекста LLM между UI и LLM
  • Сериализация событий пользовательского интерфейса в контекст LLM-дружелюбного
  • Направляющее поведение LLM для различных моделей взаимодействия
  • Создание сплоченного опыта между сообщениями и взаимодействием без осадков
  • Повышение осведомленности LLM о более широком состоянии применения

Внедряя синхронизацию контекста LLM, вы создали действительно интегрированный опыт, в котором LLM ощущается как осведомленный, отзывчивый помощник, а не просто генератор текста. Этот шаблон может быть применен к бесчисленным другим приложениям, чтобы создать более естественные, интуитивно понятные интерфейсы с AI.

9. Поздравляю!

Вы успешно завершили Colorist CodeLab! 🎉

Что вы построили

Вы создали полностью функциональное приложение Flutter, которое интегрирует API Google Gemini для интерпретации описаний цвета естественного языка. Ваше приложение теперь может:

  • Обрабатывать описания естественных языков, такие как «Sunset Orange» или «Deep Ocean Blue»
  • Используйте Близнецы, чтобы разумно перевести эти описания в значения RGB
  • Показать интерпретируемые цвета в режиме реального времени с помощью потоковых ответов
  • Обрабатывать взаимодействия с пользователями через элементы чата и пользовательского интерфейса
  • Поддерживать контекстуальную осведомленность по различным методам взаимодействия

Куда идти отсюда

Теперь, когда вы освоили основы интеграции Близнецов с Flutter, вот несколько способов продолжить ваше путешествие:

Улучшите приложение The Colorist

  • Цветовые палитры : добавьте функциональность для создания дополнительных или соответствующих цветовых схем
  • Голосовой ввод : интегрируйте распознавание речи для словесных описаний цвета
  • Управление истории : добавьте параметры в наборы имени, организации и экспорта цветов
  • Пользовательская подсказка : создайте интерфейс для пользователей для настройки системных подсказок
  • Advanced Analytics : отслеживайте, какие описания работают лучше всего или вызывают трудности

Изучите больше функций Близнецов

  • Мультимодальные входы : добавьте входы изображения, чтобы извлечь цвета с фотографий
  • Генерация контента : используйте Близнецы, чтобы генерировать контент, связанный с цветом, как описания или истории
  • Улучшения вызова функции : создайте более сложные интеграции инструментов с несколькими функциями
  • Настройки безопасности : исследуйте различные настройки безопасности и их влияние на ответы

Применить эти шаблоны к другим областям

  • Анализ документов : Создайте приложения, которые могут понимать и анализировать документы
  • Помощь в творческом письме : построить инструменты написания с помощью предложений LLM-мощности
  • Автоматизация задач : проектирование приложений, которые переводят естественный язык в автоматические задачи
  • Приложения, основанные на знаниях : создавать экспертные системы в конкретных областях

Ресурсы

Вот несколько ценных ресурсов для продолжения обучения:

Официальная документация

Поощрение курса и руководства

Сообщество

Наблюдаемая серия Agentic Flutter Agent

В Expode #59 Крэйг Лабенц и Эндрю Брогден исследуют этот коделаб, выделяя интересные части сборки приложения.

В эпизоде ​​№ 60 присоединяйтесь к Крейгу и Эндрю снова, когда они расширяют приложение CodeLab с новыми возможностями и сражаются с заставлением LLMS, как им говорят.

В эпизоде ​​№ 61 Крэйг присоединяется Крис продал, чтобы иметь новый взгляд при анализе заголовков новостей и генерирует соответствующие изображения.

Обратная связь

We'd love to hear about your experience with this codelab! Please consider providing feedback through:

Thank you for completing this codelab, and we hope you continue exploring the exciting possibilities at the intersection of Flutter and AI!