Skip to main content

flutter_rag 0.1.0 copy "flutter_rag: ^0.1.0" to clipboard
flutter_rag: ^0.1.0 copied to clipboard

A pure Dart Retrieval-Augmented Generation (RAG) engine for Flutter apps. Supports document ingestion, vector search, and LLM integration for building AI knowledge chatbots.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_rag/flutter_rag.dart';

const String openAIApiKey =
    "";

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Knowledge Assistant',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const RagChatScreen(),
    );
  }
}

class ChatMessage {
  final String text;
  final bool isUser;
  ChatMessage({required this.text, required this.isUser});
}

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

  @override
  State<RagChatScreen> createState() => _RagChatScreenState();
}

class _RagChatScreenState extends State<RagChatScreen> {
  RagEngine? _ragEngine;
  OpenAIEmbeddings? _embeddings;
  OpenAILLM? _llm;
  InMemoryVectorStore? _vectorStore;

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

  bool _isLoading = false;
  bool _isReady = false;

  String _status = "Initializing AI...";

  final List<ChatMessage> _messages = [];
  final List<String> _uploadedFiles = [];

  String _formatError(Object error) {
    final errorStr = error.toString();
    if (errorStr.contains('401') || errorStr.contains('Incorrect API key')) {
      return "Invalid OpenAI API key. Please check your API key in main.dart.";
    } else if (errorStr.contains('404')) {
      return "Service not found. Please check your connection or endpoint.";
    } else if (errorStr.contains('SocketException') ||
        errorStr.contains('Failed host lookup')) {
      return "Network error. Please check your internet connection.";
    } else {
      final firstLine = errorStr.split('\n').first;
      return firstLine.length > 120
          ? '${firstLine.substring(0, 120)}...'
          : firstLine;
    }
  }

  @override
  void initState() {
    super.initState();
    _initializeAI();
    _messages.add(
      ChatMessage(
        text: "Hello! Upload a PDF or CSV document to start asking questions.",
        isUser: false,
      ),
    );
  }

  @override
  void dispose() {
    _questionController.dispose();
    _scrollController.dispose();
    _embeddings?.dispose();
    _llm?.dispose();
    super.dispose();
  }

  Future<void> _initializeAI() async {
    setState(() {
      _isLoading = true;
    });

    try {
      _embeddings = OpenAIEmbeddings(apiKey: openAIApiKey);
      _vectorStore = InMemoryVectorStore(embeddings: _embeddings!);
      _llm = OpenAILLM(apiKey: openAIApiKey);

      final retriever = VectorStoreRetriever(
        vectorStore: _vectorStore!,
        searchK: 3,
      );

      _ragEngine = RagEngine(retriever: retriever, llm: _llm!);

      setState(() {
        _isReady = true;
        _status = "AI Engine Ready";
      });
    } catch (e) {
      setState(() {
        _status = "Initialization error";
        _messages.add(
          ChatMessage(
            text: "Initialization Error: ${_formatError(e)}",
            isUser: false,
          ),
        );
      });
    }

    setState(() {
      _isLoading = false;
    });
  }

