یک برنامه فلاتر با قدرت جمینی بسازید، یک برنامه فلاتر مجهز به جمینی بسازید

۱. یک برنامه Flutter مبتنی بر Gemini بسازید

آنچه خواهید ساخت

در این آزمایشگاه کد، شما Colorist را خواهید ساخت - یک برنامه تعاملی Flutter که قدرت Gemini API را مستقیماً به برنامه Flutter شما می‌آورد. آیا تا به حال خواسته‌اید که به کاربران اجازه دهید برنامه شما را از طریق زبان طبیعی کنترل کنند اما نمی‌دانید از کجا شروع کنید؟ این آزمایشگاه کد به شما نشان می‌دهد که چگونه.

Colorist به کاربران اجازه می‌دهد رنگ‌ها را به زبان طبیعی توصیف کنند (مانند «رنگ نارنجی غروب خورشید» یا «آبی عمیق اقیانوس») و برنامه:

  • این توضیحات را با استفاده از رابط برنامه‌نویسی کاربردی گوگل (Gemini API) پردازش می‌کند.
  • توضیحات را به مقادیر رنگی دقیق RGB تفسیر می‌کند
  • نمایش رنگ روی صفحه نمایش به صورت زنده
  • جزئیات فنی رنگ و زمینه جالبی در مورد رنگ ارائه می‌دهد
  • تاریخچه رنگ‌های اخیراً تولید شده را نگه می‌دارد

تصویر برنامه Colorist که نمایشگر رنگی و رابط چت را نشان می‌دهد

این برنامه دارای یک رابط کاربری دو صفحه‌ای با یک ناحیه نمایش رنگی و یک سیستم چت تعاملی در یک طرف و یک پنل گزارش دقیق است که تعاملات خام LLM را در طرف دیگر نشان می‌دهد. این گزارش به شما امکان می‌دهد تا بهتر بفهمید که چگونه یک ادغام LLM در زیر کاپوت کار می‌کند.

چرا این موضوع برای توسعه‌دهندگان فلاتر اهمیت دارد؟

دوره‌های LLM نحوه تعامل کاربران با برنامه‌ها را متحول می‌کنند، اما ادغام مؤثر آنها در برنامه‌های تلفن همراه و دسکتاپ چالش‌های منحصر به فردی را به همراه دارد. این آزمایشگاه کد، الگوهای عملی را به شما آموزش می‌دهد که فراتر از فراخوانی‌های خام API هستند.

سفر یادگیری شما

این آزمایشگاه کد، مراحل ساخت Colorist را گام به گام طی می‌کند:

  1. راه‌اندازی پروژه - شما با یک ساختار اولیه برنامه Flutter و پکیج colorist_ui شروع خواهید کرد.
  2. ادغام اولیه Gemini - برنامه خود را به Firebase AI Logic متصل کنید و ارتباط LLM را پیاده‌سازی کنید
  3. راهنمایی مؤثر - یک راهنمای سیستمی ایجاد کنید که LLM را برای درک توضیحات رنگ راهنمایی کند
  4. اعلان‌های تابع - ابزارهایی را تعریف کنید که LLM می‌تواند برای تنظیم رنگ‌ها در برنامه شما استفاده کند
  5. مدیریت ابزار - فراخوانی‌های تابع را از LLM پردازش کرده و آنها را به وضعیت برنامه خود متصل کنید
  6. پاسخ‌های استریمینگ - تجربه کاربری را با پاسخ‌های LLM استریمینگ بلادرنگ بهبود بخشید
  7. همگام‌سازی زمینه LLM - با آگاه کردن LLM از اقدامات کاربر، یک تجربه منسجم ایجاد کنید

آنچه یاد خواهید گرفت

  • پیکربندی منطق هوش مصنوعی فایربیس برای برنامه‌های فلاتر
  • ایجاد دستورالعمل‌های سیستمی مؤثر برای هدایت رفتار 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 تماشا کنید:

پیش‌نیازها

برای اینکه بیشترین بهره را از این آزمایشگاه کد ببرید، باید موارد زیر را داشته باشید:

  • تجربه توسعه Flutter - آشنایی با اصول اولیه Flutter و نحو Dart
  • دانش برنامه‌نویسی ناهمزمان - آشنایی با مفاهیم Futures، async/await و Streams
  • حساب کاربری فایربیس - برای راه‌اندازی فایربیس به یک حساب کاربری گوگل نیاز دارید

بیایید ساخت اولین برنامه Flutter مبتنی بر LLM شما را شروع کنیم!

۲. راه‌اندازی پروژه و سرویس اکو

در این مرحله اول، ساختار پروژه را تنظیم کرده و یک سرویس echo پیاده‌سازی خواهید کرد که بعداً با ادغام API Gemini جایگزین خواهد شد. این کار معماری برنامه را ایجاد کرده و قبل از اضافه کردن پیچیدگی فراخوانی‌های LLM، عملکرد صحیح رابط کاربری شما را تضمین می‌کند.

آنچه در این مرحله خواهید آموخت

  • راه‌اندازی یک پروژه Flutter با وابستگی‌های مورد نیاز
  • کار با پکیج colorist_ui برای کامپوننت‌های رابط کاربری
  • پیاده‌سازی سرویس پیام اکو و اتصال آن به رابط کاربری

ایجاد یک پروژه جدید فلاتر

با ایجاد یک پروژه جدید Flutter با دستور زیر شروع کنید:

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

علامت -e نشان می‌دهد که شما یک پروژه خالی بدون برنامه پیش‌فرض counter می‌خواهید. این برنامه طوری طراحی شده است که در دسکتاپ، موبایل و وب کار کند. با این حال، flutterfire در حال حاضر از لینوکس پشتیبانی نمی‌کند.

وابستگی‌ها را اضافه کنید

به دایرکتوری پروژه خود بروید و وابستگی‌های مورد نیاز را اضافه کنید:

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 : برای مدیریت وضعیت (state management)
  • logging : برای ثبت وقایع ساختاریافته
  • وابستگی‌های توسعه برای تولید کد و linting

pubspec.yaml شما چیزی شبیه به این خواهد بود:

pubspec.yaml

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

environment:
  sdk: ^3.9.2

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

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

flutter:
  uses-material-design: true

پیکربندی گزینه‌های تحلیل

custom_lint به فایل analysis_options.yaml در ریشه پروژه خود اضافه کنید:

include: package:flutter_lints/flutter.yaml

analyzer:
  plugins:
    - custom_lint

این پیکربندی، 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(chatStateProvider.notifier);
    final logStateNotifier = ref.read(logStateProvider.notifier);

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

این باعث می‌شود که یک برنامه Flutter یک سرویس echo پیاده‌سازی کند که با برگرداندن پیام کاربر، رفتار یک LLM را تقلید می‌کند.

درک معماری

بیایید کمی وقت بگذاریم تا معماری اپلیکیشن colorist را درک کنیم:

بسته‌ی colorist_ui

بسته colorist_ui کامپوننت‌های رابط کاربری از پیش ساخته شده و ابزارهای مدیریت حالت را ارائه می‌دهد:

  1. صفحه اصلی : کامپوننت اصلی رابط کاربری که موارد زیر را نمایش می‌دهد:
    • طرح‌بندی تقسیم صفحه نمایش روی دسکتاپ (ناحیه تعامل و پنل گزارش)
    • رابط کاربری تب‌بندی شده در موبایل
    • نمایشگر رنگی، رابط چت و تصاویر کوچک تاریخچه
  2. مدیریت وضعیت : این برنامه از چندین اعلان وضعیت استفاده می‌کند:
    • ChatStateNotifier : پیام‌های چت را مدیریت می‌کند.
    • ColorStateNotifier : رنگ فعلی و تاریخچه آن را مدیریت می‌کند.
    • LogStateNotifier : ورودی‌های لاگ را برای اشکال‌زدایی مدیریت می‌کند.
  3. مدیریت پیام : این برنامه از یک مدل پیام با حالت‌های مختلف استفاده می‌کند:
    • پیام‌های کاربر : توسط کاربر وارد شده است
    • پیام‌های LLM : تولید شده توسط LLM (یا فعلاً سرویس اکو شما)
    • MessageState : پیگیری می‌کند که آیا پیام‌های LLM کامل شده‌اند یا هنوز در حال ارسال هستند.

