feat: AI chat (#5383)

* chore: ai type

* chore: use patch to fix version issue

* chore: update

* chore: update

* chore: integrate client api

* chore: add schema

* chore: setup event

* chore: add event test

* chore: add test

* chore: update test

* chore: load chat message

* chore: load chat message

* chore: chat ui

* chore: disable create chat

* chore: update client api

* chore: disable chat

* chore: ui theme

* chore: ui theme

* chore: copy message

* chore: fix test

* chore: show error

* chore: update bloc

* chore: update test

* chore: lint

* chore: icon

* chore: hover

* chore: show unsupported page

* chore: adjust mobile ui

* chore: adjust view title bar

* chore: return related question

* chore: error page

* chore: error page

* chore: code format

* chore: prompt

* chore: fix test

* chore: ui adjust

* chore: disable create chat

* chore: add loading page

* chore: fix test

* chore: disable chat action

* chore: add maximum text limit
This commit is contained in:
Nathan.fooo
2024-06-03 14:27:28 +08:00
committed by GitHub
parent 4d42c9ea68
commit aec7bc847e
114 changed files with 5473 additions and 282 deletions

View File

@ -31,6 +31,8 @@ diesel.workspace = true
uuid.workspace = true
flowy-storage = { workspace = true }
client-api.workspace = true
flowy-chat = { workspace = true }
flowy-chat-pub = { workspace = true }
tracing.workspace = true
futures-core = { version = "0.3", default-features = false }
@ -61,6 +63,7 @@ dart = [
"flowy-search/dart",
"flowy-folder/dart",
"flowy-database2/dart",
"flowy-chat/dart",
]
ts = [
"flowy-user/tauri_ts",
@ -68,6 +71,7 @@ ts = [
"flowy-search/tauri_ts",
"flowy-database2/ts",
"flowy-config/tauri_ts",
"flowy-chat/tauri_ts",
]
openssl_vendored = ["flowy-sqlite/openssl_vendored"]

View File

@ -0,0 +1,47 @@
use flowy_chat::manager::{ChatManager, ChatUserService};
use flowy_chat_pub::cloud::ChatCloudService;
use flowy_error::FlowyError;
use flowy_sqlite::DBConnection;
use flowy_user::services::authenticate_user::AuthenticateUser;
use std::sync::{Arc, Weak};
pub struct ChatDepsResolver;
impl ChatDepsResolver {
pub fn resolve(
authenticate_user: Weak<AuthenticateUser>,
cloud_service: Arc<dyn ChatCloudService>,
) -> Arc<ChatManager> {
let user_service = ChatUserServiceImpl(authenticate_user);
Arc::new(ChatManager::new(cloud_service, user_service))
}
}
struct ChatUserServiceImpl(Weak<AuthenticateUser>);
impl ChatUserServiceImpl {
fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> {
let user = self
.0
.upgrade()
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?;
Ok(user)
}
}
impl ChatUserService for ChatUserServiceImpl {
fn user_id(&self) -> Result<i64, FlowyError> {
self.upgrade_user()?.user_id()
}
fn device_id(&self) -> Result<String, FlowyError> {
self.upgrade_user()?.device_id()
}
fn workspace_id(&self) -> Result<String, FlowyError> {
self.upgrade_user()?.workspace_id()
}
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> {
self.upgrade_user()?.get_sqlite_connection(uid)
}
}

View File

@ -1,6 +1,7 @@
use bytes::Bytes;
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::CollabKVDB;
use flowy_chat::manager::ChatManager;
use flowy_database2::entities::DatabaseLayoutPB;
use flowy_database2::services::share::csv::CSVFormat;
use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid};
@ -9,10 +10,11 @@ use flowy_document::entities::DocumentDataPB;
use flowy_document::manager::DocumentManager;
use flowy_document::parser::json::parser::JsonToDocumentParser;
use flowy_error::FlowyError;
use flowy_folder::entities::ViewLayoutPB;
use flowy_folder::manager::{FolderManager, FolderUser};
use flowy_folder::share::ImportType;
use flowy_folder::view_operation::{FolderOperationHandler, FolderOperationHandlers, View};
use flowy_folder::view_operation::{
FolderOperationHandler, FolderOperationHandlers, View, ViewData,
};
use flowy_folder::ViewLayout;
use flowy_folder_pub::folder_builder::NestedViewBuilder;
use flowy_search::folder::indexer::FolderIndexManagerImpl;
@ -35,12 +37,17 @@ impl FolderDepsResolver {
collab_builder: Arc<AppFlowyCollabBuilder>,
server_provider: Arc<ServerProvider>,
folder_indexer: Arc<FolderIndexManagerImpl>,
chat_manager: &Arc<ChatManager>,
) -> Arc<FolderManager> {
let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl {
authenticate_user: authenticate_user.clone(),
});
let handlers = folder_operation_handlers(document_manager.clone(), database_manager.clone());
let handlers = folder_operation_handlers(
document_manager.clone(),
database_manager.clone(),
chat_manager.clone(),
);
Arc::new(
FolderManager::new(
user.clone(),
@ -58,6 +65,7 @@ impl FolderDepsResolver {
fn folder_operation_handlers(
document_manager: Arc<DocumentManager>,
database_manager: Arc<DatabaseManager>,
chat_manager: Arc<ChatManager>,
) -> FolderOperationHandlers {
let mut map: HashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>> = HashMap::new();
@ -65,9 +73,11 @@ fn folder_operation_handlers(
map.insert(ViewLayout::Document, document_folder_operation);
let database_folder_operation = Arc::new(DatabaseFolderOperation(database_manager));
let chat_folder_operation = Arc::new(ChatFolderOperation(chat_manager));
map.insert(ViewLayout::Board, database_folder_operation.clone());
map.insert(ViewLayout::Grid, database_folder_operation.clone());
map.insert(ViewLayout::Calendar, database_folder_operation);
map.insert(ViewLayout::Chat, chat_folder_operation);
Arc::new(map)
}
@ -315,7 +325,15 @@ impl FolderOperationHandler for DatabaseFolderOperation {
},
Some(params) => {
let database_manager = self.0.clone();
let layout = layout_type_from_view_layout(layout.into());
let layout = match layout {
ViewLayout::Board => DatabaseLayoutPB::Board,
ViewLayout::Calendar => DatabaseLayoutPB::Calendar,
ViewLayout::Grid => DatabaseLayoutPB::Grid,
ViewLayout::Document | ViewLayout::Chat => {
return FutureResult::new(async move { Err(FlowyError::not_support()) });
},
};
let name = name.to_string();
let database_view_id = view_id.to_string();
@ -351,6 +369,10 @@ impl FolderOperationHandler for DatabaseFolderOperation {
Err(FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)))
});
},
ViewLayout::Chat => {
// TODO(nathan): AI
todo!("AI")
},
};
FutureResult::new(async move {
let result = database_manager.create_database_with_params(data).await;
@ -413,7 +435,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
fn did_update_view(&self, old: &View, new: &View) -> FutureResult<(), FlowyError> {
let database_layout = match new.layout {
ViewLayout::Document => {
ViewLayout::Document | ViewLayout::Chat => {
return FutureResult::new(async {
Err(FlowyError::internal().with_context("Can't handle document layout type"))
});
@ -450,11 +472,83 @@ impl CreateDatabaseExtParams {
}
}
pub fn layout_type_from_view_layout(layout: ViewLayoutPB) -> DatabaseLayoutPB {
match layout {
ViewLayoutPB::Grid => DatabaseLayoutPB::Grid,
ViewLayoutPB::Board => DatabaseLayoutPB::Board,
ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar,
ViewLayoutPB::Document => DatabaseLayoutPB::Grid,
struct ChatFolderOperation(Arc<ChatManager>);
impl FolderOperationHandler for ChatFolderOperation {
fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.open_chat(&view_id).await?;
Ok(())
})
}
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.close_chat(&view_id).await?;
Ok(())
})
}
fn delete_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.delete_chat(&view_id).await?;
Ok(())
})
}
fn duplicate_view(&self, _view_id: &str) -> FutureResult<ViewData, FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn create_view_with_view_data(
&self,
_user_id: i64,
_view_id: &str,
_name: &str,
_data: Vec<u8>,
_layout: ViewLayout,
_meta: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn create_built_in_view(
&self,
user_id: i64,
view_id: &str,
_name: &str,
_layout: ViewLayout,
) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.create_chat(&user_id, &view_id).await?;
Ok(())
})
}
fn import_from_bytes(
&self,
_uid: i64,
_view_id: &str,
_name: &str,
_import_type: ImportType,
_bytes: Vec<u8>,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn import_from_file_path(
&self,
_view_id: &str,
_name: &str,
_path: String,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
}

View File

@ -1,3 +1,4 @@
pub use chat_deps::*;
pub use collab_deps::*;
pub use database_deps::*;
pub use document_deps::*;
@ -9,6 +10,7 @@ mod collab_deps;
mod document_deps;
mod folder_deps;
mod chat_deps;
mod database_deps;
mod search_deps;
mod user_deps;

View File

@ -52,6 +52,7 @@ pub fn create_log_filter(level: String, with_crates: Vec<String>, platform: Plat
filters.push(format!("flowy_notification={}", "info"));
filters.push(format!("lib_infra={}", level));
filters.push(format!("flowy_search={}", level));
filters.push(format!("flowy_chat={}", level));
// Enable the frontend logs. DO NOT DISABLE.
// These logs are essential for debugging and verifying frontend behavior.
filters.push(format!("dart_ffi={}", level));

View File

@ -3,8 +3,10 @@ use std::sync::Arc;
use anyhow::Error;
use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin};
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::ChatMessageType;
use collab::core::origin::{CollabClient, CollabOrigin};
use collab::preclude::CollabPlugin;
use collab_entity::CollabType;
use collab_plugins::cloud_storage::postgres::SupabaseDBPlugin;
@ -14,6 +16,9 @@ use tracing::debug;
use collab_integrate::collab_builder::{
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
};
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage,
};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
};
@ -28,6 +33,7 @@ use flowy_server_pub::supabase_config::SupabaseConfiguration;
use flowy_storage::ObjectValue;
use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider};
use flowy_user_pub::entities::{Authenticator, UserTokenState};
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
use crate::integrate::server::{Server, ServerProvider};
@ -372,7 +378,12 @@ impl CollabCloudPluginProvider for ServerProvider {
collab_object.uid,
collab_object.device_id.clone(),
));
let sync_object = SyncObject::from(collab_object);
let sync_object = SyncObject::new(
&collab_object.object_id,
&collab_object.workspace_id,
collab_object.collab_type,
&collab_object.device_id,
);
let (sink, stream) = (channel.sink(), channel.stream());
let sink_config = SinkConfig::new().send_timeout(8);
let sync_plugin = SyncPlugin::new(
@ -427,3 +438,93 @@ impl CollabCloudPluginProvider for ServerProvider {
*self.user_enable_sync.read()
}
}
#[async_trait]
impl ChatCloudService for ServerProvider {
fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
) -> FutureResult<(), FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server();
let chat_id = chat_id.to_string();
let uid = *uid;
FutureResult::new(async move {
server?
.chat_service()
.create_chat(&uid, &workspace_id, &chat_id)
.await
})
}
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 get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
offset: MessageCursor,
limit: u64,
) -> FutureResult<RepeatedChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.get_chat_messages(&workspace_id, &chat_id, offset, limit)
.await
})
}
fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.get_related_message(&workspace_id, &chat_id, message_id)
.await
})
}
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 server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.generate_answer(&workspace_id, &chat_id, question_message_id)
.await
})
}
}

