flutter_gpt_engine 0.0.8 copy "flutter_gpt_engine: ^0.0.8" to clipboard
flutter_gpt_engine: ^0.0.8 copied to clipboard

Headless Flutter package for running GGUF LLMs locally on Android with streaming, conversation history, web search, device context, thinking support, benchmarking, and CPU/GPU auto-tuning.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_gpt_engine/flutter_gpt_engine.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter GPT Engine',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: const AiExamplePage(),
    );
  }
}

class AiExamplePage extends StatefulWidget {
  const AiExamplePage({super.key});

  @override
  State<AiExamplePage> createState() => _AiExamplePageState();
}

class _AiExamplePageState extends State<AiExamplePage> {
  late final LocalLlmClient _gpt;

  StreamSubscription<LocalLlmGenerationEvent>? _generationSubscription;

  final TextEditingController _controller = TextEditingController();
  final ScrollController _scrollController = ScrollController();

  bool _loadingModel = false;
  bool _generating = false;

  String _status = 'Select a GGUF model';

  String _thinkingText = '';
  String _answerText = '';

  final List<_ChatMessage> _messages = <_ChatMessage>[];

  // ============================================================
  // INIT
  // ============================================================

  @override
  void initState() {
    super.initState();

    _gpt = LocalLlmClient(
      // ========================================================
      // LOCAL MODEL CONFIG
      // ========================================================

      config: const LocalLlmConfig(
        systemPrompt: '''
You are a helpful AI assistant.

LANGUAGE RULES:
- Always reply in the same language and writing style as the user.
- If the user writes English, reply in English.
- If the user writes Bangla, reply in Bangla.
- If the user writes Banglish, reply in Banglish.

DEVICE CONTEXT RULES:
- The application may provide real device information.
- Use device information when relevant.
- Never invent unavailable device information.
- Never invent location, weather, battery, network, date or time.
''',

        threads: 4,
        contextSize: 4096,

        // null = GPU auto detect
        gpuLayers: null,

        temperature: 0.60,
        topP: 0.90,
        topK: 40,
        repeatPenalty: 1.15,

        maxTokens: 512,
        maxHistoryMessages: 8,

        // Realtime model-emitted thinking support.
        showThinking: true,
      ),

      // ========================================================
      // WEB SEARCH
      // ========================================================

      webSearchConfig: const WebSearchConfig(
        enabled: true,

        useWikipedia: true,
        useGoogle: true,
        directUrlFetch: true,
        useOfficialSourceHints: true,

        // Local model যদি clearly answer না জানে,
        // package automatically web search করবে.
        fallbackOnLocalFailure: true,

        maxResults: 3,
        maxPageCharacters: 5000,
        maxTotalContextCharacters: 9000,

        timeout: Duration(
          seconds: 12,
        ),
      ),

      // ========================================================
      // DEVICE CONTEXT
      // ========================================================

      deviceContextConfig: const DeviceContextConfig(
        // Device context ON.
        enabled: true,

        // Other device data question অনুযায়ী collect হবে.
        mode: DeviceContextMode.auto,

        // Basic
        includeDateTime: true,
        includeTimezone: true,
        includeLocale: true,

        // Device
        includeDeviceInfo: true,
        includeOsInfo: true,
        includeAppInfo: true,

        // System
        includeBattery: true,
        includeNetwork: true,
        includeStorage: true,
        includeMemory: true,
        includeScreenInfo: true,

        // Location
        includeLocation: true,
        includeAddress: true,
        includeAltitude: true,
        includeSpeed: true,
        includeHeading: true,

        // Sensors
        includeSensors: true,
        includeBarometer: true,

        // Weather
        includeWeather: true,

        // Location/weather related প্রশ্ন করলে
        // প্রয়োজন হলে permission dialog দেখাবে.
        requestLocationPermissionWhenNeeded: true,

        basicCacheDuration: Duration(
          minutes: 5,
        ),

        locationCacheDuration: Duration(
          minutes: 2,
        ),

        weatherCacheDuration: Duration(
          minutes: 15,
        ),

        sensorTimeout: Duration(
          seconds: 2,
        ),

        locationTimeout: Duration(
          seconds: 10,
        ),

        weatherTimeout: Duration(
          seconds: 8,
        ),
      ),
    );

    _listenGenerationEvents();
  }