معماری برنامه

این برنامه از معماری زیر پیروی می‌کند:

  1. لایه رابط کاربری : ارائه شده توسط بسته colorist_ui
  2. مدیریت وضعیت : از Riverpod برای مدیریت وضعیت واکنشی استفاده می‌کند.
  3. لایه سرویس : در حال حاضر شامل سرویس ساده echo شما است، این سرویس با سرویس چت Gemini جایگزین خواهد شد.
  4. ادغام LLM : در مراحل بعدی اضافه خواهد شد

این جداسازی به شما امکان می‌دهد تا بر پیاده‌سازی یکپارچه‌سازی LLM تمرکز کنید، در حالی که اجزای رابط کاربری از قبل مدیریت شده‌اند.

برنامه را اجرا کنید

برنامه را با دستور زیر اجرا کنید:

flutter run -d DEVICE

به جای DEVICE ، نام دستگاه مورد نظر خود، مانند macos ، windows ، chrome یا شناسه دستگاه را وارد کنید.

تصویر برنامه Colorist که نشان‌دهنده‌ی رندرینگ سرویس echo با استفاده از markdown است

اکنون باید برنامه Colorist را با موارد زیر مشاهده کنید:

  1. یک ناحیه نمایش رنگ با رنگ پیش‌فرض
  2. رابط چت که در آن می‌توانید پیام تایپ کنید
  3. یک پنل گزارش که تعاملات چت را نشان می‌دهد

سعی کنید پیامی مانند «من یک رنگ آبی پررنگ می‌خواهم» را تایپ کنید و دکمه ارسال را فشار دهید. سرویس اکو به سادگی پیام شما را تکرار می‌کند. در مراحل بعدی، این را با تفسیر رنگ واقعی با استفاده از Firebase AI Logic جایگزین خواهید کرد.

بعدش چی؟

در مرحله بعد، Firebase را پیکربندی کرده و یکپارچه‌سازی اولیه Gemini API را برای جایگزینی سرویس echo خود با سرویس چت Gemini پیاده‌سازی خواهید کرد. این کار به برنامه اجازه می‌دهد تا توضیحات رنگ را تفسیر کرده و پاسخ‌های هوشمند ارائه دهد.

عیب‌یابی

مشکلات بسته رابط کاربری

اگر با بسته colorist_ui به مشکل برخوردید:

  • مطمئن شوید که از آخرین نسخه استفاده می‌کنید
  • تأیید کنید که وابستگی را به درستی اضافه کرده‌اید
  • نسخه‌های متناقض بسته را بررسی کنید

خطاهای ساخت

اگر خطاهای ساخت را مشاهده کردید:

  • مطمئن شوید که آخرین نسخه پایدار کانال Flutter SDK را نصب کرده‌اید.
  • دستور flutter clean و به دنبال آن flutter pub get را اجرا کنید.
  • خروجی کنسول را برای پیام‌های خطای خاص بررسی کنید

مفاهیم کلیدی آموخته شده

  • راه‌اندازی یک پروژه Flutter با وابستگی‌های لازم
  • درک معماری برنامه و مسئولیت‌های اجزا
  • پیاده‌سازی یک سرویس ساده که رفتار یک LLM را تقلید می‌کند
  • اتصال سرویس به کامپوننت‌های رابط کاربری
  • استفاده از Riverpod برای مدیریت وضعیت

۳. یکپارچه‌سازی اولیه چت جمینی

در این مرحله، سرویس echo از مرحله قبل را با ادغام API Gemini با استفاده از Firebase AI Logic جایگزین خواهید کرد. Firebase را پیکربندی خواهید کرد، ارائه دهندگان لازم را تنظیم خواهید کرد و یک سرویس چت اولیه که با API Gemini ارتباط برقرار می‌کند، پیاده‌سازی خواهید کرد.

آنچه در این مرحله خواهید آموخت

  • راه‌اندازی Firebase در یک برنامه Flutter
  • پیکربندی منطق هوش مصنوعی فایربیس برای دسترسی به جمینی
  • ایجاد ارائه دهندگان Riverpod برای سرویس‌های Firebase و Gemini
  • پیاده‌سازی یک سرویس چت پایه با Gemini API
  • مدیریت پاسخ‌های API ناهمزمان و حالت‌های خطا

فایربیس را راه‌اندازی کنید

ابتدا، باید Firebase را برای پروژه Flutter خود راه‌اندازی کنید. این شامل ایجاد یک پروژه Firebase، افزودن برنامه خود به آن و پیکربندی تنظیمات لازم Firebase AI Logic است.

ایجاد یک پروژه فایربیس

  1. به کنسول فایربیس بروید و با حساب گوگل خود وارد شوید.
  2. روی ایجاد یک پروژه Firebase کلیک کنید یا یک پروژه موجود را انتخاب کنید.
  3. برای ایجاد پروژه خود، مراحل نصب را دنبال کنید.

منطق هوش مصنوعی فایربیس را در پروژه فایربیس خود تنظیم کنید

  1. در کنسول Firebase، به پروژه خود بروید.
  2. در نوار کناری سمت چپ، هوش مصنوعی (AI) را انتخاب کنید.
  3. در منوی کشویی AI، گزینه AI Logic را انتخاب کنید.
  4. در کارت Firebase AI Logic، گزینه Get Started را انتخاب کنید.
  5. برای فعال کردن Gemini Developer API برای پروژه خود، دستورالعمل‌ها را دنبال کنید.

نصب رابط خط فرمان FlutterFire

رابط خط فرمان FlutterFire، راه‌اندازی Firebase را در برنامه‌های Flutter ساده می‌کند:

dart pub global activate flutterfire_cli

اضافه کردن فایربیس به برنامه فلاتر

  1. بسته‌های Firebase core و Firebase AI Logic را به پروژه خود اضافه کنید:
flutter pub add firebase_core firebase_ai
  1. دستور پیکربندی FlutterFire را اجرا کنید:
flutterfire configure

این دستور:

  • از شما می‌خواهد پروژه Firebase که تازه ایجاد کرده‌اید را انتخاب کنید
  • اپلیکیشن‌های فلاتر خود را در فایربیس ثبت کنید
  • یک فایل firebase_options.dart با پیکربندی پروژه خود ایجاد کنید

این دستور به طور خودکار پلتفرم‌های انتخابی شما (iOS، اندروید، macOS، ویندوز، وب) را شناسایی کرده و آنها را به طور مناسب پیکربندی می‌کند.

پیکربندی مخصوص پلتفرم

فایربیس به حداقل نسخه‌های بالاتر از نسخه‌های پیش‌فرض فلاتر نیاز دارد. همچنین برای ارتباط با سرورهای Firebase AI Logic به دسترسی به شبکه نیاز دارد.

پیکربندی مجوزهای macOS

برای macOS، باید دسترسی به شبکه را در مجوزهای برنامه خود فعال کنید:

  1. macos/Runner/DebugProfile.entitlements را باز کنید و موارد زیر را اضافه کنید:

macos/Runner/DebugProfile.entitlements

<key>com.apple.security.network.client</key>
<true/>
  1. همچنین macos/Runner/Release.entitlements را باز کنید و همین ورودی را اضافه کنید.

تنظیمات iOS را پیکربندی کنید

برای iOS، حداقل نسخه را در بالای ios/Podfile به‌روزرسانی کنید:

ios/پادفایل

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

ایجاد ارائه دهندگان مدل Gemini

حالا شما ارائه دهندگان Riverpod را برای Firebase و Gemini ایجاد خواهید کرد. یک فایل جدید lib/providers/gemini.dart ایجاد کنید:

