Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Core has NO pigeon (dropped at the 1.0 cut; its value types are hand-written in
- **LiteRT-LM**: native libs from `native-v0.16.0` GitHub Release (LiteRT-LM pin `924e79c9`, LiteRT pin `0ff28117`). Android tarball bundles the Qualcomm QNN dispatch stack and Windows tarball bundles Intel NPU dispatch (`LiteRtDispatch.dll` + OpenVino runtime + TBB) for `PreferredBackend.npu` (Qualcomm Snapdragon / Intel LunarLake/PantherLake) — both dispatch libs are **rebuilt from the pin every release**; carrying them forward is what silently broke NPU on both platforms (see the `build-native` skill). v0.16.0: fixes the Android OpenCL per-turn memory leak (LiteRT-LM #2699, #348/#402); v0.15.0 **broke the stream-callback ABI** (4-arg → 2-arg chunk object) with no compat path, handled by a runtime probe in `stream_proxy.c`. Windows discrete GPU works again — the crash was our own dead `litert_link_capi_so` Bazel define, not an upstream regression (#2957 retracted).
- **sqlite-vec**: `flutter_gemma_rag_sqlite` fetches the per-platform `vec0` loadable from the `native-sqlite-vec-v<X>` GitHub Release (`sqlite-vec-<target>.tar.gz` + `checksums_sqlite_vec.txt`), SHA256-verified by its `hook/build.dart`. `<X>` names the **upstream sqlite-vec release** the bytes were built from; a letter suffix (`0.1.9-a`) is only for RE-releasing changed bytes under an already-published number. The loadables are NOT committed — `native/sqlite_vec/prebuilt/` is a maintainer override produced by `build_local.sh`, gitignored and `.pubignore`d.
- **large_file_handler**: `^0.5.0` (core dep; 0.5.0 declares all 6 platforms — needed for pana platform support + the dart2wasm-clean web graph)
- **Current Version**: core `flutter_gemma` `1.8.0`, `flutter_gemma_rag_sqlite` `1.3.1`, `flutter_gemma_rag_qdrant` `1.3.0`; `flutter_gemma_litertlm` `1.6.3`, `flutter_gemma_mediapipe` `1.0.5`, `flutter_gemma_embeddings` `2.1.1`, `flutter_gemma_speech` `0.5.0`; `flutter_gemma_agent` `0.2.5`, `flutter_gemma_builtin_ai` `0.2.1`, `flutter_gemma_onnx` `0.3.3`; `genkit_flutter_gemma` `0.6.1`, `genkit_hybrid` `0.2.1`
- **Current Version**: core `flutter_gemma` `1.8.1`, `flutter_gemma_rag_sqlite` `1.3.2`, `flutter_gemma_rag_qdrant` `1.3.1`; `flutter_gemma_litertlm` `1.6.3`, `flutter_gemma_mediapipe` `1.0.5`, `flutter_gemma_embeddings` `2.1.1`, `flutter_gemma_speech` `0.5.0`; `flutter_gemma_agent` `0.2.5`, `flutter_gemma_builtin_ai` `0.2.1`, `flutter_gemma_onnx` `0.3.3`; `genkit_flutter_gemma` `0.6.1`, `genkit_hybrid` `0.2.1`
- **0.15.2**: embedding unified on LiteRT C API via Dart FFI on all native platforms (Android + iOS + Desktop). Drops `localagents-rag` JVM dep on Android and the separate TFLite C 0.12.7 tarball on Desktop; `TensorFlowLiteC` pod no longer needed on iOS. Single source of truth for `TaskType.prefix` in Dart, fixes cross-platform embedding drift (#264).

## Platform-Specific Setup
Expand Down
3 changes: 3 additions & 0 deletions packages/flutter_gemma/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 1.8.1
- Add `VectorStoreRepository.flush()`; custom implementations must declare it (#492).

## 1.8.0
- Whisper output language on `getActiveStt` and `transcribe` (#500).
- **Breaking for custom `SpeechRecognizer` implementations**: `transcribe` gained `language:` and the type gained a `language` field.
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter_gemma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1720,8 +1720,13 @@ final results = await FlutterGemmaPlugin.instance.searchSimilar(
mustNot: [FieldEquals(key: 'lang', value: 'fr')],
),
);

// 5. Persist the index while the store stays open (see below)
await FlutterGemmaPlugin.instance.flushVectorStore(); // or FlutterGemma.rag.flush()
```

**Call `flush()` after indexing.** `flutter_gemma_rag_qdrant` keeps new documents in memory until the store is flushed or closed, so an index built without either is lost when the process ends — an Android app killed in the background is the ordinary case ([#492](https://github.com/DenisovAV/flutter_gemma/issues/492)). On native `flutter_gemma_rag_sqlite` it is a no-op; on web it drains the IndexedDB storage. A store that cannot persist at all throws `VectorStoreException` instead of returning. Custom `VectorStoreRepository` implementations must declare `flush()`.

A field name is checked by the store, in `configure()`. `SqliteVectorStore` is
the strict one — `^[A-Za-z][A-Za-z0-9_]*$`, and not a name `vec0` already uses
(`id`, `embedding`, `content`, `metadata`, `distance`, `k`) — because the name
Expand Down
6 changes: 3 additions & 3 deletions packages/flutter_gemma/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.8.0"
version: "1.8.1"
flutter_gemma_agent:
dependency: "direct main"
description:
Expand Down Expand Up @@ -290,14 +290,14 @@ packages:
path: "../../flutter_gemma_rag_qdrant"
relative: true
source: path
version: "1.3.0"
version: "1.3.1"
flutter_gemma_rag_sqlite:
dependency: "direct main"
description:
path: "../../flutter_gemma_rag_sqlite"
relative: true
source: path
version: "1.3.1"
version: "1.3.2"
flutter_gemma_speech:
dependency: "direct main"
description:
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_gemma/ios/flutter_gemma.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'flutter_gemma'
s.version = '1.8.0'
s.version = '1.8.1'
s.summary = 'Flutter plugin for running Gemma and other LLMs locally on iOS.'
s.description = <<-DESC
Core runtime for running Gemma 4, Gemma3n, Gemma 3, FastVLM, Qwen3,
Expand Down
8 changes: 8 additions & 0 deletions packages/flutter_gemma/lib/core/api/flutter_gemma.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,14 @@ class GemmaRag {
Future<void> initialize(String databasePath) =>
FlutterGemmaPlugin.instance.initializeVectorStore(databasePath);

/// Persist what has been indexed so far, keeping the store open.
///
/// Call it after a bulk index, and from wherever the app learns it is going
/// away. On qdrant this is what makes an index survive the process at all —
/// without it the points sit in the shard's in-RAM segment and a background
/// kill takes them. See [VectorStoreRepository.flush] for the other backends.
Future<void> flush() => FlutterGemmaPlugin.instance.flushVectorStore();

/// Add a document; its embedding is computed automatically (needs an active
/// embedding model).
Future<void> addDocument({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class UnconfiguredVectorStore implements VectorStoreRepository {
@override
Future<void> clear() async => _fail();

@override
Future<void> flush() async {}

@override
Future<void> close() async {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,64 @@ abstract class VectorStoreRepository {
/// than report an unreadable corpus as an empty one.
Future<void> clear();

/// Persist everything written so far, without closing the store.
///
/// Call it once after a bulk index. Writes are not necessarily on disk when
/// [addDocument] returns: a store is free to hold them in memory and settle
/// up later, and one of them does.
///
/// **Per backend**:
/// - Qdrant: **required.** Points added through the UniFFI shard live in its
/// in-RAM segment until the shard is flushed or unloaded. An index built
/// without this is gone when the process ends, and the corpus is embedded
/// again from scratch on the next launch.
/// - SQLite, native: a genuine no-op. The connection is in autocommit and
/// never opens a transaction, so a statement that returned is on disk.
/// - SQLite, web: **not** a no-op, and not a full guarantee either. The VFS
/// Flutter web gets is IndexedDB (OPFS needs a dedicated worker), whose
/// `xSync` does nothing and whose writes are asynchronous by design, so
/// this call is the drain. On `sqlite3` >= 3.4.0 that drain is partial: it
/// returns without awaiting a write batch already in flight (upstream
/// regression). The exposure is bounded — the VFS streams writes
/// continuously, so what is missed is the batch in flight, not the index.
///
/// [close] persists too, and on web it is the *stronger* drain: it queues
/// behind the running batch on every version. What close cannot cover is a
/// process that never gets to close — an Android app the system kills in the
/// background is the ordinary case, not the exceptional one — which is what
/// this exists for. So the two are not interchangeable: prefer this one
/// while the store stays open, and note that on qdrant a failure is reported
/// by this call and swallowed by [close].
///
/// Safe on a store that was never initialized, and safe to call repeatedly:
/// implementations must not throw for either — there is nothing pending, so
/// there is nothing to report.
///
/// Otherwise this method's job is to be **loud**. An implementation that
/// cannot persist — the write failed, or the store is on a backend with no
/// durable storage behind it — must throw [VectorStoreException] rather than
/// return normally. Silence is the failure this method exists to prevent:
/// the caller asked for durability, and a quiet success tells them they have
/// it while the index is still only in memory.
///
/// The default body is a no-op, for backends that are already durable. Note
/// that every implementation in this repository uses `implements` rather
/// than `extends`, so none of them inherits it; it is here for the contract
/// and for any future implementation that does extend.
Future<void> flush() async {}

/// Close vector store and release resources
///
/// **Resource cleanup**:
/// - Mobile: Closes SQLite database connection
/// - Web: Closes IndexedDB connection
///
/// Persists pending writes on the way out, so a [flush] immediately before
/// this adds nothing. It is not a substitute for [flush], though: a store
/// stays usable after flushing and does not after closing, and a failure
/// here is logged rather than thrown — implementations treat close as
/// cleanup the caller usually cannot act on, while [flush] reports.
///
/// Idempotent: Safe to call multiple times
Future<void> close();

Expand All @@ -154,9 +206,12 @@ abstract class VectorStoreRepository {

/// The filterable-metadata schema this store was configured with.
///
/// Concrete (bodied) member with a no-op default so that adding it does NOT
/// force an override on existing or external `implements`-ers (this is an
/// `abstract class`, not an `interface class`, so the body is inherited).
/// Concrete (bodied) member with a no-op default, so an implementation that
/// `extends` this class gets it for free. That does NOT spare an
/// `implements`-er: `implements` inherits no bodies, so every store in this
/// repository — all of which use `implements` — declares this itself, and
/// adding a bodied member here is still a source-breaking change for an
/// external `implements`-er.
/// Stores that honor [Filter] (qdrant, sqlite/vec0) override [configure] to
/// stash the schema and expose it here; everyone else keeps the empty default.
FilterSchema get filterSchema => const FilterSchema();
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter_gemma/lib/desktop/flutter_gemma_desktop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,11 @@ class FlutterGemmaDesktop extends FlutterGemmaPlugin {
);
}

@override
Future<void> flushVectorStore() async {
await ServiceRegistry.instance.vectorStoreRepository.flush();
}

@override
Future<void> addDocumentWithEmbedding({
required String id,
Expand Down
6 changes: 6 additions & 0 deletions packages/flutter_gemma/lib/flutter_gemma_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ abstract class FlutterGemmaPlugin extends PlatformInterface {
/// Initialize vector store database.
Future<void> initializeVectorStore(String databasePath);

/// Persist everything written to the vector store so far, without closing it.
///
/// See [VectorStoreRepository.flush] for what this means per backend — it is
/// required on qdrant, a no-op on native SQLite, and a partial drain on web.
Future<void> flushVectorStore();

/// Add document to vector store with pre-computed embedding.
Future<void> addDocumentWithEmbedding({
required String id,
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter_gemma/lib/mobile/flutter_gemma_mobile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,11 @@ class FlutterGemmaMobile extends FlutterGemmaPlugin {
);
}

@override
Future<void> flushVectorStore() async {
await ServiceRegistry.instance.vectorStoreRepository.flush();
}

@override
Future<void> addDocumentWithEmbedding({
required String id,
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter_gemma/lib/web/flutter_gemma_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ class FlutterGemmaWeb extends FlutterGemmaPlugin {
);
}

@override
Future<void> flushVectorStore() async {
await ServiceRegistry.instance.vectorStoreRepository.flush();
}

@override
Future<void> addDocumentWithEmbedding({
required String id,
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_gemma/macos/flutter_gemma.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'flutter_gemma'
s.version = '1.8.0'
s.version = '1.8.1'
s.summary = 'Flutter Gemma - Run Gemma AI models locally on desktop'
s.description = <<-DESC
Flutter plugin for running Gemma AI models locally on macOS using LiteRT-LM.
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_gemma/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_gemma
description: "Run Gemma and other LLMs on-device in Flutter (Android, iOS, Web, Desktop). Multimodal vision/audio, function calling, thinking mode, GPU, embeddings, RAG."
version: 1.8.0
version: 1.8.1
resolution: workspace
homepage: https://fluttergemma.dev
repository: https://github.com/DenisovAV/flutter_gemma
Expand Down
3 changes: 3 additions & 0 deletions packages/flutter_gemma_rag_qdrant/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 1.3.1
- Add `flush()`; without it an index was lost when the process ended (#492).

## 1.3.0
- **Breaking:** moved onto the official `qdrant_edge` UniFFI SDK.
- **Breaking:** a 1.x store is not readable — remove its files, then re-index.
Expand Down
8 changes: 8 additions & 0 deletions packages/flutter_gemma_rag_qdrant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Then use the unchanged RAG API:
await FlutterGemmaPlugin.instance.initializeVectorStore('rag_store'); // a directory
await FlutterGemmaPlugin.instance.addDocument(/* ... */);
final hits = await FlutterGemmaPlugin.instance.searchSimilar(query: query, topK: 5);
await FlutterGemmaPlugin.instance.flushVectorStore(); // after indexing — see below
```

`QdrantVectorStore` also honors the payload-aware `Filter` DSL on
Expand All @@ -49,6 +50,13 @@ refuses; if a schema must work on both, keep it inside sqlite's narrower set.

## Behavior notes

- **Call `flushVectorStore()` (or `FlutterGemma.rag.flush()`) after indexing.**
New points stay in the shard's in-memory segment until it is flushed or
closed. A process that ends without either — an Android app killed in the
background — loses them, and the corpus is embedded again on the next launch
([#492](https://github.com/DenisovAV/flutter_gemma/issues/492)). `close()`
persists too, but logs a failed save; `flush()` throws it as
`VectorStoreException`.
- **Cross-platform web is not supported** — `QdrantVectorStore` is native-only.
- `enableHnsw` is accepted but a no-op: qdrant decides indexing internally
(brute-forces below ~20k points, which is already faster than the Dart HNSW
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ class QdrantEdgeClient {
}
}

/// Write the in-RAM segment out to disk, keeping the shard open.
///
/// The crate declares this separately from [close]'s `unload()`, and the
/// difference is the whole point: `unload()` also persists, but it ends the
/// shard. This is what a caller reaches for after a bulk index when it wants
/// to keep using the store — and it is what stands between an index and a
/// process the OS kills without warning.
Future<void> flush() async {
_checkOpen();
try {
_shard.flush();
} catch (e) {
_rethrow(e);
}
}

/// Close the shard. Idempotent — safe to call more than once.
Future<void> close() async {
if (_closed) return;
Expand Down Expand Up @@ -356,6 +372,10 @@ class QdrantEdgeClient {
/// reached application code as a type the app could not name in a catch.
/// 0.8.0-dev.3 exports it, so it can finally be caught here.
static Never _rethrow(Object e) {
// Already ours: thrown on purpose inside a wrapper's `try` (openExisting's
// "not written by this package"). Re-wrapping it buried that message
// under "unexpected error" and lost QdrantShardLockedException's type.
if (e is QdrantException) throw e;
if (e is qe.ShardLockedEdgeException) {
throw QdrantShardLockedException(
'The shard is already open elsewhere (its write-ahead log is held by '
Expand All @@ -366,7 +386,15 @@ class QdrantEdgeClient {
if (e is qe.UniffiInternalError) {
throw QdrantException('qdrant-edge internal failure: $e');
}
throw e;
// Everything else is wrapped too, rather than rethrown raw. This used to
// be a bare `throw e`, which meant any failure that is neither an
// `EdgeException` nor a `UniffiInternalError` crossed all twelve wrapper
// methods untouched — so `on QdrantException`, the catch every caller in
// this package writes, missed it, and the store's own translation to
// `VectorStoreException` never fired. The type is the contract; an escape
// hatch that skips it is the same silent-failure shape as the bug this
// package just fixed.
throw QdrantException('qdrant-edge failed with an unexpected error: $e');
}

// ---- Filter bridge: qdrant JSON envelope → typed qe.Filter ----------------
Expand Down
29 changes: 29 additions & 0 deletions packages/flutter_gemma_rag_qdrant/lib/src/qdrant_vector_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,35 @@ class QdrantVectorStore implements VectorStoreRepository {
}
}

@override
Future<void> flush() => _serializeLifecycle(_flush);

Future<void> _flush() async {
// On the lifecycle lane, not beside it: a flush that overlapped a close
// would reach a shard the other call had already unloaded.
//
// No client means nothing was written through this store, so there is
// nothing to persist — including the case where an earlier open failed.
// Callers flush from lifecycle callbacks they cannot make conditional
// (`didChangeAppLifecycleState` and the like), so "not initialized" has to
// be quiet rather than an exception nobody can act on.
final c = _client;
if (c == null) return;
try {
await c.flush();
} on QdrantException catch (e) {
// Translated, like every other method on this class. `QdrantException`
// is not a `VectorStoreException` and is not exported from the barrel,
// so letting it out would hand the caller a type they cannot name — and
// `on VectorStoreException`, the catch the contract tells them to write,
// would miss every flush failure.
throw VectorStoreException(
'Failed to flush the qdrant shard: ${e.message}',
e,
);
}
}

@override
Future<void> close() => _serializeLifecycle(_close);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class QdrantVectorStore implements VectorStoreRepository {
'QdrantVectorStore is native-only; qdrant-edge cannot run on web',
);

@override
Future<void> flush() async {}

@override
Future<void> close() async {}
}
4 changes: 2 additions & 2 deletions packages/flutter_gemma_rag_qdrant/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_gemma_rag_qdrant
description: "qdrant-edge on-device RAG vector store for flutter_gemma, via the official qdrant_edge UniFFI SDK. Opt-in VectorStoreRepository with payload filtering. Native platforms only (no web)."
version: 1.3.0
version: 1.3.1
homepage: https://fluttergemma.dev
repository: https://github.com/DenisovAV/flutter_gemma/tree/main/packages/flutter_gemma_rag_qdrant
topics: [rag, qdrant, vector-search, embeddings, on-device]
Expand All @@ -24,7 +24,7 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_gemma: ^1.6.1
flutter_gemma: ^1.8.1
uuid: ^4.0.0
# For the store's owned-subdir path handling (qdrant_edge_v1).
path: ^1.9.0
Expand Down
Loading
Loading