From a9c1e1e72644e4c6c5dd87a030f830459aebdfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ma=C5=82ysa?= Date: Wed, 8 Apr 2026 15:17:37 +0200 Subject: [PATCH] dart format --- lib/src/api/ai_chat_provider.dart | 21 ++- lib/src/api/chat_exceptions.dart | 23 +--- lib/src/api/chat_models.dart | 11 +- lib/src/api/claude_chat_provider.dart | 48 +++---- lib/src/api/gemini_chat_provider.dart | 51 ++++---- lib/src/api/mistral_chat_provider.dart | 49 ++++--- lib/src/api/openai_chat_provider.dart | 44 +++---- lib/src/gguf/local_model.dart | 4 +- lib/src/models/llm_model_isolated.dart | 122 +++++++++--------- lib/src/models/llm_model_standard.dart | 26 ++-- lib/src/rag/chunking/text_chunker.dart | 32 ++--- lib/src/rag/document/document.dart | 28 ++-- lib/src/rag/document/document_chunk.dart | 20 +-- .../embeddings/llama_embedding_provider.dart | 18 +-- lib/src/rag/rag_engine.dart | 4 +- lib/src/rag/rag_pipeline.dart | 18 +-- .../vector_store/in_memory_vector_store.dart | 17 ++- .../api/ai_chat_provider_factory_test.dart | 12 +- test/src/models/llm_model_standard_test.dart | 6 +- 19 files changed, 269 insertions(+), 285 deletions(-) diff --git a/lib/src/api/ai_chat_provider.dart b/lib/src/api/ai_chat_provider.dart index e20da39..9cf7402 100644 --- a/lib/src/api/ai_chat_provider.dart +++ b/lib/src/api/ai_chat_provider.dart @@ -112,9 +112,7 @@ abstract class BaseAIChatProvider implements AIChatProvider { throw StateError('Provider has been disposed.'); } if (!_isInitialized) { - throw StateError( - 'Provider is not initialised. Call initialize() first.', - ); + throw StateError('Provider is not initialised. Call initialize() first.'); } } @@ -122,10 +120,7 @@ abstract class BaseAIChatProvider implements AIChatProvider { /// /// Only [NetworkException] and [RateLimitException] trigger a retry; /// all other exceptions propagate immediately without retry. - Future withRetry( - Future Function() fn, { - int maxAttempts = 3, - }) async { + Future withRetry(Future Function() fn, {int maxAttempts = 3}) async { int attempt = 0; Duration delay = const Duration(seconds: 1); while (true) { @@ -166,13 +161,13 @@ abstract class BaseAIChatProvider implements AIChatProvider { AIChatException mapHttpError(int statusCode, String body) { return switch (statusCode) { 401 || 403 => APIKeyException( - 'Authentication failed: $body', - statusCode: statusCode, - ), + 'Authentication failed: $body', + statusCode: statusCode, + ), 429 => RateLimitException( - 'Rate limit exceeded: $body', - retryAfter: _parseRetryAfter(body), - ), + 'Rate limit exceeded: $body', + retryAfter: _parseRetryAfter(body), + ), _ => AIChatException('API error: $body', statusCode: statusCode), }; } diff --git a/lib/src/api/chat_exceptions.dart b/lib/src/api/chat_exceptions.dart index c14fe8a..4f34483 100644 --- a/lib/src/api/chat_exceptions.dart +++ b/lib/src/api/chat_exceptions.dart @@ -7,11 +7,7 @@ /// - [NetworkException] — transport-level failures /// - [RateLimitException] — quota / rate-limit (HTTP 429) class AIChatException implements Exception { - const AIChatException( - this.message, { - this.statusCode, - this.cause, - }); + const AIChatException(this.message, {this.statusCode, this.cause}); /// Human-readable description of the error. final String message; @@ -35,11 +31,7 @@ class AIChatException implements Exception { /// /// Typically maps to HTTP 401 or 403 responses. class APIKeyException extends AIChatException { - const APIKeyException( - super.message, { - super.statusCode, - super.cause, - }); + const APIKeyException(super.message, {super.statusCode, super.cause}); @override String toString() => @@ -50,11 +42,7 @@ class APIKeyException extends AIChatException { /// Thrown for transport-level failures such as timeouts, DNS errors, /// or connection resets. class NetworkException extends AIChatException { - const NetworkException( - super.message, { - super.statusCode, - super.cause, - }); + const NetworkException(super.message, {super.statusCode, super.cause}); @override String toString() => @@ -79,8 +67,9 @@ class RateLimitException extends AIChatException { @override String toString() { - final retry = - retryAfter != null ? ' Retry after ${retryAfter!.inSeconds}s.' : ''; + final retry = retryAfter != null + ? ' Retry after ${retryAfter!.inSeconds}s.' + : ''; return 'RateLimitException: $message$retry'; } } diff --git a/lib/src/api/chat_models.dart b/lib/src/api/chat_models.dart index 1e88e7b..ff1c44a 100644 --- a/lib/src/api/chat_models.dart +++ b/lib/src/api/chat_models.dart @@ -18,15 +18,15 @@ class ChatMessage { /// Shorthand for a user message. const ChatMessage.user(String content) - : this(role: ChatRole.user, content: content); + : this(role: ChatRole.user, content: content); /// Shorthand for an assistant message. const ChatMessage.assistant(String content) - : this(role: ChatRole.assistant, content: content); + : this(role: ChatRole.assistant, content: content); /// Shorthand for a system instruction message. const ChatMessage.system(String content) - : this(role: ChatRole.system, content: content); + : this(role: ChatRole.system, content: content); /// The role of the message author. final ChatRole role; @@ -38,10 +38,7 @@ class ChatMessage { /// /// Other providers may need custom mapping (e.g. Gemini uses "model" /// instead of "assistant"). - Map toJson() => { - 'role': role.name, - 'content': content, - }; + Map toJson() => {'role': role.name, 'content': content}; @override String toString() => 'ChatMessage(${role.name}: $content)'; diff --git a/lib/src/api/claude_chat_provider.dart b/lib/src/api/claude_chat_provider.dart index b7a7b14..06c6a3c 100644 --- a/lib/src/api/claude_chat_provider.dart +++ b/lib/src/api/claude_chat_provider.dart @@ -69,10 +69,10 @@ class ClaudeChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return sendChatMessages( - [...?history, ChatMessage.user(message)], - parameters: parameters, - ); + return sendChatMessages([ + ...?history, + ChatMessage.user(message), + ], parameters: parameters); } @override @@ -91,10 +91,10 @@ class ClaudeChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return _doStreamRequest( - [...?history, ChatMessage.user(message)], - parameters, - ); + return _doStreamRequest([ + ...?history, + ChatMessage.user(message), + ], parameters); } @override @@ -163,9 +163,10 @@ class ClaudeChatProvider extends BaseAIChatProvider { // Claude SSE pairs "event: " lines with "data: {json}" lines. // We only care about `content_block_delta` events. String? currentEvent; - await for (final line in response.stream - .transform(utf8.decoder) - .transform(const LineSplitter())) { + await for (final line + in response.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { if (line.startsWith('event: ')) { currentEvent = line.substring(7).trim(); continue; @@ -174,8 +175,7 @@ class ClaudeChatProvider extends BaseAIChatProvider { if (currentEvent == 'content_block_delta') { try { - final json = - jsonDecode(line.substring(6)) as Map; + final json = jsonDecode(line.substring(6)) as Map; final delta = json['delta'] as Map?; if (delta?['type'] == 'text_delta') { final text = delta!['text'] as String?; @@ -197,10 +197,12 @@ class ClaudeChatProvider extends BaseAIChatProvider { required bool stream, }) { // Claude requires system messages as a separate top-level field - final systemMessages = - messages.where((m) => m.role == ChatRole.system).toList(); - final conversationMessages = - messages.where((m) => m.role != ChatRole.system).toList(); + final systemMessages = messages + .where((m) => m.role == ChatRole.system) + .toList(); + final conversationMessages = messages + .where((m) => m.role != ChatRole.system) + .toList(); final body = { 'model': (parameters?['model'] as String?) ?? _model, @@ -222,18 +224,16 @@ class ClaudeChatProvider extends BaseAIChatProvider { } Map _headers() => { - 'x-api-key': _apiKey, - 'anthropic-version': _anthropicVersion, - 'Content-Type': 'application/json', - }; + 'x-api-key': _apiKey, + 'anthropic-version': _anthropicVersion, + 'Content-Type': 'application/json', + }; ChatResponse _parseResponse(Map json) { final contentBlocks = json['content'] as List; // Concatenate all text blocks (in practice usually just one) final text = contentBlocks - .where( - (b) => (b as Map)['type'] == 'text', - ) + .where((b) => (b as Map)['type'] == 'text') .map((b) => (b as Map)['text'] as String) .join(); final usage = json['usage'] as Map?; diff --git a/lib/src/api/gemini_chat_provider.dart b/lib/src/api/gemini_chat_provider.dart index 428dd23..a310ea6 100644 --- a/lib/src/api/gemini_chat_provider.dart +++ b/lib/src/api/gemini_chat_provider.dart @@ -68,10 +68,10 @@ class GeminiChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return sendChatMessages( - [...?history, ChatMessage.user(message)], - parameters: parameters, - ); + return sendChatMessages([ + ...?history, + ChatMessage.user(message), + ], parameters: parameters); } @override @@ -90,10 +90,10 @@ class GeminiChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return _doStreamRequest( - [...?history, ChatMessage.user(message)], - parameters, - ); + return _doStreamRequest([ + ...?history, + ChatMessage.user(message), + ], parameters); } @override @@ -167,26 +167,24 @@ class GeminiChatProvider extends BaseAIChatProvider { throw mapHttpError(response.statusCode, errorBody); } - await for (final line in response.stream - .transform(utf8.decoder) - .transform(const LineSplitter())) { + await for (final line + in response.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { if (line.isEmpty || !line.startsWith('data: ')) continue; try { final json = jsonDecode(line.substring(6)) as Map; final candidates = json['candidates'] as List?; if (candidates == null || candidates.isEmpty) continue; - final content = (candidates.first as Map)['content'] - as Map?; + final content = + (candidates.first as Map)['content'] + as Map?; final parts = content?['parts'] as List?; if (parts == null || parts.isEmpty) continue; - final text = - (parts.first as Map)['text'] as String?; + final text = (parts.first as Map)['text'] as String?; if (text != null && text.isNotEmpty) yield text; } catch (e) { - dev.log( - 'Failed to parse SSE line: $line', - name: 'GeminiChatProvider', - ); + dev.log('Failed to parse SSE line: $line', name: 'GeminiChatProvider'); } } } @@ -196,10 +194,12 @@ class GeminiChatProvider extends BaseAIChatProvider { Map? parameters, ) { // Separate system messages — Gemini uses a dedicated field - final systemMessages = - messages.where((m) => m.role == ChatRole.system).toList(); - final conversationMessages = - messages.where((m) => m.role != ChatRole.system).toList(); + final systemMessages = messages + .where((m) => m.role == ChatRole.system) + .toList(); + final conversationMessages = messages + .where((m) => m.role != ChatRole.system) + .toList(); final body = { 'contents': conversationMessages @@ -207,7 +207,7 @@ class GeminiChatProvider extends BaseAIChatProvider { (m) => { 'role': m.role == ChatRole.assistant ? 'model' : 'user', 'parts': [ - {'text': m.content} + {'text': m.content}, ], }, ) @@ -240,8 +240,7 @@ class GeminiChatProvider extends BaseAIChatProvider { final candidate = (json['candidates'] as List).first as Map; final content = candidate['content'] as Map; - final text = - (content['parts'] as List).first['text'] as String; + final text = (content['parts'] as List).first['text'] as String; final usage = json['usageMetadata'] as Map?; return ChatResponse( message: ChatMessage(role: ChatRole.assistant, content: text), diff --git a/lib/src/api/mistral_chat_provider.dart b/lib/src/api/mistral_chat_provider.dart index 5f27010..eb525dd 100644 --- a/lib/src/api/mistral_chat_provider.dart +++ b/lib/src/api/mistral_chat_provider.dart @@ -61,10 +61,10 @@ class MistralChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return sendChatMessages( - [...?history, ChatMessage.user(message)], - parameters: parameters, - ); + return sendChatMessages([ + ...?history, + ChatMessage.user(message), + ], parameters: parameters); } @override @@ -83,10 +83,10 @@ class MistralChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return _doStreamRequest( - [...?history, ChatMessage.user(message)], - parameters, - ); + return _doStreamRequest([ + ...?history, + ChatMessage.user(message), + ], parameters); } @override @@ -132,12 +132,10 @@ class MistralChatProvider extends BaseAIChatProvider { Map? parameters, ) async* { final body = _buildBody(messages, parameters, stream: true); - final request = http.Request( - 'POST', - Uri.parse('$_baseUrl/chat/completions'), - ) - ..headers.addAll(_headers()) - ..body = jsonEncode(body); + final request = + http.Request('POST', Uri.parse('$_baseUrl/chat/completions')) + ..headers.addAll(_headers()) + ..body = jsonEncode(body); final http.StreamedResponse response; try { @@ -156,24 +154,23 @@ class MistralChatProvider extends BaseAIChatProvider { } // Mistral uses the same SSE format as OpenAI - await for (final line in response.stream - .transform(utf8.decoder) - .transform(const LineSplitter())) { + await for (final line + in response.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { if (line.isEmpty || line == 'data: [DONE]') continue; if (!line.startsWith('data: ')) continue; try { final json = jsonDecode(line.substring(6)) as Map; final choices = json['choices'] as List?; if (choices == null || choices.isEmpty) continue; - final delta = (choices.first as Map)['delta'] - as Map?; + final delta = + (choices.first as Map)['delta'] + as Map?; final token = delta?['content'] as String?; if (token != null && token.isNotEmpty) yield token; } catch (e) { - dev.log( - 'Failed to parse SSE line: $line', - name: 'MistralChatProvider', - ); + dev.log('Failed to parse SSE line: $line', name: 'MistralChatProvider'); } } } @@ -195,9 +192,9 @@ class MistralChatProvider extends BaseAIChatProvider { } Map _headers() => { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/json', - }; + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/json', + }; ChatResponse _parseResponse(Map json) { final choice = (json['choices'] as List).first as Map; diff --git a/lib/src/api/openai_chat_provider.dart b/lib/src/api/openai_chat_provider.dart index b80bbd4..f52e168 100644 --- a/lib/src/api/openai_chat_provider.dart +++ b/lib/src/api/openai_chat_provider.dart @@ -72,10 +72,10 @@ class OpenAIChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return sendChatMessages( - [...?history, ChatMessage.user(message)], - parameters: parameters, - ); + return sendChatMessages([ + ...?history, + ChatMessage.user(message), + ], parameters: parameters); } @override @@ -94,10 +94,10 @@ class OpenAIChatProvider extends BaseAIChatProvider { Map? parameters, }) { checkInitialized(); - return _doStreamRequest( - [...?history, ChatMessage.user(message)], - parameters, - ); + return _doStreamRequest([ + ...?history, + ChatMessage.user(message), + ], parameters); } @override @@ -143,9 +143,10 @@ class OpenAIChatProvider extends BaseAIChatProvider { Map? parameters, ) async* { final body = _buildBody(messages, parameters, stream: true); - final request = http.Request('POST', Uri.parse('$_baseUrl/chat/completions')) - ..headers.addAll(_headers()) - ..body = jsonEncode(body); + final request = + http.Request('POST', Uri.parse('$_baseUrl/chat/completions')) + ..headers.addAll(_headers()) + ..body = jsonEncode(body); final http.StreamedResponse response; try { @@ -164,9 +165,10 @@ class OpenAIChatProvider extends BaseAIChatProvider { } // Each SSE line: "data: {json}" or "data: [DONE]" - await for (final line in response.stream - .transform(utf8.decoder) - .transform(const LineSplitter())) { + await for (final line + in response.stream + .transform(utf8.decoder) + .transform(const LineSplitter())) { if (line.isEmpty || line == 'data: [DONE]') continue; if (!line.startsWith('data: ')) continue; try { @@ -174,14 +176,12 @@ class OpenAIChatProvider extends BaseAIChatProvider { final choices = json['choices'] as List?; if (choices == null || choices.isEmpty) continue; final delta = - (choices.first as Map)['delta'] as Map?; + (choices.first as Map)['delta'] + as Map?; final token = delta?['content'] as String?; if (token != null && token.isNotEmpty) yield token; } catch (e) { - dev.log( - 'Failed to parse SSE line: $line', - name: 'OpenAIChatProvider', - ); + dev.log('Failed to parse SSE line: $line', name: 'OpenAIChatProvider'); } } } @@ -203,9 +203,9 @@ class OpenAIChatProvider extends BaseAIChatProvider { } Map _headers() => { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/json', - }; + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/json', + }; ChatResponse _parseResponse(Map json) { final choice = (json['choices'] as List).first as Map; diff --git a/lib/src/gguf/local_model.dart b/lib/src/gguf/local_model.dart index 3f860f3..3d095fd 100644 --- a/lib/src/gguf/local_model.dart +++ b/lib/src/gguf/local_model.dart @@ -91,7 +91,9 @@ class LocalModel implements LlmInterface { void _ensureInitialized() { if (_model == null || !_model!.isInitialized) { - throw StateError('LocalModel is not initialized. Call loadModel() first.'); + throw StateError( + 'LocalModel is not initialized. Call loadModel() first.', + ); } } } diff --git a/lib/src/models/llm_model_isolated.dart b/lib/src/models/llm_model_isolated.dart index 3f8dee2..c696ce8 100644 --- a/lib/src/models/llm_model_isolated.dart +++ b/lib/src/models/llm_model_isolated.dart @@ -23,16 +23,15 @@ Future _llamaIsolateWorkerMain(Map args) async { final int microBatchSize = args['microBatchSize'] as int; final int maxParallelSequences = args['maxParallelSequences'] as int; final String? chatTemplate = args['chatTemplate'] as String?; - final List loras = - (args['loras'] as List) - .cast() - .map( - (m) => LoraAdapterConfig( - path: m['path'] as String, - scale: (m['scale'] as num).toDouble(), - ), - ) - .toList(); + final List loras = (args['loras'] as List) + .cast() + .map( + (m) => LoraAdapterConfig( + path: m['path'] as String, + scale: (m['scale'] as num).toDouble(), + ), + ) + .toList(); final int maxTokens = args['maxTokens'] as int; final double temp = (args['temp'] as num).toDouble(); final int topK = args['topK'] as int; @@ -40,8 +39,8 @@ Future _llamaIsolateWorkerMain(Map args) async { final double minP = (args['minP'] as num).toDouble(); final double penalty = (args['penalty'] as num).toDouble(); final int? seed = args['seed'] as int?; - final List stopSequences = - (args['stopSequences'] as List).cast(); + final List stopSequences = (args['stopSequences'] as List) + .cast(); final String? grammar = args['grammar'] as String?; final bool grammarLazy = args['grammarLazy'] as bool; final List grammarTriggers = @@ -55,8 +54,8 @@ Future _llamaIsolateWorkerMain(Map args) async { ), ) .toList(); - final List preservedTokens = - (args['preservedTokens'] as List).cast(); + final List preservedTokens = (args['preservedTokens'] as List) + .cast(); final String grammarRoot = args['grammarRoot'] as String; final bool reusePromptPrefix = args['reusePromptPrefix'] as bool; final int streamBatchTokenThreshold = @@ -139,7 +138,8 @@ Future _llamaIsolateWorkerMain(Map args) async { .listen( (chunk) { final text = chunk.choices.firstOrNull?.delta.content; - if (text != null) streamPort.send({'type': 'token', 'text': text}); + if (text != null) + streamPort.send({'type': 'token', 'text': text}); }, onDone: () { streamPort.send({'type': 'done'}); @@ -183,49 +183,47 @@ class LlmModelIsolated extends LlmModelBase { } final initPort = ReceivePort(); - _isolate = await Isolate.spawn( - _llamaIsolateWorkerMain, - { - 'modelPath': localPath, - 'mmprojPath': config.mmprojPath, - 'contextSize': config.nCtxDefault, - 'gpuLayers': config.nGpuLayersDefault, - 'batchSize': config.nBatchDefault, - 'numberOfThreads': config.nThreadsDefault, - 'numberOfThreadsBatch': config.numberOfThreadsBatchDefault, - 'microBatchSize': config.microBatchSizeDefault, - 'maxParallelSequences': config.maxParallelSequencesDefault, - 'chatTemplate': config.chatTemplate, - 'loras': config.lorasDefault - .map((l) => {'path': l.path, 'scale': l.scale}) - .toList(), - 'maxTokens': config.nPredictDefault, - 'temp': config.tempDefault, - 'topK': config.topKDefault, - 'topP': config.topPDefault, - 'minP': config.minPDefault, - 'penalty': config.penaltyRepeatDefault, - 'seed': config.seed, - 'stopSequences': config.stopSequencesDefault, - 'grammar': config.grammar, - 'grammarLazy': config.grammarLazyDefault, - 'grammarTriggers': config.grammarTriggersDefault - .map((t) => { - 'type': t.type, - 'value': t.value, - if (t.token != null) 'token': t.token, - }) - .toList(), - 'preservedTokens': config.preservedTokensDefault, - 'grammarRoot': config.grammarRootDefault, - 'reusePromptPrefix': config.reusePromptPrefixDefault, - 'streamBatchTokenThreshold': config.streamBatchTokenThresholdDefault, - 'streamBatchByteThreshold': config.streamBatchByteThresholdDefault, - 'gpuBackend': config.gpuBackendDefault.name, - 'sendPort': initPort.sendPort, - }, - debugName: 'llmcpp_LlamaWorker', - ); + _isolate = await Isolate.spawn(_llamaIsolateWorkerMain, { + 'modelPath': localPath, + 'mmprojPath': config.mmprojPath, + 'contextSize': config.nCtxDefault, + 'gpuLayers': config.nGpuLayersDefault, + 'batchSize': config.nBatchDefault, + 'numberOfThreads': config.nThreadsDefault, + 'numberOfThreadsBatch': config.numberOfThreadsBatchDefault, + 'microBatchSize': config.microBatchSizeDefault, + 'maxParallelSequences': config.maxParallelSequencesDefault, + 'chatTemplate': config.chatTemplate, + 'loras': config.lorasDefault + .map((l) => {'path': l.path, 'scale': l.scale}) + .toList(), + 'maxTokens': config.nPredictDefault, + 'temp': config.tempDefault, + 'topK': config.topKDefault, + 'topP': config.topPDefault, + 'minP': config.minPDefault, + 'penalty': config.penaltyRepeatDefault, + 'seed': config.seed, + 'stopSequences': config.stopSequencesDefault, + 'grammar': config.grammar, + 'grammarLazy': config.grammarLazyDefault, + 'grammarTriggers': config.grammarTriggersDefault + .map( + (t) => { + 'type': t.type, + 'value': t.value, + if (t.token != null) 'token': t.token, + }, + ) + .toList(), + 'preservedTokens': config.preservedTokensDefault, + 'grammarRoot': config.grammarRootDefault, + 'reusePromptPrefix': config.reusePromptPrefixDefault, + 'streamBatchTokenThreshold': config.streamBatchTokenThresholdDefault, + 'streamBatchByteThreshold': config.streamBatchByteThresholdDefault, + 'gpuBackend': config.gpuBackendDefault.name, + 'sendPort': initPort.sendPort, + }, debugName: 'llmcpp_LlamaWorker'); final initMsg = await initPort.first as Map; initPort.close(); @@ -307,7 +305,9 @@ class LlmModelIsolated extends LlmModelBase { markGenerationStart(); try { final buffer = StringBuffer(); - await for (final token in _rawWorkerStream(_buildMessage(prompt, images: images))) { + await for (final token in _rawWorkerStream( + _buildMessage(prompt, images: images), + )) { buffer.write(token); } return buffer.toString(); @@ -328,7 +328,9 @@ class LlmModelIsolated extends LlmModelBase { markGenerationStart(); try { - await for (final token in _rawWorkerStream(_buildMessage(prompt, images: images))) { + await for (final token in _rawWorkerStream( + _buildMessage(prompt, images: images), + )) { totalTokenCount += 1; yield StreamingChunk( text: token, diff --git a/lib/src/models/llm_model_standard.dart b/lib/src/models/llm_model_standard.dart index da41220..4b3cd9e 100644 --- a/lib/src/models/llm_model_standard.dart +++ b/lib/src/models/llm_model_standard.dart @@ -47,7 +47,10 @@ class LlmModelStandard extends LlmModelBase { streamBatchByteThreshold: config.streamBatchByteThresholdDefault, ); - LlamaChatMessage _buildMessage(String prompt, List? images) { + LlamaChatMessage _buildMessage( + String prompt, + List? images, + ) { if (images != null && images.isNotEmpty) { return LlamaChatMessage.withContent( role: LlamaChatRole.user, @@ -90,10 +93,9 @@ class LlmModelStandard extends LlmModelBase { markGenerationStart(); try { final buffer = StringBuffer(); - await for (final chunk in _engine!.create( - [_buildMessage(prompt, images)], - params: _genParams, - )) { + await for (final chunk in _engine!.create([ + _buildMessage(prompt, images), + ], params: _genParams)) { final text = chunk.choices.firstOrNull?.delta.content; if (text != null) buffer.write(text); } @@ -115,10 +117,9 @@ class LlmModelStandard extends LlmModelBase { markGenerationStart(); try { - await for (final chunk in _engine!.create( - [_buildMessage(prompt, images)], - params: _genParams, - )) { + await for (final chunk in _engine!.create([ + _buildMessage(prompt, images), + ], params: _genParams)) { final text = chunk.choices.firstOrNull?.delta.content; if (text == null) continue; @@ -154,10 +155,9 @@ class LlmModelStandard extends LlmModelBase { markGenerationStart(); try { - await for (final chunk in _engine!.create( - [_buildMessage(prompt, images)], - params: _genParams, - )) { + await for (final chunk in _engine!.create([ + _buildMessage(prompt, images), + ], params: _genParams)) { final text = chunk.choices.firstOrNull?.delta.content; if (text == null) continue; diff --git a/lib/src/rag/chunking/text_chunker.dart b/lib/src/rag/chunking/text_chunker.dart index 4ac1551..0fa5443 100644 --- a/lib/src/rag/chunking/text_chunker.dart +++ b/lib/src/rag/chunking/text_chunker.dart @@ -43,9 +43,9 @@ class TextChunker { this.chunkOverlap = 100, this.minChunkSize = 50, }) : assert( - chunkOverlap < chunkSize, - 'chunkOverlap must be less than chunkSize', - ); + chunkOverlap < chunkSize, + 'chunkOverlap must be less than chunkSize', + ); /// Splits [document] into a list of [DocumentChunk]. /// @@ -69,18 +69,20 @@ class TextChunker { final chunkText = text.substring(start, end).trim(); if (chunkText.length >= minChunkSize) { - chunks.add(DocumentChunk( - id: '${document.id}_chunk_$chunkIndex', - documentId: document.id, - text: chunkText, - chunkIndex: chunkIndex, - startChar: start, - endChar: end, - metadata: { - 'documentTitle': document.title, - 'documentSource': document.source, - }, - )); + chunks.add( + DocumentChunk( + id: '${document.id}_chunk_$chunkIndex', + documentId: document.id, + text: chunkText, + chunkIndex: chunkIndex, + startChar: start, + endChar: end, + metadata: { + 'documentTitle': document.title, + 'documentSource': document.source, + }, + ), + ); chunkIndex++; } diff --git a/lib/src/rag/document/document.dart b/lib/src/rag/document/document.dart index 9b55f86..340ac56 100644 --- a/lib/src/rag/document/document.dart +++ b/lib/src/rag/document/document.dart @@ -30,8 +30,8 @@ class Document { required this.content, Map? metadata, DateTime? createdAt, - }) : metadata = metadata ?? {}, - createdAt = createdAt ?? DateTime.now(); + }) : metadata = metadata ?? {}, + createdAt = createdAt ?? DateTime.now(); // ── Factory constructors ───────────────────────────────────────────────── @@ -98,20 +98,20 @@ class Document { // ── Serialization ──────────────────────────────────────────────────────── Map toJson() => { - 'id': id, - 'source': source, - 'content': content, - 'metadata': metadata, - 'createdAt': createdAt.toIso8601String(), - }; + 'id': id, + 'source': source, + 'content': content, + 'metadata': metadata, + 'createdAt': createdAt.toIso8601String(), + }; factory Document.fromJson(Map json) => Document( - id: json['id'] as String, - source: json['source'] as String, - content: json['content'] as String, - metadata: (json['metadata'] as Map?) ?? {}, - createdAt: DateTime.parse(json['createdAt'] as String), - ); + id: json['id'] as String, + source: json['source'] as String, + content: json['content'] as String, + metadata: (json['metadata'] as Map?) ?? {}, + createdAt: DateTime.parse(json['createdAt'] as String), + ); @override String toString() => diff --git a/lib/src/rag/document/document_chunk.dart b/lib/src/rag/document/document_chunk.dart index 599986f..6f29833 100644 --- a/lib/src/rag/document/document_chunk.dart +++ b/lib/src/rag/document/document_chunk.dart @@ -62,16 +62,16 @@ class DocumentChunk { // ── Serialization ──────────────────────────────────────────────────────── Map toJson() => { - 'id': id, - 'documentId': documentId, - 'text': text, - 'chunkIndex': chunkIndex, - 'startChar': startChar, - 'endChar': endChar, - // List → List (JSON compatible) - 'embedding': embedding, - 'metadata': metadata, - }; + 'id': id, + 'documentId': documentId, + 'text': text, + 'chunkIndex': chunkIndex, + 'startChar': startChar, + 'endChar': endChar, + // List → List (JSON compatible) + 'embedding': embedding, + 'metadata': metadata, + }; factory DocumentChunk.fromJson(Map json) { final rawEmbedding = json['embedding'] as List?; diff --git a/lib/src/rag/embeddings/llama_embedding_provider.dart b/lib/src/rag/embeddings/llama_embedding_provider.dart index 0ea6601..8c1a06c 100644 --- a/lib/src/rag/embeddings/llama_embedding_provider.dart +++ b/lib/src/rag/embeddings/llama_embedding_provider.dart @@ -127,17 +127,13 @@ class LlamaEmbeddingProvider implements EmbeddingProvider { } final initPort = ReceivePort(); - _isolate = await Isolate.spawn( - _llamaEmbedWorkerMain, - { - 'modelPath': modelPath, - 'contextSize': embedNCtx, - 'batchSize': batchSize, - 'numberOfThreads': numberOfThreads, - 'sendPort': initPort.sendPort, - }, - debugName: 'llmcpp_EmbedWorker', - ); + _isolate = await Isolate.spawn(_llamaEmbedWorkerMain, { + 'modelPath': modelPath, + 'contextSize': embedNCtx, + 'batchSize': batchSize, + 'numberOfThreads': numberOfThreads, + 'sendPort': initPort.sendPort, + }, debugName: 'llmcpp_EmbedWorker'); final initMsg = await initPort.first as Map; initPort.close(); diff --git a/lib/src/rag/rag_engine.dart b/lib/src/rag/rag_engine.dart index a85c011..700cbbf 100644 --- a/lib/src/rag/rag_engine.dart +++ b/lib/src/rag/rag_engine.dart @@ -208,7 +208,9 @@ class RagEngine { void _checkReady() { if (!_isReady) { - throw StateError('RagEngine is not initialized. Call initialize() first.'); + throw StateError( + 'RagEngine is not initialized. Call initialize() first.', + ); } } } diff --git a/lib/src/rag/rag_pipeline.dart b/lib/src/rag/rag_pipeline.dart index f7ebe20..b1ed266 100644 --- a/lib/src/rag/rag_pipeline.dart +++ b/lib/src/rag/rag_pipeline.dart @@ -31,8 +31,7 @@ class RagIngestionProgress { }); /// Completion fraction (0.0–1.0) - double get fraction => - totalChunks > 0 ? embeddedChunks / totalChunks : 0.0; + double get fraction => totalChunks > 0 ? embeddedChunks / totalChunks : 0.0; @override String toString() => @@ -110,8 +109,8 @@ class RagPipeline { required this.generationPlugin, TextChunker? chunker, String? promptTemplate, - }) : chunker = chunker ?? const TextChunker(), - promptTemplate = promptTemplate ?? defaultPromptTemplate; + }) : chunker = chunker ?? const TextChunker(), + promptTemplate = promptTemplate ?? defaultPromptTemplate; // ── Ingestion ───────────────────────────────────────────────────────────── @@ -216,10 +215,13 @@ class RagPipeline { } // Step 3: build prompt with context - final contextParts = results.map((r) { - final source = r.chunk.metadata['documentTitle'] ?? r.chunk.documentId; - return '[$source, similarity: ${(r.similarity * 100).toStringAsFixed(0)}%]\n${r.chunk.text}'; - }).join('\n\n---\n\n'); + final contextParts = results + .map((r) { + final source = + r.chunk.metadata['documentTitle'] ?? r.chunk.documentId; + return '[$source, similarity: ${(r.similarity * 100).toStringAsFixed(0)}%]\n${r.chunk.text}'; + }) + .join('\n\n---\n\n'); final augmentedPrompt = promptTemplate .replaceAll('{context}', contextParts) diff --git a/lib/src/rag/vector_store/in_memory_vector_store.dart b/lib/src/rag/vector_store/in_memory_vector_store.dart index 2333525..244512a 100644 --- a/lib/src/rag/vector_store/in_memory_vector_store.dart +++ b/lib/src/rag/vector_store/in_memory_vector_store.dart @@ -66,13 +66,18 @@ class InMemoryVectorStore implements VectorStore { for (final chunk in _chunks) { if (chunk.embedding == null) continue; - final similarity = VectorSimilarity.cosine(queryEmbedding, chunk.embedding!); + final similarity = VectorSimilarity.cosine( + queryEmbedding, + chunk.embedding!, + ); if (similarity >= minSimilarity) { - results.add(VectorSearchResult( - chunk: chunk, - similarity: similarity, - rank: 0, // filled in below - )); + results.add( + VectorSearchResult( + chunk: chunk, + similarity: similarity, + rank: 0, // filled in below + ), + ); } } diff --git a/test/src/api/ai_chat_provider_factory_test.dart b/test/src/api/ai_chat_provider_factory_test.dart index 072137f..5948e46 100644 --- a/test/src/api/ai_chat_provider_factory_test.dart +++ b/test/src/api/ai_chat_provider_factory_test.dart @@ -42,10 +42,7 @@ void main() { test('every provider implements AIChatProvider', () { for (final type in AIChatProviderType.values) { - expect( - AIChatProviderFactory.create(type), - isA(), - ); + expect(AIChatProviderFactory.create(type), isA()); } }); @@ -88,8 +85,11 @@ void main() { for (final type in AIChatProviderType.values) { final provider = AIChatProviderFactory.create(type); await provider.initialize({'apiKey': 'test-key'}); - expect(provider.isInitialized, isTrue, - reason: '$type should be initialized'); + expect( + provider.isInitialized, + isTrue, + reason: '$type should be initialized', + ); await provider.dispose(); } }); diff --git a/test/src/models/llm_model_standard_test.dart b/test/src/models/llm_model_standard_test.dart index 5ac6906..5ea0498 100644 --- a/test/src/models/llm_model_standard_test.dart +++ b/test/src/models/llm_model_standard_test.dart @@ -80,11 +80,7 @@ void main() { }); test('should accept config with all numeric params', () { - const config = LlmConfig( - nGpuLayers: 4, - temp: 0.5, - topP: 0.8, - ); + const config = LlmConfig(nGpuLayers: 4, temp: 0.5, topP: 0.8); final model = LlmModelStandard(config);