lib/providers/gemini.dart

import 'dart:async';

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

import '../firebase_options.dart';

part 'gemini.g.dart';

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

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

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

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

این فایل مبنای سه ارائه‌دهنده‌ی کلیدی را تعریف می‌کند. این ارائه‌دهندگان هنگام اجرای dart run build_runner توسط مولدهای کد Riverpod تولید می‌شوند. این کد از رویکرد مبتنی بر حاشیه‌نویسی Riverpod 3 با الگوهای ارائه‌دهنده‌ی به‌روز شده استفاده می‌کند.

  1. firebaseAppProvider : فایربیس را با پیکربندی پروژه شما مقداردهی اولیه می‌کند.
  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_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

این سرویس:

  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. جایگزینی سرویس اکو با سرویس چت مبتنی بر Gemini API
  2. اضافه کردن صفحات بارگذاری و خطا با استفاده از الگوی AsyncValue در Riverpod با متد 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 جمینی ارسال می‌شود.
  4. پردازش LLM : مدل Gemini متن را پردازش کرده و پاسخی تولید می‌کند.
  5. مدیریت پاسخ : برنامه پاسخ را دریافت کرده و رابط کاربری را به‌روزرسانی می‌کند.
  6. ثبت وقایع : تمام ارتباطات برای شفافیت ثبت می‌شوند.

جلسات چت و زمینه گفتگو

جلسه چت Gemini زمینه بین پیام‌ها را حفظ می‌کند و امکان تعاملات مکالمه‌ای را فراهم می‌کند. این بدان معناست که LLM تبادلات قبلی در جلسه فعلی را "به خاطر می‌سپارد" و مکالمات منسجم‌تری را امکان‌پذیر می‌سازد.

حاشیه‌نویسی keepAlive: true در ارائه‌دهنده‌ی جلسه‌ی چت شما تضمین می‌کند که این زمینه در طول چرخه‌ی حیات برنامه باقی بماند. این زمینه‌ی پایدار برای حفظ جریان طبیعی مکالمه با LLM بسیار مهم است.

بعدش چی؟

در این مرحله، می‌توانید از Gemini API هر چیزی بخواهید، زیرا هیچ محدودیتی در مورد آنچه که به آن پاسخ خواهد داد وجود ندارد. به عنوان مثال، می‌توانید از آن خلاصه‌ای از جنگ‌های گل رز بخواهید، که به هدف برنامه رنگی شما مربوط نیست.

در مرحله بعد، یک اعلان سیستمی ایجاد خواهید کرد تا Gemini را در تفسیر مؤثرتر توصیفات رنگ راهنمایی کند. این نشان می‌دهد که چگونه می‌توانید رفتار یک LLM را برای نیازهای خاص برنامه سفارشی کنید و قابلیت‌های آن را بر دامنه برنامه خود متمرکز کنید.

عیب‌یابی

مشکلات پیکربندی فایربیس

اگر در مقداردهی اولیه Firebase با خطا مواجه شدید:

  • مطمئن شوید که فایل firebase_options.dart شما به درستی تولید شده است.
  • تأیید کنید که برای دسترسی به Firebase AI Logic به طرح Blaze ارتقا یافته‌اید.

خطاهای دسترسی به API

اگر در دسترسی به API Gemini خطایی دریافت کردید:

  • تأیید کنید که صورتحساب به درستی در پروژه Firebase شما تنظیم شده است
  • بررسی کنید که Firebase AI Logic و Cloud AI API در پروژه Firebase شما فعال باشند.
  • تنظیمات اتصال شبکه و فایروال خود را بررسی کنید
  • تأیید کنید که نام مدل ( gemini-2.0-flash ) صحیح و در دسترس است

مسائل مربوط به زمینه گفتگو

اگر متوجه شدید که جمینی متن قبلی چت را به خاطر نمی‌آورد:

  • تأیید کنید که تابع chatSession با @Riverpod(keepAlive: true) ‎ حاشیه‌نویسی شده است.
  • بررسی کنید که آیا برای همه تبادل پیام‌ها از یک جلسه چت استفاده می‌کنید یا خیر.
  • قبل از ارسال پیام، تأیید کنید که جلسه چت به درستی راه‌اندازی شده است

مسائل مربوط به پلتفرم خاص

برای مشکلات خاص پلتفرم:

  • iOS/macOS: اطمینان حاصل کنید که مجوزهای مناسب تنظیم شده و حداقل نسخه‌ها پیکربندی شده‌اند.
  • اندروید: بررسی کنید که حداقل نسخه SDK به درستی تنظیم شده باشد
  • پیام‌های خطای مخصوص پلتفرم را در کنسول بررسی کنید

مفاهیم کلیدی آموخته شده

  • راه‌اندازی Firebase در یک برنامه Flutter
  • پیکربندی منطق هوش مصنوعی فایربیس برای دسترسی به جمینی
  • ایجاد ارائه دهندگان Riverpod برای خدمات ناهمزمان
  • پیاده‌سازی یک سرویس چت که با یک LLM ارتباط برقرار می‌کند
  • مدیریت حالت‌های ناهمزمان API (بارگذاری، خطا، داده‌ها)
  • درک جریان ارتباطات LLM و جلسات چت

۴. راهنمایی مؤثر برای توصیف رنگ‌ها

در این مرحله، شما یک اعلان سیستمی ایجاد و پیاده‌سازی خواهید کرد که Gemini را در تفسیر توضیحات رنگ راهنمایی می‌کند. اعلان‌های سیستمی روشی قدرتمند برای سفارشی‌سازی رفتار LLM برای وظایف خاص بدون تغییر کد شما هستند.

آنچه در این مرحله خواهید آموخت

  • درک دستورات سیستم و اهمیت آنها در برنامه‌های LLM
  • ایجاد دستورالعمل‌های مؤثر برای وظایف خاص دامنه
  • بارگیری و استفاده از اعلان‌های سیستم در یک برنامه Flutter
  • راهنمایی یک LLM برای ارائه پاسخ‌های با فرمت ثابت
  • آزمایش چگونگی تأثیر اعلان‌های سیستم بر رفتار LLM

آشنایی با دستورات سیستم

قبل از پرداختن به پیاده‌سازی، بیایید بفهمیم که اعلان‌های سیستمی چیستند و چرا مهم هستند:

پیام‌های سیستمی چیستند؟

یک اعلان سیستمی نوع خاصی از دستورالعمل است که به یک LLM داده می‌شود و زمینه، دستورالعمل‌های رفتاری و انتظارات را برای پاسخ‌های آن تعیین می‌کند. برخلاف پیام‌های کاربر، اعلان‌های سیستمی:

  • نقش و شخصیت LLM را مشخص کنید
  • تعریف دانش یا قابلیت‌های تخصصی
  • ارائه دستورالعمل‌های قالب‌بندی
  • محدودیت‌هایی برای پاسخ‌ها تعیین کنید
  • نحوه مدیریت سناریوهای مختلف را شرح دهید

به یک پیام سیستمی به عنوان ارائه «شرح وظایف» به LLM فکر کنید - این پیام به مدل می‌گوید که در طول مکالمه چگونه رفتار کند.

چرا اعلان‌های سیستم مهم هستند؟

اعلان‌های سیستم برای ایجاد تعاملات LLM سازگار و مفید بسیار مهم هستند زیرا:

  1. تضمین ثبات : مدل را هدایت کنید تا پاسخ‌ها را در قالبی ثابت ارائه دهد.
  2. بهبود ارتباط : مدل را روی دامنه خاص خود (در مورد شما، رنگ‌ها) متمرکز کنید.
  3. تعیین مرزها : تعریف کنید که مدل چه کاری باید انجام دهد و چه کاری نباید انجام دهد
  4. بهبود تجربه کاربری : ایجاد یک الگوی تعاملی طبیعی‌تر و مفیدتر
  5. کاهش پس‌پردازش : دریافت پاسخ‌ها در قالب‌هایی که تجزیه یا نمایش آنها آسان‌تر است