  // ============================================================
  // MODEL PICK + LOAD
  // ============================================================

  Future<void> _pickAndLoadModel() async {
    if (_loadingModel || _generating) {
      return;
    }

    setState(() {
      _loadingModel = true;
      _status = 'Selecting model...';
    });

    try {
      final bool loaded = await _gpt.pickModel(
        // Selected model app-private storage এ copy হবে.
        persist: true,
      );

      if (!mounted) {
        return;
      }

      if (!loaded) {
        setState(() {
          _status = _gpt.isLoaded
              ? 'Ready'
              : 'Model selection cancelled';
        });

        return;
      }

      setState(() {
        _status =
        'Ready • ${_gpt.model?.name ?? "GGUF Model"}';
      });
    } catch (e) {
      if (!mounted) {
        return;
      }

      setState(() {
        _status = 'Model load error: $e';
      });
    } finally {
      if (mounted) {
        setState(() {
          _loadingModel = false;
        });
      }
    }
  }

  // ============================================================
  // GENERATION EVENTS
  // ============================================================

  void _listenGenerationEvents() {
    _generationSubscription =
        _gpt.generationEvents.listen(
              (LocalLlmGenerationEvent event) {
            if (!mounted) {
              return;
            }

            // ======================================================
            // THINKING
            // ======================================================

            if (event.isThinking) {
              setState(() {
                _thinkingText = event.text;
                _status = 'Thinking...';
              });

              _scrollToBottom();

              return;
            }

            // ======================================================
            // SEARCHING WEB
            // ======================================================

            if (event.isSearchingWeb) {
              setState(() {
                _status = 'Searching web...';
              });

              return;
            }

            // ======================================================
            // ANSWER
            // ======================================================

            if (event.isAnswer) {
              setState(() {
                // Final answer শুরু হলেই
                // thinking UI remove হবে.
                _thinkingText = '';

                _answerText = event.text;

                if (_messages.isNotEmpty &&
                    _messages.last.role == 'assistant') {
                  _messages.last.text = event.text;
                }

                _status = 'Generating...';
              });

              _scrollToBottom();

              return;
            }

            // ======================================================
            // DONE
            // ======================================================

            if (event.isDone) {
              setState(() {
                _thinkingText = '';
                _answerText = event.text;

                if (_messages.isNotEmpty &&
                    _messages.last.role == 'assistant') {
                  _messages.last.text = event.text;
                }

                _generating = false;
                _status = 'Ready';
              });

              _scrollToBottom();
            }
          },
          onError: (Object error) {
            if (!mounted) {
              return;
            }

            setState(() {
              _generating = false;
              _thinkingText = '';
              _status = 'Error: $error';
            });
          },
        );
  }

  // ============================================================
  // DATE + TIME
  // ALWAYS ON
  // ============================================================

  String _buildAlwaysDateTimeContext() {
    final DateTime now = DateTime.now();

    final Duration offset = now.timeZoneOffset;

    final String sign =
    offset.isNegative ? '-' : '+';

    final int totalMinutes =
    offset.inMinutes.abs();

    final int offsetHour =
        totalMinutes ~/ 60;

    final int offsetMinute =
        totalMinutes % 60;

    final String offsetText =
        '$sign'
        '${offsetHour.toString().padLeft(2, '0')}:'
        '${offsetMinute.toString().padLeft(2, '0')}';

    return '''
CURRENT DEVICE DATE AND TIME:

Current Date: ${_formatDate(now)}
Current Time: ${_formatTime(now)}
Current Day: ${_weekday(now.weekday)}
Timezone Name: ${now.timeZoneName}
UTC Offset: $offsetText
ISO DateTime: ${now.toIso8601String()}

IMPORTANT DATE/TIME RULES:
- This information comes directly from the user's device.
- Treat this as the authoritative current date and time.
- If the user asks "what time is it", "ajke date koto", "today", "now",
  "current time", "current date", "what day is today", or similar,
  answer using this device information.
- Never use pretrained model knowledge to guess the current date or time.
''';
  }

