Skip to content

Commit

Permalink
chore: ai writer (#1153)
Browse files Browse the repository at this point in the history
* chore: ai writer

* chore: update test

* chore: update test

* chore: set env

* chore: rename

* chore: format nginx conf
  • Loading branch information
appflowy authored Jan 12, 2025
1 parent a5d94a0 commit b650e9e
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
sed -i 's|AI_OPENAI_API_KEY=.*|AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env
sed -i "s|AI_AWS_ACCESS_KEY_ID=.*|AI_AWS_ACCESS_KEY_ID=${{ secrets.LOCAL_AI_AWS_ACCESS_KEY_ID }}|" .env
sed -i "s|AI_AWS_SECRET_ACCESS_KEY=.*|AI_AWS_SECRET_ACCESS_KEY=${{ secrets.LOCAL_AI_AWS_SECRET_ACCESS_KEY }}|" .env
sed -i 's|AI_APPFLOWY_HOST=.*|AI_APPFLOWY_HOST=http://localhost:8000|' .env
sed -i 's|AI_APPFLOWY_HOST=.*|AI_APPFLOWY_HOST=http://localhost|' .env
sed -i 's|APPFLOWY_WEB_URL=.*|APPFLOWY_WEB_URL=http://localhost:3000|' .env
shell: bash

Expand Down
2 changes: 1 addition & 1 deletion deploy.env
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ AI_DATABASE_URL=postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POS
AI_REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
LOCAL_AI_TEST_ENABLED=false
AI_APPFLOWY_BUCKET_NAME=appflowy
AI_APPFLOWY_HOST=http://your-host
AI_APPFLOWY_HOST=${FQDN}
AI_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY}
AI_AWS_SECRET_ACCESS_KEY=${AWS_SECRET}

Expand Down
3 changes: 3 additions & 0 deletions docker-compose-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl/certificate.crt:/etc/nginx/ssl/certificate.crt
- ./nginx/ssl/private_key.key:/etc/nginx/ssl/private_key.key
#- ./nginx_logs:/var/log/nginx

# You do not need this if you have configured to use your own s3 file storage
# You can try to access http://localhost/minio/browser/appflowy in your browser
Expand Down Expand Up @@ -179,6 +180,8 @@ services:
- AI_REDIS_URL=${AI_REDIS_URL}
- AI_APPFLOWY_BUCKET_NAME=${AI_APPFLOWY_BUCKET_NAME}
- AI_APPFLOWY_HOST=${AI_APPFLOWY_HOST}
- AI_USE_MINIO=${APPFLOWY_S3_USE_MINIO}
- SUPPORT_OPENAI_V3_IMAGE_MODEL=false

appflowy_worker:
restart: on-failure
Expand Down
25 changes: 8 additions & 17 deletions libs/appflowy-ai-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::dto::{
AIModel, CalculateSimilarityParams, ChatAnswer, ChatQuestion, CompletionType, CreateChatContext,
CustomPrompt, Document, LocalAIConfig, MessageData, QuestionMetadata, RepeatedLocalAIPackage,
RepeatedRelatedQuestion, ResponseFormat, SearchDocumentsRequest, SimilarityResponse,
SummarizeRowResponse, TranslateRowData, TranslateRowResponse,
AIModel, CalculateSimilarityParams, ChatAnswer, ChatQuestion, CompleteTextParams,
CreateChatContext, Document, LocalAIConfig, MessageData, QuestionMetadata,
RepeatedLocalAIPackage, RepeatedRelatedQuestion, ResponseFormat, SearchDocumentsRequest,
SimilarityResponse, SummarizeRowResponse, TranslateRowData, TranslateRowResponse,
};
use crate::error::AIError;

Expand All @@ -12,7 +12,7 @@ use reqwest;
use reqwest::{Method, RequestBuilder, StatusCode};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use serde_json::{Map, Value};
use std::borrow::Cow;
use std::time::Duration;
use tracing::{info, trace};
Expand Down Expand Up @@ -41,24 +41,15 @@ impl AppFlowyAIClient {
Ok(())
}