  Future<void> _uploadFile() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['pdf', 'csv'],
    );

    if (result == null || result.files.single.path == null) return;

    final filePath = result.files.single.path!;
    final fileName = result.files.single.name;
    final extension = result.files.single.extension;

    setState(() {
      _isLoading = true;
      _status = "Processing $fileName...";
    });

    try {
      List<Document> docs = [];

      if (extension == "pdf") {
        final loader = PdfLoader(filePath);
        docs = await loader.load();
      }

      if (extension == "csv") {
        final loader = CsvLoader(filePath);
        docs = await loader.load();
      }

      final splitter = RecursiveCharacterTextSplitter(
        chunkSize: 500,
        chunkOverlap: 50,
      );

      final chunks = splitter.splitDocuments(docs);
      final texts = chunks.map((e) => e.pageContent).toList();

      await _vectorStore!.addTexts(texts: texts);

      setState(() {
        if (!_uploadedFiles.contains(fileName)) {
          _uploadedFiles.add(fileName);
        }
        _status = "Ready";
        _messages.add(
          ChatMessage(
            text:
                "Successfully processed $fileName. You can now ask questions about it.",
            isUser: false,
          ),
        );
      });
      _scrollToBottom();
    } catch (e) {
      setState(() {
        _status = "Error processing file";
        _messages.add(
          ChatMessage(
            text: "Failed to process file: ${_formatError(e)}",
            isUser: false,
          ),
        );
      });
      _scrollToBottom();
    }

    setState(() {
      _isLoading = false;
    });
  }

  Future<void> _askQuestion() async {
    final question = _questionController.text.trim();
    if (question.isEmpty) return;

    _questionController.clear();
    FocusScope.of(context).unfocus(); // Dismiss keyboard

    setState(() {
      _messages.add(ChatMessage(text: question, isUser: true));
      _isLoading = true;
      _status = "Thinking...";
    });

    _scrollToBottom();

    try {
      final response = await _ragEngine!.invoke(question);

      setState(() {
        _messages.add(ChatMessage(text: response.answer, isUser: false));
        _status = "Ready";
      });
    } catch (e) {
      setState(() {
        _messages.add(
          ChatMessage(text: "Error: ${_formatError(e)}", isUser: false),
        );
        _status = "Error occurred";
      });
    }

    setState(() {
      _isLoading = false;
    });
    _scrollToBottom();
  }

  void _scrollToBottom() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[50],
      appBar: AppBar(
        title: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              "Knowledge Assistant",
              style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
            ),
            if (_status.isNotEmpty)
              Text(
                _status,
                style: TextStyle(
                  fontSize: 12,
                  color: _isLoading ? Colors.orange[200] : Colors.white70,
                  fontWeight: FontWeight.normal,
                ),
              ),
          ],
        ),
        backgroundColor: Theme.of(context).colorScheme.primary,
        foregroundColor: Colors.white,
        elevation: 1,
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 8.0),
            child: IconButton(
              icon: const Icon(Icons.upload_file),
              tooltip: 'Upload Document',
              onPressed: _isReady && !_isLoading ? _uploadFile : null,
            ),
          ),
        ],
      ),
      body: SafeArea(
        child: Column(
          children: [
            if (_uploadedFiles.isNotEmpty)
              Container(
                width: double.infinity,
                padding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 8,
                ),
                color: Theme.of(
                  context,
                ).colorScheme.primaryContainer.withOpacity(0.3),
                child: SingleChildScrollView(
                  scrollDirection: Axis.horizontal,
                  child: Row(
                    children: [
                      Icon(
                        Icons.library_books,
                        size: 16,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                      const SizedBox(width: 8),
                      Text(
                        "${_uploadedFiles.length} uploaded: ${_uploadedFiles.join(', ')}",
                        style: TextStyle(
                          fontSize: 12,
                          color: Theme.of(
                            context,
                          ).colorScheme.onPrimaryContainer,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            Expanded(
              child: ListView.builder(
                controller: _scrollController,
                padding: const EdgeInsets.symmetric(
                  horizontal: 16,
                  vertical: 20,
                ),
                itemCount: _messages.length,
                itemBuilder: (context, index) {
                  final message = _messages[index];
                  return _buildMessageBubble(message);
                },
              ),
            ),
            if (_isLoading)
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 8.0),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        color: Theme.of(context).colorScheme.primary,
                      ),
                    ),
                    const SizedBox(width: 10),
                    Text(
                      _status,
                      style: TextStyle(color: Colors.grey[600], fontSize: 13),
                    ),
                  ],
                ),
              ),
            _buildInputArea(),
          ],
        ),
      ),
    );
  }

  Widget _buildMessageBubble(ChatMessage message) {
    return Align(
      alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        constraints: BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width * 0.8,
        ),
        decoration: BoxDecoration(
          color: message.isUser
              ? Theme.of(context).colorScheme.primary
              : Colors.white,
          borderRadius: BorderRadius.only(
            topLeft: const Radius.circular(16),
            topRight: const Radius.circular(16),
            bottomLeft: Radius.circular(message.isUser ? 16 : 4),
            bottomRight: Radius.circular(message.isUser ? 4 : 16),
          ),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.04),
              blurRadius: 4,
              offset: const Offset(0, 2),
            ),
          ],
        ),
        child: Text(
          message.text,
          style: TextStyle(
            color: message.isUser ? Colors.white : Colors.black87,
            fontSize: 15,
            height: 1.4,
          ),
        ),
      ),
    );
  }

  Widget _buildInputArea() {
    return Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          Expanded(
            child: TextField(
              controller: _questionController,
              decoration: InputDecoration(
                hintText: "Type your question...",
                hintStyle: TextStyle(color: Colors.grey[400]),
                filled: true,
                fillColor: Colors.grey[100],
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 20,
                  vertical: 12,
                ),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(24),
                  borderSide: BorderSide.none,
                ),
                isDense: true,
              ),
              minLines: 1,
              maxLines: 4,
              textInputAction: TextInputAction.send,
              onSubmitted: (_) {
                if (_isReady && !_isLoading) {
                  _askQuestion();
                }
              },
            ),
          ),
          const SizedBox(width: 8),
          Container(
            height: 48,
            width: 48,
            decoration: BoxDecoration(
              color: _isReady && !_isLoading
                  ? Theme.of(context).colorScheme.primary
                  : Colors.grey[300],
              shape: BoxShape.circle,
            ),
            child: IconButton(
              icon: const Icon(
                Icons.send_rounded,
                color: Colors.white,
                size: 20,
              ),
              onPressed: _isReady && !_isLoading ? _askQuestion : null,
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
145
points
28
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A pure Dart Retrieval-Augmented Generation (RAG) engine for Flutter apps. Supports document ingestion, vector search, and LLM integration for building AI knowledge chatbots.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

collection, csv, excel, http, syncfusion_flutter_pdf

More

Packages that depend on flutter_rag