  // ============================================================
  // DATE FORMAT
  // ============================================================

  String _formatDate(DateTime dateTime) {
    final String year =
    dateTime.year.toString();

    final String month =
    dateTime.month
        .toString()
        .padLeft(2, '0');

    final String day =
    dateTime.day
        .toString()
        .padLeft(2, '0');

    return '$year-$month-$day';
  }

  // ============================================================
  // TIME FORMAT
  // ============================================================

  String _formatTime(DateTime dateTime) {
    int hour = dateTime.hour;

    final String minute =
    dateTime.minute
        .toString()
        .padLeft(2, '0');

    final String second =
    dateTime.second
        .toString()
        .padLeft(2, '0');

    final String period =
    hour >= 12 ? 'PM' : 'AM';

    if (hour == 0) {
      hour = 12;
    } else if (hour > 12) {
      hour -= 12;
    }

    return '${hour.toString().padLeft(2, '0')}:'
        '$minute:$second $period';
  }

  // ============================================================
  // WEEKDAY
  // ============================================================

  String _weekday(int weekday) {
    switch (weekday) {
      case DateTime.monday:
        return 'Monday';

      case DateTime.tuesday:
        return 'Tuesday';

      case DateTime.wednesday:
        return 'Wednesday';

      case DateTime.thursday:
        return 'Thursday';

      case DateTime.friday:
        return 'Friday';

      case DateTime.saturday:
        return 'Saturday';

      case DateTime.sunday:
        return 'Sunday';

      default:
        return '';
    }
  }

  // ============================================================
  // RUNTIME SYSTEM PROMPT
  // ============================================================

  String get _runtimeSystemPrompt {
    return '''
You are a helpful AI assistant.

LANGUAGE RULES:
- Always reply in the same language and writing style as the user's latest message.
- English message -> English reply.
- Bangla message -> Bangla reply.
- Banglish message -> Banglish reply.
- Keep technical names, file names, package names, APIs and code unchanged.

${_buildAlwaysDateTimeContext()}

DEVICE CONTEXT RULES:
- Flutter GPT Engine may provide current device information.
- Device information is newer and more reliable than pretrained knowledge.
- Use device information only when relevant to the user's question.
- Never invent unavailable device information.
- Never invent battery percentage.
- Never invent network state.
- Never invent GPS coordinates.
- Never invent altitude.
- Never invent weather data.
- Never invent device model or operating system information.
- If a requested device value is unavailable, clearly say that it is unavailable.

WEB SEARCH RULES:
- Fresh public web information may be supplied when required.
- Fresh web information is newer than pretrained model knowledge.
- For current/latest/news/version/release/price/weather questions,
  prefer supplied fresh information.
- Never invent a current fact when it cannot be verified.

ANSWER STYLE:
- Answer the user's actual question directly.
- Be concise unless the user asks for details.
''';
  }

  // ============================================================
  // SEND
  // ============================================================

