chore: client api log (#644)

* chore: update logs

* chore: support stream completion
This commit is contained in:
Nathan.fooo 2024-06-23 21:42:01 +08:00 committed by GitHub
parent 72affd99e5
commit d61524d636
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 102 additions and 16 deletions

View File

@ -67,6 +67,31 @@ impl AppFlowyAIClient {
.into_data()
}
pub async fn stream_completion_text(
&self,
text: &str,
completion_type: CompletionType,
model: AIModel,
) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> {
if text.is_empty() {
return Err(AIError::InvalidRequest("Empty text".to_string()));
}
let params = json!({
"text": text,
"type": completion_type as u8,
});
let url = format!("{}/completion/stream", self.url);
let resp = self
.http_client(Method::POST, &url)?
.header(AI_MODEL_HEADER_KEY, model.to_str())
.json(&params)
.send()
.await?;
AIResponse::<()>::stream_response(resp).await
}
pub async fn summarize_row(
&self,
params: &Map<String, Value>,

View File

@ -203,7 +203,7 @@ pub enum AIModel {
GPT4o = 2,
Claude3Sonnet = 3,
Claude3Opus = 4,
Local = 5,
Local = 5, // work in progress
}
impl AIModel {
@ -212,7 +212,7 @@ impl AIModel {
AIModel::DefaultModel => "default-model",
AIModel::GPT35 => "gpt-3.5-turbo",
AIModel::GPT4o => "gpt-4o",
AIModel::Claude3Sonnet => "claude-3-sonnet-20240229",
AIModel::Claude3Sonnet => "claude-3-sonnet",
AIModel::Claude3Opus => "claude-3-opus",
AIModel::Local => "local",
}

View File

@ -1,6 +1,6 @@
use crate::appflowy_ai_client;
use appflowy_ai_client::dto::{AIModel, CompletionType};
use futures::stream::StreamExt;
#[tokio::test]
async fn continue_writing_test() {
let client = appflowy_ai_client();
@ -35,8 +35,8 @@ async fn improve_writing_test() {
#[tokio::test]
async fn make_text_shorter_text() {
let client = appflowy_ai_client();
let resp = client
.completion_text(
let stream = client
.stream_completion_text(
"I have an immense passion and deep-seated affection for Rust, a modern, multi-paradigm, high-performance programming language that I find incredibly satisfying to use due to its focus on safety, speed, and concurrency",
CompletionType::MakeShorter,
AIModel::GPT35
@ -44,8 +44,19 @@ async fn make_text_shorter_text() {
.await
.unwrap();
let stream = stream.map(|item| {
item.map(|bytes| {
String::from_utf8(bytes.to_vec())
.map(|s| s.replace('\n', ""))
.unwrap()
})
});
let lines: Vec<String> = stream.map(|message| message.unwrap()).collect().await;
let text = lines.join("");
// the response would be something like:
// I'm deeply passionate about Rust, a modern, high-performance programming language, due to its emphasis on safety, speed, and concurrency
assert!(!resp.text.is_empty());
println!("{}", resp.text);
assert!(!text.is_empty());
println!("{}", text);
}

View File

@ -815,15 +815,27 @@ impl Client {
.await?;
let access_token = self.access_token()?;
trace!("start request: {}, method: {}", url, method);
let request_builder = self
let headers = [
("client-version", self.client_version.to_string()),
("client-timestamp", ts_now.to_string()),
("device_id", self.device_id.clone()),
("ai-model", self.ai_model.read().to_str().to_string()),
];
trace!(
"start request: {}, method: {}, headers: {:?}",
url,
method,
headers
);
let mut request_builder = self
.cloud_client
.request(method, url)
.header("client-version", self.client_version.to_string())
.header("client-timestamp", ts_now.to_string())
.header("device_id", self.device_id.clone())
.header("ai-model", self.ai_model.read().to_str())
.bearer_auth(access_token);
for header in headers {
request_builder = request_builder.header(header.0, header.1);
}
Ok(request_builder)
}

View File

@ -61,7 +61,7 @@ impl Client {
workspace_id: &str,
params: CompleteTextParams,
) -> Result<CompleteTextResponse, AppResponseError> {
let url = format!("{}/api/ai/{}/complete_text", self.base_url, workspace_id);
let url = format!("{}/api/ai/{}/complete", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?

View File

@ -32,8 +32,25 @@ use tokio_retry::{Retry, RetryIf};
use tracing::{event, info, instrument, trace};
pub use infra::file_util::ChunkedBytes;
use shared_entity::dto::ai_dto::CompleteTextParams;
impl Client {
pub async fn stream_completion_text(
&self,
workspace_id: &str,
params: CompleteTextParams,
) -> Result<impl Stream<Item = Result<Bytes, AppResponseError>>, AppResponseError> {
let url = format!("{}/api/ai/{}/complete/stream", 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::<()>::answer_response_stream(resp).await
}
pub async fn create_upload(
&self,
workspace_id: &str,

View File

@ -1,9 +1,10 @@
use crate::api::util::ai_model_from_header;
use crate::state::AppState;
use actix_web::web::{Data, Json};
use actix_web::{web, HttpRequest, Scope};
use actix_web::{web, HttpRequest, HttpResponse, Scope};
use app_error::AppError;
use appflowy_ai_client::dto::{CompleteTextResponse, TranslateRowParams, TranslateRowResponse};
use futures_util::TryStreamExt;
use shared_entity::dto::ai_dto::{
CompleteTextParams, SummarizeRowData, SummarizeRowParams, SummarizeRowResponse,
};
@ -13,7 +14,8 @@ use tracing::{error, instrument};
pub fn ai_completion_scope() -> Scope {
web::scope("/api/ai/{workspace_id}")
.service(web::resource("/complete_text").route(web::post().to(complete_text_handler)))
.service(web::resource("/complete").route(web::post().to(complete_text_handler)))
.service(web::resource("/complete/stream").route(web::post().to(stream_complete_text_handler)))
.service(web::resource("/summarize_row").route(web::post().to(summarize_row_handler)))
.service(web::resource("/translate_row").route(web::post().to(translate_row_handler)))
}
@ -33,6 +35,25 @@ async fn complete_text_handler(
Ok(AppResponse::Ok().with_data(resp).into())
}
async fn stream_complete_text_handler(
state: Data<AppState>,
payload: Json<CompleteTextParams>,
req: HttpRequest,
) -> actix_web::Result<HttpResponse> {
let ai_model = ai_model_from_header(&req);
let params = payload.into_inner();
let stream = state
.ai_client
.stream_completion_text(&params.text, params.completion_type, ai_model)
.await
.map_err(|err| AppError::Internal(err.into()))?;
Ok(
HttpResponse::Ok()
.content_type("text/event-stream")
.streaming(stream.map_err(AppError::from)),
)
}
#[instrument(level = "debug", skip(state, payload), err)]
async fn summarize_row_handler(
state: Data<AppState>,