برای برنامه Colorist خود، به LLM نیاز دارید تا به طور مداوم توضیحات رنگ را تفسیر کند و مقادیر RGB را در قالب خاصی ارائه دهد.

یک فایل اعلان سیستم ایجاد کنید

ابتدا، یک فایل اعلان سیستم ایجاد خواهید کرد که در زمان اجرا بارگذاری می‌شود. این رویکرد به شما امکان می‌دهد اعلان را بدون کامپایل مجدد برنامه خود تغییر دهید.

یک فایل جدید 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 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 بین ۰.۰ و ۱.۰ را تعیین می‌کند.

این رویکرد ساختاریافته تضمین می‌کند که پاسخ‌های LLM سازگار، آموزنده و به گونه‌ای قالب‌بندی شوند که اگر بخواهید مقادیر RGB را به صورت برنامه‌نویسی استخراج کنید، تجزیه آنها آسان باشد.

به‌روزرسانی pubspec.yaml

اکنون، پایین فایل pubspec.yaml خود را به‌روزرسانی کنید تا دایرکتوری assets را شامل شود:

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/

برای به‌روزرسانی بسته‌ی دارایی‌ها، flutter pub get را اجرا کنید.

ایجاد یک ارائه دهنده اعلان سیستم

یک فایل جدید lib/providers/system_prompt.dart ایجاد کنید تا اعلان سیستم (system prompt) را بارگذاری کند:

lib/providers/system_prompt.dart

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

part 'system_prompt.g.dart';

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

این ارائه‌دهنده از سیستم بارگذاری دارایی‌های فلاتر برای خواندن فایل اعلان در زمان اجرا استفاده می‌کند.

ارائه دهنده مدل Gemini را به روز کنید

اکنون فایل lib/providers/gemini.dart خود را طوری تغییر دهید که اعلان سیستم را نیز شامل شود:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

تغییر کلیدی، اضافه کردن 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 شما به درستی فهرست دایرکتوری assets را فهرست می‌کند.
  • بررسی کنید که مسیر موجود در rootBundle.loadString() با محل فایل شما مطابقت داشته باشد.
  • دستور flutter clean و به دنبال آن flutter pub get را اجرا کنید تا بسته‌ی دارایی‌ها به‌روزرسانی شود.

پاسخ‌های متناقض

اگر LLM به طور مداوم دستورالعمل‌های قالب‌بندی شما را دنبال نمی‌کند:

  • سعی کنید الزامات قالب‌بندی را در اعلان سیستم صریح‌تر کنید
  • مثال‌های بیشتری برای نشان دادن الگوی مورد انتظار اضافه کنید
  • مطمئن شوید که فرمتی که درخواست می‌کنید برای مدل مورد نظر معقول است.

محدود کردن نرخ API

اگر با خطاهای مربوط به محدود کردن نرخ مواجه شدید:

  • توجه داشته باشید که سرویس Firebase AI Logic محدودیت‌های استفاده دارد.
  • پیاده‌سازی منطق تلاش مجدد با backoff نمایی را در نظر بگیرید.
  • کنسول فایربیس خود را برای هرگونه مشکل سهمیه‌بندی بررسی کنید

مفاهیم کلیدی آموخته شده

  • درک نقش و اهمیت اعلان‌های سیستم در برنامه‌های LLM
  • ایجاد دستورالعمل‌های مؤثر با دستورالعمل‌ها، مثال‌ها و محدودیت‌های واضح
  • بارگیری و استفاده از اعلان‌های سیستم در یک برنامه Flutter
  • هدایت رفتار LLM برای وظایف خاص دامنه
  • استفاده از مهندسی سریع برای شکل‌دهی به پاسخ‌های LLM

این مرحله نشان می‌دهد که چگونه می‌توانید بدون تغییر کد خود - صرفاً با ارائه دستورالعمل‌های واضح در اعلان سیستم - به سفارشی‌سازی قابل توجهی در رفتار LLM دست یابید.

۵. تعریف توابع برای ابزارهای LLM

در این مرحله، شما کار فعال کردن Gemini برای انجام اقدامات در برنامه خود را با پیاده‌سازی اعلان‌های تابع آغاز خواهید کرد. این ویژگی قدرتمند به LLM اجازه می‌دهد تا نه تنها مقادیر RGB را پیشنهاد دهد، بلکه آنها را از طریق فراخوانی‌های ابزار تخصصی در رابط کاربری برنامه شما تنظیم کند. با این حال، در مرحله بعدی، مشاهده درخواست‌های LLM اجرا شده در برنامه Flutter مورد نیاز خواهد بود.

آنچه در این مرحله خواهید آموخت

  • درک فراخوانی توابع LLM و مزایای آن برای برنامه‌های Flutter
  • تعریف اعلان‌های تابع مبتنی بر طرحواره برای Gemini
  • ادغام اعلان‌های تابع با مدل Gemini شما
  • به‌روزرسانی اعلان سیستم برای استفاده از قابلیت‌های ابزار

آشنایی با فراخوانی توابع

قبل از پیاده‌سازی تعریف توابع، بیایید بفهمیم که آنها چه هستند و چرا ارزشمندند:

فراخوانی تابع چیست؟

فراخوانی تابع (که گاهی اوقات "استفاده از ابزار" نامیده می‌شود) قابلیتی است که به یک LLM اجازه می‌دهد:

  1. تشخیص دهید چه زمانی یک درخواست کاربر از فراخوانی یک تابع خاص سود می‌برد
  2. یک شیء JSON ساختاریافته با پارامترهای مورد نیاز برای آن تابع تولید کنید.
  3. بگذارید برنامه شما تابع را با آن پارامترها اجرا کند
  4. نتیجه تابع را دریافت کرده و آن را در پاسخ آن بگنجانید

به جای اینکه LLM فقط توصیف کند که چه کاری باید انجام شود، فراخوانی تابع، LLM را قادر می‌سازد تا اقدامات مشخصی را در برنامه شما انجام دهد.

چرا فراخوانی تابع برای برنامه‌های فلاتر اهمیت دارد؟

فراخوانی تابع، پلی قدرتمند بین زبان طبیعی و ویژگی‌های برنامه ایجاد می‌کند:

  1. اقدام مستقیم : کاربران می‌توانند آنچه را که می‌خواهند به زبان طبیعی توصیف کنند و برنامه با اقدامات مشخص پاسخ می‌دهد.
  2. خروجی ساختاریافته : LLM به جای متنی که نیاز به تجزیه دارد، داده‌های تمیز و ساختاریافته تولید می‌کند.
  3. عملیات پیچیده : LLM را قادر می‌سازد تا به داده‌های خارجی دسترسی پیدا کند، محاسبات را انجام دهد یا وضعیت برنامه را تغییر دهد.
  4. تجربه کاربری بهتر : یکپارچه‌سازی بی‌نقصی بین مکالمه و عملکرد ایجاد می‌کند.

در برنامه Colorist شما، فراخوانی تابع به کاربران این امکان را می‌دهد که بگویند "من رنگ سبز جنگلی می‌خواهم" و رابط کاربری بلافاصله با آن رنگ به‌روزرسانی شود، بدون اینکه نیازی به تجزیه مقادیر RGB از متن باشد.

تعریف اعلان‌های تابع

یک فایل جدید lib/services/gemini_tools.dart ایجاد کنید تا اعلان‌های تابع خود را در آن تعریف کنید:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

درک تعریف توابع

