AppFlowy/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart

198 lines
5.8 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
feat: ai billing (#5741) * feat: start on AI plan+billing UI * chore: enable plan and billing * feat: cache workspace subscription + minor fixes (#5705) * feat: update api from billing * feat: add api for workspace subscription info (#5717) * feat: refactor and start integrating AI plans * feat: refine UI and add business logic for AI * feat: complete UIUX for AI and limits * chore: remove resolved todo * chore: localize remove addon dialog * chore: fix spacing issue for usage * fix: interpret subscription + usage on action * chore: update api for billing (#5735) * chore: update revisions * fix: remove subscription cache * fix: copy improvements + use consistent dialog * chore: update to the latest client api * feat: support updating billing period * Feat/ai billing cancel reason (#5752) * chore: add cancellation reason field * fix: ci add one retry for concurrent sign up * chore: merge with main * chore: half merge * chore: fix conflict * chore: observer error * chore: remove unneeded protobuf and remove unwrap * feat: added subscription plan details * chore: check error code and update sidebar toast * chore: periodically check billing state * chore: editor ai error * chore: return file upload error * chore: fmt * chore: clippy * chore: disable upload image when exceed storage limitation * chore: remove todo * chore: remove openai i18n * chore: update log * chore: update client-api to fix stream error * chore: clippy * chore: fix language file * chore: disable billing UI --------- Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg> Co-authored-by: nathan <nathan@appflowy.io>
2024-07-22 09:43:48 +02:00
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
feat: ai billing (#5741) * feat: start on AI plan+billing UI * chore: enable plan and billing * feat: cache workspace subscription + minor fixes (#5705) * feat: update api from billing * feat: add api for workspace subscription info (#5717) * feat: refactor and start integrating AI plans * feat: refine UI and add business logic for AI * feat: complete UIUX for AI and limits * chore: remove resolved todo * chore: localize remove addon dialog * chore: fix spacing issue for usage * fix: interpret subscription + usage on action * chore: update api for billing (#5735) * chore: update revisions * fix: remove subscription cache * fix: copy improvements + use consistent dialog * chore: update to the latest client api * feat: support updating billing period * Feat/ai billing cancel reason (#5752) * chore: add cancellation reason field * fix: ci add one retry for concurrent sign up * chore: merge with main * chore: half merge * chore: fix conflict * chore: observer error * chore: remove unneeded protobuf and remove unwrap * feat: added subscription plan details * chore: check error code and update sidebar toast * chore: periodically check billing state * chore: editor ai error * chore: return file upload error * chore: fmt * chore: clippy * chore: disable upload image when exceed storage limitation * chore: remove todo * chore: remove openai i18n * chore: update log * chore: update client-api to fix stream error * chore: clippy * chore: fix language file * chore: disable billing UI --------- Co-authored-by: Zack Fu Zi Xiang <speed2exe@live.com.sg> Co-authored-by: nathan <nathan@appflowy.io>
2024-07-22 09:43:48 +02:00
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart' as fixnum;
import 'ai_entities.dart';
2025-01-06 13:34:11 +08:00
import 'error.dart';
2025-01-15 19:57:47 +08:00
abstract class AIRepository {
Future<void> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
List<String> sourceIds = const [],
List<AiWriterRecord> history = const [],
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,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
required void Function() onLocalAIInitializing,
});
}
class AppFlowyAIService implements AIRepository {
@override
Future<(String, CompletionStream)?> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
List<String> sourceIds = const [],
List<AiWriterRecord> history = const [],
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,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
required void Function() onLocalAIInitializing,
}) async {
final stream = AppFlowyCompletionStream(
onStart: onStart,
2025-03-20 13:30:42 +08:00
processMessage: processMessage,
processAssistMessage: processAssistMessage,
processError: onError,
onLocalAIInitializing: onLocalAIInitializing,
onEnd: onEnd,
);
final records = history.map((record) => record.toPB()).toList();
final payload = CompleteTextPB(
text: text,
completionType: completionType,
format: format?.toPB(),
streamPort: fixnum.Int64(stream.nativePort),
objectId: objectId ?? '',
ragIds: [
if (objectId != null) objectId,
...sourceIds,
].unique(),
history: records,
);
return AIEventCompleteText(payload).send().fold(
(task) => (task.taskId, stream),
(error) {
Log.error(error);
return null;
},
);
}
}
abstract class CompletionStream {
CompletionStream({
required this.onStart,
2025-03-20 13:30:42 +08:00
required this.processMessage,
required this.processAssistMessage,
required this.processError,
required this.onLocalAIInitializing,
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;
final void Function() onLocalAIInitializing;
final Future<void> Function() onEnd;
}
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,
required super.onEnd,
required super.onLocalAIInitializing,
}) {
_startListening();
}
final RawReceivePort _port = RawReceivePort();
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
int get nativePort => _port.sendPort.nativePort;
void _startListening() {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
2025-03-30 15:15:59 +08:00
await _handleEvent(event);
},
);
}
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
final colonIndex = event.indexOf(':');
final hasColon = colonIndex != -1;
final prefix = hasColon ? event.substring(0, colonIndex) : event;
final content = hasColon ? event.substring(colonIndex + 1) : '';
switch (prefix) {
case AIStreamEventPrefix.aiMaxRequired:
processError(AIError(message: content, code: AIErrorCode.other));
break;
case AIStreamEventPrefix.start:
await onStart();
break;
case AIStreamEventPrefix.data:
await processMessage(content);
break;
case AIStreamEventPrefix.comment:
await processAssistMessage(content);
break;
case AIStreamEventPrefix.finish:
await onEnd();
break;
case AIStreamEventPrefix.localAINotReady:
onLocalAIInitializing();
break;
case AIStreamEventPrefix.error:
processError(AIError(message: content, code: AIErrorCode.other));
break;
default:
Log.debug('Unknown AI event: $event');
break;
}
}
}