  Future<void> _sendMessage() async {
    final String text =
    _controller.text.trim();

    if (text.isEmpty) {
      return;
    }

    // Model must be loaded first.
    if (!_gpt.isLoaded) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text(
            'Please select a GGUF model first.',
          ),
        ),
      );

      return;
    }

    if (_generating) {
      return;
    }

    FocusScope.of(context).unfocus();

    _controller.clear();

    setState(() {
      // User message
      _messages.add(
        _ChatMessage(
          role: 'user',
          text: text,
        ),
      );

      // Empty assistant message.
      // Stream events will update this message.
      _messages.add(
        _ChatMessage(
          role: 'assistant',
          text: '',
        ),
      );

      _thinkingText = '';
      _answerText = '';

      _generating = true;
      _status = 'Preparing...';
    });

    _scrollToBottom();

    try {
      // ========================================================
      // SMART GENERATE
      // ========================================================

      await for (final String _ in _gpt.smartGenerate(
        text,

        // IMPORTANT:
        // This is rebuilt EVERY message,
        // therefore Date/Time is always current.
        systemPrompt: _runtimeSystemPrompt,

        // Realtime Think
        showThinking: true,

        // Local model fail করলে web fallback
        fallbackOnLocalFailure: true,

        // Latest/current/etc -> auto web search
        searchMode: WebSearchMode.auto,
      )) {
        // No UI work required here.
        //
        // generationEvents listener handles:
        // thinking
        // searchingWeb
        // answer
        // done
      }

      if (!mounted) {
        return;
      }

      setState(() {
        _generating = false;
        _thinkingText = '';

        // Extra final safety.
        if (_gpt.answerText.trim().isNotEmpty) {
          _answerText =
              _gpt.answerText.trim();

          if (_messages.isNotEmpty &&
              _messages.last.role == 'assistant') {
            _messages.last.text =
                _answerText;
          }
        }

        _status = 'Ready';
      });

      _scrollToBottom();
    } catch (e) {
      if (!mounted) {
        return;
      }

      setState(() {
        _generating = false;
        _thinkingText = '';

        if (_messages.isNotEmpty &&
            _messages.last.role == 'assistant' &&
            _messages.last.text.trim().isEmpty) {
          _messages.last.text =
          'Error: $e';
        }

        _status = 'Error';
      });

      _scrollToBottom();
    }
  }

  // ============================================================
  // STOP
  // ============================================================

  Future<void> _stopGeneration() async {
    await _gpt.stop();

    if (!mounted) {
      return;
    }

    setState(() {
      _generating = false;

      // Thinking immediately remove.
      _thinkingText = '';

      // Keep already-generated final answer.
      if (_gpt.answerText.trim().isNotEmpty &&
          _messages.isNotEmpty &&
          _messages.last.role == 'assistant') {
        _messages.last.text =
            _gpt.answerText.trim();
      }

      _status = 'Stopped';
    });

    _scrollToBottom();
  }

  // ============================================================
  // SCROLL
  // ============================================================

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback(
          (_) {
        if (!_scrollController.hasClients) {
          return;
        }

        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(
            milliseconds: 180,
          ),
          curve: Curves.easeOut,
        );
      },
    );
  }

  // ============================================================
  // DISPOSE
  // ============================================================

  @override
  void dispose() {
    _generationSubscription?.cancel();

    _controller.dispose();
    _scrollController.dispose();

    _gpt.dispose();

    super.dispose();
  }

  // ============================================================
  // UI
  // ============================================================

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ========================================================
      // APP BAR
      // ========================================================

      appBar: AppBar(
        title: const Text(
          'Flutter GPT Engine',
        ),
      ),

      // ========================================================
      // BODY
      // ========================================================

      body: SafeArea(
        child: Column(
          children: [
            // ==================================================
            // MODEL SELECT BUTTON
            // ==================================================

            Padding(
              padding: const EdgeInsets.fromLTRB(
                16,
                12,
                16,
                6,
              ),
              child: SizedBox(
                width: double.infinity,
                child: ElevatedButton.icon(
                  onPressed:
                  _loadingModel || _generating
                      ? null
                      : _pickAndLoadModel,

                  icon: _loadingModel
                      ? const SizedBox(
                    width: 18,
                    height: 18,
                    child:
                    CircularProgressIndicator(
                      strokeWidth: 2,
                    ),
                  )
                      : const Icon(
                    Icons.folder_open,
                  ),

                  label: Text(
                    _loadingModel
                        ? 'Loading Model...'
                        : _gpt.isLoaded
                        ? 'Change GGUF Model'
                        : 'Select GGUF Model',
                  ),
                ),
              ),
            ),

            // ==================================================
            // MODEL NAME
            // ==================================================

            if (_gpt.isLoaded)
              Padding(
                padding:
                const EdgeInsets.symmetric(
                  horizontal: 16,
                ),
                child: Row(
                  children: [
                    const Icon(
                      Icons.memory,
                      size: 16,
                    ),

                    const SizedBox(
                      width: 6,
                    ),

                    Expanded(
                      child: Text(
                        _gpt.model?.name ??
                            'GGUF Model',
                        maxLines: 1,
                        overflow:
                        TextOverflow.ellipsis,
                        style:
                        Theme.of(context)
                            .textTheme
                            .bodySmall,
                      ),
                    ),
                  ],
                ),
              ),

            // ==================================================
            // STATUS
            // ==================================================

            Padding(
              padding: const EdgeInsets.fromLTRB(
                16,
                6,
                16,
                8,
              ),
              child: Row(
                children: [
                  if (_loadingModel ||
                      _generating) ...[
                    const SizedBox(
                      width: 14,
                      height: 14,
                      child:
                      CircularProgressIndicator(
                        strokeWidth: 2,
                      ),
                    ),

                    const SizedBox(
                      width: 8,
                    ),
                  ],

                  Expanded(
                    child: Text(
                      _status,
                      style:
                      Theme.of(context)
                          .textTheme
                          .bodySmall,
                    ),
                  ),
                ],
              ),
            ),

            const Divider(
              height: 1,
            ),

            // ==================================================
            // CHAT AREA
            // ==================================================

            Expanded(
              child: _messages.isEmpty &&
                  _thinkingText
                      .trim()
                      .isEmpty
                  ? _buildEmptyState()
                  : ListView.builder(
                controller:
                _scrollController,

                padding:
                const EdgeInsets.all(
                  16,
                ),

                itemCount:
                _messages.length +
                    (_thinkingText
                        .trim()
                        .isNotEmpty
                        ? 1
                        : 0),

                itemBuilder:
                    (context, index) {
                  if (index <
                      _messages.length) {
                    final _ChatMessage
                    message =
                    _messages[index];

                    // Empty assistant message
                    // final output না আসা পর্যন্ত
                    // bubble দেখাবো না.
                    if (message.role ==
                        'assistant' &&
                        message.text
                            .trim()
                            .isEmpty) {
                      return const SizedBox
                          .shrink();
                    }

                    return _MessageBubble(
                      message: message,
                    );
                  }

                  // ======================================
                  // THINKING
                  // ======================================

                  return _ThinkingBubble(
                    text: _thinkingText,
                  );
                },
              ),
            ),

            const Divider(
              height: 1,
            ),

            // ==================================================
            // INPUT AREA
            // ==================================================

            Padding(
              padding: const EdgeInsets.all(
                12,
              ),
              child: Row(
                crossAxisAlignment:
                CrossAxisAlignment.end,
                children: [
                  // ============================================
                  // TEXT INPUT
                  // ============================================

                  Expanded(
                    child: TextField(
                      controller: _controller,

                      enabled:
                      !_loadingModel,

                      minLines: 1,
                      maxLines: 5,

                      textInputAction:
                      TextInputAction.send,

                      decoration:
                      InputDecoration(
                        hintText:
                        _gpt.isLoaded
                            ? 'Ask anything...'
                            : 'Select a GGUF model first...',

                        border:
                        const OutlineInputBorder(),

                        contentPadding:
                        const EdgeInsets
                            .symmetric(
                          horizontal: 14,
                          vertical: 12,
                        ),
                      ),

                      onSubmitted: (_) {
                        if (!_generating) {
                          _sendMessage();
                        }
                      },
                    ),
                  ),

                  const SizedBox(
                    width: 8,
                  ),

                  // ============================================
                  // SEND / STOP
                  // ============================================

                  if (_generating)
                    IconButton.filled(
                      tooltip: 'Stop',

                      onPressed:
                      _stopGeneration,

                      icon: const Icon(
                        Icons.stop,
                      ),
                    )
                  else
                    IconButton.filled(
                      tooltip: 'Send',

                      onPressed:
                      _gpt.isLoaded
                          ? _sendMessage
                          : null,

                      icon: const Icon(
                        Icons.send,
                      ),
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  // ============================================================
  // EMPTY STATE
  // ============================================================

  Widget _buildEmptyState() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(
          24,
        ),
        child: Column(
          mainAxisSize:
          MainAxisSize.min,
          children: [
            Icon(
              Icons.smart_toy_outlined,
              size: 64,
              color: Theme.of(context)
                  .colorScheme
                  .primary,
            ),

            const SizedBox(
              height: 16,
            ),

            Text(
              _gpt.isLoaded
                  ? 'AI Ready'
                  : 'Select a GGUF Model',
              style: Theme.of(context)
                  .textTheme
                  .titleLarge,
            ),

            const SizedBox(
              height: 8,
            ),

            Text(
              _gpt.isLoaded
                  ? 'Ask anything to start chatting.'
                  : 'Select a local .gguf model file to start.',
              textAlign:
              TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

// ============================================================
// CHAT MESSAGE
// ============================================================

class _ChatMessage {
  _ChatMessage({
    required this.role,
    required this.text,
  });

  final String role;

  String text;
}

// ============================================================
// MESSAGE BUBBLE
// ============================================================

class _MessageBubble extends StatelessWidget {
  const _MessageBubble({
    required this.message,
  });

  final _ChatMessage message;

  @override
  Widget build(BuildContext context) {
    final bool isUser =
        message.role == 'user';

    return Align(
      alignment: isUser
          ? Alignment.centerRight
          : Alignment.centerLeft,

      child: Container(
        constraints:
        const BoxConstraints(
          maxWidth: 700,
        ),

        margin: const EdgeInsets.only(
          bottom: 12,
        ),

        padding:
        const EdgeInsets.symmetric(
          horizontal: 14,
          vertical: 12,
        ),

        decoration: BoxDecoration(
          color: isUser
              ? Theme.of(context)
              .colorScheme
              .primaryContainer
              : Theme.of(context)
              .colorScheme
              .surfaceContainerHighest,

          borderRadius:
          BorderRadius.circular(
            16,
          ),
        ),

        child: SelectableText(
          message.text,
        ),
      ),
    );
  }
}

// ============================================================
// THINKING BUBBLE
// ============================================================

class _ThinkingBubble
    extends StatelessWidget {
  const _ThinkingBubble({
    required this.text,
  });

  final String text;

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment:
      Alignment.centerLeft,

      child: Container(
        constraints:
        const BoxConstraints(
          maxWidth: 700,
        ),

        margin: const EdgeInsets.only(
          bottom: 12,
        ),

        padding:
        const EdgeInsets.all(
          14,
        ),

        decoration: BoxDecoration(
          color: Theme.of(context)
              .colorScheme
              .surfaceContainerHighest,

          borderRadius:
          BorderRadius.circular(
            16,
          ),
        ),

        child: Column(
          crossAxisAlignment:
          CrossAxisAlignment.start,
          children: [
            const Row(
              mainAxisSize:
              MainAxisSize.min,
              children: [
                Icon(
                  Icons
                      .psychology_alt_outlined,
                  size: 18,
                ),

                SizedBox(
                  width: 6,
                ),

                Text(
                  'Thinking...',
                  style: TextStyle(
                    fontWeight:
                    FontWeight.w600,
                  ),
                ),
              ],
            ),

            const SizedBox(
              height: 8,
            ),

            SelectableText(
              text,
            ),
          ],
        ),
      ),
    );
  }
}
2
likes
130
points
379
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Headless Flutter package for running GGUF LLMs locally on Android with streaming, conversation history, web search, device context, thinking support, benchmarking, and CPU/GPU auto-tuning.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

battery_plus, connectivity_plus, device_info_plus, file_picker, flutter, geocoding, geolocator, html, llama_flutter_android, package_info_plus, path_provider, sensors_plus, system_info2

More

Packages that depend on flutter_gpt_engine