initialize static method
Initialize RagEngine with all dependencies.
This method handles:
- Copying tokenizer asset to documents directory
- Initializing the tokenizer
- Loading the ONNX embedding model
- Initializing the RAG database
config - Configuration containing asset paths and options.
onProgress - Optional callback for initialization status updates.
Example:
final rag = await RagEngine.initialize(
config: RagConfig.fromAssets(
tokenizerAsset: 'assets/tokenizer.json',
modelAsset: 'assets/model.onnx',
),
onProgress: (status) => setState(() => _status = status),
);
Implementation
static Future<RagEngine> initialize({
required RagConfig config,
void Function(String status)? onProgress,
}) async {
// 0. Auto-initialize Rust library (safe to call multiple times)
await _ensureRustInitialized();
// 1. Get app documents directory
final dir = await getApplicationDocumentsDirectory();
final dbPath = "${dir.path}/${config.databaseName ?? 'rag.sqlite'}";
final tokenizerPath =
config.preparedTokenizerPath ?? "${dir.path}/tokenizer.json";
final modelPath =
config.preparedModelPath ??
"${dir.path}/${config.modelAsset.split('/').last}";
// 2. Copy and initialize tokenizer
onProgress?.call('Initializing tokenizer...');
if (config.preparedTokenizerPath == null) {
await _copyAssetToFile(config.tokenizerAsset, tokenizerPath);
}
await initTokenizer(tokenizerPath: tokenizerPath);
final vocabSize = getVocabSize();
// 3. Prepare ONNX embedding model (Copy logic)
onProgress?.call('Preparing embedding model...');
// Copy model asset to file (optimized for memory)
if (config.preparedModelPath == null) {
await _copyAssetToFile(config.modelAsset, modelPath);
}
final normalizedMaxChunkChars = normalizeMaxChunkChars(
config.maxChunkChars,
context: 'RagEngine.initialize',
);
final normalizedOverlapChars = normalizeOverlapChars(
config.overlapChars,
context: 'RagEngine.initialize',
);
warnThreadConfigConflict(
threadLevel: config.threadLevel,
embeddingIntraOpNumThreads: config.embeddingIntraOpNumThreads,
context: 'RagEngine.initialize',
);
// Default to half the cores if not specified to prevent full CPU usage
// Calculate threads based on configuration
int threads;
final totalCores = Platform.numberOfProcessors;
if (config.threadLevel != null) {
// 1. Thread Level (Percentage based)
switch (config.threadLevel!) {
case ThreadUseLevel.low:
threads = (totalCores * 0.2).ceil();
break;
case ThreadUseLevel.medium:
threads = (totalCores * 0.4).ceil();
break;
case ThreadUseLevel.high:
threads = (totalCores * 0.8).ceil();
break;
}
} else if (config.embeddingIntraOpNumThreads != null) {
// 2. Manual Count
threads = config.embeddingIntraOpNumThreads!;
} else {
// 3. Priority: Default (50% safe fallback)
threads = (totalCores > 1 ? (totalCores / 2).ceil() : 1);
}
// Ensure at least 1 thread
if (threads < 1) threads = 1;
debugPrint(
'[RagEngine] Configured ONNX embedding threads: $threads (Total Cores: $totalCores)',
);
// Init EmbeddingService on a background worker isolate.
// Thread config is passed as a raw int; the worker creates
// OrtSessionOptions internally (native objects can't cross isolate
// boundaries).
onProgress?.call('Loading embedding model...');
var embeddingWorkerInitialized = false;
try {
await EmbeddingService.init(
modelPath: modelPath,
intraOpNumThreads: threads,
);
embeddingWorkerInitialized = true;
// 4. Probe the actual model output dimension and configure Rust before
// any database or MMAP write can occur. The host explicitly supplies the
// variance profile; the filename and dimension are never used to infer it.
onProgress?.call('Validating VABQ profile...');
final probe = await EmbeddingService.embed(_kFingerprintProbeText);
final expectedDimension = config.expectedEmbeddingDimension;
if (expectedDimension != null && probe.length != expectedDimension) {
RagModelPackManifest.validateExpectedEmbeddingDimension(
expectedDimension: expectedDimension,
actualDimension: probe.length,
);
}
await vabq_config.configureVabqProfile(
profile: config.vabqProfile == VabqProfile.none
? null
: vabqProfileWireName(config.vabqProfile),
embeddingDimension: probe.length,
);
// 5. Initialize database connection pool
onProgress?.call('Initializing connection pool...');
await initDbPool(dbPath: dbPath, maxSize: 4);
// 6. Initialize RAG service
onProgress?.call('Initializing database...');
final ragService = SourceRagService(
dbPath: dbPath,
modelPath: modelPath,
maxChunkChars: normalizedMaxChunkChars,
overlapChars: normalizedOverlapChars,
);
await ragService.init(deferIndexWarmup: config.deferIndexWarmup);
// 7. Resolve the embedding fingerprint gate. The probe was also used to
// validate the explicit VABQ profile before persistence was initialized.
// Fingerprint resolution must happen AFTER migration_meta exists.
onProgress?.call('Validating embedding fingerprint...');
final currentFingerprint = computeEmbeddingFingerprint(
modelBasename: embeddingModelBasename(modelPath),
dim: probe.length,
quant: embeddingQuantizationFingerprintAxis(config.vabqProfile),
);
final initialLock = await _resolveFingerprintGate(currentFingerprint);
if (initialLock != null) {
debugPrint(
'[RagEngine] Embedding fingerprint mismatch detected: '
'stored="${initialLock.stored}", current="$currentFingerprint", '
'remaining=${initialLock.remainingChunks}, '
'resume=${initialLock.resumeInProgress}. '
'Search/ingest will be locked until reembedAll() or clearAndRestart().',
);
}
onProgress?.call('Ready!');
return RagEngine._(
ragService: ragService,
dbPath: dbPath,
vocabSize: vocabSize,
currentEmbeddingFingerprint: currentFingerprint,
initialLock: initialLock,
deferIndexWarmup: config.deferIndexWarmup,
);
} catch (_) {
if (embeddingWorkerInitialized) {
await EmbeddingService.disposeAsync();
}
rethrow;
}
}