pub async fn stream_completion_text<T: Into<Option<CompletionType>>>(
pub async fn stream_completion_text(
&self,
text: &str,
completion_type: T,
custom_prompt: Option<CustomPrompt>,
params: CompleteTextParams,
model: AIModel,
) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> {
let completion_type = completion_type.into();
if text.is_empty() {
if params.text.is_empty() {
return Err(AIError::InvalidRequest("Empty text".to_string()));
}

let params = json!({
"text": text,
"type": completion_type.map(|t| t as u8),
"custom_prompt": custom_prompt,
});

let url = format!("{}/completion/stream", self.url);
let resp = self
.async_http_client(Method::POST, &url)?
Expand Down
31 changes: 31 additions & 0 deletions libs/appflowy-ai-client/src/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,34 @@ pub struct CalculateSimilarityParams {
pub struct SimilarityResponse {
pub score: f64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompletionMetadata {
pub object_id: String,
pub rag_ids: Option<Vec<String>>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompleteTextParams {
pub text: String,
pub completion_type: Option<CompletionType>,
pub custom_prompt: Option<CustomPrompt>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<CompletionMetadata>,
}

impl CompleteTextParams {
pub fn new_with_completion_type(
text: String,
completion_type: CompletionType,
metadata: Option<CompletionMetadata>,
) -> Self {
Self {
text,
completion_type: Some(completion_type),
custom_prompt: None,
metadata,
}
}
}
41 changes: 22 additions & 19 deletions libs/appflowy-ai-client/tests/chat_test/completion_test.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use crate::appflowy_ai_client;
use appflowy_ai_client::client::collect_stream_text;
use appflowy_ai_client::dto::{AIModel, CompletionType};
use appflowy_ai_client::dto::{AIModel, CompleteTextParams, CompletionType};
#[tokio::test]
async fn continue_writing_test() {
let client = appflowy_ai_client();
let params = CompleteTextParams {
text: "I feel hungry".to_string(),
completion_type: Some(CompletionType::ImproveWriting),
custom_prompt: None,
metadata: None,
};
let stream = client
.stream_completion_text(
"I feel hungry",
CompletionType::ContinueWriting,
None,
AIModel::Claude3Sonnet,
)
.stream_completion_text(params, AIModel::GPT4oMini)
.await
.unwrap();
let text = collect_stream_text(stream).await;
Expand All @@ -21,13 +22,14 @@ async fn continue_writing_test() {
#[tokio::test]
async fn improve_writing_test() {
let client = appflowy_ai_client();
let params = CompleteTextParams {
text: "I fell tired because i sleep not very well last night".to_string(),
completion_type: Some(CompletionType::ImproveWriting),
custom_prompt: None,
metadata: None,
};
let stream = client
.stream_completion_text(
"I fell tired because i sleep not very well last night",
CompletionType::ImproveWriting,
None,
AIModel::GPT4oMini,
)
.stream_completion_text(params, AIModel::GPT4oMini)
.await
.unwrap();

Expand All @@ -40,13 +42,14 @@ async fn improve_writing_test() {
#[tokio::test]
async fn make_text_shorter_text() {
let client = appflowy_ai_client();
let params = CompleteTextParams {
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".to_string(),
completion_type: Some(CompletionType::MakeShorter),
custom_prompt: None,
metadata: None,
};
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,
None,
AIModel::GPT4oMini
)
.stream_completion_text(params, AIModel::GPT4oMini)
.await
.unwrap();

Expand Down
4 changes: 2 additions & 2 deletions libs/database/src/file/file_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ where
pub async fn get_blob_metadata(
&self,
workspace_id: &Uuid,
store_key: &str,
metadata_key: &str,
) -> Result<AFBlobMetadataRow, AppError> {
let metadata = get_blob_metadata(&self.pg_pool, workspace_id, store_key).await?;
let metadata = get_blob_metadata(&self.pg_pool, workspace_id, metadata_key).await?;
Ok(metadata)
}

Expand Down
10 changes: 8 additions & 2 deletions libs/database/src/resource_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,22 @@ pub async fn delete_blob_metadata(
pub async fn get_blob_metadata(
pg_pool: &PgPool,
workspace_id: &Uuid,
file_id: &str,
metadata_key: &str,
) -> Result<AFBlobMetadataRow, AppError> {
tracing::trace!(
"get_blob_metadata: workspace_id: {}, metadata_key: {}",
workspace_id,
metadata_key
);
// file_id is the BlobPath's blob_metadata_key
let metadata = sqlx::query_as!(
AFBlobMetadataRow,
r#"
SELECT * FROM af_blob_metadata
WHERE workspace_id = $1 AND file_id = $2
"#,
workspace_id,
file_id,
metadata_key,
)
.fetch_one(pg_pool)
.await?;
Expand Down
17 changes: 0 additions & 17 deletions libs/shared-entity/src/dto/ai_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,6 @@ pub struct SummarizeRowResponse {
pub text: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompleteTextParams {
pub text: String,
pub completion_type: Option<CompletionType>,
pub custom_prompt: Option<CustomPrompt>,
}

impl CompleteTextParams {
pub fn new_with_completion_type(text: String, completion_type: CompletionType) -> Self {
Self {
text,
completion_type: Some(completion_type),
custom_prompt: None,
}
}
}

#[derive(Debug)]
pub enum StringOrMessage {
Left(String),
Expand Down
107 changes: 52 additions & 55 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ events {
http {
# docker dns resolver
resolver 127.0.0.11 valid=10s;
#error_log /var/log/nginx/error.log debug;

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
default upgrade;
'' close;
}

map $http_origin $cors_origin {
Expand Down Expand Up @@ -50,14 +51,14 @@ http {
# GoTrue
location /gotrue/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Max-Age' 3600 always;
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Headers' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Max-Age' 3600 always;
add_header 'Content-Type' 'text/plain charset=UTF-8' always;
add_header 'Content-Length' 0 always;
return 204;
}

proxy_pass $gotrue_backend;
Expand All @@ -81,57 +82,12 @@ http {
proxy_read_timeout 86400;
}

# AppFlowy-Cloud
location /api/chat {
proxy_pass $appflowy_cloud_backend;

proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding on;
proxy_buffering off;
proxy_cache off;

proxy_read_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
}

location /api/import {
proxy_pass $appflowy_cloud_backend;

# Set headers
proxy_set_header X-Request-Id $request_id;
proxy_set_header Host $http_host;

# Handle CORS
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, Accept' always;
add_header 'Access-Control-Max-Age' 3600 always;

# Timeouts
proxy_read_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;

# Disable buffering for large file uploads
proxy_request_buffering off;
proxy_buffering off;
proxy_cache off;
client_max_body_size 2G;
}

location /api {
proxy_pass $appflowy_cloud_backend;

proxy_set_header X-Request-Id $request_id;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# Set CORS headers for other requests

if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
Expand All @@ -150,8 +106,49 @@ http {
proxy_request_buffering off;
client_max_body_size 256M;
}

# AppFlowy-Cloud
location /api/chat {
proxy_pass $appflowy_cloud_backend;

proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding on;
proxy_buffering off;
proxy_cache off;

proxy_read_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
}

location /api/import {
proxy_pass $appflowy_cloud_backend;

# Set headers
proxy_set_header X-Request-Id $request_id;
proxy_set_header Host $http_host;

# Handle CORS
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, Accept' always;
add_header 'Access-Control-Max-Age' 3600 always;

# Timeouts
proxy_read_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;

# Disable buffering for large file uploads
proxy_request_buffering off;
proxy_buffering off;
proxy_cache off;
client_max_body_size 2G;
}
}


# AppFlowy AI
location /ai {
proxy_pass $appflowy_ai_backend;
Expand Down
Loading

0 comments on commit b650e9e

Please sign in to comment.