بیایید ببینیم این کد چه کاری انجام می‌دهد:

  1. نامگذاری تابع : شما تابع خود را set_color نامگذاری می‌کنید تا هدف آن را به وضوح نشان دهد.
  2. شرح تابع : شما توضیح واضحی ارائه می‌دهید که به LLM کمک می‌کند تا بفهمد چه زمانی از آن استفاده کند.
  3. تعاریف پارامتر : شما پارامترهای ساختاریافته را با توضیحات خاص خودشان تعریف می‌کنید:
    • red : جزء قرمز RGB، که به صورت عددی بین 0.0 و 1.0 مشخص می‌شود.
    • green : مؤلفه سبز RGB، که به صورت عددی بین ۰.۰ و ۱.۰ مشخص می‌شود.
    • blue : مؤلفه آبی RGB، که به صورت عددی بین 0.0 و 1.0 مشخص می‌شود.
  4. انواع طرحواره : شما از Schema.number() برای نشان دادن مقادیر عددی استفاده می‌کنید.
  5. مجموعه ابزارها : شما فهرستی از ابزارهایی که شامل تعریف تابع شما هستند را ایجاد می‌کنید.

این رویکرد ساختاریافته به دانشجویان کارشناسی ارشد مدیریت بازرگانی (LLM) در دانشگاه Gemini کمک می‌کند تا موارد زیر را درک کنند:

  • چه زمانی باید این تابع را فراخوانی کند
  • چه پارامترهایی را باید ارائه دهد
  • چه محدودیت‌هایی برای آن پارامترها اعمال می‌شود (مانند محدوده مقادیر)

ارائه دهنده مدل Gemini را به روز کنید

اکنون، فایل lib/providers/gemini.dart خود را طوری تغییر دهید که هنگام مقداردهی اولیه مدل Gemini، اعلان‌های تابع را نیز شامل شود:

lib/providers/gemini.dart

import 'dart:async';

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

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

part 'gemini.g.dart';

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

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

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

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

تغییر کلیدی، اضافه کردن پارامتر tools: geminiTools.tools هنگام ایجاد مدل مولد است. این کار باعث می‌شود Gemini از توابعی که برای فراخوانی در دسترس هستند، آگاه شود.

به‌روزرسانی اعلان سیستم

حالا باید اعلان سیستم خود را تغییر دهید تا به LLM در مورد استفاده از ابزار جدید set_color دستور دهید. assets/system_prompt.md را به‌روزرسانی کنید:

assets/system_prompt.md

# Colorist System Prompt

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

## Your Capabilities

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

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

## How to Respond to User Inputs

When users describe a color:

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

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

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

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

## When Descriptions are Unclear

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

## Important Guidelines

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

تغییرات کلیدی در اعلان سیستم عبارتند از:

  1. معرفی ابزار : به جای درخواست مقادیر RGB فرمت‌شده، اکنون ابزار set_color را به LLM اطلاع می‌دهید.
  2. فرآیند اصلاح‌شده : شما مرحله ۳ را از «قالب‌بندی مقادیر در پاسخ» به «استفاده از ابزار برای تنظیم مقادیر» تغییر می‌دهید.
  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 انتظار دارد که نتیجه تابع برگردانده شود

در وضعیت فعلی برنامه شما، سه مرحله اول در حال انجام هستند، اما شما هنوز مرحله ۴ یا ۵ (مدیریت فراخوانی‌های تابع) را پیاده‌سازی نکرده‌اید، که در مرحله بعدی انجام خواهید داد.

جزئیات فنی: چگونه Gemini تصمیم می‌گیرد چه زمانی از توابع استفاده کند

Gemini تصمیمات هوشمندانه‌ای در مورد زمان استفاده از توابع بر اساس موارد زیر می‌گیرد:

  1. قصد کاربر : اینکه آیا درخواست کاربر به بهترین شکل توسط یک تابع ارائه می‌شود یا خیر
  2. مرتبط بودن عملکرد : اینکه عملکردهای موجود چقدر با وظیفه مطابقت دارند
  3. در دسترس بودن پارامتر : آیا می‌توان با اطمینان مقادیر پارامتر را تعیین کرد؟
  4. دستورالعمل‌های سیستم : راهنمایی از طریق اعلان سیستم شما در مورد نحوه استفاده از عملکرد

با ارائه تعریف‌های واضح تابع و دستورالعمل‌های سیستمی، شما Gemini را طوری تنظیم کرده‌اید که درخواست‌های توصیف رنگ را به عنوان فرصت‌هایی برای فراخوانی تابع set_color تشخیص دهد.

بعدش چی؟

در مرحله بعد، شما هندلرهایی را برای فراخوانی‌های تابع از Gemini پیاده‌سازی خواهید کرد. این کار حلقه را کامل می‌کند و به توضیحات کاربر اجازه می‌دهد تا از طریق فراخوانی‌های تابع LLM، تغییرات رنگ واقعی را در رابط کاربری ایجاد کنند.

عیب‌یابی

مشکلات مربوط به اعلان تابع

اگر در تعریف توابع با خطا مواجه شدید:

  • بررسی کنید که نام‌ها و انواع پارامترها با آنچه انتظار می‌رود مطابقت داشته باشند
  • تأیید کنید که نام تابع واضح و توصیفی است
  • اطمینان حاصل کنید که توضیحات تابع، هدف آن را به طور دقیق توضیح می‌دهد.

مشکلات اعلان سیستم

اگر LLM سعی در استفاده از تابع ندارد:

  • تأیید کنید که اعلان سیستم شما به وضوح به LLM دستور می‌دهد که از ابزار set_color استفاده کند.
  • بررسی کنید که مثال موجود در اعلان سیستم، نحوه‌ی استفاده از تابع را نشان دهد
  • سعی کنید دستورالعمل استفاده از ابزار را واضح‌تر کنید

مسائل عمومی

اگر با مشکلات دیگری مواجه شدید:

  • کنسول را برای هرگونه خطای مربوط به تعریف توابع بررسی کنید.
  • تأیید کنید که ابزارها به درستی به مدل منتقل شده‌اند
  • اطمینان حاصل کنید که تمام کدهای تولید شده توسط Riverpod به‌روز هستند.

مفاهیم کلیدی آموخته شده

  • تعریف اعلان‌های تابع برای گسترش قابلیت‌های LLM در برنامه‌های Flutter
  • ایجاد طرحواره‌های پارامتری برای جمع‌آوری داده‌های ساختاریافته
  • ادغام اعلان‌های تابع با مدل Gemini
  • به‌روزرسانی پیام‌های سیستم برای تشویق به استفاده از عملکرد
  • درک نحوه انتخاب و فراخوانی توابع توسط LLMها

این مرحله نشان می‌دهد که چگونه LLMها می‌توانند شکاف بین ورودی زبان طبیعی و فراخوانی‌های تابع ساختاریافته را پر کنند و زمینه را برای ادغام یکپارچه بین ویژگی‌های مکالمه و برنامه فراهم کنند.

۶. پیاده‌سازی جابجایی ابزار

در این مرحله، شما هندلرهایی را برای فراخوانی‌های تابع از Gemini پیاده‌سازی خواهید کرد. این کار حلقه ارتباط بین ورودی‌های زبان طبیعی و ویژگی‌های کاربردی ملموس را تکمیل می‌کند و به LLM اجازه می‌دهد تا رابط کاربری شما را بر اساس توضیحات کاربر مستقیماً دستکاری کند.

آنچه در این مرحله خواهید آموخت

  • درک کامل خط لوله فراخوانی تابع در برنامه‌های LLM
  • Processing function calls from Gemini in a Flutter application
  • Implementing function handlers that modify application state
  • Handling function responses and returning results to the LLM
  • Creating a complete communication flow between LLM and UI
  • Logging function calls and responses for transparency

Understanding the function calling pipeline

Before diving into implementation, let's understand the complete function calling pipeline:

The end-to-end flow

  1. User input : User describes a color in natural language (eg, "forest green")
  2. LLM processing : Gemini analyzes the description and decides to call the set_color function
  3. Function call generation : Gemini creates a structured JSON with parameters (red, green, blue values)
  4. Function call reception : Your app receives this structured data from Gemini
  5. Function execution : Your app executes the function with the provided parameters
  6. State update : The function updates your app's state (changing the displayed color)
  7. Response generation : Your function returns results back to the LLM
  8. Response incorporation : The LLM incorporates these results into its final response
  9. UI update : Your UI reacts to the state change, displaying the new color

