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

@ -41,6 +41,7 @@ flowy-error = { workspace = true, features = ["impl_from_serde", "impl_from_reqw
flowy-server-pub = { workspace = true }
flowy-encrypt = { workspace = true }
flowy-storage = { workspace = true }
flowy-chat-pub = { workspace = true }
mime_guess = "2.0"
url = "2.4"
tokio-util = "0.7"

View File

@ -0,0 +1,128 @@
use crate::af_cloud::AFServer;
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::{
CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage,
};
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType};
use flowy_error::FlowyError;
use futures_util::StreamExt;
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
pub(crate) struct AFCloudChatCloudServiceImpl<T> {
pub inner: T,
}
#[async_trait]
impl<T> ChatCloudService for AFCloudChatCloudServiceImpl<T>
where
T: AFServer,
{
fn create_chat(
&self,
_uid: &i64,
workspace_id: &str,
chat_id: &str,
) -> FutureResult<(), 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 params = CreateChatParams {
chat_id,
name: "".to_string(),
rag_ids: vec![],
};
try_get_client?
.create_chat(&workspace_id, params)
.await
.map_err(FlowyError::from)?;
Ok(())
})
}
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 try_get_client = self.inner.try_get_client();
let params = CreateChatMessageParams {
content: message,
message_type,
};
let stream = try_get_client?
.create_chat_message(&workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(stream.boxed())
}
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 try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let resp = try_get_client?
.get_chat_messages(&workspace_id, &chat_id, offset, limit)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
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 try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let resp = try_get_client?
.get_chat_related_question(&workspace_id, &chat_id, message_id)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
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?
.generate_question_answer(&workspace_id, &chat_id, question_message_id)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
}

View File

@ -42,10 +42,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: object_id.clone(),
collab_type: collab_type.clone(),
},
inner: QueryCollab::new(object_id.clone(), collab_type.clone()),
};
match try_get_client?.get_collab(params).await {
Ok(data) => {
@ -81,10 +78,7 @@ where
let client = try_get_client?;
let params = object_ids
.into_iter()
.map(|object_id| QueryCollab {
object_id,
collab_type: object_ty.clone(),
})
.map(|object_id| QueryCollab::new(object_id, object_ty.clone()))
.collect();
let results = client.batch_get_collab(&workspace_id, params).await?;
check_request_workspace_id_is_match(

View File

@ -37,10 +37,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: document_id.to_string(),
collab_type: CollabType::Document,
},
inner: QueryCollab::new(document_id.to_string(), CollabType::Document),
};
let doc_state = try_get_client?
.get_collab(params)
@ -82,10 +79,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: document_id.clone(),
collab_type: CollabType::Document,
},
inner: QueryCollab::new(document_id.clone(), CollabType::Document),
};
let doc_state = try_get_client?
.get_collab(params)

View File

