parent
c79267d42a
commit
2fae260e4f
|
|
@ -5474,6 +5474,7 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"app-error",
|
"app-error",
|
||||||
|
"appflowy-ai-client",
|
||||||
"chrono",
|
"chrono",
|
||||||
"collab-entity",
|
"collab-entity",
|
||||||
"database-entity",
|
"database-entity",
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ tonic-proto.workspace = true
|
||||||
appflowy-collaborate = { path = "services/appflowy-collaborate" }
|
appflowy-collaborate = { path = "services/appflowy-collaborate" }
|
||||||
|
|
||||||
# ai
|
# ai
|
||||||
appflowy-ai-client = { path = "libs/appflowy-ai-client" }
|
appflowy-ai-client = { workspace = true, features = ["dto", "client-api"] }
|
||||||
|
|
||||||
collab = { workspace = true }
|
collab = { workspace = true }
|
||||||
collab-document = { workspace = true }
|
collab-document = { workspace = true }
|
||||||
|
|
@ -195,6 +195,7 @@ lazy_static = "1.4.0"
|
||||||
tonic = "0.11"
|
tonic = "0.11"
|
||||||
prost = "0.12"
|
prost = "0.12"
|
||||||
tonic-proto = { path = "libs/tonic-proto" }
|
tonic-proto = { path = "libs/tonic-proto" }
|
||||||
|
appflowy-ai-client = { path = "libs/appflowy-ai-client" }
|
||||||
|
|
||||||
# collaboration
|
# collaboration
|
||||||
yrs = "0.18.7"
|
yrs = "0.18.7"
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,6 @@ CLOUDFLARE_TUNNEL_TOKEN=
|
||||||
|
|
||||||
# AppFlowy AI
|
# AppFlowy AI
|
||||||
APPFLOWY_AI_OPENAI_API_KEY=
|
APPFLOWY_AI_OPENAI_API_KEY=
|
||||||
APPFLOWY_AI_SERVER_HOST=ai
|
|
||||||
APPFLOWY_AI_SERVER_PORT=5001
|
APPFLOWY_AI_SERVER_PORT=5001
|
||||||
|
|
||||||
# AppFlowy History
|
# AppFlowy History
|
||||||
|
|
|
||||||
1
dev.env
1
dev.env
|
|
@ -100,7 +100,6 @@ CLOUDFLARE_TUNNEL_TOKEN=
|
||||||
|
|
||||||
# AppFlowy AI
|
# AppFlowy AI
|
||||||
APPFLOWY_AI_OPENAI_API_KEY=
|
APPFLOWY_AI_OPENAI_API_KEY=
|
||||||
APPFLOWY_AI_SERVER_HOST=localhost
|
|
||||||
APPFLOWY_AI_SERVER_PORT=5001
|
APPFLOWY_AI_SERVER_PORT=5001
|
||||||
|
|
||||||
# AppFlowy History
|
# AppFlowy History
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,17 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"], optional = true }
|
||||||
serde = { version = "1.0.199", features = ["derive"] }
|
serde = { version = "1.0.199", features = ["derive"], optional = true }
|
||||||
serde_json = "1.0"
|
serde_json = { version = "1.0", optional = true }
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.58"
|
||||||
anyhow = "1.0.81"
|
anyhow = "1.0.81"
|
||||||
tracing = "0.1"
|
tracing = { version = "0.1", optional = true }
|
||||||
serde_repr = "0.1.19"
|
serde_repr = { version = "0.1.19", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.37.0", features = ["macros", "test-util"] }
|
tokio = { version = "1.37.0", features = ["macros", "test-util"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
verbose_log = []
|
client-api = ["dto", "reqwest", "serde", "serde_json", "tracing", "serde_repr"]
|
||||||
|
dto = ["serde", "serde_json", "serde_repr"]
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::dto::{
|
use crate::dto::{
|
||||||
CompletionResponse, CompletionType, Document, SearchDocumentsRequest, SummarizeRowResponse,
|
CompleteTextResponse, CompletionType, Document, SearchDocumentsRequest, SummarizeRowResponse,
|
||||||
TranslateRowResponse,
|
TranslateRowResponse,
|
||||||
};
|
};
|
||||||
use crate::error::AIError;
|
use crate::error::AIError;
|
||||||
|
|
@ -27,7 +27,7 @@ impl AppFlowyAIClient {
|
||||||
&self,
|
&self,
|
||||||
text: &str,
|
text: &str,
|
||||||
completion_type: CompletionType,
|
completion_type: CompletionType,
|
||||||
) -> Result<CompletionResponse, AIError> {
|
) -> Result<CompleteTextResponse, AIError> {
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
return Err(AIError::InvalidRequest("Empty text".to_string()));
|
return Err(AIError::InvalidRequest("Empty text".to_string()));
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +43,7 @@ impl AppFlowyAIClient {
|
||||||
.json(¶ms)
|
.json(¶ms)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
AIResponse::<CompletionResponse>::from_response(resp)
|
AIResponse::<CompleteTextResponse>::from_response(resp)
|
||||||
.await?
|
.await?
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub struct TranslateRowResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct CompletionResponse {
|
pub struct CompleteTextResponse {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
#[cfg(feature = "client-api")]
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
|
||||||
|
#[cfg(feature = "dto")]
|
||||||
pub mod dto;
|
pub mod dto;
|
||||||
|
|
||||||
|
#[cfg(feature = "client-api")]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,9 @@ use url::Url;
|
||||||
use crate::ws::ConnectInfo;
|
use crate::ws::ConnectInfo;
|
||||||
use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated};
|
use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated};
|
||||||
use gotrue_entity::dto::{GotrueTokenResponse, UpdateGotrueUserParams, User};
|
use gotrue_entity::dto::{GotrueTokenResponse, UpdateGotrueUserParams, User};
|
||||||
use shared_entity::dto::ai_dto::{SummarizeRowParams, SummarizeRowResponse};
|
use shared_entity::dto::ai_dto::{
|
||||||
|
CompleteTextParams, CompleteTextResponse, SummarizeRowParams, SummarizeRowResponse,
|
||||||
|
};
|
||||||
|
|
||||||
pub const X_COMPRESSION_TYPE: &str = "X-Compression-Type";
|
pub const X_COMPRESSION_TYPE: &str = "X-Compression-Type";
|
||||||
pub const X_COMPRESSION_BUFFER_SIZE: &str = "X-Compression-Buffer-Size";
|
pub const X_COMPRESSION_BUFFER_SIZE: &str = "X-Compression-Buffer-Size";
|
||||||
|
|
@ -1300,7 +1302,7 @@ impl Client {
|
||||||
params: SummarizeRowParams,
|
params: SummarizeRowParams,
|
||||||
) -> Result<SummarizeRowResponse, AppResponseError> {
|
) -> Result<SummarizeRowResponse, AppResponseError> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/api/workspace/{}/summarize_row",
|
"{}/api/ai/{}/summarize_row",
|
||||||
self.base_url, params.workspace_id
|
self.base_url, params.workspace_id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1317,6 +1319,25 @@ impl Client {
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", skip_all)]
|
||||||
|
pub async fn completion_text(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
params: CompleteTextParams,
|
||||||
|
) -> Result<CompleteTextResponse, AppResponseError> {
|
||||||
|
let url = format!("{}/api/ai/{}/complete_text", self.base_url, workspace_id);
|
||||||
|
let resp = self
|
||||||
|
.http_client_with_auth(Method::POST, &url)
|
||||||
|
.await?
|
||||||
|
.json(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
log_request_id(&resp);
|
||||||
|
AppResponse::<CompleteTextResponse>::from_response(resp)
|
||||||
|
.await?
|
||||||
|
.into_data()
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn http_client_with_auth(
|
pub async fn http_client_with_auth(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ database-entity.workspace = true
|
||||||
collab-entity = { workspace = true }
|
collab-entity = { workspace = true }
|
||||||
app-error = { workspace = true }
|
app-error = { workspace = true }
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
|
appflowy-ai-client = { workspace = true, features = ["dto"] }
|
||||||
|
|
||||||
actix-web = { version = "4.4.1", default-features = false, features = ["http2"], optional = true }
|
actix-web = { version = "4.4.1", default-features = false, features = ["http2"], optional = true }
|
||||||
validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true }
|
validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
|
pub use appflowy_ai_client::dto::*;
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct SummarizeRowParams {
|
pub struct SummarizeRowParams {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
|
|
@ -27,3 +28,9 @@ pub enum SummarizeRowData {
|
||||||
pub struct SummarizeRowResponse {
|
pub struct SummarizeRowResponse {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CompleteTextParams {
|
||||||
|
pub text: String,
|
||||||
|
pub completion_type: CompletionType,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::state::AppState;
|
||||||
|
use actix_web::web::{Data, Json};
|
||||||
|
use actix_web::{web, Scope};
|
||||||
|
use app_error::AppError;
|
||||||
|
use appflowy_ai_client::dto::CompleteTextResponse;
|
||||||
|
use shared_entity::dto::ai_dto::{
|
||||||
|
CompleteTextParams, SummarizeRowData, SummarizeRowParams, SummarizeRowResponse,
|
||||||
|
};
|
||||||
|
use shared_entity::response::{AppResponse, JsonAppResponse};
|
||||||
|
use tracing::{error, instrument};
|
||||||
|
|
||||||
|
pub fn ai_tool_scope() -> Scope {
|
||||||
|
web::scope("/api/ai/{workspace_id}")
|
||||||
|
.service(web::resource("/complete_text").route(web::post().to(complete_text_handler)))
|
||||||
|
.service(web::resource("/summarize_row").route(web::post().to(summarize_row_handler)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn complete_text_handler(
|
||||||
|
state: Data<AppState>,
|
||||||
|
payload: Json<CompleteTextParams>,
|
||||||
|
) -> actix_web::Result<JsonAppResponse<CompleteTextResponse>> {
|
||||||
|
let params = payload.into_inner();
|
||||||
|
let resp = state
|
||||||
|
.ai_client
|
||||||
|
.completion_text(¶ms.text, params.completion_type)
|
||||||
|
.await
|
||||||
|
.map_err(|err| AppError::Internal(err.into()))?;
|
||||||
|
Ok(AppResponse::Ok().with_data(resp).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(state, payload), err)]
|
||||||
|
async fn summarize_row_handler(
|
||||||
|
state: Data<AppState>,
|
||||||
|
payload: Json<SummarizeRowParams>,
|
||||||
|
) -> actix_web::Result<Json<AppResponse<SummarizeRowResponse>>> {
|
||||||
|
let params = payload.into_inner();
|
||||||
|
match params.data {
|
||||||
|
SummarizeRowData::Identity { .. } => {
|
||||||
|
return Err(AppError::InvalidRequest("Identity data is not supported".to_string()).into());
|
||||||
|
},
|
||||||
|
SummarizeRowData::Content(content) => {
|
||||||
|
if content.is_empty() {
|
||||||
|
return Ok(
|
||||||
|
AppResponse::Ok()
|
||||||
|
.with_data(SummarizeRowResponse {
|
||||||
|
text: "No content".to_string(),
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = state.ai_client.summarize_row(&content).await;
|
||||||
|
let resp = match result {
|
||||||
|
Ok(resp) => SummarizeRowResponse { text: resp.text },
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to summarize row: {:?}", err);
|
||||||
|
SummarizeRowResponse {
|
||||||
|
text: "No content".to_string(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AppResponse::Ok().with_data(resp).into())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod ai_tool;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod file_storage;
|
pub mod file_storage;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ use collab_rt_protocol::validate_encode_collab;
|
||||||
use database::collab::CollabStorage;
|
use database::collab::CollabStorage;
|
||||||
use database::user::select_uid_from_email;
|
use database::user::select_uid_from_email;
|
||||||
use database_entity::dto::*;
|
use database_entity::dto::*;
|
||||||
use shared_entity::dto::ai_dto::{SummarizeRowData, SummarizeRowParams, SummarizeRowResponse};
|
|
||||||
use shared_entity::dto::workspace_dto::*;
|
use shared_entity::dto::workspace_dto::*;
|
||||||
use shared_entity::response::AppResponseError;
|
use shared_entity::response::AppResponseError;
|
||||||
use shared_entity::response::{AppResponse, JsonAppResponse};
|
use shared_entity::response::{AppResponse, JsonAppResponse};
|
||||||
|
|
@ -132,9 +131,6 @@ pub fn workspace_scope() -> Scope {
|
||||||
// for GET request
|
// for GET request
|
||||||
.route(web::post().to(batch_get_collab_handler)),
|
.route(web::post().to(batch_get_collab_handler)),
|
||||||
)
|
)
|
||||||
.service(
|
|
||||||
web::resource("/{workspace_id}/summarize_row").route(web::post().to(summary_row_handler)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn collab_scope() -> Scope {
|
pub fn collab_scope() -> Scope {
|
||||||
|
|
@ -1000,40 +996,3 @@ async fn parser_realtime_msg(
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip(state, payload), err)]
|
|
||||||
async fn summary_row_handler(
|
|
||||||
state: Data<AppState>,
|
|
||||||
payload: Json<SummarizeRowParams>,
|
|
||||||
) -> Result<Json<AppResponse<SummarizeRowResponse>>> {
|
|
||||||
let params = payload.into_inner();
|
|
||||||
match params.data {
|
|
||||||
SummarizeRowData::Identity { .. } => {
|
|
||||||
return Err(AppError::InvalidRequest("Identity data is not supported".to_string()).into());
|
|
||||||
},
|
|
||||||
SummarizeRowData::Content(content) => {
|
|
||||||
if content.is_empty() {
|
|
||||||
return Ok(
|
|
||||||
AppResponse::Ok()
|
|
||||||
.with_data(SummarizeRowResponse {
|
|
||||||
text: "No content".to_string(),
|
|
||||||
})
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = state.ai_client.summarize_row(&content).await;
|
|
||||||
let resp = match result {
|
|
||||||
Ok(resp) => SummarizeRowResponse { text: resp.text },
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to summarize row: {:?}", err);
|
|
||||||
SummarizeRowResponse {
|
|
||||||
text: "No content".to_string(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(AppResponse::Ok().with_data(resp).into())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::api::ws::ws_scope;
|
||||||
use crate::mailer::Mailer;
|
use crate::mailer::Mailer;
|
||||||
use access_control::access::{enable_access_control, AccessControl};
|
use access_control::access::{enable_access_control, AccessControl};
|
||||||
|
|
||||||
|
use crate::api::ai_tool::ai_tool_scope;
|
||||||
use crate::api::chat::chat_scope;
|
use crate::api::chat::chat_scope;
|
||||||
use crate::biz::actix_ws::server::RealtimeServerActor;
|
use crate::biz::actix_ws::server::RealtimeServerActor;
|
||||||
use crate::biz::collab::access_control::{
|
use crate::biz::collab::access_control::{
|
||||||
|
|
@ -139,6 +140,7 @@ pub async fn run_actix_server(
|
||||||
.service(ws_scope())
|
.service(ws_scope())
|
||||||
.service(file_storage_scope())
|
.service(file_storage_scope())
|
||||||
.service(chat_scope())
|
.service(chat_scope())
|
||||||
|
.service(ai_tool_scope())
|
||||||
.service(metrics_scope())
|
.service(metrics_scope())
|
||||||
.app_data(Data::new(state.metrics.registry.clone()))
|
.app_data(Data::new(state.metrics.registry.clone()))
|
||||||
.app_data(Data::new(state.metrics.request_metrics.clone()))
|
.app_data(Data::new(state.metrics.request_metrics.clone()))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
use appflowy_ai_client::dto::CompletionType;
|
||||||
|
use client_api_test::TestClient;
|
||||||
|
use shared_entity::dto::ai_dto::CompleteTextParams;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn improve_writing_test() {
|
||||||
|
let test_client = TestClient::new_user().await;
|
||||||
|
let workspace_id = test_client.workspace_id().await;
|
||||||
|
let params = CompleteTextParams {
|
||||||
|
text: "I feel hungry".to_string(),
|
||||||
|
completion_type: CompletionType::ImproveWriting,
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = test_client
|
||||||
|
.api_client
|
||||||
|
.completion_text(&workspace_id, params)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(resp.text.contains("hungry"));
|
||||||
|
}
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
mod complete_text;
|
||||||
mod summarize_row;
|
mod summarize_row;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue