feat: Run Local AI model in AppFlowy (#5655)

* chore: load plugin

* chore: sidecar

* chore: fix test

* chore: clippy

* chore: save chat config

* chore: arc plugin

* chore: add plugins

* chore: clippy

* chore: test streaming

* chore: config chat

* chore: stream message

* chore: response with local ai

* chore: fix compile

* chore: config ui

* chore: fix load plugin

* chore: add docs

* chore: update docs

* chore: disable local ai

* chore: fix compile

* chore: clippy
This commit is contained in:
Nathan.fooo 2024-06-30 17:38:39 +08:00 committed by GitHub
parent 3bcadff152
commit e1c68c1b72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 3494 additions and 396 deletions

View File

@ -170,7 +170,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -191,4 +191,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3

View File

@ -0,0 +1,195 @@
import 'dart:io';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
import 'package:protobuf/protobuf.dart';
part 'setting_local_ai_bloc.freezed.dart';
class SettingsAILocalBloc
extends Bloc<SettingsAILocalEvent, SettingsAILocalState> {
SettingsAILocalBloc() : super(const SettingsAILocalState()) {
on<SettingsAILocalEvent>(_handleEvent);
}
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
SettingsAILocalEvent event,
Emitter<SettingsAILocalState> emit,
) async {
await event.when(
started: _handleStarted,
didUpdateAISetting: (settings) async {
_handleDidUpdateAISetting(settings, emit);
},
updateChatBin: (chatBinPath) async {
await _handleUpdatePath(
filePath: chatBinPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatBinPath: chatBinPath.trim(),
chatBinPathError: null,
),
errorUpdater: (error) => state.copyWith(chatBinPathError: error),
);
},
updateChatModelPath: (chatModelPath) async {
await _handleUpdatePath(
filePath: chatModelPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatModelPath: chatModelPath.trim(),
chatModelPathError: null,
),
errorUpdater: (error) => state.copyWith(chatModelPathError: error),
);
},
toggleLocalAI: () async {
emit(state.copyWith(localAIEnabled: !state.localAIEnabled));
},
saveSetting: () async {
_handleSaveSetting();
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventGetLocalAISetting().send();
result.fold(
(setting) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(setting));
}
},
(err) => Log.error('Failed to get local AI setting: $err'),
);
}
/// Handles the event to update the AI settings in the state.
void _handleDidUpdateAISetting(
LocalLLMSettingPB settings,
Emitter<SettingsAILocalState> emit,
) {
final newState = state.copyWith(
aiSettings: settings,
chatBinPath: settings.chatBinPath,
chatModelPath: settings.chatModelPath,
localAIEnabled: settings.enabled,
loadingState: const LoadingState.finish(),
);
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Handles updating file paths (both chat binary and chat model paths).
Future<void> _handleUpdatePath({
required String filePath,
required Emitter<SettingsAILocalState> emit,
required SettingsAILocalState Function() stateUpdater,
required SettingsAILocalState Function(String) errorUpdater,
}) async {
filePath = filePath.trim();
if (filePath.isEmpty) {
emit(stateUpdater());
return;
}
final validationError = await _validatePath(filePath);
if (validationError != null) {
emit(errorUpdater(validationError));
return;
}
final newState = stateUpdater();
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Validates the provided file path.
Future<String?> _validatePath(String filePath) async {
if (!isAbsolutePath(filePath)) {
return "$filePath must be absolute";
}
if (!await pathExists(filePath)) {
return "$filePath does not exist";
}
return null;
}
/// Handles saving the updated settings.
void _handleSaveSetting() {
if (state.aiSettings == null) return;
state.aiSettings!.freeze();
final newSetting = state.aiSettings!.rebuild((value) {
value
..chatBinPath = state.chatBinPath ?? value.chatBinPath
..chatModelPath = state.chatModelPath ?? value.chatModelPath
..enabled = state.localAIEnabled;
});
ChatEventUpdateLocalAISetting(newSetting).send().then((result) {
result.fold(
(_) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(newSetting));
}
},
(err) => Log.error('Failed to update local AI setting: $err'),
);
});
}
/// Determines if the save button should be enabled based on the state.
bool _saveButtonEnabled(SettingsAILocalState newState) {
return newState.chatBinPathError == null &&
newState.chatModelPathError == null &&
newState.chatBinPath != null &&
newState.chatModelPath != null;
}
}
@freezed
class SettingsAILocalEvent with _$SettingsAILocalEvent {
const factory SettingsAILocalEvent.started() = _Started;
const factory SettingsAILocalEvent.didUpdateAISetting(
LocalLLMSettingPB settings,
) = _GetAISetting;
const factory SettingsAILocalEvent.updateChatBin(String chatBinPath) =
_UpdateChatBin;
const factory SettingsAILocalEvent.updateChatModelPath(String chatModelPath) =
_UpdateChatModelPath;
const factory SettingsAILocalEvent.toggleLocalAI() = _EnableLocalAI;
const factory SettingsAILocalEvent.saveSetting() = _SaveSetting;
}
@freezed
class SettingsAILocalState with _$SettingsAILocalState {
const factory SettingsAILocalState({
LocalLLMSettingPB? aiSettings,
String? chatBinPath,
String? chatBinPathError,
String? chatModelPath,
String? chatModelPathError,
@Default(false) bool localAIEnabled,
@Default(false) bool saveButtonEnabled,
@Default(LoadingState.loading()) LoadingState loadingState,
}) = _SettingsAILocalState;
}
/// Checks if a given file path is absolute.
bool isAbsolutePath(String filePath) {
return path.isAbsolute(filePath);
}
/// Checks if a given file or directory path exists.
Future<bool> pathExists(String filePath) async {
final file = File(filePath);
final directory = Directory(filePath);
return await file.exists() || await directory.exists();
}

View File

@ -1,3 +1,7 @@
import 'package:appflowy/workspace/application/settings/ai/setting_local_ai_bloc.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@ -40,22 +44,26 @@ class SettingsAIView extends StatelessWidget {
create: (_) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (_, __) => SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
description:
LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),
children: const [
AIModelSeclection(),
_AISearchToggle(value: false),
],
),
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
description:
LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),
children: const [
AIModelSelection(),
_AISearchToggle(value: false),
// Disable local AI configuration for now. It's not ready for production.
// LocalAIConfiguration(),
],
);
},
),
);
}
}
class AIModelSeclection extends StatelessWidget {
const AIModelSeclection({super.key});
class AIModelSelection extends StatelessWidget {
const AIModelSelection({super.key});
@override
Widget build(BuildContext context) {
@ -161,3 +169,111 @@ class _AISearchToggle extends StatelessWidget {
);
}
}
class LocalAIConfiguration extends StatelessWidget {
const LocalAIConfiguration({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
SettingsAILocalBloc()..add(const SettingsAILocalEvent.started()),
child: BlocBuilder<SettingsAILocalBloc, SettingsAILocalState>(
builder: (context, state) {
return state.loadingState.when(
loading: () {
return const SizedBox.shrink();
},
finish: () {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AIConfigurateTextField(
title: 'chat bin path',
hitText: '',
errorText: state.chatBinPathError ?? '',
value: state.aiSettings?.chatBinPath ?? '',
onChanged: (value) {
context.read<SettingsAILocalBloc>().add(
SettingsAILocalEvent.updateChatBin(value),
);
},
),
const VSpace(16),
AIConfigurateTextField(
title: 'chat model path',
hitText: '',
errorText: state.chatModelPathError ?? '',
value: state.aiSettings?.chatModelPath ?? '',
onChanged: (value) {
context.read<SettingsAILocalBloc>().add(
SettingsAILocalEvent.updateChatModelPath(value),
);
},
),
const VSpace(16),
Toggle(
value: state.localAIEnabled,
onChanged: (_) => context
.read<SettingsAILocalBloc>()
.add(const SettingsAILocalEvent.toggleLocalAI()),
),
const VSpace(16),
FlowyButton(
disable: !state.saveButtonEnabled,
text: const FlowyText("save"),
onTap: () {
context.read<SettingsAILocalBloc>().add(
const SettingsAILocalEvent.saveSetting(),
);
},
),
],
);
},
);
},
),
);
}
}
class AIConfigurateTextField extends StatelessWidget {
const AIConfigurateTextField({
required this.title,
required this.hitText,
required this.errorText,
required this.value,
required this.onChanged,
super.key,
});
final String title;
final String hitText;
final String errorText;
final String value;
final void Function(String) onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
title,
),
const VSpace(8),
RoundedInputField(
hintText: hitText,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
normalBorderColor: Theme.of(context).colorScheme.outline,
errorBorderColor: Theme.of(context).colorScheme.error,
cursorColor: Theme.of(context).colorScheme.primary,
errorText: errorText,
initialValue: value,
onChanged: onChanged,
),
],
);
}
}

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -772,7 +772,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"again",
"anyhow",
@ -821,7 +821,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -833,7 +833,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"futures-channel",
"futures-util",
@ -907,7 +907,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-trait",
@ -931,7 +931,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-trait",
@ -961,7 +961,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"collab",
@ -981,7 +981,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"bytes",
@ -996,7 +996,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"chrono",
@ -1034,7 +1034,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-stream",
@ -1073,7 +1073,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -1098,7 +1098,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"async-trait",
@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"collab",
@ -1294,12 +1294,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
@ -1344,7 +1341,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.6",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1455,7 +1452,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -1856,6 +1853,7 @@ dependencies = [
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sidecar",
"flowy-sqlite",
"futures",
"lib-dispatch",
@ -2319,6 +2317,24 @@ dependencies = [
"serde_repr",
]
[[package]]
name = "flowy-sidecar"
version = "0.1.0"
dependencies = [
"anyhow",
"crossbeam-utils",
"lib-infra",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "flowy-sqlite"
version = "0.1.0"
@ -2430,7 +2446,6 @@ dependencies = [
"anyhow",
"base64 0.21.5",
"chrono",
"client-api",
"collab",
"collab-entity",
"flowy-error",
@ -2899,7 +2914,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"futures-util",
@ -2916,7 +2931,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -3348,7 +3363,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -3596,6 +3611,7 @@ dependencies = [
"async-trait",
"atomic_refcell",
"bytes",
"cfg-if",
"chrono",
"futures",
"futures-core",
@ -4216,9 +4232,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oneshot"
@ -4855,7 +4871,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -4876,7 +4892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.47",
@ -5690,9 +5706,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.111"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
dependencies = [
"itoa 1.0.6",
"ryu",
@ -5840,7 +5856,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -6847,9 +6863,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
[dependencies]
serde_json.workspace = true
@ -106,10 +106,10 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }

View File

@ -215,7 +215,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -235,7 +235,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -561,7 +561,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"again",
"anyhow",
@ -610,7 +610,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -622,7 +622,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"futures-channel",
"futures-util",
@ -800,7 +800,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -825,7 +825,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"async-trait",
@ -1039,7 +1039,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -1666,7 +1666,6 @@ dependencies = [
"anyhow",
"base64 0.21.7",
"chrono",
"client-api",
"collab",
"collab-entity",
"flowy-error",
@ -1924,7 +1923,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"futures-util",
@ -1941,7 +1940,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -2242,7 +2241,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -2383,6 +2382,7 @@ dependencies = [
"async-trait",
"atomic_refcell",
"bytes",
"cfg-if 1.0.0",
"chrono",
"futures",
"futures-core",
@ -3956,7 +3956,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",

View File

@ -20,7 +20,6 @@ flowy-derive = { path = "../../rust-lib/build-tool/flowy-derive" }
flowy-codegen = { path = "../../rust-lib/build-tool/flowy-codegen" }
flowy-document = { path = "../../rust-lib/flowy-document" }
flowy-folder = { path = "../../rust-lib/flowy-folder" }
flowy-storage = { path = "../../rust-lib/flowy-storage" }
lib-infra = { path = "../../rust-lib/lib-infra" }
bytes = { version = "1.5" }
protobuf = { version = "2.28.0" }
@ -55,7 +54,7 @@ yrs = "0.18.8"
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
[profile.dev]
opt-level = 0
@ -68,10 +67,10 @@ opt-level = 3
codegen-units = 1
[patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -746,7 +746,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"again",
"anyhow",
@ -795,7 +795,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -807,7 +807,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"futures-channel",
"futures-util",
@ -890,7 +890,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-trait",
@ -914,7 +914,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-trait",
@ -944,7 +944,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"collab",
@ -964,7 +964,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"bytes",
@ -979,7 +979,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"chrono",
@ -1017,7 +1017,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-stream",
@ -1056,7 +1056,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -1081,7 +1081,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"async-trait",
@ -1098,7 +1098,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"collab",
@ -1284,9 +1284,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
@ -1331,7 +1331,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.10",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1442,7 +1442,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -1893,6 +1893,7 @@ dependencies = [
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sidecar",
"flowy-sqlite",
"futures",
"lib-dispatch",
@ -2356,6 +2357,24 @@ dependencies = [
"serde_repr",
]
[[package]]
name = "flowy-sidecar"
version = "0.1.0"
dependencies = [
"anyhow",
"crossbeam-utils",
"lib-infra",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "flowy-sqlite"
version = "0.1.0"
@ -2467,7 +2486,6 @@ dependencies = [
"anyhow",
"base64 0.21.7",
"chrono",
"client-api",
"collab",
"collab-entity",
"flowy-error",
@ -2973,7 +2991,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"futures-util",
@ -2990,7 +3008,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -3427,7 +3445,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -3680,6 +3698,7 @@ dependencies = [
"async-trait",
"atomic_refcell",
"bytes",
"cfg-if",
"chrono",
"futures",
"futures-core",
@ -4936,7 +4955,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -4957,7 +4976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.55",
@ -5782,9 +5801,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.114"
version = "1.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
dependencies = [
"indexmap 2.2.6",
"itoa 1.0.10",
@ -5935,7 +5954,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
[dependencies]
serde_json.workspace = true
@ -107,10 +107,10 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -664,7 +664,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"again",
"anyhow",
@ -713,7 +713,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -725,7 +725,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"futures-channel",
"futures-util",
@ -768,7 +768,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-trait",
@ -792,7 +792,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-trait",
@ -822,7 +822,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"collab",
@ -842,7 +842,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"bytes",
@ -857,7 +857,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"chrono",
@ -895,7 +895,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"async-stream",
@ -934,7 +934,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bincode",
@ -959,7 +959,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"async-trait",
@ -976,7 +976,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d"
dependencies = [
"anyhow",
"collab",
@ -1146,12 +1146,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
@ -1279,7 +1276,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -1471,9 +1468,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if",
]
@ -1669,22 +1666,30 @@ name = "flowy-chat"
version = "0.1.0"
dependencies = [
"allo-isolate",
"anyhow",
"bytes",
"dashmap",
"dotenv",
"flowy-chat-pub",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sidecar",
"flowy-sqlite",
"futures",
"lib-dispatch",
"lib-infra",
"log",
"parking_lot 0.12.1",
"protobuf",
"serde",
"serde_json",
"strum_macros 0.21.1",
"tokio",
"tokio-stream",
"tracing",
"tracing-subscriber",
"uuid",
"validator",
]
@ -1957,6 +1962,7 @@ dependencies = [
"fancy-regex 0.11.0",
"flowy-codegen",
"flowy-derive",
"flowy-sidecar",
"flowy-sqlite",
"lib-dispatch",
"protobuf",
@ -2147,6 +2153,24 @@ dependencies = [
"serde_repr",
]
[[package]]
name = "flowy-sidecar"
version = "0.1.0"
dependencies = [
"anyhow",
"crossbeam-utils",
"lib-infra",
"log",
"once_cell",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "flowy-sqlite"
version = "0.1.0"
@ -2572,7 +2596,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"futures-util",
@ -2589,7 +2613,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -2954,7 +2978,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"bytes",
@ -3104,6 +3128,7 @@ dependencies = [
"atomic_refcell",
"brotli",
"bytes",
"cfg-if",
"chrono",
"futures",
"futures-core",
@ -3352,9 +3377,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
@ -3562,9 +3587,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oneshot"
@ -4951,9 +4976,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.111"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
@ -5054,7 +5079,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d61524d63605aa010afa6a734cbbe4fb4cd68ea1#d61524d63605aa010afa6a734cbbe4fb4cd68ea1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
dependencies = [
"anyhow",
"app-error",
@ -5752,9 +5777,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",

View File

@ -31,6 +31,8 @@ members = [
"flowy-search-pub",
"flowy-chat",
"flowy-chat-pub",
"flowy-storage-pub",
"flowy-sidecar",
]
resolver = "2"
@ -66,6 +68,7 @@ collab-integrate = { workspace = true, path = "collab-integrate" }
flowy-date = { workspace = true, path = "flowy-date" }
flowy-chat = { workspace = true, path = "flowy-chat" }
flowy-chat-pub = { workspace = true, path = "flowy-chat-pub" }
flowy-sidecar = { workspace = true, path = "flowy-sidecar" }
anyhow = "1.0"
tracing = "0.1.40"
bytes = "1.5.0"
@ -93,11 +96,11 @@ validator = { version = "0.16.1", features = ["derive"] }
# Please using the following command to update the revision id
# Current directory: frontend
# Run the script:
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d61524d63605aa010afa6a734cbbe4fb4cd68ea1" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
[profile.dev]
opt-level = 1
@ -136,10 +139,10 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }

View File

@ -5,7 +5,7 @@ use flowy_chat::entities::ChatMessageListPB;
use flowy_chat::notification::ChatNotification;
use flowy_chat_pub::cloud::ChatMessageType;
use futures_util::StreamExt;
use std::time::Duration;
#[tokio::test]
@ -19,8 +19,8 @@ async fn af_cloud_create_chat_message_test() {
let chat_id = view.id.clone();
let chat_service = test.server_provider.get_server().unwrap().chat_service();
for i in 0..10 {
let mut stream = chat_service
.send_chat_message(
let _ = chat_service
.save_question(
&current_workspace.id,
&chat_id,
&format!("hello world {}", i),
@ -28,9 +28,6 @@ async fn af_cloud_create_chat_message_test() {
)
.await
.unwrap();
while let Some(message) = stream.next().await {
message.unwrap();
}
}
let rx = test
.notification_sender
@ -77,8 +74,8 @@ async fn af_cloud_load_remote_system_message_test() {
let chat_service = test.server_provider.get_server().unwrap().chat_service();
for i in 0..10 {
let mut stream = chat_service
.send_chat_message(
let _ = chat_service
.save_question(
&current_workspace.id,
&chat_id,
&format!("hello server {}", i),
@ -86,9 +83,6 @@ async fn af_cloud_load_remote_system_message_test() {
)
.await
.unwrap();
while let Some(message) = stream.next().await {
message.unwrap();
}
}
let rx = test

View File

@ -12,7 +12,7 @@ use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
pub type StreamAnswer = BoxStream<'static, Result<Bytes, AppResponseError>>;
pub type StreamAnswer = BoxStream<'static, Result<Bytes, FlowyError>>;
pub type StreamComplete = BoxStream<'static, Result<Bytes, AppResponseError>>;
#[async_trait]
pub trait ChatCloudService: Send + Sync + 'static {
@ -23,15 +23,7 @@ pub trait ChatCloudService: Send + Sync + 'static {
chat_id: &str,
) -> FutureResult<(), FlowyError>;
async fn send_chat_message(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError>;
fn send_question(
fn save_question(
&self,
workspace_id: &str,
chat_id: &str,
@ -47,13 +39,20 @@ pub trait ChatCloudService: Send + Sync + 'static {
question_id: i64,
) -> FutureResult<ChatMessage, FlowyError>;
async fn stream_answer(
async fn ask_question(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> Result<StreamAnswer, FlowyError>;
async fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError>;
fn get_chat_messages(
&self,
workspace_id: &str,
@ -69,13 +68,6 @@ pub trait ChatCloudService: Send + Sync + 'static {
message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError>;
fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> FutureResult<ChatMessage, FlowyError>;
async fn stream_complete(
&self,
workspace_id: &str,

View File

@ -11,6 +11,8 @@ flowy-notification = { workspace = true }
flowy-error = { path = "../flowy-error", features = [
"impl_from_dispatch_error",
"impl_from_collab_folder",
"impl_from_sqlite",
"impl_from_sidecar"
] }
lib-dispatch = { workspace = true }
tracing.workspace = true
@ -27,6 +29,17 @@ tokio.workspace = true
futures.workspace = true
allo-isolate = { version = "^0.1", features = ["catch-unwind"] }
log = "0.4.21"
flowy-sidecar = { workspace = true, features = ["verbose"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
anyhow = "1.0.86"
tokio-stream = "0.1.15"
parking_lot.workspace = true
[dev-dependencies]
dotenv = "0.15.0"
uuid.workspace = true
tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter", "ansi", "json"] }
[build-dependencies]
flowy-codegen.workspace = true
@ -35,3 +48,4 @@ flowy-codegen.workspace = true
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"]
web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"]

View File

@ -0,0 +1,5 @@
CHAT_BIN_PATH=
LOCAL_AI_MODEL_DIR=
LOCAL_AI_CHAT_MODEL_NAME=
LOCAL_AI_EMBEDDING_MODEL_NAME=

View File

@ -1,4 +1,5 @@
use crate::chat_manager::ChatUserService;
use crate::chat_service_impl::ChatService;
use crate::entities::{
ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB,
};
@ -25,7 +26,7 @@ pub struct Chat {
chat_id: String,
uid: i64,
user_service: Arc<dyn ChatUserService>,
cloud_service: Arc<dyn ChatCloudService>,
chat_service: Arc<ChatService>,
prev_message_state: Arc<RwLock<PrevMessageState>>,
latest_message_id: Arc<AtomicI64>,
stop_stream: Arc<AtomicBool>,
@ -37,12 +38,12 @@ impl Chat {
uid: i64,
chat_id: String,
user_service: Arc<dyn ChatUserService>,
cloud_service: Arc<dyn ChatCloudService>,
chat_service: Arc<ChatService>,
) -> Chat {
Chat {
uid,
chat_id,
cloud_service,
chat_service,
user_service,
prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)),
latest_message_id: Default::default(),
@ -92,8 +93,8 @@ impl Chat {
let workspace_id = self.user_service.workspace_id()?;
let question = self
.cloud_service
.send_question(&workspace_id, &self.chat_id, message, message_type)
.chat_service
.save_question(&workspace_id, &self.chat_id, message, message_type)
.await
.map_err(|err| {
error!("Failed to send question: {}", err);
@ -109,12 +110,12 @@ impl Chat {
let stop_stream = self.stop_stream.clone();
let chat_id = self.chat_id.clone();
let question_id = question.message_id;
let cloud_service = self.cloud_service.clone();
let cloud_service = self.chat_service.clone();
let user_service = self.user_service.clone();
tokio::spawn(async move {
let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port));
match cloud_service
.stream_answer(&workspace_id, &chat_id, question_id)
.ask_question(&workspace_id, &chat_id, question_id)
.await
{
Ok(mut stream) => {
@ -300,7 +301,7 @@ impl Chat {
);
let workspace_id = self.user_service.workspace_id()?;
let chat_id = self.chat_id.clone();
let cloud_service = self.cloud_service.clone();
let cloud_service = self.chat_service.clone();
let user_service = self.user_service.clone();
let uid = self.uid;
let prev_message_state = self.prev_message_state.clone();
@ -369,7 +370,7 @@ impl Chat {
) -> Result<RepeatedRelatedQuestionPB, FlowyError> {
let workspace_id = self.user_service.workspace_id()?;
let resp = self
.cloud_service
.chat_service
.get_related_message(&workspace_id, &self.chat_id, message_id)
.await?;
@ -391,7 +392,7 @@ impl Chat {
);
let workspace_id = self.user_service.workspace_id()?;
let answer = self
.cloud_service
.chat_service
.generate_answer(&workspace_id, &self.chat_id, question_message_id)
.await?;

View File

@ -1,9 +1,13 @@
use crate::chat::Chat;
use crate::chat_service_impl::ChatService;
use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB};
use crate::local_ai::llm_chat::{LocalChatLLMChat, LocalLLMSetting};
use crate::persistence::{insert_chat, ChatTable};
use dashmap::DashMap;
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType};
use flowy_error::{FlowyError, FlowyResult};
use flowy_sidecar::manager::SidecarManager;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_sqlite::DBConnection;
use lib_infra::util::timestamp;
use std::sync::Arc;
@ -17,25 +21,56 @@ pub trait ChatUserService: Send + Sync + 'static {
}
pub struct ChatManager {
pub(crate) cloud_service: Arc<dyn ChatCloudService>,
pub(crate) user_service: Arc<dyn ChatUserService>,
pub chat_service: Arc<ChatService>,
pub user_service: Arc<dyn ChatUserService>,
chats: Arc<DashMap<String, Arc<Chat>>>,
store_preferences: Arc<KVStorePreferences>,
}
const LOCAL_AI_SETTING_KEY: &str = "local_ai_setting";
impl ChatManager {
pub fn new(
cloud_service: Arc<dyn ChatCloudService>,
user_service: impl ChatUserService,
store_preferences: Arc<KVStorePreferences>,
) -> ChatManager {
let user_service = Arc::new(user_service);
let local_ai_setting = store_preferences
.get_object::<LocalLLMSetting>(LOCAL_AI_SETTING_KEY)
.unwrap_or_default();
let sidecar_manager = Arc::new(SidecarManager::new());
// setup local AI chat plugin
let local_llm_ctrl = Arc::new(LocalChatLLMChat::new(sidecar_manager));
// setup local chat service
let chat_service = Arc::new(ChatService::new(
user_service.clone(),
cloud_service,
local_llm_ctrl,
local_ai_setting,
));
Self {
cloud_service,
chat_service,
user_service,
chats: Arc::new(DashMap::new()),
store_preferences,
}
}
pub fn update_local_ai_setting(&self, setting: LocalLLMSetting) -> FlowyResult<()> {
self.chat_service.update_local_ai_setting(setting.clone())?;
self
.store_preferences
.set_object(LOCAL_AI_SETTING_KEY, setting)?;
Ok(())
}
pub fn get_local_ai_setting(&self) -> FlowyResult<LocalLLMSetting> {
let setting = self.chat_service.get_local_ai_setting();
Ok(setting)
}
pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
trace!("open chat: {}", chat_id);
self.chats.entry(chat_id.to_string()).or_insert_with(|| {
@ -43,20 +78,24 @@ impl ChatManager {
self.user_service.user_id().unwrap(),
chat_id.to_string(),
self.user_service.clone(),
self.cloud_service.clone(),
self.chat_service.clone(),
))
});
self.chat_service.notify_open_chat(chat_id);
Ok(())
}
pub async fn close_chat(&self, _chat_id: &str) -> Result<(), FlowyError> {
pub async fn close_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
trace!("close chat: {}", chat_id);
self.chat_service.notify_close_chat(chat_id);
Ok(())
}
pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
if let Some((_, chat)) = self.chats.remove(chat_id) {
chat.close();
self.chat_service.notify_close_chat(chat_id);
}
Ok(())
}
@ -64,7 +103,7 @@ impl ChatManager {
pub async fn create_chat(&self, uid: &i64, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
let workspace_id = self.user_service.workspace_id()?;
self
.cloud_service
.chat_service
.create_chat(uid, &workspace_id, chat_id)
.await?;
save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?;
@ -73,7 +112,7 @@ impl ChatManager {
self.user_service.user_id().unwrap(),
chat_id.to_string(),
self.user_service.clone(),
self.cloud_service.clone(),
self.chat_service.clone(),
));
self.chats.insert(chat_id.to_string(), chat.clone());
Ok(chat)
@ -101,7 +140,7 @@ impl ChatManager {
self.user_service.user_id().unwrap(),
chat_id.to_string(),
self.user_service.clone(),
self.cloud_service.clone(),
self.chat_service.clone(),
));
self.chats.insert(chat_id.to_string(), chat.clone());
Ok(chat)
@ -183,6 +222,10 @@ fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> {
chat_id: chat_id.to_string(),
created_at: timestamp(),
name: "".to_string(),
local_model_path: "".to_string(),
local_model_name: "".to_string(),
local_enabled: false,
sync_to_cloud: true,
};
insert_chat(conn, &row)?;

View File

@ -0,0 +1,243 @@
use crate::chat_manager::ChatUserService;
use crate::local_ai::llm_chat::{LocalChatLLMChat, LocalLLMSetting};
use crate::persistence::select_single_message;
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageType, CompletionType, MessageCursor,
RepeatedChatMessage, RepeatedRelatedQuestion, StreamAnswer, StreamComplete,
};
use flowy_error::{FlowyError, FlowyResult};
use futures::{StreamExt, TryStreamExt};
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
use parking_lot::RwLock;
use std::sync::Arc;
use tracing::{error, info, trace};
pub struct ChatService {
pub cloud_service: Arc<dyn ChatCloudService>,
user_service: Arc<dyn ChatUserService>,
local_llm_chat: Arc<LocalChatLLMChat>,
local_llm_setting: Arc<RwLock<LocalLLMSetting>>,
}
impl ChatService {
pub fn new(
user_service: Arc<dyn ChatUserService>,
cloud_service: Arc<dyn ChatCloudService>,
local_llm_ctrl: Arc<LocalChatLLMChat>,
local_llm_setting: LocalLLMSetting,
) -> Self {
if local_llm_setting.enabled {
setup_local_chat(&local_llm_setting, local_llm_ctrl.clone());
}
Self {
user_service,
cloud_service,
local_llm_chat: local_llm_ctrl,
local_llm_setting: Arc::new(RwLock::new(local_llm_setting)),
}
}
pub fn notify_open_chat(&self, chat_id: &str) {
if self.local_llm_setting.read().enabled {
trace!("[Chat Plugin] notify open chat: {}", chat_id);
let chat_id = chat_id.to_string();
let weak_ctrl = Arc::downgrade(&self.local_llm_chat);
tokio::spawn(async move {
if let Some(ctrl) = weak_ctrl.upgrade() {
if let Err(err) = ctrl.create_chat(&chat_id).await {
error!("[Chat Plugin] failed to open chat: {:?}", err);
}
}
});
}
}
pub fn notify_close_chat(&self, chat_id: &str) {
if self.local_llm_setting.read().enabled {
trace!("[Chat Plugin] notify close chat: {}", chat_id);
let weak_ctrl = Arc::downgrade(&self.local_llm_chat);
let chat_id = chat_id.to_string();
tokio::spawn(async move {
if let Some(ctrl) = weak_ctrl.upgrade() {
if let Err(err) = ctrl.close_chat(&chat_id).await {
error!("[Chat Plugin] failed to close chat: {:?}", err);
}
}
});
}
}
pub fn get_local_ai_setting(&self) -> LocalLLMSetting {
self.local_llm_setting.read().clone()
}
pub fn update_local_ai_setting(&self, setting: LocalLLMSetting) -> FlowyResult<()> {
setting.validate()?;
setup_local_chat(&setting, self.local_llm_chat.clone());
*self.local_llm_setting.write() = setting;
Ok(())
}
fn get_message_content(&self, message_id: i64) -> FlowyResult<String> {
let uid = self.user_service.user_id()?;
let conn = self.user_service.sqlite_connection(uid)?;
let content = select_single_message(conn, message_id)?
.map(|data| data.content)
.ok_or_else(|| {
FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id))
})?;
Ok(content)
}
}
#[async_trait]
impl ChatCloudService for ChatService {
fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
) -> FutureResult<(), FlowyError> {
self.cloud_service.create_chat(uid, workspace_id, chat_id)
}
fn save_question(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> FutureResult<ChatMessage, FlowyError> {
self
.cloud_service
.save_question(workspace_id, chat_id, message, message_type)
}
fn save_answer(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
question_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
self
.cloud_service
.save_answer(workspace_id, chat_id, message, question_id)
}
async fn ask_question(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> Result<StreamAnswer, FlowyError> {
if self.local_llm_setting.read().enabled {
let content = self.get_message_content(message_id)?;
let stream = self
.local_llm_chat
.ask_question(chat_id, &content)
.await?
.map_err(FlowyError::from);
Ok(stream.boxed())
} else {
self
.cloud_service
.ask_question(workspace_id, chat_id, message_id)
.await
}
}
async fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
if self.local_llm_setting.read().enabled {
let content = self.get_message_content(question_message_id)?;
let _answer = self
.local_llm_chat
.generate_answer(chat_id, &content)
.await?;
todo!()
} else {
self
.cloud_service
.generate_answer(workspace_id, chat_id, question_message_id)
.await
}
}
fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
offset: MessageCursor,
limit: u64,
) -> FutureResult<RepeatedChatMessage, FlowyError> {
self
.cloud_service
.get_chat_messages(workspace_id, chat_id, offset, limit)
}
fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError> {
if self.local_llm_setting.read().enabled {
FutureResult::new(async move {
Ok(RepeatedRelatedQuestion {
message_id,
items: vec![],
})
})
} else {
self
.cloud_service
.get_related_message(workspace_id, chat_id, message_id)
}
}
async fn stream_complete(
&self,
workspace_id: &str,
text: &str,
complete_type: CompletionType,
) -> Result<StreamComplete, FlowyError> {
if self.local_llm_setting.read().enabled {
todo!()
} else {
self
.cloud_service
.stream_complete(workspace_id, text, complete_type)
.await
}
}
}
fn setup_local_chat(local_llm_setting: &LocalLLMSetting, llm_chat_ctrl: Arc<LocalChatLLMChat>) {
if local_llm_setting.enabled {
if let Ok(config) = local_llm_setting.chat_config() {
tokio::spawn(async move {
trace!("[Chat Plugin] setup local chat: {:?}", config);
if let Err(err) = llm_chat_ctrl.init_chat_plugin(config).await {
error!("[Chat Plugin] failed to setup plugin: {:?}", err);
}
});
}
} else {
tokio::spawn(async move {
match llm_chat_ctrl.destroy_chat_plugin().await {
Ok(_) => info!("[Chat Plugin] destroy plugin successfully"),
Err(err) => error!("[Chat Plugin] failed to destroy plugin: {:?}", err),
}
});
}
}

View File

@ -1,3 +1,4 @@
use crate::local_ai::llm_chat::LocalLLMSetting;
use flowy_chat_pub::cloud::{
ChatMessage, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion,
};
@ -206,6 +207,38 @@ impl From<RepeatedRelatedQuestion> for RepeatedRelatedQuestionPB {
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct LocalLLMSettingPB {
#[pb(index = 1)]
pub chat_bin_path: String,
#[pb(index = 2)]
pub chat_model_path: String,
#[pb(index = 3)]
pub enabled: bool,
}
impl From<LocalLLMSetting> for LocalLLMSettingPB {
fn from(value: LocalLLMSetting) -> Self {
LocalLLMSettingPB {
chat_bin_path: value.chat_bin_path,
chat_model_path: value.chat_model_path,
enabled: value.enabled,
}
}
}
impl From<LocalLLMSettingPB> for LocalLLMSetting {
fn from(value: LocalLLMSettingPB) -> Self {
LocalLLMSetting {
chat_bin_path: value.chat_bin_path,
chat_model_path: value.chat_model_path,
enabled: value.enabled,
}
}
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct CompleteTextPB {
#[pb(index = 1)]

View File

@ -1,4 +1,5 @@
use flowy_chat_pub::cloud::ChatMessageType;
use std::sync::{Arc, Weak};
use validator::Validate;
@ -93,9 +94,15 @@ pub(crate) async fn get_answer_handler(
) -> DataResult<ChatMessagePB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner();
let message = chat_manager
.generate_answer(&data.chat_id, data.message_id)
.await?;
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let message = chat_manager
.generate_answer(&data.chat_id, data.message_id)
.await?;
let _ = tx.send(message);
Ok::<_, FlowyError>(())
});
let message = rx.await?;
data_result_ok(message)
}
@ -113,6 +120,26 @@ pub(crate) async fn stop_stream_handler(
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn get_local_ai_setting_handler(
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> DataResult<LocalLLMSettingPB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let setting = chat_manager.get_local_ai_setting()?;
let pb = setting.into();
data_result_ok(pb)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn update_local_ai_setting_handler(
data: AFPluginData<LocalLLMSettingPB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> Result<(), FlowyError> {
let data = data.into_inner();
let chat_manager = upgrade_chat_manager(chat_manager)?;
chat_manager.update_local_ai_setting(data.into())?;
Ok(())
}
pub(crate) async fn start_complete_text_handler(
data: AFPluginData<CompleteTextPB>,
tools: AFPluginState<Arc<AITools>>,

View File

@ -11,7 +11,7 @@ use crate::event_handler::*;
pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin {
let user_service = Arc::downgrade(&chat_manager.upgrade().unwrap().user_service);
let cloud_service = Arc::downgrade(&chat_manager.upgrade().unwrap().cloud_service);
let cloud_service = Arc::downgrade(&chat_manager.upgrade().unwrap().chat_service);
let ai_tools = Arc::new(AITools::new(cloud_service, user_service));
AFPlugin::new()
.name("Flowy-Chat")
@ -23,6 +23,11 @@ pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin {
.event(ChatEvent::GetRelatedQuestion, get_related_question_handler)
.event(ChatEvent::GetAnswerForQuestion, get_answer_handler)
.event(ChatEvent::StopStream, stop_stream_handler)
.event(ChatEvent::GetLocalAISetting, get_local_ai_setting_handler)
.event(
ChatEvent::UpdateLocalAISetting,
update_local_ai_setting_handler,
)
.event(ChatEvent::CompleteText, start_complete_text_handler)
.event(ChatEvent::StopCompleteText, stop_complete_text_handler)
}
@ -49,9 +54,15 @@ pub enum ChatEvent {
#[event(input = "ChatMessageIdPB", output = "ChatMessagePB")]
GetAnswerForQuestion = 5,
#[event(input = "LocalLLMSettingPB")]
UpdateLocalAISetting = 6,
#[event(output = "LocalLLMSettingPB")]
GetLocalAISetting = 7,
#[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")]
CompleteText = 6,
CompleteText = 8,
#[event(input = "CompleteTextTaskPB")]
StopCompleteText = 7,
StopCompleteText = 9,
}

View File

@ -3,7 +3,9 @@ pub mod event_map;
mod chat;
pub mod chat_manager;
mod chat_service_impl;
pub mod entities;
pub mod local_ai;
pub mod notification;
mod persistence;
mod protobuf;

View File

@ -0,0 +1,128 @@
use anyhow::anyhow;
use bytes::Bytes;
use flowy_error::FlowyError;
use flowy_sidecar::core::parser::{DefaultResponseParser, ResponseParser};
use flowy_sidecar::core::plugin::Plugin;
use flowy_sidecar::error::{RemoteError, SidecarError};
use serde_json::json;
use serde_json::Value as JsonValue;
use std::sync::Weak;
use tokio_stream::wrappers::ReceiverStream;
use tracing::instrument;
pub struct ChatPluginOperation {
plugin: Weak<Plugin>,
}
impl ChatPluginOperation {
pub fn new(plugin: Weak<Plugin>) -> Self {
ChatPluginOperation { plugin }
}
fn get_plugin(&self) -> Result<std::sync::Arc<Plugin>, SidecarError> {
self
.plugin
.upgrade()
.ok_or_else(|| SidecarError::Internal(anyhow!("Plugin is dropped")))
}
async fn send_request<T: ResponseParser>(
&self,
method: &str,
params: JsonValue,
) -> Result<T::ValueType, SidecarError> {
let plugin = self.get_plugin()?;
let mut request = json!({ "method": method });
request
.as_object_mut()
.unwrap()
.extend(params.as_object().unwrap().clone());
plugin.async_request::<T>("handle", &request).await
}
pub async fn create_chat(&self, chat_id: &str) -> Result<(), SidecarError> {
self
.send_request::<DefaultResponseParser>("create_chat", json!({ "chat_id": chat_id }))
.await
}
pub async fn close_chat(&self, chat_id: &str) -> Result<(), SidecarError> {
self
.send_request::<DefaultResponseParser>("close_chat", json!({ "chat_id": chat_id }))
.await
}
pub async fn send_message(&self, chat_id: &str, message: &str) -> Result<String, SidecarError> {
self
.send_request::<ChatResponseParser>(
"answer",
json!({ "chat_id": chat_id, "params": { "content": message } }),
)
.await
}
#[instrument(level = "debug", skip(self), err)]
pub async fn stream_message(
&self,
chat_id: &str,
message: &str,
) -> Result<ReceiverStream<Result<Bytes, SidecarError>>, FlowyError> {
let plugin = self
.get_plugin()
.map_err(|err| FlowyError::internal().with_context(err.to_string()))?;
let params = json!({
"chat_id": chat_id,
"method": "stream_answer",
"params": { "content": message }
});
plugin
.stream_request::<ChatStreamResponseParser>("handle", &params)
.map_err(|err| FlowyError::internal().with_context(err.to_string()))
}
pub async fn get_related_questions(&self, chat_id: &str) -> Result<Vec<JsonValue>, SidecarError> {
self
.send_request::<ChatRelatedQuestionsResponseParser>(
"related_question",
json!({ "chat_id": chat_id }),
)
.await
}
}
pub struct ChatResponseParser;
impl ResponseParser for ChatResponseParser {
type ValueType = String;
fn parse_json(json: JsonValue) -> Result<Self::ValueType, RemoteError> {
json
.get("data")
.and_then(|data| data.as_str())
.map(String::from)
.ok_or(RemoteError::ParseResponse(json))
}
}
pub struct ChatStreamResponseParser;
impl ResponseParser for ChatStreamResponseParser {
type ValueType = Bytes;
fn parse_json(json: JsonValue) -> Result<Self::ValueType, RemoteError> {
json
.as_str()
.map(|message| Bytes::from(message.to_string()))
.ok_or(RemoteError::ParseResponse(json))
}
}
pub struct ChatRelatedQuestionsResponseParser;
impl ResponseParser for ChatRelatedQuestionsResponseParser {
type ValueType = Vec<JsonValue>;
fn parse_json(json: JsonValue) -> Result<Self::ValueType, RemoteError> {
json
.get("data")
.and_then(|data| data.as_array()).cloned()
.ok_or(RemoteError::ParseResponse(json))
}
}

View File

@ -0,0 +1,50 @@
use anyhow::anyhow;
use flowy_sidecar::core::parser::ResponseParser;
use flowy_sidecar::core::plugin::Plugin;
use flowy_sidecar::error::{RemoteError, SidecarError};
use serde_json::json;
use serde_json::Value as JsonValue;
use std::sync::Weak;
pub struct EmbeddingPluginOperation {
plugin: Weak<Plugin>,
}
impl EmbeddingPluginOperation {
pub fn new(plugin: Weak<Plugin>) -> Self {
EmbeddingPluginOperation { plugin }
}
pub async fn calculate_similarity(
&self,
message1: &str,
message2: &str,
) -> Result<f64, SidecarError> {
let plugin = self
.plugin
.upgrade()
.ok_or(SidecarError::Internal(anyhow!("Plugin is dropped")))?;
let params =
json!({"method": "calculate_similarity", "params": {"src": message1, "dest": message2}});
plugin
.async_request::<SimilarityResponseParser>("handle", &params)
.await
}
}
pub struct SimilarityResponseParser;
impl ResponseParser for SimilarityResponseParser {
type ValueType = f64;
fn parse_json(json: JsonValue) -> Result<Self::ValueType, RemoteError> {
if json.is_object() {
if let Some(data) = json.get("data") {
if let Some(score) = data.get("score").and_then(|v| v.as_f64()) {
return Ok(score);
}
}
}
Err(RemoteError::ParseResponse(json))
}
}

View File

@ -0,0 +1,315 @@
use crate::local_ai::chat_plugin::ChatPluginOperation;
use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult};
use flowy_sidecar::core::plugin::{Plugin, PluginId, PluginInfo};
use flowy_sidecar::error::SidecarError;
use flowy_sidecar::manager::SidecarManager;
use log::error;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::{Arc, Weak};
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::time::timeout;
use tokio_stream::wrappers::ReceiverStream;
use tracing::{info, instrument, trace};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LocalLLMSetting {
pub chat_bin_path: String,
pub chat_model_path: String,
pub enabled: bool,
}
impl LocalLLMSetting {
pub fn validate(&self) -> FlowyResult<()> {
ChatPluginConfig::new(&self.chat_bin_path, &self.chat_model_path)?;
Ok(())
}
pub fn chat_config(&self) -> FlowyResult<ChatPluginConfig> {
let config = ChatPluginConfig::new(&self.chat_bin_path, &self.chat_model_path)?;
Ok(config)
}
}
pub struct LocalChatLLMChat {
sidecar_manager: Arc<SidecarManager>,
state: RwLock<LLMState>,
state_notify: tokio::sync::broadcast::Sender<LLMState>,
plugin_config: RwLock<Option<ChatPluginConfig>>,
}
impl LocalChatLLMChat {
pub fn new(sidecar_manager: Arc<SidecarManager>) -> Self {
let (state_notify, _) = tokio::sync::broadcast::channel(10);
Self {
sidecar_manager,
state: RwLock::new(LLMState::Loading),
state_notify,
plugin_config: Default::default(),
}
}
async fn update_state(&self, state: LLMState) {
*self.state.write().await = state.clone();
let _ = self.state_notify.send(state);
}
/// Waits for the plugin to be ready.
///
/// The wait_plugin_ready method is an asynchronous function designed to ensure that the chat
/// plugin is in a ready state before allowing further operations. This is crucial for maintaining
/// the correct sequence of operations and preventing errors that could occur if operations are
/// attempted on an unready plugin.
///
/// # Returns
///
/// A `FlowyResult<()>` indicating success or failure.
async fn wait_plugin_ready(&self) -> FlowyResult<()> {
let is_loading = self.state.read().await.is_loading();
if !is_loading {
return Ok(());
}
info!("[Chat Plugin] wait for chat plugin to be ready");
let mut rx = self.state_notify.subscribe();
let timeout_duration = Duration::from_secs(30);
let result = timeout(timeout_duration, async {
while let Ok(state) = rx.recv().await {
if state.is_ready() {
break;
}
}
})
.await;
match result {
Ok(_) => {
trace!("[Chat Plugin] chat plugin is ready");
Ok(())
},
Err(_) => Err(
FlowyError::local_ai().with_context("Timeout while waiting for chat plugin to be ready"),
),
}
}
/// Retrieves the chat plugin.
///
/// # Returns
///
/// A `FlowyResult<Weak<Plugin>>` containing a weak reference to the plugin.
async fn get_chat_plugin(&self) -> FlowyResult<Weak<Plugin>> {
let plugin_id = self.state.read().await.plugin_id()?;
let plugin = self.sidecar_manager.get_plugin(plugin_id).await?;
Ok(plugin)
}
/// Creates a new chat session.
///
/// # Arguments
///
/// * `chat_id` - A string slice containing the unique identifier for the chat session.
///
/// # Returns
///
/// A `FlowyResult<()>` indicating success or failure.
pub async fn create_chat(&self, chat_id: &str) -> FlowyResult<()> {
trace!("[Chat Plugin] create chat: {}", chat_id);
self.wait_plugin_ready().await?;
let plugin = self.get_chat_plugin().await?;
let operation = ChatPluginOperation::new(plugin);
operation.create_chat(chat_id).await?;
Ok(())
}
/// Closes an existing chat session.
///
/// # Arguments
///
/// * `chat_id` - A string slice containing the unique identifier for the chat session to close.
///
/// # Returns
///
/// A `FlowyResult<()>` indicating success or failure.
pub async fn close_chat(&self, chat_id: &str) -> FlowyResult<()> {
trace!("[Chat Plugin] close chat: {}", chat_id);
let plugin = self.get_chat_plugin().await?;
let operation = ChatPluginOperation::new(plugin);
operation.close_chat(chat_id).await?;
Ok(())
}
/// Asks a question and returns a stream of responses.
///
/// # Arguments
///
/// * `chat_id` - A string slice containing the unique identifier for the chat session.
/// * `message` - A string slice containing the question or message to send.
///
/// # Returns
///
/// A `FlowyResult<ReceiverStream<anyhow::Result<Bytes, SidecarError>>>` containing a stream of responses.
pub async fn ask_question(
&self,
chat_id: &str,
message: &str,
) -> FlowyResult<ReceiverStream<anyhow::Result<Bytes, SidecarError>>> {
trace!("[Chat Plugin] ask question: {}", message);
self.wait_plugin_ready().await?;
let plugin = self.get_chat_plugin().await?;
let operation = ChatPluginOperation::new(plugin);
let stream = operation.stream_message(chat_id, message).await?;
Ok(stream)
}
/// Generates a complete answer for a given message.
///
/// # Arguments
///
/// * `chat_id` - A string slice containing the unique identifier for the chat session.
/// * `message` - A string slice containing the message to generate an answer for.
///
/// # Returns
///
/// A `FlowyResult<String>` containing the generated answer.
pub async fn generate_answer(&self, chat_id: &str, message: &str) -> FlowyResult<String> {
let plugin = self.get_chat_plugin().await?;
let operation = ChatPluginOperation::new(plugin);
let answer = operation.send_message(chat_id, message).await?;
Ok(answer)
}
#[instrument(skip_all, err)]
pub async fn destroy_chat_plugin(&self) -> FlowyResult<()> {
if let Ok(plugin_id) = self.state.read().await.plugin_id() {
if let Err(err) = self.sidecar_manager.remove_plugin(plugin_id).await {
error!("remove plugin failed: {:?}", err);
}
}
self.update_state(LLMState::Uninitialized).await;
Ok(())
}
#[instrument(skip_all, err)]
pub async fn init_chat_plugin(&self, config: ChatPluginConfig) -> FlowyResult<()> {
if self.state.read().await.is_ready() {
if let Some(existing_config) = self.plugin_config.read().await.as_ref() {
if existing_config == &config {
trace!("[Chat Plugin] chat plugin already initialized with the same config");
return Ok(());
} else {
trace!(
"[Chat Plugin] existing config: {:?}, new config:{:?}",
existing_config,
config
);
}
}
}
// Initialize chat plugin if the config is different
// If the chat_bin_path is different, remove the old plugin
if let Err(err) = self.destroy_chat_plugin().await {
error!("[Chat Plugin] failed to destroy plugin: {:?}", err);
}
self.update_state(LLMState::Loading).await;
// create new plugin
trace!("[Chat Plugin] create chat plugin: {:?}", config);
let plugin_info = PluginInfo {
name: "chat_plugin".to_string(),
exec_path: config.chat_bin_path.clone(),
};
let plugin_id = self.sidecar_manager.create_plugin(plugin_info).await?;
// init plugin
trace!("[Chat Plugin] init chat plugin model: {:?}", plugin_id);
let model_path = config.chat_model_path.clone();
let plugin = self.sidecar_manager.init_plugin(
plugin_id,
serde_json::json!({
"absolute_chat_model_path": model_path,
}),
)?;
info!("[Chat Plugin] {} setup success", plugin);
self.plugin_config.write().await.replace(config);
self.update_state(LLMState::Ready { plugin_id }).await;
Ok(())
}
}
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct ChatPluginConfig {
chat_bin_path: PathBuf,
chat_model_path: PathBuf,
}
impl ChatPluginConfig {
pub fn new(chat_bin: &str, chat_model_path: &str) -> FlowyResult<Self> {
let chat_bin_path = PathBuf::from(chat_bin);
if !chat_bin_path.exists() {
return Err(FlowyError::invalid_data().with_context(format!(
"Chat binary path does not exist: {:?}",
chat_bin_path
)));
}
if !chat_bin_path.is_file() {
return Err(FlowyError::invalid_data().with_context(format!(
"Chat binary path is not a file: {:?}",
chat_bin_path
)));
}
// Check if local_model_dir exists and is a directory
let chat_model_path = PathBuf::from(&chat_model_path);
if !chat_model_path.exists() {
return Err(
FlowyError::invalid_data()
.with_context(format!("Local model does not exist: {:?}", chat_model_path)),
);
}
if !chat_model_path.is_file() {
return Err(
FlowyError::invalid_data()
.with_context(format!("Local model is not a file: {:?}", chat_model_path)),
);
}
Ok(Self {
chat_bin_path,
chat_model_path,
})
}
}
#[derive(Debug, Clone)]
pub enum LLMState {
Uninitialized,
Loading,
Ready { plugin_id: PluginId },
}
impl LLMState {
fn plugin_id(&self) -> FlowyResult<PluginId> {
match self {
LLMState::Ready { plugin_id } => Ok(*plugin_id),
_ => Err(FlowyError::local_ai().with_context("chat plugin is not ready")),
}
}
fn is_loading(&self) -> bool {
matches!(self, LLMState::Loading)
}
#[allow(dead_code)]
fn is_uninitialized(&self) -> bool {
matches!(self, LLMState::Uninitialized)
}
fn is_ready(&self) -> bool {
matches!(self, LLMState::Ready { .. })
}
}

View File

@ -0,0 +1,4 @@
pub mod chat_plugin;
pub mod llm_chat;
pub mod embedding_plugin;

View File

@ -4,7 +4,8 @@ use flowy_sqlite::{
diesel, insert_into,
query_dsl::*,
schema::{chat_message_table, chat_message_table::dsl},
DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable,
DBConnection, ExpressionMethods, Identifiable, Insertable, OptionalExtension, QueryResult,
Queryable,
};
#[derive(Queryable, Insertable, Identifiable)]
@ -69,3 +70,14 @@ pub fn select_chat_messages(
let messages: Vec<ChatMessageTable> = query.load::<ChatMessageTable>(&mut *conn)?;
Ok(messages)
}
pub fn select_single_message(
mut conn: DBConnection,
message_id_val: i64,
) -> QueryResult<Option<ChatMessageTable>> {
let message = dsl::chat_message_table
.filter(chat_message_table::message_id.eq(message_id_val))
.first::<ChatMessageTable>(&mut *conn)
.optional()?;
Ok(message)
}

View File

@ -1,9 +1,10 @@
use diesel::sqlite::SqliteConnection;
use flowy_sqlite::upsert::excluded;
use flowy_sqlite::{
diesel,
query_dsl::*,
schema::{chat_table, chat_table::dsl},
DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable,
AsChangeset, DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable,
};
#[derive(Clone, Default, Queryable, Insertable, Identifiable)]
@ -13,6 +14,22 @@ pub struct ChatTable {
pub chat_id: String,
pub created_at: i64,
pub name: String,
pub local_model_path: String,
pub local_model_name: String,
pub local_enabled: bool,
pub sync_to_cloud: bool,
}
#[derive(AsChangeset, Identifiable, Default, Debug)]
#[diesel(table_name = chat_table)]
#[diesel(primary_key(chat_id))]
pub struct ChatTableChangeset {
pub chat_id: String,
pub name: Option<String>,
pub local_model_path: Option<String>,
pub local_model_name: Option<String>,
pub local_enabled: Option<bool>,
pub sync_to_cloud: Option<bool>,
}
pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult<usize> {
@ -27,6 +44,16 @@ pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult<
.execute(&mut *conn)
}
#[allow(dead_code)]
pub fn update_chat_local_model(
conn: &mut SqliteConnection,
changeset: ChatTableChangeset,
) -> QueryResult<usize> {
let filter = dsl::chat_table.filter(chat_table::chat_id.eq(changeset.chat_id.clone()));
let affected_row = diesel::update(filter).set(changeset).execute(conn)?;
Ok(affected_row)
}
#[allow(dead_code)]
pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<ChatTable> {
let row = dsl::chat_table

View File

@ -0,0 +1,41 @@
use crate::util::LocalAITest;
use tokio_stream::StreamExt;
#[tokio::test]
async fn load_chat_model_test() {
if let Ok(test) = LocalAITest::new() {
let plugin_id = test.init_chat_plugin().await;
let chat_id = uuid::Uuid::new_v4().to_string();
let resp = test
.send_chat_message(&chat_id, plugin_id, "hello world")
.await;
eprintln!("chat response: {:?}", resp);
let embedding_plugin_id = test.init_embedding_plugin().await;
let score = test.calculate_similarity(embedding_plugin_id, &resp, "Hello! How can I help you today? Is there something specific you would like to know or discuss").await;
assert!(score > 0.8);
// let questions = test.related_question(&chat_id, plugin_id).await;
// assert_eq!(questions.len(), 3);
// eprintln!("related questions: {:?}", questions);
}
}
#[tokio::test]
async fn stream_local_model_test() {
if let Ok(test) = LocalAITest::new() {
let plugin_id = test.init_chat_plugin().await;
let chat_id = uuid::Uuid::new_v4().to_string();
let mut resp = test
.stream_chat_message(&chat_id, plugin_id, "hello world")
.await;
let mut list = vec![];
while let Some(s) = resp.next().await {
list.push(String::from_utf8(s.unwrap().to_vec()).unwrap());
}
let answer = list.join("");
eprintln!("chat response: {:?}", answer);
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
}

View File

@ -0,0 +1,2 @@
pub mod chat_test;
pub mod util;

View File

@ -0,0 +1,171 @@
use anyhow::Result;
use bytes::Bytes;
use flowy_sidecar::manager::SidecarManager;
use serde_json::json;
use std::path::PathBuf;
use std::sync::Once;
use tokio_stream::wrappers::ReceiverStream;
use flowy_chat::local_ai::chat_plugin::ChatPluginOperation;
use flowy_chat::local_ai::embedding_plugin::EmbeddingPluginOperation;
use flowy_sidecar::core::plugin::{PluginId, PluginInfo};
use flowy_sidecar::error::SidecarError;
use tracing_subscriber::fmt::Subscriber;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
pub struct LocalAITest {
config: LocalAIConfiguration,
manager: SidecarManager,
}
impl LocalAITest {
pub fn new() -> Result<Self> {
let config = LocalAIConfiguration::new()?;
let manager = SidecarManager::new();
Ok(Self { config, manager })
}
pub async fn init_chat_plugin(&self) -> PluginId {
let info = PluginInfo {
name: "chat".to_string(),
exec_path: self.config.chat_bin_path.clone(),
};
let plugin_id = self.manager.create_plugin(info).await.unwrap();
self
.manager
.init_plugin(
plugin_id,
json!({
"absolute_chat_model_path":self.config.chat_model_absolute_path(),
}),
)
.unwrap();
plugin_id
}
pub async fn init_embedding_plugin(&self) -> PluginId {
let info = PluginInfo {
name: "embedding".to_string(),
exec_path: self.config.embedding_bin_path.clone(),
};
let plugin_id = self.manager.create_plugin(info).await.unwrap();
let embedding_model_path = self.config.embedding_model_absolute_path();
self
.manager
.init_plugin(
plugin_id,
json!({
"absolute_model_path":embedding_model_path,
}),
)
.unwrap();
plugin_id
}
pub async fn send_chat_message(
&self,
chat_id: &str,
plugin_id: PluginId,
message: &str,
) -> String {
let plugin = self.manager.get_plugin(plugin_id).await.unwrap();
let operation = ChatPluginOperation::new(plugin);
operation.send_message(chat_id, message).await.unwrap()
}
pub async fn stream_chat_message(
&self,
chat_id: &str,
plugin_id: PluginId,
message: &str,
) -> ReceiverStream<Result<Bytes, SidecarError>> {
let plugin = self.manager.get_plugin(plugin_id).await.unwrap();
let operation = ChatPluginOperation::new(plugin);
operation.stream_message(chat_id, message).await.unwrap()
}
pub async fn related_question(
&self,
chat_id: &str,
plugin_id: PluginId,
) -> Vec<serde_json::Value> {
let plugin = self.manager.get_plugin(plugin_id).await.unwrap();
let operation = ChatPluginOperation::new(plugin);
operation.get_related_questions(chat_id).await.unwrap()
}
pub async fn calculate_similarity(
&self,
plugin_id: PluginId,
message1: &str,
message2: &str,
) -> f64 {
let plugin = self.manager.get_plugin(plugin_id).await.unwrap();
let operation = EmbeddingPluginOperation::new(plugin);
operation
.calculate_similarity(message1, message2)
.await
.unwrap()
}
}
pub struct LocalAIConfiguration {
model_dir: String,
chat_bin_path: PathBuf,
chat_model_name: String,
embedding_bin_path: PathBuf,
embedding_model_name: String,
}
impl LocalAIConfiguration {
pub fn new() -> Result<Self> {
dotenv::dotenv().ok();
setup_log();
// load from .env
let model_dir = dotenv::var("LOCAL_AI_MODEL_DIR")?;
let chat_bin_path = PathBuf::from(dotenv::var("CHAT_BIN_PATH")?);
let chat_model_name = dotenv::var("LOCAL_AI_CHAT_MODEL_NAME")?;
let embedding_bin_path = PathBuf::from(dotenv::var("EMBEDDING_BIN_PATH")?);
let embedding_model_name = dotenv::var("LOCAL_AI_EMBEDDING_MODEL_NAME")?;
Ok(Self {
model_dir,
chat_bin_path,
chat_model_name,
embedding_bin_path,
embedding_model_name,
})
}
pub fn chat_model_absolute_path(&self) -> String {
format!("{}/{}", self.model_dir, self.chat_model_name)
}
pub fn embedding_model_absolute_path(&self) -> String {
format!("{}/{}", self.model_dir, self.embedding_model_name)
}
}
pub fn setup_log() {
static START: Once = Once::new();
START.call_once(|| {
let level = "trace";
let mut filters = vec![];
filters.push(format!("flowy_sidecar={}", level));
std::env::set_var("RUST_LOG", filters.join(","));
let subscriber = Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.with_line_number(true)
.with_ansi(true)
.finish();
subscriber.try_init().unwrap();
});
}

View File

@ -1,13 +1,13 @@
use std::sync::Weak;
use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use crate::entities::{KeyPB, KeyValuePB};
pub(crate) async fn set_key_value_handler(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
data: AFPluginData<KeyValuePB>,
) -> FlowyResult<()> {
let data = data.into_inner();
@ -25,7 +25,7 @@ pub(crate) async fn set_key_value_handler(
}
pub(crate) async fn get_key_value_handler(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
data: AFPluginData<KeyPB>,
) -> DataResult<KeyValuePB, FlowyError> {
match store_preferences.upgrade() {
@ -42,7 +42,7 @@ pub(crate) async fn get_key_value_handler(
}
pub(crate) async fn remove_key_value_handler(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
data: AFPluginData<KeyPB>,
) -> FlowyResult<()> {
match store_preferences.upgrade() {

View File

@ -3,12 +3,12 @@ use std::sync::Weak;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use lib_dispatch::prelude::AFPlugin;
use crate::event_handler::*;
pub fn init(store_preferences: Weak<StorePreferences>) -> AFPlugin {
pub fn init(store_preferences: Weak<KVStorePreferences>) -> AFPlugin {
AFPlugin::new()
.name(env!("CARGO_PKG_NAME"))
.state(store_preferences)

View File

@ -9,7 +9,7 @@ use flowy_server_pub::af_cloud_config::AFCloudConfiguration;
use flowy_server_pub::supabase_config::SupabaseConfiguration;
use flowy_user::services::entities::URL_SAFE_ENGINE;
use lib_infra::file_util::copy_dir_recursive;
use lib_infra::util::Platform;
use lib_infra::util::OperatingSystem;
use crate::integrate::log::create_log_filter;
@ -94,7 +94,7 @@ impl AppFlowyCoreConfig {
},
Some(config) => make_user_data_folder(&custom_application_path, &config.base_url),
};
let log_filter = create_log_filter("info".to_owned(), vec![], Platform::from(&platform));
let log_filter = create_log_filter("info".to_owned(), vec![], OperatingSystem::from(&platform));
AppFlowyCoreConfig {
app_version,
@ -112,7 +112,7 @@ impl AppFlowyCoreConfig {
self.log_filter = create_log_filter(
level.to_owned(),
with_crates,
Platform::from(&self.platform),
OperatingSystem::from(&self.platform),
);
self
}

View File

@ -1,6 +1,7 @@
use flowy_chat::chat_manager::{ChatManager, ChatUserService};
use flowy_chat_pub::cloud::ChatCloudService;
use flowy_error::FlowyError;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_sqlite::DBConnection;
use flowy_user::services::authenticate_user::AuthenticateUser;
use std::sync::{Arc, Weak};
@ -11,9 +12,14 @@ impl ChatDepsResolver {
pub fn resolve(
authenticate_user: Weak<AuthenticateUser>,
cloud_service: Arc<dyn ChatCloudService>,
store_preferences: Arc<KVStorePreferences>,
) -> Arc<ChatManager> {
let user_service = ChatUserServiceImpl(authenticate_user);
Arc::new(ChatManager::new(cloud_service, user_service))
Arc::new(ChatManager::new(
cloud_service,
user_service,
store_preferences,
))
}
}

View File

@ -19,7 +19,7 @@ use flowy_folder::view_operation::{
use flowy_folder::ViewLayout;
use flowy_folder_pub::folder_builder::NestedViewBuilder;
use flowy_search::folder::indexer::FolderIndexManagerImpl;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user::services::authenticate_user::AuthenticateUser;
use lib_dispatch::prelude::ToBytes;
use lib_infra::future::FutureResult;
@ -38,7 +38,7 @@ impl FolderDepsResolver {
collab_builder: Arc<AppFlowyCollabBuilder>,
server_provider: Arc<ServerProvider>,
folder_indexer: Arc<FolderIndexManagerImpl>,
store_preferences: Arc<StorePreferences>,
store_preferences: Arc<KVStorePreferences>,
operation_handlers: FolderOperationHandlers,
) -> Arc<FolderManager> {
let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl {

View File

@ -4,7 +4,7 @@ use flowy_database2::DatabaseManager;
use flowy_error::FlowyResult;
use flowy_folder::manager::FolderManager;
use flowy_folder_pub::folder_builder::ParentChildViews;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user::services::authenticate_user::AuthenticateUser;
use flowy_user::user_manager::UserManager;
use flowy_user_pub::workspace_service::UserWorkspaceService;
@ -19,7 +19,7 @@ impl UserDepsResolver {
authenticate_user: Arc<AuthenticateUser>,
collab_builder: Arc<AppFlowyCollabBuilder>,
server_provider: Arc<ServerProvider>,
store_preference: Arc<StorePreferences>,
store_preference: Arc<KVStorePreferences>,
database_manager: Arc<DatabaseManager>,
folder_manager: Arc<FolderManager>,
) -> Arc<UserManager> {

View File

@ -1,4 +1,4 @@
use lib_infra::util::Platform;
use lib_infra::util::OperatingSystem;
use lib_log::stream_log::StreamLogSender;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@ -8,7 +8,7 @@ use crate::AppFlowyCoreConfig;
static INIT_LOG: AtomicBool = AtomicBool::new(false);
pub(crate) fn init_log(
config: &AppFlowyCoreConfig,
platform: &Platform,
platform: &OperatingSystem,
stream_log_sender: Option<Arc<dyn StreamLogSender>>,
) {
#[cfg(debug_assertions)]
@ -25,11 +25,15 @@ pub(crate) fn init_log(
}
}
pub fn create_log_filter(level: String, with_crates: Vec<String>, platform: Platform) -> String {
pub fn create_log_filter(
level: String,
with_crates: Vec<String>,
platform: OperatingSystem,
) -> String {
let mut level = std::env::var("RUST_LOG").unwrap_or(level);
#[cfg(debug_assertions)]
if matches!(platform, Platform::IOS) {
if matches!(platform, OperatingSystem::IOS) {
level = "trace".to_string();
}
@ -53,7 +57,8 @@ pub fn create_log_filter(level: String, with_crates: Vec<String>, platform: Plat
filters.push(format!("lib_infra={}", level));
filters.push(format!("flowy_search={}", level));
filters.push(format!("flowy_chat={}", level));
filters.push(format!("flowy_storage={}", level));
filters.push(format!("flowy_chat={}", level));
filters.push(format!("flowy_sidecar={}", level));
filters.push(format!("flowy_ai={}", level));
// Enable the frontend logs. DO NOT DISABLE.
// These logs are essential for debugging and verifying frontend behavior.

View File

@ -14,7 +14,7 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl};
use flowy_server_pub::af_cloud_config::AFCloudConfiguration;
use flowy_server_pub::supabase_config::SupabaseConfiguration;
use flowy_server_pub::AuthenticatorType;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user_pub::entities::*;
use crate::AppFlowyCoreConfig;
@ -59,7 +59,7 @@ pub struct ServerProvider {
providers: RwLock<HashMap<Server, Arc<dyn AppFlowyServer>>>,
pub(crate) encryption: RwLock<Arc<dyn AppFlowyEncryption>>,
#[allow(dead_code)]
pub(crate) store_preferences: Weak<StorePreferences>,
pub(crate) store_preferences: Weak<KVStorePreferences>,
pub(crate) user_enable_sync: RwLock<bool>,
/// The authenticator type of the user.
@ -72,7 +72,7 @@ impl ServerProvider {
pub fn new(
config: AppFlowyCoreConfig,
server: Server,
store_preferences: Weak<StorePreferences>,
store_preferences: Weak<KVStorePreferences>,
server_user: impl ServerUser + 'static,
) -> Self {
let user = Arc::new(server_user);

View File

@ -18,8 +18,7 @@ use collab_integrate::collab_builder::{
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
};
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage,
StreamAnswer, StreamComplete,
ChatCloudService, ChatMessage, MessageCursor, RepeatedChatMessage, StreamAnswer, StreamComplete,
};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
@ -552,24 +551,7 @@ impl ChatCloudService for ServerProvider {
})
}
async fn send_chat_message(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let message = message.to_string();
let server = self.get_server()?;
server
.chat_service()
.send_chat_message(&workspace_id, &chat_id, &message, message_type)
.await
}
fn send_question(
fn save_question(
&self,
workspace_id: &str,
chat_id: &str,
@ -584,7 +566,7 @@ impl ChatCloudService for ServerProvider {
FutureResult::new(async move {
server?
.chat_service()
.send_question(&workspace_id, &chat_id, &message, message_type)
.save_question(&workspace_id, &chat_id, &message, message_type)
.await
})
}
@ -608,7 +590,7 @@ impl ChatCloudService for ServerProvider {
})
}
async fn stream_answer(
async fn ask_question(
&self,
workspace_id: &str,
chat_id: &str,
@ -619,7 +601,7 @@ impl ChatCloudService for ServerProvider {
let server = self.get_server()?;
server
.chat_service()
.stream_answer(&workspace_id, &chat_id, message_id)
.ask_question(&workspace_id, &chat_id, message_id)
.await
}
@ -658,21 +640,17 @@ impl ChatCloudService for ServerProvider {
})
}
fn generate_answer(
async fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
) -> Result<ChatMessage, FlowyError> {
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.generate_answer(&workspace_id, &chat_id, question_message_id)
.await
})
server?
.chat_service()
.generate_answer(workspace_id, chat_id, question_message_id)
.await
}
async fn stream_complete(

View File

@ -16,7 +16,7 @@ use flowy_error::{FlowyError, FlowyResult};
use flowy_folder::manager::FolderManager;
use flowy_server::af_cloud::define::ServerUser;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_storage::manager::StorageManager;
use flowy_user::services::authenticate_user::AuthenticateUser;
use flowy_user::services::entities::UserConfig;
@ -25,7 +25,7 @@ use flowy_user::user_manager::UserManager;
use lib_dispatch::prelude::*;
use lib_dispatch::runtime::AFPluginRuntime;
use lib_infra::priority_task::{TaskDispatcher, TaskRunner};
use lib_infra::util::Platform;
use lib_infra::util::OperatingSystem;
use lib_log::stream_log::StreamLogSender;
use module::make_plugins;
@ -57,7 +57,7 @@ pub struct AppFlowyCore {
pub event_dispatcher: Arc<AFPluginDispatcher>,
pub server_provider: Arc<ServerProvider>,
pub task_dispatcher: Arc<RwLock<TaskDispatcher>>,
pub store_preference: Arc<StorePreferences>,
pub store_preference: Arc<KVStorePreferences>,
pub search_manager: Arc<SearchManager>,
pub chat_manager: Arc<ChatManager>,
pub storage_manager: Arc<StorageManager>,
@ -69,7 +69,7 @@ impl AppFlowyCore {
runtime: Arc<AFPluginRuntime>,
stream_log_sender: Option<Arc<dyn StreamLogSender>>,
) -> Self {
let platform = Platform::from(&config.platform);
let platform = OperatingSystem::from(&config.platform);
#[allow(clippy::if_same_then_else)]
if cfg!(debug_assertions) {
@ -102,7 +102,7 @@ impl AppFlowyCore {
#[instrument(skip(config, runtime))]
async fn init(config: AppFlowyCoreConfig, runtime: Arc<AFPluginRuntime>) -> Self {
// Init the key value database
let store_preference = Arc::new(StorePreferences::new(&config.storage_path).unwrap());
let store_preference = Arc::new(KVStorePreferences::new(&config.storage_path).unwrap());
info!("🔥{:?}", &config);
let task_scheduler = TaskDispatcher::new(Duration::from_secs(2));
@ -175,8 +175,11 @@ impl AppFlowyCore {
Arc::downgrade(&storage_manager.storage_service),
);
let chat_manager =
ChatDepsResolver::resolve(Arc::downgrade(&authenticate_user), server_provider.clone());
let chat_manager = ChatDepsResolver::resolve(
Arc::downgrade(&authenticate_user),
server_provider.clone(),
store_preference.clone(),
);
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Some(Arc::downgrade(
&authenticate_user,

View File

@ -33,7 +33,7 @@ collab-plugins = { workspace = true, optional = true }
collab-folder = { workspace = true, optional = true }
client-api = { workspace = true, optional = true }
tantivy = { version = "0.21.1", optional = true }
flowy-sidecar = { workspace = true, optional = true }
[features]
impl_from_dispatch_error = ["lib-dispatch"]
@ -49,6 +49,7 @@ impl_from_collab_folder = ["collab-folder"]
impl_from_collab_database = ["collab-database"]
impl_from_url = ["url"]
impl_from_tantivy = ["tantivy"]
impl_from_sidecar = ["flowy-sidecar"]
impl_from_sqlite = ["flowy-sqlite", "r2d2"]
impl_from_appflowy_cloud = ["client-api"]

View File

@ -280,6 +280,9 @@ pub enum ErrorCode {
#[error("Workspace data not match")]
WorkspaceDataNotMatch = 97,
#[error("Local AI error")]
LocalAIError = 98,
}
impl ErrorCode {

View File

@ -118,6 +118,7 @@ impl FlowyError {
ErrorCode::FolderIndexManagerUnavailable
);
static_flowy_error!(workspace_data_not_match, ErrorCode::WorkspaceDataNotMatch);
static_flowy_error!(local_ai, ErrorCode::LocalAIError);
}
impl std::convert::From<ErrorCode> for FlowyError {

View File

@ -25,5 +25,7 @@ mod cloud;
#[cfg(feature = "impl_from_url")]
mod url;
#[cfg(feature = "impl_from_sidecar")]
mod sidecar;
#[cfg(feature = "impl_from_tantivy")]
mod tantivy;

View File

@ -0,0 +1,8 @@
use crate::{ErrorCode, FlowyError};
use flowy_sidecar::error::SidecarError;
impl std::convert::From<SidecarError> for FlowyError {
fn from(error: SidecarError) -> Self {
FlowyError::new(ErrorCode::LocalAIError, error)
}
}

View File

@ -30,7 +30,7 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService};
use flowy_folder_pub::folder_builder::ParentChildViews;
use flowy_search_pub::entities::FolderIndexManager;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use parking_lot::RwLock;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
@ -51,7 +51,7 @@ pub struct FolderManager {
pub(crate) operation_handlers: FolderOperationHandlers,
pub cloud_service: Arc<dyn FolderCloudService>,
pub(crate) folder_indexer: Arc<dyn FolderIndexManager>,
pub(crate) store_preferences: Arc<StorePreferences>,
pub(crate) store_preferences: Arc<KVStorePreferences>,
}
impl FolderManager {
@ -61,7 +61,7 @@ impl FolderManager {
operation_handlers: FolderOperationHandlers,
cloud_service: Arc<dyn FolderCloudService>,
folder_indexer: Arc<dyn FolderIndexManager>,
store_preferences: Arc<StorePreferences>,
store_preferences: Arc<KVStorePreferences>,
) -> FlowyResult<Self> {
let mutex_folder = Arc::new(MutexFolder::default());
let manager = Self {

View File

@ -5,10 +5,10 @@ use client_api::entity::{
RepeatedChatMessage,
};
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType, StreamAnswer, StreamComplete,
ChatCloudService, ChatMessage, ChatMessageType, StreamAnswer, StreamComplete,
};
use flowy_error::FlowyError;
use futures_util::StreamExt;
use futures_util::{StreamExt, TryStreamExt};
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
@ -46,27 +46,7 @@ where
})
}
async fn send_chat_message(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError> {
let try_get_client = self.inner.try_get_client();
let params = CreateChatMessageParams {
content: message.to_string(),
message_type,
};
let stream = try_get_client?
.create_chat_qa_message(workspace_id, chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(stream.boxed())
}
fn send_question(
fn save_question(
&self,
workspace_id: &str,
chat_id: &str,
@ -83,7 +63,7 @@ where
FutureResult::new(async move {
let message = try_get_client?
.create_question(&workspace_id, &chat_id, params)
.save_question(&workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(message)
@ -107,14 +87,14 @@ where
FutureResult::new(async move {
let message = try_get_client?
.create_answer(&workspace_id, &chat_id, params)
.save_answer(&workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(message)
})
}
async fn stream_answer(
async fn ask_question(
&self,
workspace_id: &str,
chat_id: &str,
@ -122,10 +102,25 @@ where
) -> Result<StreamAnswer, FlowyError> {
let try_get_client = self.inner.try_get_client();
let stream = try_get_client?
.stream_answer(workspace_id, chat_id, message_id)
.ask_question(workspace_id, chat_id, message_id)
.await
.map_err(FlowyError::from)?
.map_err(FlowyError::from);
Ok(stream.boxed())
}
async fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> Result<ChatMessage, FlowyError> {
let try_get_client = self.inner.try_get_client();
let resp = try_get_client?
.generate_answer(workspace_id, chat_id, question_message_id)
.await
.map_err(FlowyError::from)?;
Ok(stream.boxed())
Ok(resp)
}
fn get_chat_messages(
@ -169,25 +164,6 @@ where
})
}
fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let resp = try_get_client?
.get_answer(&workspace_id, &chat_id, question_message_id)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
async fn stream_complete(
&self,
workspace_id: &str,

View File

@ -1,8 +1,6 @@
use client_api::entity::ai_dto::{CompletionType, RepeatedRelatedQuestion};
use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage};
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, StreamAnswer, StreamComplete,
};
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, StreamAnswer, StreamComplete};
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
@ -22,17 +20,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
})
}
async fn send_chat_message(
&self,
_workspace_id: &str,
_chat_id: &str,
_message: &str,
_message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError> {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
}
fn send_question(
fn save_question(
&self,
_workspace_id: &str,
_chat_id: &str,
@ -56,7 +44,7 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
})
}
async fn stream_answer(
async fn ask_question(
&self,
_workspace_id: &str,
_chat_id: &str,
@ -88,15 +76,13 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
})
}
fn generate_answer(
async fn generate_answer(
&self,
_workspace_id: &str,
_chat_id: &str,
_question_message_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
FutureResult::new(async move {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
) -> Result<ChatMessage, FlowyError> {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
}
async fn stream_complete(

View File

@ -0,0 +1,23 @@
[package]
name = "flowy-sidecar"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1.0" }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
once_cell = "1.19.0"
thiserror = "1.0"
serde_json = "1.0.117"
tracing.workspace = true
crossbeam-utils = "0.8.20"
log = "0.4.21"
parking_lot.workspace = true
tokio-stream = "0.1.15"
lib-infra.workspace = true
[features]
verbose = []

View File

@ -0,0 +1,5 @@
pub mod parser;
pub mod plugin;
pub mod rpc_loop;
mod rpc_object;
pub mod rpc_peer;

View File

@ -0,0 +1,71 @@
use crate::core::rpc_object::RpcObject;
use crate::error::{ReadError, RemoteError};
use serde_json::{json, Value as JsonValue};
use std::io::BufRead;
#[derive(Debug, Default)]
pub struct MessageReader(String);
impl MessageReader {
/// Attempts to read the next line from the stream and parse it as
/// an RPC object.
///
/// # Errors
///
/// This function will return an error if there is an underlying
/// I/O error, if the stream is closed, or if the message is not
/// a valid JSON object.
pub fn next<R: BufRead>(&mut self, reader: &mut R) -> Result<RpcObject, ReadError> {
self.0.clear();
let _ = reader.read_line(&mut self.0)?;
if self.0.is_empty() {
Err(ReadError::Disconnect)
} else {
self.parse(&self.0)
}
}
/// Attempts to parse a &str as an RPC Object.
///
/// This should not be called directly unless you are writing tests.
#[doc(hidden)]
pub fn parse(&self, s: &str) -> Result<RpcObject, ReadError> {
match serde_json::from_str::<JsonValue>(s) {
Ok(val) => {
if !val.is_object() {
Err(ReadError::NotObject(s.to_string()))
} else {
Ok(val.into())
}
},
Err(_) => Ok(RpcObject(json!({"message": s.to_string()}))),
}
}
}
pub type RequestId = u64;
#[derive(Debug, Clone)]
/// An RPC call, which may be either a notification or a request.
pub enum Call<R> {
Message(JsonValue),
/// An id and an RPC Request
Request(RequestId, R),
/// A malformed request: the request contained an id, but could
/// not be parsed. The client will receive an error.
InvalidRequest(RequestId, RemoteError),
}
pub trait ResponseParser {
type ValueType: Send + Sync + 'static;
fn parse_json(payload: JsonValue) -> Result<Self::ValueType, RemoteError>;
}
pub struct DefaultResponseParser;
impl ResponseParser for DefaultResponseParser {
type ValueType = ();
fn parse_json(_payload: JsonValue) -> Result<Self::ValueType, RemoteError> {
Ok(())
}
}

View File

@ -0,0 +1,205 @@
use crate::error::SidecarError;
use crate::manager::WeakSidecarState;
use std::fmt::{Display, Formatter};
use crate::core::parser::ResponseParser;
use crate::core::rpc_loop::RpcLoop;
use crate::core::rpc_peer::{CloneableCallback, OneShotCallback};
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use std::io::BufReader;
use std::path::PathBuf;
use std::process::{Child, Stdio};
use std::sync::Arc;
use std::thread;
use std::time::Instant;
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info};
#[derive(
Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct PluginId(pub(crate) i64);
impl From<i64> for PluginId {
fn from(id: i64) -> Self {
PluginId(id)
}
}
/// The `Peer` trait defines the interface for the opposite side of the RPC channel,
/// designed to be used behind a pointer or as a trait object.
pub trait Peer: Send + Sync + 'static {
/// Clones the peer into a boxed trait object.
fn box_clone(&self) -> Arc<dyn Peer>;
/// Sends an RPC notification to the peer with the specified method and parameters.
fn send_rpc_notification(&self, method: &str, params: &JsonValue);
fn stream_rpc_request(&self, method: &str, params: &JsonValue, f: CloneableCallback);
fn async_send_rpc_request(&self, method: &str, params: &JsonValue, f: Box<dyn OneShotCallback>);
/// Sends a synchronous RPC request to the peer and waits for the result.
/// Returns the result of the request or an error.
fn send_rpc_request(&self, method: &str, params: &JsonValue) -> Result<JsonValue, SidecarError>;
/// Checks if there is an incoming request pending, intended to reduce latency for bulk operations done in the background.
fn request_is_pending(&self) -> bool;
/// Schedules a timer to execute the handler's `idle` function after the specified `Instant`.
/// Note: This is not a high-fidelity timer. Regular RPC messages will always take priority over idle tasks.
fn schedule_timer(&self, after: Instant, token: usize);
}
/// The `Peer` trait object.
pub type RpcPeer = Arc<dyn Peer>;
pub struct RpcCtx {
pub peer: RpcPeer,
}
#[derive(Clone)]
pub struct Plugin {
peer: RpcPeer,
pub(crate) id: PluginId,
pub(crate) name: String,
#[allow(dead_code)]
pub(crate) process: Arc<Child>,
}
impl Display for Plugin {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}, plugin id: {:?}, process id: {}",
self.name,
self.id,
self.process.id()
)
}
}
impl Plugin {
pub fn initialize(&self, value: JsonValue) -> Result<(), SidecarError> {
self.peer.send_rpc_request("initialize", &value)?;
Ok(())
}
pub fn request(&self, method: &str, params: &JsonValue) -> Result<JsonValue, SidecarError> {
self.peer.send_rpc_request(method, params)
}
pub async fn async_request<P: ResponseParser>(
&self,
method: &str,
params: &JsonValue,
) -> Result<P::ValueType, SidecarError> {
let (tx, rx) = tokio::sync::oneshot::channel();
self.peer.async_send_rpc_request(
method,
params,
Box::new(move |result| {
let _ = tx.send(result);
}),
);
let value = rx.await.map_err(|err| {
SidecarError::Internal(anyhow!("error waiting for async response: {:?}", err))
})??;
let value = P::parse_json(value)?;
Ok(value)
}
pub fn stream_request<P: ResponseParser>(
&self,
method: &str,
params: &JsonValue,
) -> Result<ReceiverStream<Result<P::ValueType, SidecarError>>, SidecarError> {
let (tx, stream) = tokio::sync::mpsc::channel(100);
let stream = ReceiverStream::new(stream);
let callback = CloneableCallback::new(move |result| match result {
Ok(json) => {
let result = P::parse_json(json).map_err(SidecarError::from);
let _ = tx.blocking_send(result);
},
Err(err) => {
let _ = tx.blocking_send(Err(err));
},
});
self.peer.stream_rpc_request(method, params, callback);
Ok(stream)
}
pub fn shutdown(&self) {
match self.peer.send_rpc_request("shutdown", &json!({})) {
Ok(_) => {
info!("shutting down plugin {}", self);
},
Err(err) => {
error!("error sending shutdown to plugin {}: {:?}", self, err);
},
}
}
}
pub struct PluginInfo {
pub name: String,
pub exec_path: PathBuf,
}
pub(crate) async fn start_plugin_process(
plugin_info: PluginInfo,
id: PluginId,
state: WeakSidecarState,
) -> Result<(), anyhow::Error> {
let (tx, rx) = tokio::sync::oneshot::channel();
let spawn_result = thread::Builder::new()
.name(format!("<{}> core host thread", &plugin_info.name))
.spawn(move || {
info!("Load {} plugin", &plugin_info.name);
let child = std::process::Command::new(&plugin_info.exec_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn();
match child {
Ok(mut child) => {
let child_stdin = child.stdin.take().unwrap();
let child_stdout = child.stdout.take().unwrap();
let mut looper = RpcLoop::new(child_stdin);
let peer: RpcPeer = Arc::new(looper.get_raw_peer());
let name = plugin_info.name.clone();
peer.send_rpc_notification("ping", &JsonValue::Array(Vec::new()));
let plugin = Plugin {
peer,
process: Arc::new(child),
name,
id,
};
state.plugin_connect(Ok(plugin));
let _ = tx.send(());
let mut state = state;
let err = looper.mainloop(
&plugin_info.name,
|| BufReader::new(child_stdout),
&mut state,
);
state.plugin_exit(id, err);
},
Err(err) => {
let _ = tx.send(());
state.plugin_connect(Err(err))
},
}
});
if let Err(err) = spawn_result {
error!("[RPC] thread spawn failed for {:?}, {:?}", id, err);
return Err(err.into());
}
rx.await?;
Ok(())
}

View File

@ -0,0 +1,269 @@
use crate::core::parser::{Call, MessageReader};
use crate::core::plugin::RpcCtx;
use crate::core::rpc_object::RpcObject;
use crate::core::rpc_peer::{RawPeer, ResponsePayload, RpcState};
use crate::error::{ReadError, RemoteError, SidecarError};
use serde::de::DeserializeOwned;
use std::io::{BufRead, Write};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use tracing::{error, trace};
const MAX_IDLE_WAIT: Duration = Duration::from_millis(5);
pub trait Handler {
type Request: DeserializeOwned;
fn handle_request(
&mut self,
ctx: &RpcCtx,
rpc: Self::Request,
) -> Result<ResponsePayload, RemoteError>;
#[allow(unused_variables)]
fn idle(&mut self, ctx: &RpcCtx, token: usize) {}
}
/// A helper type which shuts down the runloop if a panic occurs while
/// handling an RPC.
struct PanicGuard<'a, W: Write + 'static>(&'a RawPeer<W>);
impl<'a, W: Write + 'static> Drop for PanicGuard<'a, W> {
/// Implements the cleanup behavior when the guard is dropped.
///
/// This method is automatically called when the `PanicGuard` goes out of scope.
/// It checks if a panic is occurring and, if so, logs an error message and
/// disconnects the peer.
fn drop(&mut self) {
// - If no panic is occurring, this method does nothing.
// - If a panic is detected:
// 1. An error message is logged.
// 2. The `disconnect()` method is called on the peer.
if thread::panicking() {
error!("[RPC] panic guard hit, closing run loop");
self.0.disconnect();
}
}
}
/// A structure holding the state of a main loop for handling RPC's.
pub struct RpcLoop<W: Write + 'static> {
reader: MessageReader,
peer: RawPeer<W>,
}
impl<W: Write + Send> RpcLoop<W> {
/// Creates a new `RpcLoop` with the given output stream (which is used for
/// sending requests and notifications, as well as responses).
pub fn new(writer: W) -> Self {
let rpc_peer = RawPeer(Arc::new(RpcState::new(writer)));
RpcLoop {
reader: MessageReader::default(),
peer: rpc_peer,
}
}
/// Gets a reference to the peer.
pub fn get_raw_peer(&self) -> RawPeer<W> {
self.peer.clone()
}
/// Starts the event loop, reading lines from the reader until EOF or an error occurs.
///
/// Returns `Ok()` if EOF is reached, otherwise returns the underlying `ReadError`.
///
/// # Note:
/// The reader is provided via a closure to avoid needing `Send`. The main loop runs on a separate I/O thread that calls this closure at startup.
/// Calls to the handler occur on the caller's thread and maintain the order from the channel. Currently, there can only be one outstanding incoming request.
/// Starts and manages the main event loop for processing RPC messages.
///
/// This function is the core of the RPC system, handling incoming messages,
/// dispatching requests to the appropriate handler, and managing the overall
/// lifecycle of the RPC communication.
///
/// # Arguments
///
/// * `&mut self` - A mutable reference to the `RpcLoop` instance.
/// * `_plugin_name: &str` - The name of the plugin (currently unused in the function body).
/// * `buffer_read_fn: BufferReadFn` - A closure that returns a `BufRead` instance for reading input.
/// * `handler: &mut H` - A mutable reference to the handler implementing the `Handler` trait.
///
/// # Type Parameters
///
/// * `R: BufRead` - The type returned by `buffer_read_fn`, must implement `BufRead`.
/// * `BufferReadFn: Send + FnOnce() -> R` - The type of the closure that provides the input reader.
/// * `H: Handler` - The type of the handler, must implement the `Handler` trait.
///
/// # Returns
///
/// * `Result<(), ReadError>` - Returns `Ok(())` if the loop exits normally (EOF),
/// or an error if an unrecoverable error occurs.
///
/// # Behavior
///
/// 1. Creates a new `RpcCtx` with a clone of the `RawPeer`.
/// 2. Spawns a separate thread for reading input using `crossbeam_utils::thread::scope`.
/// 3. In the reading thread:
/// - Continuously reads and parses JSON messages from the input.
/// - Handles responses by calling `handle_response` on the peer.
/// - Puts other messages into the peer's queue using `put_rpc_object`.
/// 4. In the main thread:
/// - Retrieves messages using `next_read`.
/// - Processes requests by calling the handler's `handle_request` method.
/// - Sends responses back using the peer's `respond` method.
/// 5. Continues looping until an error occurs or the peer is disconnected.
pub fn mainloop<R, BufferReadFn, H>(
&mut self,
_plugin_name: &str,
buffer_read_fn: BufferReadFn,
handler: &mut H,
) -> Result<(), ReadError>
where
R: BufRead,
BufferReadFn: Send + FnOnce() -> R,
H: Handler,
{
// uses `crossbeam_utils::thread::scope` for thread management,
// which offers several advantages over `std::thread`:
// 1. Scoped Threads: Guarantees thread termination when the scope ends,
// preventing resource leaks.
// 2. Simplified Lifetime Management: Allows threads to borrow data from
// their parent stack frame, enabling more ergonomic code.
// 3. Improved Safety: Prevents threads from outliving the data they operate on,
// reducing risks of data races and use-after-free errors.
// 4. Efficiency: Potentially more efficient due to known thread lifetimes,
// leading to better resource management.
// 5. Error Propagation: Simplifies propagating errors from spawned threads
// back to the parent thread.
// 6. Consistency with Rust's Ownership Model: Aligns well with Rust's
// ownership and borrowing rules.
// 7. Automatic Thread Joining: No need for manual thread joining, reducing
// the risk of thread management errors.
let exit = crossbeam_utils::thread::scope(|scope| {
let peer = self.get_raw_peer();
peer.reset_needs_exit();
let ctx = RpcCtx {
peer: Arc::new(peer.clone()),
};
// 1. Spawn a new thread for reading data from a stream.
// 2. Continuously read data from the stream.
// 3. Parse the data as JSON.
// 4. Handle the JSON data as either a response or another type of JSON object.
// 5. Manage errors and connection status.
scope.spawn(move |_| {
let mut stream = buffer_read_fn();
loop {
if self.peer.needs_exit() {
trace!("read loop exit");
break;
}
let json = match self.reader.next(&mut stream) {
Ok(json) => json,
Err(err) => {
if self.peer.0.is_blocking() {
self.peer.disconnect();
}
self.peer.put_rpc_object(Err(err));
break;
},
};
if json.is_response() {
let request_id = json.get_id().unwrap();
match json.into_response() {
Ok(resp) => {
let resp = resp.map_err(SidecarError::from);
self.peer.handle_response(request_id, resp);
},
Err(msg) => {
error!("[RPC] failed to parse response: {}", msg);
self
.peer
.handle_response(request_id, Err(SidecarError::InvalidResponse));
},
}
} else {
self.peer.put_rpc_object(Ok(json));
}
}
});
// Main processing loop
loop {
// `PanicGuard` is a critical safety mechanism in the RPC system. It's designed to detect
// panics that occur during RPC request handling and ensure that the system shuts down
// gracefully, preventing resource leaks and maintaining system integrity.
//
let _guard = PanicGuard(&peer);
let read_result = next_read(&peer, &ctx);
let json = match read_result {
Ok(json) => json,
Err(err) => {
peer.disconnect();
return err;
},
};
match json.into_rpc::<H::Request>() {
Ok(Call::Request(id, cmd)) => {
// Handle request sent from the client. For example from python executable.
trace!("[RPC] received request: {}", id);
let result = handler.handle_request(&ctx, cmd);
peer.respond(result, id);
},
Ok(Call::InvalidRequest(id, err)) => {
trace!("[RPC] received invalid request: {}", id);
peer.respond(Err(err), id)
},
Err(err) => {
error!("[RPC] error parsing message: {:?}", err);
peer.disconnect();
return ReadError::UnknownRequest(err);
},
Ok(Call::Message(_msg)) => {
#[cfg(feature = "verbose")]
trace!("[RPC {}]: {}", _plugin_name, _msg);
},
}
}
})
.unwrap();
if exit.is_disconnect() {
Ok(())
} else {
Err(exit)
}
}
}
/// retrieves the next available read result from a peer, performing idle work if no result is
/// immediately available.
fn next_read<W>(peer: &RawPeer<W>, _ctx: &RpcCtx) -> Result<RpcObject, ReadError>
where
W: Write + Send,
{
loop {
// Continuously checks if there is a result available from the peer using
if let Some(result) = peer.try_get_rx() {
return result;
}
let time_to_next_timer = match peer.check_timers() {
Some(Ok(_token)) => continue,
Some(Err(duration)) => Some(duration),
None => None,
};
// Ensures the function does not block indefinitely by setting a maximum wait time
let idle_timeout = time_to_next_timer
.unwrap_or(MAX_IDLE_WAIT)
.min(MAX_IDLE_WAIT);
if let Some(result) = peer.get_rx_timeout(idle_timeout) {
return result;
}
}
}

View File

@ -0,0 +1,111 @@
use crate::core::parser::{Call, RequestId};
use crate::core::rpc_peer::{Response, ResponsePayload};
use serde::de::{DeserializeOwned, Error};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct RpcObject(pub Value);
impl RpcObject {
/// Returns the 'id' of the underlying object, if present.
pub fn get_id(&self) -> Option<RequestId> {
self.0.get("id").and_then(Value::as_u64)
}
/// Returns the 'method' field of the underlying object, if present.
pub fn get_method(&self) -> Option<&str> {
self.0.get("method").and_then(Value::as_str)
}
/// Returns `true` if this object looks like an RPC response;
/// that is, if it has an 'id' field and does _not_ have a 'method'
/// field.
pub fn is_response(&self) -> bool {
self.0.get("id").is_some() && self.0.get("method").is_none()
}
/// Converts a JSON-RPC response into a structured `Response` object.
///
/// This function validates and parses a JSON-RPC response, ensuring it contains the necessary fields,
/// and then transforms it into a structured `Response` object. The response must contain either a
/// "result" or an "error" field, but not both. If the response contains a "result" field, it may also
/// include streaming data, indicated by a nested "stream" field.
///
/// # Errors
///
/// This function will return an error if:
/// - The "id" field is missing.
/// - The response contains both "result" and "error" fields, or neither.
/// - The "stream" field within the "result" is missing "type" or "data" fields.
/// - The "stream" type is invalid (i.e., not "streaming" or "end").
///
/// # Returns
///
/// - `Ok(Ok(ResponsePayload::Json(result)))`: If the response contains a valid "result".
/// - `Ok(Ok(ResponsePayload::Streaming(data)))`: If the response contains streaming data of type "streaming".
/// - `Ok(Ok(ResponsePayload::StreamEnd(json!({}))))`: If the response contains streaming data of type "end".
/// - `Err(String)`: If any validation or parsing errors occur.
///.
pub fn into_response(mut self) -> Result<Response, String> {
// Ensure 'id' field is present
self
.get_id()
.ok_or_else(|| "Response requires 'id' field.".to_string())?;
// Ensure the response contains exactly one of 'result' or 'error'
let has_result = self.0.get("result").is_some();
let has_error = self.0.get("error").is_some();
if has_result == has_error {
return Err("RPC response must contain exactly one of 'error' or 'result' fields.".into());
}
// Handle the 'result' field if present
if let Some(mut result) = self.0.as_object_mut().and_then(|obj| obj.remove("result")) {
if let Some(mut stream) = result.as_object_mut().and_then(|obj| obj.remove("stream")) {
if let Some((has_more, data)) = stream.as_object_mut().and_then(|obj| {
let has_more = obj.remove("has_more")?.as_bool().unwrap_or(false);
let data = obj.remove("data")?;
Some((has_more, data))
}) {
return match has_more {
true => Ok(Ok(ResponsePayload::Streaming(data))),
false => Ok(Ok(ResponsePayload::StreamEnd(data))),
};
} else {
return Err("Stream response must contain 'type' and 'data' fields.".into());
}
}
Ok(Ok(ResponsePayload::Json(result)))
} else {
// Handle the 'error' field
let error = self.0.as_object_mut().unwrap().remove("error").unwrap();
Err(format!("Error handling response: {:?}", error))
}
}
/// Converts the underlying `Value` into either an RPC notification or request.
pub fn into_rpc<R>(self) -> Result<Call<R>, serde_json::Error>
where
R: DeserializeOwned,
{
let id = self.get_id();
match id {
Some(id) => match serde_json::from_value::<R>(self.0) {
Ok(resp) => Ok(Call::Request(id, resp)),
Err(err) => Ok(Call::InvalidRequest(id, err.into())),
},
None => match self.0.get("message").and_then(|value| value.as_str()) {
None => Err(serde_json::Error::missing_field("message")),
Some(s) => Ok(Call::Message(s.to_string().into())),
},
}
}
}
impl From<Value> for RpcObject {
fn from(v: Value) -> RpcObject {
RpcObject(v)
}
}

View File

@ -0,0 +1,500 @@
use crate::core::plugin::{Peer, PluginId};
use crate::core::rpc_object::RpcObject;
use crate::error::{ReadError, RemoteError, SidecarError};
use parking_lot::{Condvar, Mutex};
use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value as JsonValue};
use std::collections::{BTreeMap, BinaryHeap, VecDeque};
use std::fmt::Display;
use std::io::Write;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{mpsc, Arc};
use std::time::{Duration, Instant};
use std::{cmp, io};
use tokio_stream::Stream;
use tracing::{error, trace, warn};
pub struct PluginCommand<T> {
pub plugin_id: PluginId,
pub cmd: T,
}
impl<T: Serialize> Serialize for PluginCommand<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut v = serde_json::to_value(&self.cmd).map_err(ser::Error::custom)?;
v["params"]["plugin_id"] = json!(self.plugin_id);
v.serialize(serializer)
}
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for PluginCommand<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct PluginIdHelper {
plugin_id: PluginId,
}
let v = JsonValue::deserialize(deserializer)?;
let plugin_id = PluginIdHelper::deserialize(&v)
.map_err(de::Error::custom)?
.plugin_id;
let cmd = T::deserialize(v).map_err(de::Error::custom)?;
Ok(PluginCommand { plugin_id, cmd })
}
}
pub struct RpcState<W: Write> {
rx_queue: Mutex<VecDeque<Result<RpcObject, ReadError>>>,
rx_cvar: Condvar,
writer: Mutex<W>,
id: AtomicUsize,
pending: Mutex<BTreeMap<usize, ResponseHandler>>,
timers: Mutex<BinaryHeap<Timer>>,
needs_exit: AtomicBool,
is_blocking: AtomicBool,
}
impl<W: Write> RpcState<W> {
/// Creates a new `RawPeer` instance.
///
/// # Arguments
///
/// * `writer` - An object implementing the `Write` trait, used for sending messages.
///
/// # Returns
///
/// A new `RawPeer` instance wrapped in an `Arc`.
pub fn new(writer: W) -> Self {
RpcState {
rx_queue: Mutex::new(VecDeque::new()),
rx_cvar: Condvar::new(),
writer: Mutex::new(writer),
id: AtomicUsize::new(0),
pending: Mutex::new(BTreeMap::new()),
timers: Mutex::new(BinaryHeap::new()),
needs_exit: AtomicBool::new(false),
is_blocking: Default::default(),
}
}
pub fn is_blocking(&self) -> bool {
self.is_blocking.load(Ordering::Acquire)
}
}
pub struct RawPeer<W: Write + 'static>(pub(crate) Arc<RpcState<W>>);
impl<W: Write + Send + 'static> Peer for RawPeer<W> {
fn box_clone(&self) -> Arc<dyn Peer> {
Arc::new((*self).clone())
}
fn send_rpc_notification(&self, method: &str, params: &JsonValue) {
if let Err(e) = self.send(&json!({
"method": method,
"params": params,
})) {
error!(
"send error on send_rpc_notification method {}: {}",
method, e
);
}
}
fn stream_rpc_request(&self, method: &str, params: &JsonValue, f: CloneableCallback) {
self.send_rpc(method, params, ResponseHandler::StreamCallback(Arc::new(f)));
}
fn async_send_rpc_request(&self, method: &str, params: &JsonValue, f: Box<dyn OneShotCallback>) {
self.send_rpc(method, params, ResponseHandler::Callback(f));
}
fn send_rpc_request(&self, method: &str, params: &JsonValue) -> Result<JsonValue, SidecarError> {
let (tx, rx) = mpsc::channel();
self.0.is_blocking.store(true, Ordering::Release);
self.send_rpc(method, params, ResponseHandler::Chan(tx));
rx.recv().unwrap_or(Err(SidecarError::PeerDisconnect))
}
fn request_is_pending(&self) -> bool {
let queue = self.0.rx_queue.lock();
!queue.is_empty()
}
fn schedule_timer(&self, after: Instant, token: usize) {
self.0.timers.lock().push(Timer {
fire_after: after,
token,
});
}
}
impl<W: Write> RawPeer<W> {
/// Sends a JSON value to the peer.
///
/// # Arguments
///
/// * `json` - A reference to a `JsonValue` to be sent.
///
/// # Returns
///
/// A `Result` indicating success or an `io::Error` if the write operation fails.
///
/// # Notes
///
/// This function serializes the JSON value, appends a newline, and writes it to the underlying writer.
fn send(&self, json: &JsonValue) -> Result<(), io::Error> {
let mut s = serde_json::to_string(json).unwrap();
s.push('\n');
self.0.writer.lock().write_all(s.as_bytes())
}
/// Sends a response to a previous RPC request.
///
/// # Arguments
///
/// * `result` - The `Response` to be sent.
/// * `id` - The ID of the request being responded to.
///
/// # Notes
///
/// This function constructs a JSON response and sends it using the `send` method.
/// It handles both successful results and errors.
pub(crate) fn respond(&self, result: Response, id: u64) {
let mut response = json!({ "id": id });
match result {
Ok(result) => match result {
ResponsePayload::Json(value) => response["result"] = value,
ResponsePayload::Streaming(_) | ResponsePayload::StreamEnd(_) => {
error!("stream response not supported")
},
},
Err(error) => response["error"] = json!(error),
};
if let Err(e) = self.send(&response) {
error!("[RPC] error {} sending response to RPC {:?}", e, id);
}
}
/// Sends an RPC request.
///
/// # Arguments
///
/// * `method` - The name of the RPC method to be called.
/// * `params` - The parameters for the RPC call.
/// * `response_handler` - A `ResponseHandler` to handle the response.
///
/// # Notes
///
/// This function generates a unique ID for the request, stores the response handler,
/// and sends the RPC request. If sending fails, it immediately invokes the response handler with an error.
fn send_rpc(&self, method: &str, params: &JsonValue, response_handler: ResponseHandler) {
trace!("[RPC] call method: {} params: {:?}", method, params);
let id = self.0.id.fetch_add(1, Ordering::Relaxed);
{
let mut pending = self.0.pending.lock();
pending.insert(id, response_handler);
}
// Call the ResponseHandler if the send fails. Otherwise, the response will be
// called in handle_response.
if let Err(e) = self.send(&json!({
"id": id,
"method": method,
"params": params,
})) {
let mut pending = self.0.pending.lock();
if let Some(rh) = pending.remove(&id) {
rh.invoke(Err(SidecarError::Io(e)));
}
}
}
/// Processes an incoming response to an RPC request.
///
/// This function is responsible for handling responses received from the peer, matching them
/// to their corresponding requests, and invoking the appropriate callbacks. It supports both
/// one-time responses and streaming responses.
///
/// # Arguments
///
/// * `&self` - A reference to the `RawPeer` instance.
/// * `request_id: u64` - The unique identifier of the request to which this is a response.
/// * `resp: Result<ResponsePayload, SidecarError>` - The response payload or an error.
///
/// # Behavior
///
/// 1. Retrieves and removes the response handler for the given `request_id` from the pending requests.
/// 2. Determines if the response is part of a stream.
/// 3. For streaming responses:
/// - If it's not the end of the stream, re-inserts the stream callback for future messages.
/// - If it's the end of the stream, logs this information.
/// 4. Converts the response payload to JSON.
/// 5. Invokes the response handler with the JSON data or error.
///
/// # Concurrency
///
/// This function uses mutex locks to ensure thread-safe access to shared data structures.
/// It's designed to be called from multiple threads safely.
///
/// # Error Handling
///
/// - If no handler is found for the `request_id`, an error is logged.
/// - If a non-stream response payload is `None`, a warning is logged.
/// - Errors in the response are propagated to the response handler.
pub(crate) fn handle_response(
&self,
request_id: u64,
resp: Result<ResponsePayload, SidecarError>,
) {
let request_id = request_id as usize;
let handler = {
let mut pending = self.0.pending.lock();
pending.remove(&request_id)
};
let is_stream = resp.as_ref().map(|resp| resp.is_stream()).unwrap_or(false);
match handler {
Some(response_handler) => {
if is_stream {
let is_stream_end = resp
.as_ref()
.map(|resp| resp.is_stream_end())
.unwrap_or(false);
if !is_stream_end {
// when steam is not end, we need to put the stream callback back to pending in order to
// receive the next stream message.
if let Some(callback) = response_handler.get_stream_callback() {
let mut pending = self.0.pending.lock();
pending.insert(request_id, ResponseHandler::StreamCallback(callback));
}
} else {
trace!("[RPC] {} stream end", request_id);
}
}
let json = resp.map(|resp| resp.into_json());
match json {
Ok(Some(json)) => {
response_handler.invoke(Ok(json));
},
Ok(None) => {
if !is_stream {
warn!("[RPC] only stream response can be None");
}
},
Err(err) => {
response_handler.invoke(Err(err));
},
}
},
None => error!("[RPC] id {}'s handle not found", request_id),
}
}
/// Get a message from the receive queue if available.
pub(crate) fn try_get_rx(&self) -> Option<Result<RpcObject, ReadError>> {
let mut queue = self.0.rx_queue.lock();
queue.pop_front()
}
/// Get a message from the receive queue, waiting for at most `Duration`
/// and returning `None` if no message is available.
pub(crate) fn get_rx_timeout(&self, dur: Duration) -> Option<Result<RpcObject, ReadError>> {
let mut queue = self.0.rx_queue.lock();
let result = self.0.rx_cvar.wait_for(&mut queue, dur);
if result.timed_out() {
return None;
}
queue.pop_front()
}
/// Adds a message to the receive queue. The message should only
/// be `None` if the read thread is exiting.
pub(crate) fn put_rpc_object(&self, json: Result<RpcObject, ReadError>) {
let mut queue = self.0.rx_queue.lock();
queue.push_back(json);
self.0.rx_cvar.notify_one();
}
/// Checks the status of the most imminent timer.
///
/// # Returns
///
/// - `Some(Ok(usize))`: If the most imminent timer has expired, returns its token.
/// - `Some(Err(Duration))`: If the most imminent timer has not yet expired, returns the time until it expires.
/// - `None`: If no timers are registered.
pub(crate) fn check_timers(&self) -> Option<Result<usize, Duration>> {
let mut timers = self.0.timers.lock();
match timers.peek() {
None => return None,
Some(t) => {
let now = Instant::now();
if t.fire_after > now {
return Some(Err(t.fire_after - now));
}
},
}
Some(Ok(timers.pop().unwrap().token))
}
/// send disconnect error to pending requests.
pub(crate) fn disconnect(&self) {
trace!("[RPC] disconnecting peer");
let mut pending = self.0.pending.lock();
let ids = pending.keys().cloned().collect::<Vec<_>>();
for id in &ids {
let callback = pending.remove(id).unwrap();
callback.invoke(Err(SidecarError::PeerDisconnect));
}
self.0.needs_exit.store(true, Ordering::Relaxed);
}
/// Checks if the RPC system needs to exit.
pub(crate) fn needs_exit(&self) -> bool {
self.0.needs_exit.load(Ordering::Relaxed)
}
pub(crate) fn reset_needs_exit(&self) {
self.0.needs_exit.store(false, Ordering::SeqCst);
}
}
impl<W: Write> Clone for RawPeer<W> {
fn clone(&self) -> Self {
RawPeer(self.0.clone())
}
}
#[derive(Clone, Debug)]
pub enum ResponsePayload {
Json(JsonValue),
Streaming(JsonValue),
StreamEnd(JsonValue),
}
impl ResponsePayload {
pub fn empty_json() -> Self {
ResponsePayload::Json(json!({}))
}
pub fn is_stream(&self) -> bool {
matches!(
self,
ResponsePayload::Streaming(_) | ResponsePayload::StreamEnd(_)
)
}
pub fn is_stream_end(&self) -> bool {
matches!(self, ResponsePayload::StreamEnd(_))
}
pub fn into_json(self) -> Option<JsonValue> {
match self {
ResponsePayload::Json(v) => Some(v),
ResponsePayload::Streaming(v) => Some(v),
ResponsePayload::StreamEnd(_) => None,
}
}
}
impl Display for ResponsePayload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResponsePayload::Json(v) => write!(f, "{}", v),
ResponsePayload::Streaming(_) => write!(f, "stream start"),
ResponsePayload::StreamEnd(_) => write!(f, "stream end"),
}
}
}
pub type Response = Result<ResponsePayload, RemoteError>;
pub trait ResponseStream: Stream<Item = Result<JsonValue, SidecarError>> + Unpin + Send {}
impl<T> ResponseStream for T where T: Stream<Item = Result<JsonValue, SidecarError>> + Unpin + Send {}
enum ResponseHandler {
Chan(mpsc::Sender<Result<JsonValue, SidecarError>>),
Callback(Box<dyn OneShotCallback>),
StreamCallback(Arc<CloneableCallback>),
}
impl ResponseHandler {
pub fn get_stream_callback(&self) -> Option<Arc<CloneableCallback>> {
match self {
ResponseHandler::StreamCallback(cb) => Some(cb.clone()),
_ => None,
}
}
}
pub trait OneShotCallback: Send {
fn call(self: Box<Self>, result: Result<JsonValue, SidecarError>);
}
impl<F: Send + FnOnce(Result<JsonValue, SidecarError>)> OneShotCallback for F {
fn call(self: Box<Self>, result: Result<JsonValue, SidecarError>) {
(self)(result)
}
}
pub trait Callback: Send + Sync {
fn call(&self, result: Result<JsonValue, SidecarError>);
}
impl<F: Send + Sync + Fn(Result<JsonValue, SidecarError>)> Callback for F {
fn call(&self, result: Result<JsonValue, SidecarError>) {
(*self)(result)
}
}
#[derive(Clone)]
pub struct CloneableCallback {
callback: Arc<dyn Callback>,
}
impl CloneableCallback {
pub fn new<C: Callback + 'static>(callback: C) -> Self {
CloneableCallback {
callback: Arc::new(callback),
}
}
pub fn call(&self, result: Result<JsonValue, SidecarError>) {
self.callback.call(result)
}
}
impl ResponseHandler {
fn invoke(self, result: Result<JsonValue, SidecarError>) {
match self {
ResponseHandler::Chan(tx) => {
let _ = tx.send(result);
},
ResponseHandler::StreamCallback(cb) => {
cb.call(result);
},
ResponseHandler::Callback(f) => f.call(result),
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct Timer {
fire_after: Instant,
token: usize,
}
impl Ord for Timer {
fn cmp(&self, other: &Timer) -> cmp::Ordering {
other.fire_after.cmp(&self.fire_after)
}
}
impl PartialOrd for Timer {
fn partial_cmp(&self, other: &Timer) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}

View File

@ -0,0 +1,174 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value as JsonValue};
use std::{fmt, io};
/// The error type of `tauri-utils`.
#[derive(Debug, thiserror::Error)]
pub enum SidecarError {
/// An IO error occurred on the underlying communication channel.
#[error(transparent)]
Io(#[from] io::Error),
/// The peer returned an error.
#[error("Remote error: {0}")]
RemoteError(RemoteError),
/// The peer closed the connection.
#[error("Peer closed the connection.")]
PeerDisconnect,
/// The peer sent a response containing the id, but was malformed.
#[error("Invalid response.")]
InvalidResponse,
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
#[derive(Debug)]
pub enum ReadError {
/// An error occurred in the underlying stream
Io(io::Error),
/// The message was not valid JSON.
Json(serde_json::Error),
/// The message was not a JSON object.
NotObject(String),
/// The the method and params were not recognized by the handler.
UnknownRequest(serde_json::Error),
/// The peer closed the connection.
Disconnect,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum RemoteError {
/// The JSON was valid, but was not a correctly formed request.
///
/// This Error is used internally, and should not be returned by
/// clients.
#[error("Invalid request: {0:?}")]
InvalidRequest(Option<JsonValue>),
#[error("Invalid response: {0}")]
InvalidResponse(JsonValue),
#[error("Parse response: {0}")]
ParseResponse(JsonValue),
/// A custom error, defined by the client.
#[error("Custom error: {message}")]
Custom {
code: i64,
message: String,
data: Option<JsonValue>,
},
/// An error that cannot be represented by an error object.
///
/// This error is intended to accommodate clients that return arbitrary
/// error values. It should not be used for new errors.
#[error("Unknown error: {0}")]
Unknown(JsonValue),
}
impl ReadError {
/// Returns `true` iff this is the `ReadError::Disconnect` variant.
pub fn is_disconnect(&self) -> bool {
matches!(*self, ReadError::Disconnect)
}
}
impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ReadError::Io(ref err) => write!(f, "I/O Error: {:?}", err),
ReadError::Json(ref err) => write!(f, "JSON Error: {:?}", err),
ReadError::NotObject(s) => write!(f, "Expected JSON object, found: {}", s),
ReadError::UnknownRequest(ref err) => write!(f, "Unknown request: {:?}", err),
ReadError::Disconnect => write!(f, "Peer closed the connection."),
}
}
}
impl From<serde_json::Error> for ReadError {
fn from(err: serde_json::Error) -> ReadError {
ReadError::Json(err)
}
}
impl From<io::Error> for ReadError {
fn from(err: io::Error) -> ReadError {
ReadError::Io(err)
}
}
impl From<serde_json::Error> for RemoteError {
fn from(err: serde_json::Error) -> RemoteError {
RemoteError::InvalidRequest(Some(json!(err.to_string())))
}
}
impl From<RemoteError> for SidecarError {
fn from(err: RemoteError) -> SidecarError {
SidecarError::RemoteError(err)
}
}
#[derive(Deserialize, Serialize)]
struct ErrorHelper {
code: i64,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<JsonValue>,
}
impl<'de> Deserialize<'de> for RemoteError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v = JsonValue::deserialize(deserializer)?;
let resp = match ErrorHelper::deserialize(&v) {
Ok(resp) => resp,
Err(_) => return Ok(RemoteError::Unknown(v)),
};
Ok(match resp.code {
-32600 => RemoteError::InvalidRequest(resp.data),
_ => RemoteError::Custom {
code: resp.code,
message: resp.message,
data: resp.data,
},
})
}
}
impl Serialize for RemoteError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (code, message, data) = match self {
RemoteError::InvalidRequest(ref d) => (-32600, "Invalid request".to_string(), d.clone()),
RemoteError::Custom {
code,
ref message,
ref data,
} => (*code, message.clone(), data.clone()),
RemoteError::Unknown(_) => {
panic!("The 'Unknown' error variant is not intended for client use.")
},
RemoteError::InvalidResponse(resp) => (
-1,
"Invalid response".to_string(),
Some(json!(resp.to_string())),
),
RemoteError::ParseResponse(resp) => (
-1,
"Invalid response".to_string(),
Some(json!(resp.to_string())),
),
};
let err = ErrorHelper {
code,
message,
data,
};
err.serialize(serializer)
}
}

View File

@ -0,0 +1,3 @@
pub mod core;
pub mod error;
pub mod manager;

View File

@ -0,0 +1,201 @@
use crate::core::parser::ResponseParser;
use crate::core::plugin::{start_plugin_process, Plugin, PluginId, PluginInfo, RpcCtx};
use crate::core::rpc_loop::Handler;
use crate::core::rpc_peer::{PluginCommand, ResponsePayload};
use crate::error::{ReadError, RemoteError, SidecarError};
use anyhow::anyhow;
use lib_infra::util::{get_operating_system, OperatingSystem};
use parking_lot::Mutex;
use serde_json::Value;
use std::io;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Weak};
use tracing::{error, info, instrument, trace, warn};
pub struct SidecarManager {
state: Arc<Mutex<SidecarState>>,
plugin_id_counter: Arc<AtomicI64>,
operating_system: OperatingSystem,
}
impl Default for SidecarManager {
fn default() -> Self {
Self::new()
}
}
impl SidecarManager {
pub fn new() -> Self {
SidecarManager {
state: Arc::new(Mutex::new(SidecarState {
plugins: Vec::new(),
})),
plugin_id_counter: Arc::new(Default::default()),
operating_system: get_operating_system(),
}
}
pub async fn create_plugin(&self, plugin_info: PluginInfo) -> Result<PluginId, SidecarError> {
if self.operating_system.is_not_desktop() {
return Err(SidecarError::Internal(anyhow!(
"plugin not supported on this platform"
)));
}
let plugin_id = PluginId::from(self.plugin_id_counter.fetch_add(1, Ordering::SeqCst));
let weak_state = WeakSidecarState(Arc::downgrade(&self.state));
start_plugin_process(plugin_info, plugin_id, weak_state).await?;
Ok(plugin_id)
}
pub async fn get_plugin(&self, plugin_id: PluginId) -> Result<Weak<Plugin>, SidecarError> {
let state = self.state.lock();
let plugin = state
.plugins
.iter()
.find(|p| p.id == plugin_id)
.ok_or(anyhow!("plugin not found"))?;
Ok(Arc::downgrade(plugin))
}
#[instrument(skip(self), err)]
pub async fn remove_plugin(&self, id: PluginId) -> Result<(), SidecarError> {
if self.operating_system.is_not_desktop() {
return Err(SidecarError::Internal(anyhow!(
"plugin not supported on this platform"
)));
}
info!("[RPC] removing plugin {:?}", id);
self.state.lock().plugin_disconnect(id, Ok(()));
Ok(())
}
pub fn init_plugin(&self, id: PluginId, init_params: Value) -> Result<Arc<Plugin>, SidecarError> {
if self.operating_system.is_not_desktop() {
return Err(SidecarError::Internal(anyhow!(
"plugin not supported on this platform"
)));
}
let state = self.state.lock();
let plugin = state
.plugins
.iter()
.find(|p| p.id == id)
.ok_or(anyhow!("plugin not found"))?;
plugin.initialize(init_params)?;
Ok(plugin.clone())
}
pub fn send_request<P: ResponseParser>(
&self,
id: PluginId,
method: &str,
request: Value,
) -> Result<P::ValueType, SidecarError> {
let state = self.state.lock();
let plugin = state
.plugins
.iter()
.find(|p| p.id == id)
.ok_or(anyhow!("plugin not found"))?;
let resp = plugin.request(method, &request)?;
let value = P::parse_json(resp)?;
Ok(value)
}
pub async fn async_send_request<P: ResponseParser>(
&self,
id: PluginId,
method: &str,
request: Value,
) -> Result<P::ValueType, SidecarError> {
let plugin = self
.state
.lock()
.plugins
.iter()
.find(|p| p.id == id)
.ok_or(anyhow!("plugin not found"))
.cloned()?;
let value = plugin.async_request::<P>(method, &request).await?;
Ok(value)
}
}
pub struct SidecarState {
plugins: Vec<Arc<Plugin>>,
}
impl SidecarState {
pub fn plugin_connect(&mut self, plugin: Result<Plugin, io::Error>) {
match plugin {
Ok(plugin) => {
info!("[RPC] {} connected", plugin);
self.plugins.push(Arc::new(plugin));
},
Err(err) => {
warn!("plugin failed to connect: {:?}", err);
},
}
}
pub fn plugin_disconnect(
&mut self,
id: PluginId,
error: Result<(), ReadError>,
) -> Option<Arc<Plugin>> {
if let Err(err) = error {
error!("[RPC] plugin {:?} exited with result {:?}", id, err)
}
let running_idx = self.plugins.iter().position(|p| p.id == id);
match running_idx {
Some(idx) => {
let plugin = self.plugins.remove(idx);
plugin.shutdown();
Some(plugin)
},
None => {
warn!("[RPC] plugin {:?} not found", id);
None
},
}
}
}
#[derive(Clone)]
pub struct WeakSidecarState(Weak<Mutex<SidecarState>>);
impl WeakSidecarState {
pub fn upgrade(&self) -> Option<Arc<Mutex<SidecarState>>> {
self.0.upgrade()
}
pub fn plugin_connect(&self, plugin: Result<Plugin, io::Error>) {
if let Some(state) = self.upgrade() {
state.lock().plugin_connect(plugin)
}
}
pub fn plugin_exit(&self, plugin: PluginId, error: Result<(), ReadError>) {
if let Some(core) = self.upgrade() {
core.lock().plugin_disconnect(plugin, error);
}
}
}
impl Handler for WeakSidecarState {
type Request = PluginCommand<String>;
fn handle_request(
&mut self,
_ctx: &RpcCtx,
rpc: Self::Request,
) -> Result<ResponsePayload, RemoteError> {
trace!("handling request: {:?}", rpc.cmd);
Ok(ResponsePayload::empty_json())
}
}

View File

@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@ -0,0 +1,13 @@
-- Your SQL goes here
ALTER TABLE chat_table ADD COLUMN local_model_path TEXT NOT NULL DEFAULT '';
ALTER TABLE chat_table ADD COLUMN local_model_name TEXT NOT NULL DEFAULT '';
ALTER TABLE chat_table ADD COLUMN local_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE chat_table ADD COLUMN sync_to_cloud BOOLEAN NOT NULL DEFAULT TRUE;
CREATE TABLE chat_local_setting_table
(
chat_id TEXT PRIMARY KEY NOT NULL,
local_model_path TEXT NOT NULL,
local_model_name TEXT NOT NULL DEFAULT ''
);

View File

@ -11,13 +11,13 @@ use crate::sqlite_impl::{Database, PoolConfig};
const DB_NAME: &str = "cache.db";
/// [StorePreferences] uses a sqlite database to store key value pairs.
/// [KVStorePreferences] uses a sqlite database to store key value pairs.
/// Most of the time, it used to storage AppFlowy configuration.
#[derive(Clone)]
pub struct StorePreferences {
pub struct KVStorePreferences {
database: Option<Database>,
}
impl StorePreferences {
impl KVStorePreferences {
#[tracing::instrument(level = "trace", err)]
pub fn new(root: &str) -> Result<Self, anyhow::Error> {
if !Path::new(root).exists() {
@ -138,7 +138,7 @@ mod tests {
use serde::{Deserialize, Serialize};
use tempfile::TempDir;
use crate::kv::StorePreferences;
use crate::kv::KVStorePreferences;
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
struct Person {
@ -150,7 +150,7 @@ mod tests {
fn kv_store_test() {
let tempdir = TempDir::new().unwrap();
let path = tempdir.into_path();
let store = StorePreferences::new(path.to_str().unwrap()).unwrap();
let store = KVStorePreferences::new(path.to_str().unwrap()).unwrap();
store.set_str("1", "hello".to_string());
assert_eq!(store.get_str("1").unwrap(), "hello");

View File

@ -1,5 +1,13 @@
// @generated automatically by Diesel CLI.
diesel::table! {
chat_local_setting_table (chat_id) {
chat_id -> Text,
local_model_path -> Text,
local_model_name -> Text,
}
}
diesel::table! {
chat_message_table (message_id) {
message_id -> BigInt,
@ -17,6 +25,10 @@ diesel::table! {
chat_id -> Text,
created_at -> BigInt,
name -> Text,
local_model_path -> Text,
local_model_name -> Text,
local_enabled -> Bool,
sync_to_cloud -> Bool,
}
}
@ -103,6 +115,7 @@ diesel::table! {
}
diesel::allow_tables_to_appear_in_same_query!(
chat_local_setting_table,
chat_message_table,
chat_table,
collab_snapshot,

View File

@ -242,6 +242,7 @@ impl Display for UploadTask {
}
}
}
impl Eq for UploadTask {}
impl PartialEq for UploadTask {

View File

@ -1,5 +1,5 @@
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user_pub::cloud::UserCloudConfig;
use flowy_user_pub::entities::*;
use lib_dispatch::prelude::*;
@ -25,8 +25,8 @@ fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc
}
fn upgrade_store_preferences(
store: AFPluginState<Weak<StorePreferences>>,
) -> FlowyResult<Arc<StorePreferences>> {
store: AFPluginState<Weak<KVStorePreferences>>,
) -> FlowyResult<Arc<KVStorePreferences>> {
let store = store
.upgrade()
.ok_or(FlowyError::internal().with_context("The store preferences is already drop"))?;
@ -151,7 +151,7 @@ const APPEARANCE_SETTING_CACHE_KEY: &str = "appearance_settings";
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn set_appearance_setting(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
data: AFPluginData<AppearanceSettingsPB>,
) -> Result<(), FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
@ -165,7 +165,7 @@ pub async fn set_appearance_setting(
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_appearance_setting(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
) -> DataResult<AppearanceSettingsPB, FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
match store_preferences.get_str(APPEARANCE_SETTING_CACHE_KEY) {
@ -187,7 +187,7 @@ const DATE_TIME_SETTINGS_CACHE_KEY: &str = "date_time_settings";
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn set_date_time_settings(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
data: AFPluginData<DateTimeSettingsPB>,
) -> Result<(), FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
@ -202,7 +202,7 @@ pub async fn set_date_time_settings(
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_date_time_settings(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
) -> DataResult<DateTimeSettingsPB, FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
match store_preferences.get_str(DATE_TIME_SETTINGS_CACHE_KEY) {
@ -227,7 +227,7 @@ const NOTIFICATION_SETTINGS_CACHE_KEY: &str = "notification_settings";
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn set_notification_settings(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
data: AFPluginData<NotificationSettingsPB>,
) -> Result<(), FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
@ -238,7 +238,7 @@ pub async fn set_notification_settings(
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_notification_settings(
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
) -> DataResult<NotificationSettingsPB, FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
match store_preferences.get_str(NOTIFICATION_SETTINGS_CACHE_KEY) {
@ -348,7 +348,7 @@ pub async fn sign_in_with_provider_handler(
pub async fn set_encrypt_secret_handler(
manager: AFPluginState<Weak<UserManager>>,
data: AFPluginData<UserSecretPB>,
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let store_preferences = upgrade_store_preferences(store_preferences)?;
@ -408,7 +408,7 @@ pub async fn check_encrypt_secret_handler(
pub async fn set_cloud_config_handler(
manager: AFPluginState<Weak<UserManager>>,
data: AFPluginData<UpdateCloudConfigPB>,
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let session = manager.get_session()?;
@ -468,7 +468,7 @@ pub async fn set_cloud_config_handler(
#[tracing::instrument(level = "info", skip_all, err)]
pub async fn get_cloud_config_handler(
manager: AFPluginState<Weak<UserManager>>,
store_preferences: AFPluginState<Weak<StorePreferences>>,
store_preferences: AFPluginState<Weak<KVStorePreferences>>,
) -> DataResult<CloudSettingPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let session = manager.get_session()?;

View File

@ -1,4 +1,4 @@
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user_pub::session::Session;
use serde_json::{json, Value};
use std::sync::Arc;
@ -8,7 +8,7 @@ const MIGRATION_USER_NO_USER_UUID: &str = "migration_user_no_user_uuid";
pub fn migrate_session_with_user_uuid(
session_cache_key: &str,
store_preferences: &Arc<StorePreferences>,
store_preferences: &Arc<KVStorePreferences>,
) -> Option<Session> {
if !store_preferences.get_bool(MIGRATION_USER_NO_USER_UUID)
&& store_preferences

View File

@ -5,7 +5,7 @@ use crate::services::sqlite_sql::user_sql::vacuum_database;
use collab_integrate::CollabKVDB;
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_sqlite::DBConnection;
use flowy_user_pub::entities::UserWorkspace;
use flowy_user_pub::session::Session;
@ -19,12 +19,12 @@ pub struct AuthenticateUser {
pub user_config: UserConfig,
pub(crate) database: Arc<UserDB>,
pub(crate) user_paths: UserPaths,
store_preferences: Arc<StorePreferences>,
store_preferences: Arc<KVStorePreferences>,
session: Arc<parking_lot::RwLock<Option<Session>>>,
}
impl AuthenticateUser {
pub fn new(user_config: UserConfig, store_preferences: Arc<StorePreferences>) -> Self {
pub fn new(user_config: UserConfig, store_preferences: Arc<KVStorePreferences>) -> Self {
let user_paths = UserPaths::new(user_config.storage_path.clone());
let database = Arc::new(UserDB::new(user_paths.clone()));
let session = Arc::new(parking_lot::RwLock::new(None));

View File

@ -2,12 +2,12 @@ use std::sync::Arc;
use flowy_encrypt::generate_encryption_secret;
use flowy_error::FlowyResult;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user_pub::cloud::UserCloudConfig;
const CLOUD_CONFIG_KEY: &str = "af_user_cloud_config";
fn generate_cloud_config(uid: i64, store_preference: &Arc<StorePreferences>) -> UserCloudConfig {
fn generate_cloud_config(uid: i64, store_preference: &Arc<KVStorePreferences>) -> UserCloudConfig {
let config = UserCloudConfig::new(generate_encryption_secret());
let key = cache_key_for_cloud_config(uid);
store_preference.set_object(&key, config.clone()).unwrap();
@ -16,7 +16,7 @@ fn generate_cloud_config(uid: i64, store_preference: &Arc<StorePreferences>) ->
pub fn save_cloud_config(
uid: i64,
store_preference: &Arc<StorePreferences>,
store_preference: &Arc<KVStorePreferences>,
config: UserCloudConfig,
) -> FlowyResult<()> {
tracing::info!("save user:{} cloud config: {}", uid, config);
@ -31,7 +31,7 @@ fn cache_key_for_cloud_config(uid: i64) -> String {
pub fn get_cloud_config(
uid: i64,
store_preference: &Arc<StorePreferences>,
store_preference: &Arc<KVStorePreferences>,
) -> Option<UserCloudConfig> {
let key = cache_key_for_cloud_config(uid);
store_preference.get_object::<UserCloudConfig>(&key)
@ -39,7 +39,7 @@ pub fn get_cloud_config(
pub fn get_or_create_cloud_config(
uid: i64,
store_preferences: &Arc<StorePreferences>,
store_preferences: &Arc<KVStorePreferences>,
) -> UserCloudConfig {
let key = cache_key_for_cloud_config(uid);
store_preferences
@ -47,7 +47,7 @@ pub fn get_or_create_cloud_config(
.unwrap_or_else(|| generate_cloud_config(uid, store_preferences))
}
pub fn get_encrypt_secret(uid: i64, store_preference: &Arc<StorePreferences>) -> Option<String> {
pub fn get_encrypt_secret(uid: i64, store_preference: &Arc<KVStorePreferences>) -> Option<String> {
let key = cache_key_for_cloud_config(uid);
store_preference
.get_object::<UserCloudConfig>(&key)

View File

@ -26,7 +26,7 @@ use flowy_error::FlowyError;
use flowy_folder_pub::cloud::gen_view_id;
use flowy_folder_pub::entities::{AppFlowyData, ImportData};
use flowy_folder_pub::folder_builder::{ParentChildViews, ViewBuilder};
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_user_pub::cloud::{UserCloudService, UserCollabParams};
use flowy_user_pub::entities::{user_awareness_object_id, Authenticator};
use flowy_user_pub::session::Session;
@ -62,7 +62,7 @@ pub(crate) fn prepare_import(path: &str) -> anyhow::Result<ImportedFolder> {
return Err(anyhow!("The path: {} is not exist", path));
}
let user_paths = UserPaths::new(path.to_string());
let other_store_preferences = Arc::new(StorePreferences::new(path)?);
let other_store_preferences = Arc::new(KVStorePreferences::new(path)?);
migrate_session_with_user_uuid("appflowy_session_cache", &other_store_preferences);
let imported_session = other_store_preferences
.get_object::<Session>("appflowy_session_cache")

View File

@ -4,7 +4,7 @@ use collab_user::core::MutexUserAwareness;
use flowy_error::{internal_error, ErrorCode, FlowyResult};
use flowy_server_pub::AuthenticatorType;
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::kv::KVStorePreferences;
use flowy_sqlite::schema::user_table;
use flowy_sqlite::ConnectionPool;
use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
@ -48,7 +48,7 @@ use super::manager_user_workspace::save_user_workspace;
pub struct UserManager {
pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
pub(crate) store_preferences: Arc<StorePreferences>,
pub(crate) store_preferences: Arc<KVStorePreferences>,
pub(crate) user_awareness: Arc<Mutex<Option<MutexUserAwareness>>>,
pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>,
pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>,
@ -63,7 +63,7 @@ pub struct UserManager {
impl UserManager {
pub fn new(
cloud_services: Arc<dyn UserCloudServiceProvider>,
store_preferences: Arc<StorePreferences>,
store_preferences: Arc<KVStorePreferences>,
collab_builder: Weak<AppFlowyCollabBuilder>,
authenticate_user: Arc<AuthenticateUser>,
user_workspace_service: Arc<dyn UserWorkspaceService>,
@ -110,7 +110,7 @@ impl UserManager {
}
}
pub fn get_store_preferences(&self) -> Weak<StorePreferences> {
pub fn get_store_preferences(&self) -> Weak<KVStorePreferences> {
Arc::downgrade(&self.store_preferences)
}

View File

@ -23,6 +23,7 @@ tracing.workspace = true
atomic_refcell = "0.1"
allo-isolate = { version = "^0.1", features = ["catch-unwind"], optional = true }
futures = "0.3.30"
cfg-if = "1.0.0"
[dev-dependencies]
rand = "0.8.5"

View File

@ -23,5 +23,6 @@ if_wasm! {
pub mod isolate_stream;
pub mod priority_task;
pub mod ref_map;
pub mod stream_util;
pub mod util;
pub mod validator_fn;

View File

@ -0,0 +1,21 @@
use futures_core::Stream;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::sync::mpsc;
use tokio::sync::mpsc::{Receiver, Sender};
struct BoundedStream<T> {
recv: Receiver<T>,
}
impl<T> Stream for BoundedStream<T> {
type Item = T;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<T>> {
Pin::into_inner(self).recv.poll_recv(cx)
}
}
pub fn mpsc_channel_stream<T: Unpin>(size: usize) -> (Sender<T>, impl Stream<Item = T>) {
let (tx, rx) = mpsc::channel(size);
let stream = BoundedStream { recv: rx };
(tx, stream)
}

View File

@ -67,9 +67,8 @@ pub fn md5<T: AsRef<[u8]>>(data: T) -> String {
let md5 = format!("{:x}", md5::compute(data));
md5
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Platform {
pub enum OperatingSystem {
Unknown,
Windows,
Linux,
@ -78,33 +77,62 @@ pub enum Platform {
Android,
}
impl Platform {
impl OperatingSystem {
pub fn is_not_ios(&self) -> bool {
!matches!(self, Platform::IOS)
!matches!(self, OperatingSystem::IOS)
}
pub fn is_desktop(&self) -> bool {
matches!(
self,
OperatingSystem::Windows | OperatingSystem::Linux | OperatingSystem::MacOS
)
}
pub fn is_not_desktop(&self) -> bool {
!self.is_desktop()
}
}
impl From<String> for Platform {
impl From<String> for OperatingSystem {
fn from(s: String) -> Self {
Platform::from(s.as_str())
OperatingSystem::from(s.as_str())
}
}
impl From<&String> for Platform {
impl From<&String> for OperatingSystem {
fn from(s: &String) -> Self {
Platform::from(s.as_str())
OperatingSystem::from(s.as_str())
}
}
impl From<&str> for Platform {
impl From<&str> for OperatingSystem {
fn from(s: &str) -> Self {
match s {
"windows" => Platform::Windows,
"linux" => Platform::Linux,
"macos" => Platform::MacOS,
"ios" => Platform::IOS,
"android" => Platform::Android,
_ => Platform::Unknown,
"windows" => OperatingSystem::Windows,
"linux" => OperatingSystem::Linux,
"macos" => OperatingSystem::MacOS,
"ios" => OperatingSystem::IOS,
"android" => OperatingSystem::Android,
_ => OperatingSystem::Unknown,
}
}
}
pub fn get_operating_system() -> OperatingSystem {
cfg_if::cfg_if! {
if #[cfg(target_os = "android")] {
OperatingSystem::Android
} else if #[cfg(target_os = "ios")] {
OperatingSystem::IOS
} else if #[cfg(target_os = "macos")] {
OperatingSystem::MacOS
} else if #[cfg(target_os = "windows")] {
OperatingSystem::Windows
} else if #[cfg(target_os = "linux")] {
OperatingSystem::Linux
} else {
OperatingSystem::Unknown
}
}
}

View File

@ -4,7 +4,7 @@ use std::sync::{Arc, RwLock};
use chrono::Local;
use lazy_static::lazy_static;
use lib_infra::util::Platform;
use lib_infra::util::OperatingSystem;
use tracing::subscriber::set_global_default;
use tracing_appender::rolling::Rotation;
use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender};
@ -29,7 +29,7 @@ pub struct Builder {
env_filter: String,
file_appender: RollingFileAppender,
#[allow(dead_code)]
platform: Platform,
platform: OperatingSystem,
stream_log_sender: Option<Arc<dyn StreamLogSender>>,
}
@ -37,7 +37,7 @@ impl Builder {
pub fn new(
name: &str,
directory: &str,
platform: &Platform,
platform: &OperatingSystem,
stream_log_sender: Option<Arc<dyn StreamLogSender>>,
) -> Self {
let file_appender = RollingFileAppender::builder()