@ -93,10 +93,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: workspace_id.clone(),
collab_type: CollabType::Folder,
},
inner: QueryCollab::new(workspace_id.clone(), CollabType::Folder),
};
let doc_state = try_get_client?
.get_collab(params)
@ -140,10 +137,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id,
collab_type,
},
inner: QueryCollab::new(object_id, collab_type),
};
let doc_state = try_get_client?
.get_collab(params)
@ -167,10 +161,12 @@ where
FutureResult::new(async move {
let params = objects
.into_iter()
.map(|object| CollabParams {
object_id: object.object_id,
encoded_collab_v1: object.encoded_collab_v1,
collab_type: object.collab_type,
.map(|object| {
CollabParams::new(
object.object_id,
object.collab_type,
object.encoded_collab_v1,
)
})
.collect::<Vec<_>>();
try_get_client?

View File

@ -1,9 +1,11 @@
pub(crate) use chat::*;
pub(crate) use database::*;
pub(crate) use document::*;
pub(crate) use file_storage::*;
pub(crate) use folder::*;
pub(crate) use user::*;
mod chat;
mod database;
mod document;
mod file_storage;

View File

@ -348,10 +348,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id,
collab_type: CollabType::UserAwareness,
},
inner: QueryCollab::new(object_id, CollabType::UserAwareness),
};
let resp = try_get_client?.get_collab(params).await?;
check_request_workspace_id_is_match(
@ -381,10 +378,10 @@ where
FutureResult::new(async move {
let client = try_get_client?;
let params = CreateCollabParams {
workspace_id: collab_object.workspace_id.clone(),
object_id: collab_object.object_id.clone(),
workspace_id: collab_object.workspace_id,
object_id: collab_object.object_id,
collab_type: collab_object.collab_type,
encoded_collab_v1: data,
collab_type: collab_object.collab_type.clone(),
};
client.create_collab(params).await?;
Ok(())
@ -401,10 +398,12 @@ where
FutureResult::new(async move {
let params = objects
.into_iter()
.map(|object| CollabParams {
object_id: object.object_id,
encoded_collab_v1: object.encoded_collab,
collab_type: object.collab_type,
.map(|object| {
CollabParams::new(
object.object_id,
u8::from(object.collab_type).into(),
object.encoded_collab,
)
})
.collect::<Vec<_>>();
try_get_client?

View File

@ -89,6 +89,7 @@ pub fn from_af_workspace_member(member: AFWorkspaceMember) -> WorkspaceMember {
email: member.email,
role: from_af_role(member.role),
name: member.name,
avatar_url: member.avatar_url,
}
}

View File

@ -10,6 +10,7 @@ use client_api::ws::{
ConnectState, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel,
};
use client_api::{Client, ClientConfiguration};
use flowy_chat_pub::cloud::ChatCloudService;
use flowy_storage::ObjectStorageService;
use rand::Rng;
use semver::Version;
@ -31,9 +32,10 @@ use flowy_user_pub::entities::UserTokenState;
use lib_dispatch::prelude::af_spawn;
use crate::af_cloud::impls::{
AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl,
AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl,
AFCloudChatCloudServiceImpl, AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl,
AFCloudFileStorageServiceImpl, AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl,
};
use crate::AppFlowyServer;
pub(crate) type AFCloudClient = Client;
@ -214,6 +216,13 @@ impl AppFlowyServer for AppFlowyCloudServer {
})
}
fn chat_service(&self) -> Arc<dyn ChatCloudService> {
let server = AFServerImpl {
client: self.get_client(),
};
Arc::new(AFCloudChatCloudServiceImpl { inner: server })
}
fn subscribe_ws_state(&self) -> Option<WSConnectStateReceiver> {
Some(self.ws_client.subscribe_connect_state())
}

View File

@ -0,0 +1,66 @@
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage};
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream};
use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
pub(crate) struct DefaultChatCloudServiceImpl;
#[async_trait]
impl ChatCloudService for DefaultChatCloudServiceImpl {
fn create_chat(
&self,
_uid: &i64,
_workspace_id: &str,
_chat_id: &str,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
}
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 get_chat_messages(
&self,
_workspace_id: &str,
_chat_id: &str,
_offset: MessageCursor,
_limit: u64,
) -> FutureResult<RepeatedChatMessage, FlowyError> {
FutureResult::new(async move {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
}
fn get_related_message(
&self,
_workspace_id: &str,
_chat_id: &str,
_message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError> {
FutureResult::new(async move {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
}
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."))
})
}
}

View File

@ -8,4 +8,5 @@ mod server;
#[cfg(feature = "enable_supabase")]
pub mod supabase;
mod default_impl;
pub mod util;

View File

@ -6,11 +6,13 @@ use std::sync::Arc;
use anyhow::Error;
use client_api::collab_sync::ServerCollabMessage;
use flowy_chat_pub::cloud::ChatCloudService;
use parking_lot::RwLock;
use tokio_stream::wrappers::WatchStream;
#[cfg(feature = "enable_supabase")]
use {collab_entity::CollabObject, collab_plugins::cloud_storage::RemoteCollabStorage};
use crate::default_impl::DefaultChatCloudServiceImpl;
use flowy_database_pub::cloud::DatabaseCloudService;
use flowy_document_pub::cloud::DocumentCloudService;
use flowy_folder_pub::cloud::FolderCloudService;
@ -94,6 +96,10 @@ pub trait AppFlowyServer: Send + Sync + 'static {
/// An `Arc` wrapping the `DocumentCloudService` interface.
fn document_service(&self) -> Arc<dyn DocumentCloudService>;
fn chat_service(&self) -> Arc<dyn ChatCloudService> {
Arc::new(DefaultChatCloudServiceImpl)
}
/// Manages collaborative objects within a remote storage system. This includes operations such as
/// checking storage status, retrieving updates and snapshots, and dispatching updates. The service
/// also provides subscription capabilities for real-time updates.