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-30 20:12:20 +08:00
|
|
|
enum LocalAIStreamingState {
|
|
|
|
notReady,
|
|
|
|
disabled,
|
|
|
|
}
|
|
|
|
|
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-20 13:30:42 +08:00
|
|
|
required Future<void> Function(String text) processMessage,
|
|
|
|
required Future<void> Function(String text) processAssistMessage,
|
2024-06-25 01:59:38 +02:00
|
|
|
required Future<void> Function() onEnd,
|
|
|
|
required void Function(AIError error) onError,
|
2025-03-30 20:12:20 +08:00
|
|
|
required void Function(LocalAIStreamingState state)
|
|
|
|
onLocalAIStreamingStateChange,
|
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,
|
2025-03-20 13:30:42 +08:00
|
|
|
required Future<void> Function(String text) processMessage,
|
|
|
|
required Future<void> Function(String text) processAssistMessage,
|
2024-06-25 01:59:38 +02:00
|
|
|
required Future<void> Function() onEnd,
|
|
|
|
required void Function(AIError error) onError,
|
2025-03-30 20:12:20 +08:00
|
|
|
required void Function(LocalAIStreamingState state)
|
|
|
|
onLocalAIStreamingStateChange,
|
2024-06-25 01:59:38 +02:00
|
|
|
}) async {
|
2025-03-03 13:35:51 +08:00
|
|
|
final stream = AppFlowyCompletionStream(
|
|
|
|
onStart: onStart,
|
2025-03-20 13:30:42 +08:00
|
|
|
processMessage: processMessage,
|
|
|
|
processAssistMessage: processAssistMessage,
|
|
|
|
processError: onError,
|
2025-03-30 20:12:20 +08:00
|
|
|
onLocalAIStreamingStateChange: onLocalAIStreamingStateChange,
|
2025-03-03 13:35:51 +08:00
|
|
|
onEnd: onEnd,
|
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,
|
2025-03-20 13:30:42 +08:00
|
|
|
required this.processMessage,
|
|
|
|
required this.processAssistMessage,
|
|
|
|
required this.processError,
|
2025-03-30 20:12:20 +08:00
|
|
|
required this.onLocalAIStreamingStateChange,
|
2025-03-03 13:35:51 +08:00
|
|
|
required this.onEnd,
|
|
|
|
});
|
|
|
|
|
|
|
|
final Future<void> Function() onStart;
|
2025-03-20 13:30:42 +08:00
|
|
|
final Future<void> Function(String text) processMessage;
|
|
|
|
final Future<void> Function(String text) processAssistMessage;
|
|
|
|
final void Function(AIError error) processError;
|
2025-03-30 20:12:20 +08:00
|
|
|
final void Function(LocalAIStreamingState state)
|
|
|
|
onLocalAIStreamingStateChange;
|
2025-03-03 13:35:51 +08:00
|
|
|
final Future<void> Function() onEnd;
|
2024-06-25 01:59:38 +02:00
|
|
|
}
|
|
|
|
|
2025-03-03 13:35:51 +08:00
|
|
|
class AppFlowyCompletionStream extends CompletionStream {
|
|
|
|
AppFlowyCompletionStream({
|
|
|
|
required super.onStart,
|
2025-03-20 13:30:42 +08:00
|
|
|
required super.processMessage,
|
|
|
|
required super.processAssistMessage,
|
|
|
|
required super.processError,
|
2025-03-03 13:35:51 +08:00
|
|
|
required super.onEnd,
|
2025-03-30 20:12:20 +08:00
|
|
|
required super.onLocalAIStreamingStateChange,
|
2025-03-03 13:35:51 +08:00
|
|
|
}) {
|
|
|
|
_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 {
|
2025-03-30 15:15:59 +08:00
|
|
|
await _handleEvent(event);
|
2024-06-25 01:59:38 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> dispose() async {
|
|
|
|
await _controller.close();
|
|
|
|
await _subscription.cancel();
|
|
|
|
_port.close();
|
|
|
|
}
|
2025-03-30 15:15:59 +08:00
|
|
|
|
|
|
|
Future<void> _handleEvent(String event) async {
|
|
|
|
// Check simple matches first
|
|
|
|
if (event == AIStreamEventPrefix.aiResponseLimit) {
|
|
|
|
processError(
|
|
|
|
AIError(
|
|
|
|
message: LocaleKeys.ai_textLimitReachedDescription.tr(),
|
|
|
|
code: AIErrorCode.aiResponseLimitExceeded,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event == AIStreamEventPrefix.aiImageResponseLimit) {
|
|
|
|
processError(
|
|
|
|
AIError(
|
|
|
|
message: LocaleKeys.ai_imageLimitReachedDescription.tr(),
|
|
|
|
code: AIErrorCode.aiImageResponseLimitExceeded,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, parse out prefix:content
|
2025-03-30 20:12:20 +08:00
|
|
|
if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) {
|
|
|
|
processError(
|
|
|
|
AIError(
|
|
|
|
message: event.substring(AIStreamEventPrefix.aiMaxRequired.length),
|
|
|
|
code: AIErrorCode.other,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.start)) {
|
|
|
|
await onStart();
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.data)) {
|
|
|
|
await processMessage(
|
|
|
|
event.substring(AIStreamEventPrefix.data.length),
|
|
|
|
);
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.comment)) {
|
|
|
|
await processAssistMessage(
|
|
|
|
event.substring(AIStreamEventPrefix.comment.length),
|
|
|
|
);
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.finish)) {
|
|
|
|
await onEnd();
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.localAIDisabled)) {
|
|
|
|
onLocalAIStreamingStateChange(
|
|
|
|
LocalAIStreamingState.disabled,
|
|
|
|
);
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) {
|
|
|
|
onLocalAIStreamingStateChange(
|
|
|
|
LocalAIStreamingState.notReady,
|
|
|
|
);
|
|
|
|
} else if (event.startsWith(AIStreamEventPrefix.error)) {
|
|
|
|
processError(
|
|
|
|
AIError(
|
|
|
|
message: event.substring(AIStreamEventPrefix.error.length),
|
|
|
|
code: AIErrorCode.other,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
Log.debug('Unknown AI event: $event');
|
2025-03-30 15:15:59 +08:00
|
|
|
}
|
|
|
|
}
|
2024-06-25 01:59:38 +02:00
|
|
|
}
|