Merge branch 'main' into billing-support-3
This commit is contained in:
commit
fdc7055b76
|
|
@ -62,7 +62,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- test_service: "appflowy_cloud"
|
- test_service: "appflowy_cloud"
|
||||||
test_cmd: "--workspace --exclude appflowy-history --exclude appflowy-ai-client"
|
test_cmd: "--workspace --exclude appflowy-history --exclude appflowy-ai-client --features ai-test-enabled"
|
||||||
- test_service: "appflowy_history"
|
- test_service: "appflowy_history"
|
||||||
test_cmd: "-p appflowy-history"
|
test_cmd: "-p appflowy-history"
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -79,18 +79,19 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
# log level
|
# log level
|
||||||
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||||
sed -i 's/GOTRUE_SMTP_USER=.*/GOTRUE_SMTP_USER=${{ secrets.CI_GOTRUE_SMTP_USER }}/' .env
|
sed -i 's|GOTRUE_SMTP_USER=.*|GOTRUE_SMTP_USER=${{ secrets.CI_GOTRUE_SMTP_USER }}|' .env
|
||||||
sed -i 's/GOTRUE_SMTP_PASS=.*/GOTRUE_SMTP_PASS=${{ secrets.CI_GOTRUE_SMTP_PASS }}/' .env
|
sed -i 's|GOTRUE_SMTP_PASS=.*|GOTRUE_SMTP_PASS=${{ secrets.CI_GOTRUE_SMTP_PASS }}|' .env
|
||||||
sed -i 's/GOTRUE_SMTP_ADMIN_EMAIL=.*/GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.CI_GOTRUE_SMTP_ADMIN_EMAIL }}/' .env
|
sed -i 's|GOTRUE_SMTP_ADMIN_EMAIL=.*|GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.CI_GOTRUE_SMTP_ADMIN_EMAIL }}|' .env
|
||||||
sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env
|
sed -i 's|GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*|GOTRUE_EXTERNAL_GOOGLE_ENABLED=true|' .env
|
||||||
sed -i 's/GOTRUE_MAILER_AUTOCONFIRM=.*/GOTRUE_MAILER_AUTOCONFIRM=false/' .env
|
sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=false|' .env
|
||||||
sed -i 's/API_EXTERNAL_URL=http:\/\/your-host/API_EXTERNAL_URL=http:\/\/localhost/' .env
|
sed -i 's|API_EXTERNAL_URL=http://your-host|API_EXTERNAL_URL=http://localhost|' .env
|
||||||
sed -i 's/GOTRUE_RATE_LIMIT_EMAIL_SENT=100/GOTRUE_RATE_LIMIT_EMAIL_SENT=1000/' .env
|
sed -i 's|GOTRUE_RATE_LIMIT_EMAIL_SENT=100|GOTRUE_RATE_LIMIT_EMAIL_SENT=1000|' .env
|
||||||
sed -i 's/APPFLOWY_MAILER_SMTP_USERNAME=.*/APPFLOWY_MAILER_SMTP_USERNAME=${{ secrets.CI_GOTRUE_SMTP_USER }}/' .env
|
sed -i 's|APPFLOWY_MAILER_SMTP_USERNAME=.*|APPFLOWY_MAILER_SMTP_USERNAME=${{ secrets.CI_GOTRUE_SMTP_USER }}|' .env
|
||||||
sed -i 's/APPFLOWY_MAILER_SMTP_PASSWORD=.*/APPFLOWY_MAILER_SMTP_PASSWORD=${{ secrets.CI_GOTRUE_SMTP_PASS }}/' .env
|
sed -i 's|APPFLOWY_MAILER_SMTP_PASSWORD=.*|APPFLOWY_MAILER_SMTP_PASSWORD=${{ secrets.CI_GOTRUE_SMTP_PASS }}|' .env
|
||||||
sed -i 's/APPFLOWY_AI_OPENAI_API_KEY=.*/APPFLOWY_AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}/' .env
|
sed -i 's|APPFLOWY_AI_OPENAI_API_KEY=.*|APPFLOWY_AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env
|
||||||
sed -i 's/APPFLOWY_INDEXER_REDIS_URL=.*/APPFLOWY_INDEXER_REDIS_URL=redis:\/\/localhost:6379/' .env
|
sed -i 's|APPFLOWY_INDEXER_REDIS_URL=.*|APPFLOWY_INDEXER_REDIS_URL=redis://localhost:6379|' .env
|
||||||
sed -i 's/APPFLOWY_INDEXER_DATABASE_URL=.*/APPFLOWY_INDEXER_DATABASE_URL=postgres:\/\/postgres:password@localhost:5432\/postgres/' .env
|
sed -i 's|APPFLOWY_INDEXER_DATABASE_URL=.*|APPFLOWY_INDEXER_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres|' .env
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Update Nginx Configuration
|
- name: Update Nginx Configuration
|
||||||
# the wasm-pack headless tests will run on random ports, so we need to allow all origins
|
# the wasm-pack headless tests will run on random ports, so we need to allow all origins
|
||||||
|
|
|
||||||
|
|
@ -240,3 +240,4 @@ collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
history = []
|
history = []
|
||||||
|
ai-test-enabled = []
|
||||||
|
|
@ -143,6 +143,8 @@ services:
|
||||||
- "5001:5001"
|
- "5001:5001"
|
||||||
environment:
|
environment:
|
||||||
- OPENAI_API_KEY=${APPFLOWY_AI_OPENAI_API_KEY}
|
- OPENAI_API_KEY=${APPFLOWY_AI_OPENAI_API_KEY}
|
||||||
|
- LOCAL_AI_AWS_ACCESS_KEY_ID=${LOCAL_AI_AWS_ACCESS_KEY_ID}
|
||||||
|
- LOCAL_AI_AWS_SECRET_ACCESS_KEY=${LOCAL_AI_AWS_SECRET_ACCESS_KEY}
|
||||||
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT}
|
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT}
|
||||||
- APPFLOWY_AI_DATABASE_URL=${APPFLOWY_AI_DATABASE_URL}
|
- APPFLOWY_AI_DATABASE_URL=${APPFLOWY_AI_DATABASE_URL}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::dto::{
|
use crate::dto::{
|
||||||
AIModel, ChatAnswer, ChatQuestion, CompleteTextResponse, CompletionType, Document,
|
AIModel, ChatAnswer, ChatQuestion, CompleteTextResponse, CompletionType, Document,
|
||||||
EmbeddingRequest, EmbeddingResponse, MessageData, RepeatedRelatedQuestion,
|
EmbeddingRequest, EmbeddingResponse, LocalAIConfig, MessageData, RepeatedLocalAIPackage,
|
||||||
SearchDocumentsRequest, SummarizeRowResponse, TranslateRowData, TranslateRowResponse,
|
RepeatedRelatedQuestion, SearchDocumentsRequest, SummarizeRowResponse, TranslateRowData,
|
||||||
|
TranslateRowResponse,
|
||||||
};
|
};
|
||||||
use crate::error::AIError;
|
use crate::error::AIError;
|
||||||
|
|
||||||
|
|
@ -236,6 +237,25 @@ impl AppFlowyAIClient {
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_local_ai_package(
|
||||||
|
&self,
|
||||||
|
platform: &str,
|
||||||
|
) -> Result<RepeatedLocalAIPackage, AIError> {
|
||||||
|
let url = format!("{}/local_ai/version?platform={platform}", self.url);
|
||||||
|
let resp = self.http_client(Method::GET, &url)?.send().await?;
|
||||||
|
AIResponse::<RepeatedLocalAIPackage>::from_response(resp)
|
||||||
|
.await?
|
||||||
|
.into_data()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_local_ai_config(&self, platform: &str) -> Result<LocalAIConfig, AIError> {
|
||||||
|
let url = format!("{}/local_ai/config?platform={platform}", self.url);
|
||||||
|
let resp = self.http_client(Method::GET, &url)?.send().await?;
|
||||||
|
AIResponse::<LocalAIConfig>::from_response(resp)
|
||||||
|
.await?
|
||||||
|
.into_data()
|
||||||
|
}
|
||||||
|
|
||||||
fn http_client(&self, method: Method, url: &str) -> Result<RequestBuilder, AIError> {
|
fn http_client(&self, method: Method, url: &str) -> Result<RequestBuilder, AIError> {
|
||||||
let request_builder = self.client.request(method, url);
|
let request_builder = self.client.request(method, url);
|
||||||
Ok(request_builder)
|
Ok(request_builder)
|
||||||
|
|
|
||||||
|
|
@ -233,3 +233,38 @@ impl FromStr for AIModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RepeatedLocalAIPackage(pub Vec<AppFlowyAIPlugin>);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct AppFlowyAIPlugin {
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
pub url: String,
|
||||||
|
pub etag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct LLMModel {
|
||||||
|
pub llm_id: i64,
|
||||||
|
pub provider: String,
|
||||||
|
pub embedding_model: ModelInfo,
|
||||||
|
pub chat_model: ModelInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct ModelInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub file_name: String,
|
||||||
|
pub file_size: i64,
|
||||||
|
pub requirements: String,
|
||||||
|
pub download_url: String,
|
||||||
|
pub desc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct LocalAIConfig {
|
||||||
|
pub models: Vec<LLMModel>,
|
||||||
|
pub plugin: AppFlowyAIPlugin,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,25 @@ async fn stream_test() {
|
||||||
let messages: Vec<String> = stream.map(|message| message.unwrap()).collect().await;
|
let messages: Vec<String> = stream.map(|message| message.unwrap()).collect().await;
|
||||||
println!("final answer: {}", messages.join(""));
|
println!("final answer: {}", messages.join(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn download_package_test() {
|
||||||
|
let client = appflowy_ai_client();
|
||||||
|
let packages = client.get_local_ai_package("macos").await.unwrap();
|
||||||
|
assert!(!packages.0.is_empty());
|
||||||
|
println!("packages: {:?}", packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_local_ai_config_test() {
|
||||||
|
let client = appflowy_ai_client();
|
||||||
|
let config = client.get_local_ai_config("macos").await.unwrap();
|
||||||
|
assert!(!config.models.is_empty());
|
||||||
|
|
||||||
|
assert!(!config.models[0].embedding_model.download_url.is_empty());
|
||||||
|
assert!(!config.models[0].chat_model.download_url.is_empty());
|
||||||
|
|
||||||
|
assert!(!config.plugin.version.is_empty());
|
||||||
|
assert!(!config.plugin.url.is_empty());
|
||||||
|
println!("packages: {:?}", config);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,20 @@ impl Client {
|
||||||
client_id: &str,
|
client_id: &str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let reqwest_client = reqwest::Client::new();
|
let reqwest_client = reqwest::Client::new();
|
||||||
let client_version = Version::parse(client_id).unwrap_or_else(|_| Version::new(0, 5, 0));
|
let client_version = Version::parse(client_id).unwrap_or_else(|_| {
|
||||||
|
warn!("Failed to parse client version, defaulting to 0.6.3");
|
||||||
|
Version::new(0, 6, 3)
|
||||||
|
});
|
||||||
|
|
||||||
|
// The latest version of appflowy frontend application is 0.6.3.
|
||||||
|
// Ensure the client version is at least 0.6.3. Just in case client passes a lower version.
|
||||||
|
let min_version = Version::new(0, 6, 3);
|
||||||
|
let client_version = if client_version < min_version {
|
||||||
|
warn!("Client version is less than 0.6.3, setting it to 0.6.3");
|
||||||
|
min_version
|
||||||
|
} else {
|
||||||
|
client_version
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use crate::http::log_request_id;
|
||||||
use crate::Client;
|
use crate::Client;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use shared_entity::dto::ai_dto::{
|
use shared_entity::dto::ai_dto::{
|
||||||
CompleteTextParams, CompleteTextResponse, SummarizeRowParams, SummarizeRowResponse,
|
CompleteTextParams, CompleteTextResponse, LocalAIConfig, SummarizeRowParams,
|
||||||
TranslateRowParams, TranslateRowResponse,
|
SummarizeRowResponse, TranslateRowParams, TranslateRowResponse,
|
||||||
};
|
};
|
||||||
use shared_entity::response::{AppResponse, AppResponseError};
|
use shared_entity::response::{AppResponse, AppResponseError};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
@ -73,4 +73,25 @@ impl Client {
|
||||||
.await?
|
.await?
|
||||||
.into_data()
|
.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "info", skip_all, err)]
|
||||||
|
pub async fn get_local_ai_config(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
platform: &str,
|
||||||
|
) -> Result<LocalAIConfig, AppResponseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/ai/{}/local/config?platform={platform}",
|
||||||
|
self.base_url, workspace_id
|
||||||
|
);
|
||||||
|
let resp = self
|
||||||
|
.http_client_with_auth(Method::GET, &url)
|
||||||
|
.await?
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
log_request_id(&resp);
|
||||||
|
AppResponse::<LocalAIConfig>::from_response(resp)
|
||||||
|
.await?
|
||||||
|
.into_data()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ use crate::state::AppState;
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::{Data, Json};
|
||||||
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
||||||
use app_error::AppError;
|
use app_error::AppError;
|
||||||
use appflowy_ai_client::dto::{CompleteTextResponse, TranslateRowParams, TranslateRowResponse};
|
use appflowy_ai_client::dto::{
|
||||||
|
CompleteTextResponse, LocalAIConfig, TranslateRowParams, TranslateRowResponse,
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::{stream, TryStreamExt};
|
use futures_util::{stream, TryStreamExt};
|
||||||
|
use serde::Deserialize;
|
||||||
use shared_entity::dto::ai_dto::{
|
use shared_entity::dto::ai_dto::{
|
||||||
CompleteTextParams, SummarizeRowData, SummarizeRowParams, SummarizeRowResponse,
|
CompleteTextParams, SummarizeRowData, SummarizeRowParams, SummarizeRowResponse,
|
||||||
};
|
};
|
||||||
|
|
@ -20,6 +23,7 @@ pub fn ai_completion_scope() -> Scope {
|
||||||
.service(web::resource("/complete/stream").route(web::post().to(stream_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("/summarize_row").route(web::post().to(summarize_row_handler)))
|
||||||
.service(web::resource("/translate_row").route(web::post().to(translate_row_handler)))
|
.service(web::resource("/translate_row").route(web::post().to(translate_row_handler)))
|
||||||
|
.service(web::resource("/local/config").route(web::get().to(local_ai_config_handler)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn complete_text_handler(
|
async fn complete_text_handler(
|
||||||
|
|
@ -123,3 +127,45 @@ async fn translate_row_handler(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ConfigQuery {
|
||||||
|
platform: String,
|
||||||
|
}
|
||||||
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
|
async fn local_ai_config_handler(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
query: web::Query<ConfigQuery>,
|
||||||
|
// req: HttpRequest,
|
||||||
|
) -> actix_web::Result<Json<AppResponse<LocalAIConfig>>> {
|
||||||
|
let platform = match query.into_inner().platform.as_str() {
|
||||||
|
"macos" => "macos",
|
||||||
|
"linux" => "ubuntu",
|
||||||
|
"ubuntu" => "ubuntu",
|
||||||
|
"windows" => "windows",
|
||||||
|
_ => {
|
||||||
|
return Err(AppError::InvalidRequest("Invalid platform".to_string()).into());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = state
|
||||||
|
.ai_client
|
||||||
|
.get_local_ai_config(platform)
|
||||||
|
.await
|
||||||
|
.map_err(|err| AppError::AIServiceUnavailable(err.to_string()))?;
|
||||||
|
Ok(AppResponse::Ok().with_data(config).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn device_info_from_headers(headers: &HeaderMap) -> std::result::Result<String, AppError> {
|
||||||
|
// headers
|
||||||
|
// .get("device_id")
|
||||||
|
// .ok_or(AppError::InvalidRequest(
|
||||||
|
// "Missing device_id header".to_string(),
|
||||||
|
// ))
|
||||||
|
// .and_then(|header| {
|
||||||
|
// header
|
||||||
|
// .to_str()
|
||||||
|
// .map_err(|err| AppError::InvalidRequest(format!("Failed to parse device_id: {}", err)))
|
||||||
|
// })
|
||||||
|
// .map(|s| s.to_string())
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
use client_api_test::TestClient;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_local_ai_config_test() {
|
||||||
|
let test_client = TestClient::new_user().await;
|
||||||
|
let workspace_id = test_client.workspace_id().await;
|
||||||
|
let config = test_client
|
||||||
|
.api_client
|
||||||
|
.get_local_ai_config(&workspace_id, "macos")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
assert!(!config.models.is_empty());
|
||||||
|
assert!(!config.models[0].embedding_model.download_url.is_empty());
|
||||||
|
assert!(config.models[0].embedding_model.file_size > 10);
|
||||||
|
assert!(!config.models[0].chat_model.download_url.is_empty());
|
||||||
|
assert!(config.models[0].chat_model.file_size > 10);
|
||||||
|
|
||||||
|
assert!(!config.plugin.version.is_empty());
|
||||||
|
assert!(!config.plugin.url.is_empty());
|
||||||
|
println!("config: {:?}", config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
mod chat_test;
|
mod chat_test;
|
||||||
mod complete_text;
|
mod complete_text;
|
||||||
|
// mod local_ai_test;
|
||||||
mod summarize_row;
|
mod summarize_row;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
|
#[cfg(feature = "ai-test-enabled")]
|
||||||
|
mod ai_test;
|
||||||
mod collab;
|
mod collab;
|
||||||
|
mod collab_history;
|
||||||
|
mod file_test;
|
||||||
mod gotrue;
|
mod gotrue;
|
||||||
|
mod search;
|
||||||
mod sql_test;
|
mod sql_test;
|
||||||
mod user;
|
mod user;
|
||||||
mod websocket;
|
mod websocket;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
|
|
||||||
mod ai_test;
|
|
||||||
mod collab_history;
|
|
||||||
mod file_test;
|
|
||||||
mod search;
|
|
||||||
mod yrs_version;
|
mod yrs_version;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue