better ai engine error handling
This commit is contained in:
+138
-58
@@ -20,6 +20,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:exceptions/exceptions.dart';
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:flutter_mozilla_components/ml_utils.dart';
|
import 'package:flutter_mozilla_components/ml_utils.dart';
|
||||||
@@ -27,6 +28,7 @@ import 'package:nullability/nullability.dart';
|
|||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
@@ -62,36 +64,44 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> predictDocumentTopic(Set<String> titles) async {
|
Future<Result<String?>> predictDocumentTopic(Set<String> titles) async {
|
||||||
if (!ref.read(
|
if (!ref.read(
|
||||||
generalSettingsWithDefaultsProvider.select(
|
generalSettingsWithDefaultsProvider.select(
|
||||||
(settings) => settings.enableLocalAiFeatures,
|
(settings) => settings.enableLocalAiFeatures,
|
||||||
),
|
),
|
||||||
)) {
|
)) {
|
||||||
return null;
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (titles.isNotEmpty) {
|
if (titles.isNotEmpty) {
|
||||||
if (_topicCache.get(titles) case final String title) {
|
if (_topicCache.get(titles) case final String title) {
|
||||||
return title;
|
return Result.success(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final title = await _engineLock.synchronized(() async {
|
final title = await _engineLock.synchronized(() async {
|
||||||
await _initialLoadComplete.future;
|
await _initialLoadComplete.future;
|
||||||
|
|
||||||
final title = await _service.predictDocumentTopic(titles);
|
final title = await Result.fromAsync(
|
||||||
|
() async => await _service.predictDocumentTopic(titles),
|
||||||
|
);
|
||||||
|
title.onSuccess((title) => _topicCache.set(titles, title));
|
||||||
|
|
||||||
return _topicCache.set(titles, title);
|
return title;
|
||||||
}, timeout: const Duration(seconds: 120));
|
}, timeout: const Duration(seconds: 120));
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
return null;
|
return Result.failure(
|
||||||
|
const ErrorMessage(
|
||||||
|
source: 'Document Title Prediction',
|
||||||
|
message: 'Timeout',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>?> suggestDocuments({
|
Future<List<String>?> suggestDocuments({
|
||||||
@@ -126,15 +136,30 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
|
|||||||
...assignedDocumentsProcessed,
|
...assignedDocumentsProcessed,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final neighbors = embeddings.mapNotNull(
|
return embeddings.fold(
|
||||||
(embeddings) => findNearestNeighborsRecursive(
|
(embeddings) {
|
||||||
embeddings: embeddings,
|
final neighbors = embeddings.mapNotNull(
|
||||||
assignedDocuments: assignedDocumentsProcessed,
|
(embeddings) =>
|
||||||
unassignedDocuments: unassignedDocumentsProcessed,
|
findNearestNeighborsRecursive(
|
||||||
).map((neighbor) => processedDocuments[neighbor] ?? neighbor).toList(),
|
embeddings: embeddings,
|
||||||
);
|
assignedDocuments: assignedDocumentsProcessed,
|
||||||
|
unassignedDocuments: unassignedDocumentsProcessed,
|
||||||
|
)
|
||||||
|
.map((neighbor) => processedDocuments[neighbor] ?? neighbor)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
return neighbors;
|
return neighbors;
|
||||||
|
},
|
||||||
|
onFailure: (errorMessage) {
|
||||||
|
logger.e(
|
||||||
|
errorMessage.message,
|
||||||
|
error: errorMessage.details,
|
||||||
|
stackTrace: errorMessage.stackTrace,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SuggestedContainer>?> suggestClusters({
|
Future<List<SuggestedContainer>?> suggestClusters({
|
||||||
@@ -164,42 +189,66 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
|
|||||||
unassignedDocumentsProcessed,
|
unassignedDocumentsProcessed,
|
||||||
);
|
);
|
||||||
|
|
||||||
final clusters = embeddings.mapNotNull(
|
return embeddings.fold(
|
||||||
(embeddings) => clusterEmbeddings(embeddings: embeddings.values.toList())
|
(embeddings) async {
|
||||||
.map(
|
final clusters = embeddings.mapNotNull(
|
||||||
(cluster) =>
|
(embeddings) =>
|
||||||
cluster.map((i) => embeddings.keys.elementAt(i)).toList(),
|
clusterEmbeddings(embeddings: embeddings.values.toList())
|
||||||
)
|
.map(
|
||||||
.toList(),
|
(cluster) => cluster
|
||||||
|
.map((i) => embeddings.keys.elementAt(i))
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final clusterResult = await clusters.mapNotNull(
|
||||||
|
(cluster) => Future.wait(
|
||||||
|
cluster.map((clusterTitles) async {
|
||||||
|
final originalTitles = clusterTitles
|
||||||
|
.map((title) => processedDocuments[title] ?? title)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
final topic = await predictDocumentTopic(originalTitles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
topic: topic.fold(
|
||||||
|
(topic) => topic,
|
||||||
|
onFailure: (errorMessage) {
|
||||||
|
logger.e(
|
||||||
|
errorMessage.message,
|
||||||
|
error: errorMessage.details,
|
||||||
|
stackTrace: errorMessage.stackTrace,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tabIds: originalTitles
|
||||||
|
.map(
|
||||||
|
(title) => unassignedDocumentsInput.entries
|
||||||
|
.firstWhere((entry) => entry.value == title)
|
||||||
|
.key,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return clusterResult;
|
||||||
|
},
|
||||||
|
onFailure: (errorMessage) {
|
||||||
|
logger.e(
|
||||||
|
errorMessage.message,
|
||||||
|
error: errorMessage.details,
|
||||||
|
stackTrace: errorMessage.stackTrace,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final clusterResult = await clusters.mapNotNull(
|
|
||||||
(cluster) => Future.wait(
|
|
||||||
cluster.map((clusterTitles) async {
|
|
||||||
final originalTitles = clusterTitles
|
|
||||||
.map((title) => processedDocuments[title] ?? title)
|
|
||||||
.toSet();
|
|
||||||
|
|
||||||
final topic = await predictDocumentTopic(originalTitles);
|
|
||||||
|
|
||||||
return (
|
|
||||||
topic: topic,
|
|
||||||
tabIds: originalTitles
|
|
||||||
.map(
|
|
||||||
(title) => unassignedDocumentsInput.entries
|
|
||||||
.firstWhere((entry) => entry.value == title)
|
|
||||||
.key,
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return clusterResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<double>>?> generateDocumentEmbeddings(
|
Future<Result<Map<String, List<double>>?>> generateDocumentEmbeddings(
|
||||||
List<String> documents,
|
List<String> documents,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
@@ -216,24 +265,30 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
|
|||||||
final generatedEmbeddings = await _engineLock.synchronized(() async {
|
final generatedEmbeddings = await _engineLock.synchronized(() async {
|
||||||
await _initialLoadComplete.future;
|
await _initialLoadComplete.future;
|
||||||
|
|
||||||
final embeddings = await _service.generateDocumentEmbeddings(
|
final embeddings = await Result.fromAsync(
|
||||||
documents,
|
() async => await _service.generateDocumentEmbeddings(documents),
|
||||||
);
|
);
|
||||||
|
|
||||||
return embeddings;
|
return embeddings;
|
||||||
}, timeout: const Duration(seconds: 120));
|
}, timeout: const Duration(seconds: 120));
|
||||||
|
|
||||||
|
if (!generatedEmbeddings.isSuccess) {
|
||||||
|
return Result.failure(generatedEmbeddings.error!);
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < embeddingsToGenerate.length; i++) {
|
for (var i = 0; i < embeddingsToGenerate.length; i++) {
|
||||||
embeddings[embeddingsToGenerate[i]] = generatedEmbeddings[i];
|
embeddings[embeddingsToGenerate[i]] = generatedEmbeddings.value[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return Result.success({
|
||||||
for (final MapEntry(:key, :value) in embeddings.entries)
|
for (final MapEntry(:key, :value) in embeddings.entries)
|
||||||
if (value != null) key: value,
|
if (value != null) key: value,
|
||||||
};
|
});
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
return null;
|
return Result.failure(
|
||||||
|
const ErrorMessage(source: 'Document Embeddings', message: 'Timeout'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,11 +338,23 @@ Future<String?> topicSuggestion(
|
|||||||
.read(geckoInferenceRepositoryProvider.notifier)
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
.predictDocumentTopic(titles.value);
|
.predictDocumentTopic(titles.value);
|
||||||
|
|
||||||
if (ref.mounted && topic.isNotEmpty) {
|
return topic.fold(
|
||||||
ref.keepAlive();
|
(topic) {
|
||||||
}
|
if (ref.mounted && topic.isNotEmpty) {
|
||||||
|
ref.keepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
return topic;
|
return topic;
|
||||||
|
},
|
||||||
|
onFailure: (errorMessage) {
|
||||||
|
logger.e(
|
||||||
|
errorMessage.message,
|
||||||
|
error: errorMessage.details,
|
||||||
|
stackTrace: errorMessage.stackTrace,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
@@ -359,6 +426,19 @@ Future<List<String>?> containerTabSuggestions(
|
|||||||
.read(geckoInferenceRepositoryProvider.notifier)
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
.predictDocumentTopic(
|
.predictDocumentTopic(
|
||||||
assignedTitles.value.map((tab) => tab.$2).toSet(),
|
assignedTitles.value.map((tab) => tab.$2).toSet(),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
(result) => result.fold(
|
||||||
|
(value) => value,
|
||||||
|
onFailure: (errorMessage) {
|
||||||
|
logger.e(
|
||||||
|
errorMessage.message,
|
||||||
|
error: errorMessage.details,
|
||||||
|
stackTrace: errorMessage.stackTrace,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ref.mounted && topic != null) {
|
if (ref.mounted && topic != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user