The complete communication cycle is essential for proper LLM integration. When an LLM makes a function call, it doesn't simply send the request and move on. Instead, it waits for your application to execute the function and return results. The LLM then uses these results to formulate its final response, creating a natural conversation flow that acknowledges the actions taken.

Implement function handlers

Let's update your lib/services/gemini_tools.dart file to add handlers for function calls:

lib/services/gemini_tools.dart

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

part 'gemini_tools.g.dart';

class GeminiTools {
  GeminiTools(this.ref);

  final Ref ref;

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

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

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

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

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

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

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

Understanding the function handlers

Let's break down what these function handlers do:

  1. handleFunctionCall : A central dispatcher that:
    • Logs the function call for transparency in the log panel
    • Routes to the appropriate handler based on the function name
    • Returns a structured response that will be sent back to the LLM
  2. handleSetColor : The specific handler for your set_color function that:
    • Extracts RGB values from the arguments map
    • Converts them to the expected types (doubles)
    • Updates the application's color state using the colorStateNotifier
    • Creates a structured response with success status and current color information
    • Logs the function results for debugging
  3. handleUnknownFunction : A fallback handler for unknown functions that:
    • Logs a warning about the unsupported function
    • Returns an error response to the LLM

The handleSetColor function is particularly important as it bridges the gap between the LLM's natural language understanding and concrete UI changes.

Update the Gemini chat service to process function calls and responses

Now, let's update the lib/services/gemini_chat_service.dart file to process function calls from the LLM responses and send the results back to the LLM:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

Understanding the flow of communication

The key addition here is the complete handling of function calls and responses:

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

This code:

  1. Checks if the LLM response contains any function calls
  2. For each function call, invokes your handleFunctionCall method with the function name and arguments
  3. Collects the results of each function call
  4. Sends these results back to the LLM using Content.functionResponses
  5. Processes the LLM's response to the function results
  6. Updates the UI with the final response text

This creates a round trip flow:

  • User → LLM: Requests a color
  • LLM → App: Function calls with parameters
  • App → User: New color displayed
  • App → LLM: Function results
  • LLM → User: Final response incorporating function results

Generate Riverpod code

Run the build runner command to generate the needed Riverpod code:

dart run build_runner build --delete-conflicting-outputs

Run and test the complete flow

Now run your application:

flutter run -d DEVICE

Colorist App Screenshot showing the Gemini LLM responding with a function call

Try entering various color descriptions:

  • "I'd like a deep crimson red"
  • "Show me a calming sky blue"
  • "Give me the color of fresh mint leaves"
  • "I want to see a warm sunset orange"
  • "Make it a rich royal purple"

Now you should see:

  1. Your message appearing in the chat interface
  2. Gemini's response appearing in the chat
  3. Function calls being logged in the log panel
  4. Function results being logged immediately after
  5. The color rectangle updating to display the described color
  6. RGB values updating to show the new color's components
  7. Gemini's final response appearing, often commenting on the color that was set

The log panel provides insight into what's happening behind the scenes. You'll see:

  • The exact function calls Gemini is making
  • The parameters it's choosing for each RGB value
  • The results your function is returning
  • The follow-up responses from Gemini

The color state notifier

The colorStateNotifier you're using to update colors is part of the colorist_ui package. It manages:

  • The current color displayed in the UI
  • The color history (last 10 colors)
  • Notification of state changes to UI components

When you call updateColor with new RGB values, it:

  1. Creates a new ColorData object with the provided values
  2. Updates the current color in the app state
  3. Adds the color to the history
  4. Triggers UI updates through Riverpod's state management

The UI components in the colorist_ui package watch this state and automatically update when it changes, creating a reactive experience.

Understanding error handling

Your implementation includes robust error handling:

  1. Try-catch block : Wraps all LLM interactions to catch any exceptions
  2. Error logging : Records errors in the log panel with stack traces
  3. User feedback : Provides a friendly error message in the chat
  4. State cleanup : Finalizes the message state even if an error occurs

This ensures the app remains stable and provides appropriate feedback even when issues occur with the LLM service or function execution.

The power of function calling for user experience

What you've accomplished here demonstrates how LLMs can create powerful natural interfaces:

  1. Natural language interface : Users express intent in everyday language
  2. Intelligent interpretation : The LLM translates vague descriptions into precise values
  3. Direct manipulation : The UI updates in response to natural language
  4. Contextual responses : The LLM provides conversational context about the changes
  5. Low cognitive load : Users don't need to understand RGB values or color theory

This pattern of using LLM function calling to bridge natural language and UI actions can be extended to countless other domains beyond color selection.

بعدش چی؟

In the next step, you'll enhance the user experience by implementing streaming responses. Rather than waiting for the complete response, you'll process text chunks and function calls as they are received, creating a more responsive and engaging application.

Troubleshooting

Function call issues

If Gemini isn't calling your functions or parameters are incorrect:

  • Verify your function declaration matches what's described in the system prompt
  • Check that parameter names and types are consistent
  • Ensure your system prompt explicitly instructs the LLM to use the tool
  • Verify the function name in your handler matches exactly what's in the declaration
  • Examine the log panel for detailed information on function calls

Function response issues

If function results aren't being properly sent back to the LLM:

  • Check that your function returns a properly formatted Map
  • Verify that the Content.functionResponses is being constructed correctly
  • Look for any errors in the log related to function responses
  • Ensure you're using the same chat session for the response

Color display issues

If colors aren't displaying correctly:

  • Ensure RGB values are properly converted to doubles (LLM might send them as integers)
  • Verify that values are in the expected range (0.0 to 1.0)
  • Check that the color state notifier is being called correctly
  • Examine the log for the exact values being passed to the function

General problems

For general issues:

  • Examine the logs for errors or warnings
  • Verify Firebase AI Logic connectivity
  • Check for any type mismatches in function parameters
  • Ensure all Riverpod generated code is up to date

Key concepts learned

  • Implementing a complete function calling pipeline in Flutter
  • Creating full communication between an LLM and your application
  • Processing structured data from LLM responses
  • Sending function results back to the LLM for incorporation into responses
  • Using the log panel to gain visibility into LLM-application interactions
  • Connecting natural language inputs to concrete UI changes

With this step complete, your app now demonstrates one of the most powerful patterns for LLM integration: translating natural language inputs into concrete UI actions, while maintaining a coherent conversation that acknowledges these actions. This creates an intuitive, conversational interface that feels magical to users.

7. Streaming responses for better UX

In this step, you'll enhance the user experience by implementing streaming responses from Gemini. Instead of waiting for the entire response to be generated, you'll process text chunks and function calls as they are received, creating a more responsive and engaging application.

What you'll cover in this step

  • The importance of streaming for LLM-powered applications
  • Implementing streaming LLM responses in a Flutter application
  • Processing partial text chunks as they arrive from the API
  • Managing conversation state to prevent message conflicts
  • Handling function calls in streaming responses
  • Creating visual indicators for in-progress responses

Why streaming matters for LLM applications

Before implementing, let's understand why streaming responses are crucial for creating excellent user experiences with LLMs:

Improved user experience

Streaming responses provide several significant user experience benefits:

  1. Reduced perceived latency : Users see text start appearing immediately (typically within 100-300ms), rather than waiting several seconds for a complete response. This perception of immediacy dramatically improves user satisfaction.
  2. Natural conversational rhythm : The gradual appearance of text mimics how humans communicate, creating a more natural dialogue experience.
  3. Progressive information processing : Users can begin processing information as it arrives, rather than being overwhelmed by a large block of text all at once.
  4. Opportunity for early interruption : In a full application, users could potentially interrupt or redirect the LLM if they see it going in an unhelpful direction.
  5. Visual confirmation of activity : The streaming text provides immediate feedback that the system is working, reducing uncertainty.

Technical advantages

Beyond UX improvements, streaming offers technical benefits:

  1. Early function execution : Function calls can be detected and executed as soon as they appear in the stream, without waiting for the complete response.
  2. Incremental UI updates : You can update your UI progressively as new information arrives, creating a more dynamic experience.
  3. Conversation state management : Streaming provides clear signals about when responses are complete vs. still in progress, enabling better state management.
  4. Reduced timeout risks : With non-streaming responses, long-running generations risk connection timeouts. Streaming establishes the connection early and maintains it.

For your Colorist app, implementing streaming means users will see both text responses and color changes appearing more promptly, creating a significantly more responsive experience.

Add conversation state management

First, let's add a state provider to track whether the app is currently handling a streaming response. Update your lib/services/gemini_chat_service.dart file:

lib/services/gemini_chat_service.dart

import 'dart:async';

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

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

Understanding the streaming implementation

Let's break down what this code does:

  1. Conversation state tracking :
    • A conversationStateProvider tracks whether the app is currently processing a response
    • The state transitions from idlebusy while processing, then back to idle
    • This prevents multiple concurrent requests that could conflict
  2. Stream initialization :
    • sendMessageStream() returns a Stream of response chunks instead of a Future with the complete response
    • Each chunk may contain text, function calls, or both
  3. Progressive processing :
    • await for processes each chunk as it arrives in real-time
    • Text is appended to the UI immediately, creating the streaming effect
    • Function calls are executed as soon as they're detected
  4. Function call handling :
    • When a function call is detected in a chunk, it's executed immediately
    • Results are sent back to the LLM through another streaming call
    • The LLM's response to these results is also processed in a streaming fashion
  5. Error handling and cleanup :
    • try / catch provides robust error handling
    • The finally block ensures conversation state is reset properly
    • Message is always finalized, even if errors occur

This implementation creates a responsive, reliable streaming experience while maintaining proper conversation state.

Update the main screen to connect conversation state

Modify your lib/main.dart file to pass the conversation state to the main screen:

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

The key change here is passing the conversationState to the MainScreen widget. The MainScreen (provided by the colorist_ui package) will use this state to disable the text input while a response is being processed.

This creates a cohesive user experience where the UI reflects the current state of the conversation.

Generate Riverpod code

Run the build runner command to generate the needed Riverpod code:

dart run build_runner build --delete-conflicting-outputs

Run and test streaming responses

Run your application:

flutter run -d DEVICE

Colorist App Screenshot showing the Gemini LLM responding in a streaming fashion

Now try testing the streaming behavior with various color descriptions. Try descriptions like:

  • "Show me the deep teal color of the ocean at twilight"
  • "I'd like to see a vibrant coral that reminds me of tropical flowers"
  • "Create a muted olive green like old army fatigues"

The streaming technical flow in detail

Let's examine exactly what happens when streaming a response:

Connection establishment

When you call sendMessageStream() , the following happens:

  1. The app establishes a connection to the Firebase AI Logic service
  2. The user request is sent to the service
  3. The server begins processing the request
  4. The stream connection remains open, ready to transmit chunks

Chunk transmission

As Gemini generates content, chunks are sent through the stream:

  1. The server sends text chunks as they're generated (typically a few words or sentences)
  2. When Gemini decides to make a function call, it sends the function call information
  3. Additional text chunks may follow function calls
  4. The stream continues until the generation is complete

Progressive processing

Your app processes each chunk incrementally:

  1. Each text chunk is appended to the existing response
  2. Function calls are executed as soon as they're detected
  3. The UI updates in real-time with both text and function results
  4. State is tracked to show the response is still streaming

Stream completion

When the generation is complete:

  1. The stream is closed by the server
  2. Your await for loop exits naturally
  3. The message is marked as complete
  4. The conversation state is set back to idle
  5. The UI updates to reflect the completed state

Streaming vs. non-streaming comparison

To better understand the benefits of streaming, let's compare streaming vs. non-streaming approaches:

جنبه

Non-Streaming

پخش جریانی

Perceived latency

User sees nothing until complete response is ready

User sees first words within milliseconds

تجربه کاربری

Long wait followed by sudden text appearance

Natural, progressive text appearance

State management

Simpler (messages are either pending or complete)

More complex (messages can be in a streaming state)

Function execution

Occurs only after complete response

Occurs during response generation

Implementation complexity

Simpler to implement

Requires additional state management

Error recovery

All-or-nothing response

Partial responses may still be useful

Code complexity

Less complex

More complex due to stream handling

For an application like Colorist, the UX benefits of streaming outweigh the implementation complexity, especially for color interpretations that might take several seconds to generate.

Best practices for streaming UX

When implementing streaming in your own LLM applications, consider these best practices:

  1. Clear visual indicators : Always provide clear visual cues that distinguish streaming vs. complete messages
  2. Input blocking : Disable user input during streaming to prevent multiple overlapping requests
  3. Error recovery : Design your UI to handle graceful recovery if streaming is interrupted
  4. State transitions : Ensure smooth transitions between idle, streaming, and complete states
  5. Progress visualization : Consider subtle animations or indicators that show active processing
  6. Cancellation options : In a complete app, provide ways for users to cancel in-progress generations
  7. Function result integration : Design your UI to handle function results appearing mid-stream
  8. Performance optimization : Minimize UI rebuilds during rapid stream updates

The colorist_ui package implements many of these best practices for you, but they're important considerations for any streaming LLM implementation.

بعدش چی؟

In the next step, you'll implement LLM synchronization by notifying Gemini when users select colors from history. This will create a more cohesive experience where the LLM is aware of user-initiated changes to the application state.

Troubleshooting

Stream processing issues

If you encounter issues with stream processing:

  • Symptoms : Partial responses, missing text, or abrupt stream termination
  • Solution : Check network connectivity and ensure proper async/await patterns in your code
  • Diagnosis : Examine the log panel for error messages or warnings related to stream processing
  • Fix : Ensure all stream processing uses proper error handling with try / catch blocks

Missing function calls

If function calls aren't being detected in the stream:

  • Symptoms : Text appears but colors don't update, or log shows no function calls
  • Solution : Verify the system prompt's instructions about using function calls
  • Diagnosis : Check the log panel to see if function calls are being received
  • Fix : Adjust your system prompt to more explicitly instruct the LLM to use the set_color tool

General error handling

For any other issues:

  • Step 1 : Check the log panel for error messages
  • Step 2 : Verify Firebase AI Logic connectivity
  • Step 3 : Ensure all Riverpod generated code is up to date
  • Step 4 : Review the streaming implementation for any missing await statements

Key concepts learned

  • Implementing streaming responses with the Gemini API for more responsive UX
  • Managing conversation state to handle streaming interactions properly
  • Processing real-time text and function calls as they arrive
  • Creating responsive UIs that update incrementally during streaming
  • Handling concurrent streams with proper async patterns
  • Providing appropriate visual feedback during streaming responses

By implementing streaming, you've significantly enhanced the user experience of your Colorist app, creating a more responsive, engaging interface that feels truly conversational.

8. LLM Context Synchronization

In this bonus step, you'll implement LLM Context Synchronization by notifying Gemini when users select colors from history. This creates a more cohesive experience where the LLM is aware of user actions in the interface, not just their explicit messages.

What you'll cover in this step

  • Creating LLM Context Synchronization between your UI and the LLM
  • Serializing UI events into context the LLM can understand
  • Updating conversation context based on user actions
  • Creating a coherent experience across different interaction methods
  • Enhancing LLM context awareness beyond explicit chat messages

Understanding LLM Context Synchronization

Traditional chatbots only respond to explicit user messages, creating a disconnect when users interact with the app through other means. LLM Context Synchronization addresses this limitation:

Why LLM Context Synchronization matters

When users interact with your app through UI elements (like selecting a color from history), the LLM has no way of knowing what happened unless you explicitly tell it. LLM Context Synchronization:

