Skip to content

Commit

Permalink
chore: complete text endpoints (#557)
Browse files Browse the repository at this point in the history
* chore: complete text endpoints
  • Loading branch information
appflowy authored May 16, 2024
1 parent c79267d commit 2fae260
Show file tree
Hide file tree
Showing 17 changed files with 140 additions and 56 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ tonic-proto.workspace = true
appflowy-collaborate = { path = "services/appflowy-collaborate" }

# ai
appflowy-ai-client = { path = "libs/appflowy-ai-client" }
appflowy-ai-client = { workspace = true, features = ["dto", "client-api"] }

collab = { workspace = true }
collab-document = { workspace = true }
Expand Down Expand Up @@ -195,6 +195,7 @@ lazy_static = "1.4.0"
tonic = "0.11"
prost = "0.12"
tonic-proto = { path = "libs/tonic-proto" }
appflowy-ai-client = { path = "libs/appflowy-ai-client" }

# collaboration
yrs = "0.18.7"
Expand Down
1 change: 0 additions & 1 deletion deploy.env
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ CLOUDFLARE_TUNNEL_TOKEN=

# AppFlowy AI
APPFLOWY_AI_OPENAI_API_KEY=
APPFLOWY_AI_SERVER_HOST=ai
APPFLOWY_AI_SERVER_PORT=5001

# AppFlowy History
Expand Down
1 change: 0 additions & 1 deletion dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ CLOUDFLARE_TUNNEL_TOKEN=

# AppFlowy AI
APPFLOWY_AI_OPENAI_API_KEY=
APPFLOWY_AI_SERVER_HOST=localhost
APPFLOWY_AI_SERVER_PORT=5001

# AppFlowy History
Expand Down
13 changes: 7 additions & 6 deletions libs/appflowy-ai-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"] }
serde = { version = "1.0.199", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.12", features = ["json", "rustls-tls", "cookies"], optional = true }
serde = { version = "1.0.199", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
thiserror = "1.0.58"
anyhow = "1.0.81"
tracing = "0.1"
serde_repr = "0.1.19"
tracing = { version = "0.1", optional = true }
serde_repr = { version = "0.1.19", optional = true }

[dev-dependencies]
tokio = { version = "1.37.0", features = ["macros", "test-util"] }

[features]
verbose_log = []
client-api = ["dto", "reqwest", "serde", "serde_json", "tracing", "serde_repr"]
dto = ["serde", "serde_json", "serde_repr"]
6 changes: 3 additions & 3 deletions libs/appflowy-ai-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::dto::{
CompletionResponse, CompletionType, Document, SearchDocumentsRequest, SummarizeRowResponse,
CompleteTextResponse, CompletionType, Document, SearchDocumentsRequest, SummarizeRowResponse,
TranslateRowResponse,
};
use crate::error::AIError;
Expand Down Expand Up @@ -27,7 +27,7 @@ impl AppFlowyAIClient {
&self,
text: &str,
completion_type: CompletionType,
) -> Result<CompletionResponse, AIError> {
) -> Result<CompleteTextResponse, AIError> {
if text.is_empty() {
return Err(AIError::InvalidRequest("Empty text".to_string()));
}
Expand All @@ -43,7 +43,7 @@ impl AppFlowyAIClient {
.json(&params)
.send()
.await?;
AIResponse::<CompletionResponse>::from_response(resp)
AIResponse::<CompleteTextResponse>::from_response(resp)
.await?
.into_data()
}
Expand Down
2 changes: 1 addition & 1 deletion libs/appflowy-ai-client/src/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub struct TranslateRowResponse {
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompletionResponse {
pub struct CompleteTextResponse {
pub text: String,
}

Expand Down
5 changes: 5 additions & 0 deletions libs/appflowy-ai-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[cfg(feature = "client-api")]
pub mod client;

#[cfg(feature = "dto")]
pub mod dto;

#[cfg(feature = "client-api")]
pub mod error;
25 changes: 23 additions & 2 deletions libs/client-api/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ use url::Url;
use crate::ws::ConnectInfo;
use gotrue_entity::dto::SignUpResponse::{Authenticated, NotAuthenticated};
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_BUFFER_SIZE: &str = "X-Compression-Buffer-Size";
Expand Down Expand Up @@ -1300,7 +1302,7 @@ impl Client {
params: SummarizeRowParams,
) -> Result<SummarizeRowResponse, AppResponseError> {
let url = format!(
"{}/api/workspace/{}/summarize_row",
"{}/api/ai/{}/summarize_row",
self.base_url, params.workspace_id
);

Expand All @@ -1317,6 +1319,25 @@ impl Client {
.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(&params)
.send()
.await?;
log_request_id(&resp);
AppResponse::<CompleteTextResponse>::from_response(resp)
.await?
.into_data()
}

#[instrument(level = "debug", skip_all, err)]
pub async fn http_client_with_auth(
&self,
Expand Down
1 change: 1 addition & 0 deletions libs/shared-entity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ database-entity.workspace = true
collab-entity = { workspace = true }
app-error = { workspace = true }
chrono = "0.4.31"
appflowy-ai-client = { workspace = true, features = ["dto"] }

actix-web = { version = "4.4.1", default-features = false, features = ["http2"], optional = true }
validator = { version = "0.16", features = ["validator_derive", "derive"], optional = true }
Expand Down
7 changes: 7 additions & 0 deletions libs/shared-entity/src/dto/ai_dto.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

pub use appflowy_ai_client::dto::*;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SummarizeRowParams {
pub workspace_id: String,
Expand All @@ -27,3 +28,9 @@ pub enum SummarizeRowData {
pub struct SummarizeRowResponse {
pub text: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompleteTextParams {
pub text: String,
pub completion_type: CompletionType,
}
66 changes: 66 additions & 0 deletions src/api/ai_tool.rs
Original file line number Diff line number Diff line change
@@ -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(&params.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 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod ai_tool;
pub mod chat;
pub mod file_storage;
pub mod metrics;
Expand Down
41 changes: 0 additions & 41 deletions src/api/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ use collab_rt_protocol::validate_encode_collab;
use database::collab::CollabStorage;
use database::user::select_uid_from_email;
use database_entity::dto::*;
use shared_entity::dto::ai_dto::{SummarizeRowData, SummarizeRowParams, SummarizeRowResponse};
use shared_entity::dto::workspace_dto::*;
use shared_entity::response::AppResponseError;
use shared_entity::response::{AppResponse, JsonAppResponse};
Expand Down Expand Up @@ -132,9 +131,6 @@ pub fn workspace_scope() -> Scope {
// for GET request
.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 {
Expand Down Expand Up @@ -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())
},
}
}
2 changes: 2 additions & 0 deletions src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::api::ws::ws_scope;
use crate::mailer::Mailer;
use access_control::access::{enable_access_control, AccessControl};

use crate::api::ai_tool::ai_tool_scope;
use crate::api::chat::chat_scope;
use crate::biz::actix_ws::server::RealtimeServerActor;
use crate::biz::collab::access_control::{
Expand Down Expand Up @@ -139,6 +140,7 @@ pub async fn run_actix_server(
.service(ws_scope())
.service(file_storage_scope())
.service(chat_scope())
.service(ai_tool_scope())
.service(metrics_scope())
.app_data(Data::new(state.metrics.registry.clone()))
.app_data(Data::new(state.metrics.request_metrics.clone()))
Expand Down
20 changes: 20 additions & 0 deletions tests/ai_test/complete_text.rs
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions tests/ai_test/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod complete_text;
mod summarize_row;

0 comments on commit 2fae260

Please sign in to comment.