View File

@ -10,6 +10,7 @@ use tokio::sync::RwLock;
use tracing::{debug, error, event, info, instrument};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType};
use flowy_chat::manager::ChatManager;
use flowy_database2::DatabaseManager;
use flowy_document::manager::DocumentManager;
use flowy_error::{FlowyError, FlowyResult};
@ -57,6 +58,7 @@ pub struct AppFlowyCore {
pub task_dispatcher: Arc<RwLock<TaskDispatcher>>,
pub store_preference: Arc<StorePreferences>,
pub search_manager: Arc<SearchManager>,
pub chat_manager: Arc<ChatManager>,
}
impl AppFlowyCore {
@ -137,6 +139,7 @@ impl AppFlowyCore {
document_manager,
collab_builder,
search_manager,
chat_manager,
) = async {
/// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded
/// on demand based on the [CollabPluginConfig].
@ -164,6 +167,8 @@ impl AppFlowyCore {
Arc::downgrade(&(server_provider.clone() as Arc<dyn ObjectStorageService>)),
);
let chat_manager =
ChatDepsResolver::resolve(Arc::downgrade(&authenticate_user), server_provider.clone());
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(None));
let folder_manager = FolderDepsResolver::resolve(
Arc::downgrade(&authenticate_user),
@ -172,6 +177,7 @@ impl AppFlowyCore {
collab_builder.clone(),
server_provider.clone(),
folder_indexer.clone(),
&chat_manager,
)
.await;
@ -195,6 +201,7 @@ impl AppFlowyCore {
document_manager,
collab_builder,
search_manager,
chat_manager,
)
}
.await;
@ -230,6 +237,7 @@ impl AppFlowyCore {
Arc::downgrade(&user_manager),
Arc::downgrade(&document_manager),
Arc::downgrade(&search_manager),
Arc::downgrade(&chat_manager),
),
));
@ -244,6 +252,7 @@ impl AppFlowyCore {
task_dispatcher,
store_preference,
search_manager,
chat_manager,
}
}

View File

@ -1,3 +1,4 @@
use flowy_chat::manager::ChatManager;
use std::sync::Weak;
use flowy_database2::DatabaseManager;
@ -13,6 +14,7 @@ pub fn make_plugins(
user_session: Weak<UserManager>,
document_manager2: Weak<DocumentManager2>,
search_manager: Weak<SearchManager>,
chat_manager: Weak<ChatManager>,
) -> Vec<AFPlugin> {
let store_preferences = user_session
.upgrade()
@ -25,6 +27,7 @@ pub fn make_plugins(
let config_plugin = flowy_config::event_map::init(store_preferences);
let date_plugin = flowy_date::event_map::init();
let search_plugin = flowy_search::event_map::init(search_manager);
let chat_plugin = flowy_chat::event_map::init(chat_manager);
vec![
user_plugin,
folder_plugin,
@ -33,5 +36,6 @@ pub fn make_plugins(
config_plugin,
date_plugin,
search_plugin,
chat_plugin,
]
}