2024-06-25 01:59:38 +02:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:ffi';
|
|
|
|
import 'dart:isolate';
|
|
|
|
|
2024-07-22 09:43:48 +02:00
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
2025-03-18 17:14:20 +08:00
|
|
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
2025-03-03 13:35:51 +08:00
|
|
|
import 'package:appflowy/shared/list_extension.dart';
|
2024-06-25 01:59:38 +02:00
|
|
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
2025-03-03 13:35:51 +08:00
|
|
|
import 'package:appflowy_backend/log.dart';
|
2024-08-01 23:13:35 +08:00
|
|
|
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
2024-06-25 01:59:38 +02:00
|
|
|
import 'package:appflowy_result/appflowy_result.dart';
|
2024-07-22 09:43:48 +02:00
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
2024-06-25 01:59:38 +02:00
|
|
|
import 'package:fixnum/fixnum.dart' as fixnum;
|
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
import 'ai_entities.dart';
|
2025-01-06 13:34:11 +08:00
|
|
|
import 'error.dart';
|
2025-01-15 19:57:47 +08:00
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
abstract class AIRepository {
|
|
|
|
Future<void> streamCompletion({
|
|
|
|
String? objectId,
|
|
|
|
required String text,
|
|
|
|
PredefinedFormat? format,
|
2025-03-18 17:14:20 +08:00
|
|
|
List<String> sourceIds = const [],
|
|
|
|
List<AiWriterRecord> history = const [],
|
2025-03-03 13:35:51 +08:00
|
|
|
required CompletionTypePB completionType,
|
2024-06-25 01:59:38 +02:00
|
|
|
required Future<void> Function() onStart,
|
2025-03-03 13:35:51 +08:00
|
|
|
required Future<void> Function(String text) onProcess,
|
2024-06-25 01:59:38 +02:00
|
|
|
required Future<void> Function() onEnd,
|
|
|
|
required void Function(AIError error) onError,
|
2025-03-03 13:35:51 +08:00
|
|
|
});
|
|
|
|
}
|
2024-06-25 01:59:38 +02:00
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
class AppFlowyAIService implements AIRepository {
|
2024-06-25 01:59:38 +02:00
|
|
|
@override
|
2025-03-03 13:35:51 +08:00
|
|
|
Future<(String, CompletionStream)?> streamCompletion({
|
2025-01-12 22:33:21 +08:00
|
|
|
String? objectId,
|
2024-06-25 01:59:38 +02:00
|
|
|
required String text,
|
2025-03-03 13:35:51 +08:00
|
|
|
PredefinedFormat? format,
|
|
|
|
List<String> sourceIds = const [],
|
2025-03-18 17:14:20 +08:00
|
|
|
List<AiWriterRecord> history = const [],
|
2024-06-25 01:59:38 +02:00
|
|
|
required CompletionTypePB completionType,
|
|
|
|
required Future<void> Function() onStart,
|
|
|
|
required Future<void> Function(String text) onProcess,
|
|
|
|
required Future<void> Function() onEnd,
|
|
|
|
required void Function(AIError error) onError,
|
|
|
|
}) async {
|
2025-03-03 13:35:51 +08:00
|
|
|
final stream = AppFlowyCompletionStream(
|
|
|
|
onStart: onStart,
|
|
|
|
onProcess: onProcess,
|
|
|
|
onEnd: onEnd,
|
|
|
|
onError: onError,
|
2025-03-20 11:41:49 +08:00
|
|
|
onComment: (String text) async {
|
|
|
|
Log.info('Comment: $text');
|
|
|
|
},
|
2024-06-25 01:59:38 +02:00
|
|
|
);
|
2025-01-12 22:33:21 +08:00
|
|
|
|
2025-03-18 17:14:20 +08:00
|
|
|
final records = history.map((record) => record.toPB()).toList();
|
|
|
|
|
2024-06-25 01:59:38 +02:00
|
|
|
final payload = CompleteTextPB(
|
|
|
|
text: text,
|
|
|
|
completionType: completionType,
|
2025-03-03 13:35:51 +08:00
|
|
|
format: format?.toPB(),
|
2024-06-25 01:59:38 +02:00
|
|
|
streamPort: fixnum.Int64(stream.nativePort),
|
2025-03-03 13:35:51 +08:00
|
|
|
objectId: objectId ?? '',
|
|
|
|
ragIds: [
|
|
|
|
if (objectId != null) objectId,
|
|
|
|
...sourceIds,
|
|
|
|
].unique(),
|
2025-03-18 17:14:20 +08:00
|
|
|
history: records,
|
2024-06-25 01:59:38 +02:00
|
|
|
);
|
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
return AIEventCompleteText(payload).send().fold(
|
|
|
|
(task) => (task.taskId, stream),
|
|
|
|
(error) {
|
|
|
|
Log.error(error);
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
);
|
2024-06-25 01:59:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
abstract class CompletionStream {
|
|
|
|
CompletionStream({
|
|
|
|
required this.onStart,
|
|
|
|
required this.onProcess,
|
2025-03-20 11:41:49 +08:00
|
|
|
required this.onComment,
|
2025-03-03 13:35:51 +08:00
|
|
|
required this.onEnd,
|
|
|
|
required this.onError,
|
|
|
|
});
|
|
|
|
|
|
|
|
final Future<void> Function() onStart;
|
|
|
|
final Future<void> Function(String text) onProcess;
|
2025-03-20 11:41:49 +08:00
|
|
|
final Future<void> Function(String text) onComment;
|
2025-03-03 13:35:51 +08:00
|
|
|
final Future<void> Function() onEnd;
|
|
|
|
final void Function(AIError error) onError;
|
2024-06-25 01:59:38 +02:00
|
|
|
}
|
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
class AppFlowyCompletionStream extends CompletionStream {
|
|
|
|
AppFlowyCompletionStream({
|
|
|
|
required super.onStart,
|
|
|
|
required super.onProcess,
|
2025-03-20 11:41:49 +08:00
|
|
|
required super.onComment,
|
2025-03-03 13:35:51 +08:00
|
|
|
required super.onEnd,
|
|
|
|
required super.onError,
|
|
|
|
}) {
|
|
|
|
_startListening();
|
|
|
|
}
|
|
|
|
|
|
|
|
final RawReceivePort _port = RawReceivePort();
|
|
|
|
final StreamController<String> _controller = StreamController.broadcast();
|
|
|
|
late StreamSubscription<String> _subscription;
|
|
|
|
int get nativePort => _port.sendPort.nativePort;
|
|
|
|
|
|
|
|
void _startListening() {
|
2024-06-25 01:59:38 +02:00
|
|
|
_port.handler = _controller.add;
|
|
|
|
_subscription = _controller.stream.listen(
|
|
|
|
(event) async {
|
2024-07-22 09:43:48 +02:00
|
|
|
if (event == "AI_RESPONSE_LIMIT") {
|
|
|
|
onError(
|
|
|
|
AIError(
|
2025-03-03 13:35:51 +08:00
|
|
|
message: LocaleKeys.ai_textLimitReachedDescription.tr(),
|
2024-07-22 09:43:48 +02:00
|
|
|
code: AIErrorCode.aiResponseLimitExceeded,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-01-22 09:42:24 +08:00
|
|
|
if (event == "AI_IMAGE_RESPONSE_LIMIT") {
|
|
|
|
onError(
|
|
|
|
AIError(
|
2025-03-03 13:35:51 +08:00
|
|
|
message: LocaleKeys.ai_imageLimitReachedDescription.tr(),
|
2025-01-22 09:42:24 +08:00
|
|
|
code: AIErrorCode.aiImageResponseLimitExceeded,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:52:08 +08:00
|
|
|
if (event.startsWith("AI_MAX_REQUIRED:")) {
|
|
|
|
final msg = event.substring(16);
|
|
|
|
onError(
|
|
|
|
AIError(
|
|
|
|
message: msg,
|
2025-03-03 13:35:51 +08:00
|
|
|
code: AIErrorCode.other,
|
2025-02-03 20:52:08 +08:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-25 01:59:38 +02:00
|
|
|
if (event.startsWith("start:")) {
|
|
|
|
await onStart();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.startsWith("data:")) {
|
|
|
|
await onProcess(event.substring(5));
|
|
|
|
}
|
|
|
|
|
2025-03-20 11:44:02 +08:00
|
|
|
if (event.startsWith("comment:")) {
|
|
|
|
await onComment(event.substring(8));
|
|
|
|
}
|
|
|
|
|
2024-06-25 01:59:38 +02:00
|
|
|
if (event.startsWith("finish:")) {
|
|
|
|
await onEnd();
|
|
|
|
}
|
2024-07-22 09:43:48 +02:00
|
|
|
|
2024-06-25 01:59:38 +02:00
|
|
|
if (event.startsWith("error:")) {
|
2025-03-03 13:35:51 +08:00
|
|
|
onError(
|
|
|
|
AIError(message: event.substring(6), code: AIErrorCode.other),
|
|
|
|
);
|
2024-06-25 01:59:38 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> dispose() async {
|
|
|
|
await _controller.close();
|
|
|
|
await _subscription.cancel();
|
|
|
|
_port.close();
|
|
|
|
}
|
|
|
|
}
|