  1. Maintains context : Keeps the LLM informed about all relevant user actions
  2. Creates coherence : Produces a cohesive experience where the LLM acknowledges UI interactions
  3. Enhances intelligence : Allows the LLM to respond appropriately to all user actions
  4. Improves user experience : Makes the entire application feel more integrated and responsive
  5. Reduces user effort : Eliminates the need for users to manually explain their UI actions

In your Colorist app, when a user selects a color from history, you want Gemini to acknowledge this action and comment intelligently about the selected color, maintaining the illusion of a seamless, aware assistant.

Update the Gemini chat service for color selection notifications

First, you'll add a method to the GeminiChatService to notify the LLM when a user selects a color from history. Update your lib/services/gemini_chat_service.dart file:

lib/services/gemini_chat_service.dart

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

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

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

part 'gemini_chat_service.g.dart';

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

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

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

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

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

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

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

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

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

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

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

The key addition is the notifyColorSelection method, which:

  1. Takes a ColorData object representing the selected color
  2. Encodes it to a JSON format that can be included in a message
  3. Sends a specially formatted message to the LLM indicating a user selection
  4. Reuses the existing sendMessage method to handle the notification

This approach avoids duplication by utilizing your existing message handling infrastructure.

Update main app to connect color selection notifications

Now, modify your lib/main.dart file to pass the color selection notification function to the main screen:

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

The key change is adding the notifyColorSelection callback, which connects the UI event (selecting a color from history) to the LLM notification system.

Update the system prompt

Now, you need to update your system prompt to instruct the LLM on how to respond to color selection notifications. Modify your assets/system_prompt.md file:

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

The key addition is the "When Users Select Historical Colors" section, which:

  1. Explains the concept of history selection notifications to the LLM
  2. Provides an example of what these notifications look like
  3. Shows an example of an appropriate response
  4. Sets expectations for acknowledging the selection and commenting on the color

This helps the LLM understand how to respond appropriately to these special messages.

Generate Riverpod Code

Run the build runner command to generate the needed Riverpod code:

dart run build_runner build --delete-conflicting-outputs

Run and test LLM Context Synchronization

Run your application:

flutter run -d DEVICE

Colorist App Screenshot showing the Gemini LLM responding to a selection from color history

Testing the LLM Context Synchronization involves:

  1. First, generate a few colors by describing them in the chat
    • "Show me a vibrant purple"
    • "I'd like a forest green"
    • "Give me a bright red"
  2. Then, click on one of the color thumbnails in the history strip

You should observe:

  1. The selected color appears in the main display
  2. A user message appears in the chat indicating the color selection
  3. The LLM responds by acknowledging the selection and commenting on the color
  4. The entire interaction feels natural and cohesive

This creates a seamless experience where the LLM is aware of and responds appropriately to both direct messages and UI interactions.

How LLM Context Synchronization works

Let's explore the technical details of how this synchronization works:

Data Flow

  1. User action : User clicks a color in the history strip
  2. UI event : The MainScreen widget detects this selection
  3. Callback execution : The notifyColorSelection callback is triggered
  4. Message creation : A specially formatted message is created with the color data
  5. LLM processing : The message is sent to Gemini, which recognizes the format
  6. Contextual response : Gemini responds appropriately based on the system prompt
  7. UI update : The response appears in the chat, creating a cohesive experience

Data serialization

A key aspect of this approach is how you serialize the color data:

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

The toLLMContextMap() method (provided by the colorist_ui package) converts a ColorData object into a map with key properties that the LLM can understand. This typically includes:

  • RGB values (red, green, blue)
  • Hex code representation
  • Any name or description associated with the color

By formatting this data consistently and including it in the message, you ensure the LLM has all the information it needs to respond appropriately.

Broader applications of LLM Context Synchronization

This pattern of notifying the LLM about UI events has numerous applications beyond color selection:

Other use cases

  1. Filter changes : Notify the LLM when users apply filters to data
  2. Navigation events : Inform the LLM when users navigate to different sections
  3. Selection changes : Update the LLM when users select items from lists or grids
  4. Preference updates : Tell the LLM when users change settings or preferences
  5. Data manipulation : Notify the LLM when users add, edit, or delete data

In each case, the pattern remains the same:

  1. Detect the UI event
  2. Serialize relevant data
  3. Send a specially formatted notification to the LLM
  4. Guide the LLM to respond appropriately through the system prompt

Best practices for LLM Context Synchronization

Based on your implementation, here are some best practices for effective LLM Context Synchronization:

1. Consistent formatting

Use a consistent format for notifications so the LLM can easily identify them:

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

2. Rich context

Include enough detail in notifications for the LLM to respond intelligently. For colors, this means RGB values, hex codes, and any other relevant properties.

3. Clear instructions

Provide explicit instructions in the system prompt about how to handle notifications, ideally with examples.

4. Natural integration

Design notifications to flow naturally in the conversation, not as technical interruptions.

5. Selective notification

Only notify the LLM about actions that are relevant to the conversation. Not every UI event needs to be communicated.

Troubleshooting

Notification issues

If the LLM isn't responding properly to color selections:

  • Check that the notification message format matches what's described in the system prompt
  • Verify that the color data is being properly serialized
  • Ensure the system prompt has clear instructions for handling selections
  • Look for any errors in the chat service when sending notifications

Context management

If the LLM seems to lose context:

  • Check that the chat session is being maintained properly
  • Verify that conversation states transition correctly
  • Ensure that notifications are being sent through the same chat session

General problems

For general issues:

  • Examine the logs for errors or warnings
  • Verify Firebase AI Logic connectivity
  • Check for any type mismatches in function parameters
  • Ensure all Riverpod generated code is up to date

Key concepts learned

  • Creating LLM Context Synchronization between UI and LLM
  • Serializing UI events into LLM-friendly context
  • Guiding LLM behavior for different interaction patterns
  • Creating a cohesive experience across message and non-message interactions
  • Enhancing LLM awareness of the broader application state

By implementing LLM Context Synchronization, you've created a truly integrated experience where the LLM feels like an aware, responsive assistant rather than just a text generator. This pattern can be applied to countless other applications to create more natural, intuitive AI-powered interfaces.

۹. تبریک می‌گویم!

You've successfully completed the Colorist codelab! 🎉

What you've built

You've created a fully functional Flutter application that integrates Google's Gemini API to interpret natural language color descriptions. Your app can now:

  • Process natural language descriptions like "sunset orange" or "deep ocean blue"
  • Use Gemini to intelligently translate these descriptions into RGB values
  • Display the interpreted colors in real-time with streaming responses
  • Handle user interactions through both chat and UI elements
  • Maintain contextual awareness across different interaction methods

از اینجا به کجا برویم؟

Now that you've mastered the basics of integrating Gemini with Flutter, here are some ways to continue your journey:

Enhance your Colorist app

  • Color palettes : Add functionality to generate complementary or matching color schemes
  • Voice input : Integrate speech recognition for verbal color descriptions
  • History management : Add options to name, organize, and export color sets
  • Custom prompting : Create an interface for users to customize system prompts
  • Advanced analytics : Track which descriptions work best or cause difficulties

Explore more Gemini features

  • Multimodal inputs : Add image inputs to extract colors from photos
  • Content generation : Use Gemini to generate color-related content like descriptions or stories
  • Function calling enhancements : Create more complex tool integrations with multiple functions
  • Safety settings : Explore different safety settings and their impact on responses

Apply these patterns to other domains

  • Document analysis : Create apps that can understand and analyze documents
  • Creative writing assistance : Build writing tools with LLM-powered suggestions
  • Task automation : Design apps that translate natural language into automated tasks
  • Knowledge-based applications : Create expert systems in specific domains

منابع

Here are some valuable resources to continue your learning:

اسناد رسمی

Prompting course and guide

جامعه

Observable Flutter Agentic series

In expisode #59, Craig Labenz and Andrew Brogden explore this codelab, highlighting interesting parts of the app build.

In episode #60, join Craig and Andrew again as they extend the codelab app with new capabilities and fight with making LLMs do as they are told.

In episode #61, Craig is joined by Chris Sells to have a fresh take at analysing news headlines and generates corresponding images.

بازخورد

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!