feat: Chat file metadata (#1133)

* chore: chat question metadata

* chore: update

* chore: update env

* chore: update env
This commit is contained in:
Nathan.fooo 2025-01-07 12:46:55 +08:00 committed by GitHub
parent cd10010477
commit 5195dae3a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 157 additions and 81 deletions

View File

@ -98,9 +98,10 @@ jobs:
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|AI_OPENAI_API_KEY=.*|AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env
sed -i "s|LOCAL_AI_AWS_ACCESS_KEY_ID=.*|LOCAL_AI_AWS_ACCESS_KEY_ID=${{ secrets.LOCAL_AI_AWS_ACCESS_KEY_ID }}|" .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|LOCAL_AI_AWS_SECRET_ACCESS_KEY=.*|LOCAL_AI_AWS_SECRET_ACCESS_KEY=${{ secrets.LOCAL_AI_AWS_SECRET_ACCESS_KEY }}|" .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|APPFLOWY_WEB_URL=.*|APPFLOWY_WEB_URL=http://localhost:3000|' .env sed -i 's|APPFLOWY_WEB_URL=.*|APPFLOWY_WEB_URL=http://localhost:3000|' .env
shell: bash shell: bash

View File

@ -19,6 +19,9 @@ REDIS_PORT=6379
MINIO_HOST=minio MINIO_HOST=minio
MINIO_PORT=9000 MINIO_PORT=9000
AWS_ACCESS_KEY=minioadmin
AWS_SECRET=minioadmin
# AppFlowy Cloud # AppFlowy Cloud
## URL that connects to the gotrue docker container ## URL that connects to the gotrue docker container
APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999 APPFLOWY_GOTRUE_BASE_URL=http://gotrue:9999
@ -110,8 +113,8 @@ APPFLOWY_S3_CREATE_BUCKET=true
# Keep this as true if you are using other S3 compatible storage provider other than AWS. # Keep this as true if you are using other S3 compatible storage provider other than AWS.
APPFLOWY_S3_USE_MINIO=true APPFLOWY_S3_USE_MINIO=true
APPFLOWY_S3_MINIO_URL=http://${MINIO_HOST}:${MINIO_PORT} # change this if you are using a different address for minio APPFLOWY_S3_MINIO_URL=http://${MINIO_HOST}:${MINIO_PORT} # change this if you are using a different address for minio
APPFLOWY_S3_ACCESS_KEY=minioadmin APPFLOWY_S3_ACCESS_KEY=${AWS_ACCESS_KEY}
APPFLOWY_S3_SECRET_KEY=minioadmin APPFLOWY_S3_SECRET_KEY=${AWS_SECRET}
APPFLOWY_S3_BUCKET=appflowy APPFLOWY_S3_BUCKET=appflowy
#APPFLOWY_S3_REGION=us-east-1 #APPFLOWY_S3_REGION=us-east-1
@ -146,12 +149,16 @@ NGINX_PORT=80
NGINX_TLS_PORT=443 NGINX_TLS_PORT=443
# AppFlowy AI # AppFlowy AI
APPFLOWY_AI_OPENAI_API_KEY= AI_OPENAI_API_KEY=
APPFLOWY_AI_SERVER_PORT=5001 AI_SERVER_PORT=5001
APPFLOWY_AI_SERVER_HOST=ai AI_SERVER_HOST=ai
APPFLOWY_AI_DATABASE_URL=postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} AI_DATABASE_URL=postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
APPFLOWY_AI_REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} AI_REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
APPFLOWY_LOCAL_AI_TEST_ENABLED=false LOCAL_AI_TEST_ENABLED=false
AI_APPFLOWY_BUCKET_NAME=appflowy
AI_APPFLOWY_HOST=http://your-host
AI_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY}
AI_AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
# AppFlowy Indexer # AppFlowy Indexer
APPFLOWY_INDEXER_ENABLED=true APPFLOWY_INDEXER_ENABLED=true

24
dev.env
View File

@ -6,6 +6,10 @@ APPFLOWY_WEBSOCKET_MAILBOX_SIZE=6000
APPFLOWY_DATABASE_MAX_CONNECTIONS=40 APPFLOWY_DATABASE_MAX_CONNECTIONS=40
APPFLOWY_DOCUMENT_CONTENT_SPLIT_LEN=8000 APPFLOWY_DOCUMENT_CONTENT_SPLIT_LEN=8000
# AWS
AWS_ACCESS_KEY=minioadmin
AWS_SECRET=minioadmin
# This file is used to set the environment variables for local development # This file is used to set the environment variables for local development
# Copy this file to .env and change the values as needed # Copy this file to .env and change the values as needed
@ -83,8 +87,8 @@ GOTRUE_EXTERNAL_APPLE_REDIRECT_URI=http://localhost:9999/callback
APPFLOWY_S3_CREATE_BUCKET=true APPFLOWY_S3_CREATE_BUCKET=true
APPFLOWY_S3_USE_MINIO=true APPFLOWY_S3_USE_MINIO=true
APPFLOWY_S3_MINIO_URL=http://localhost:9000 # change this if you are using a different address for minio APPFLOWY_S3_MINIO_URL=http://localhost:9000 # change this if you are using a different address for minio
APPFLOWY_S3_ACCESS_KEY=minioadmin APPFLOWY_S3_ACCESS_KEY=${AWS_ACCESS_KEY}
APPFLOWY_S3_SECRET_KEY=minioadmin APPFLOWY_S3_SECRET_KEY=${AWS_SECRET}
APPFLOWY_S3_BUCKET=appflowy APPFLOWY_S3_BUCKET=appflowy
#APPFLOWY_S3_REGION=us-east-1 #APPFLOWY_S3_REGION=us-east-1
@ -113,12 +117,16 @@ GF_SECURITY_ADMIN_PASSWORD=password
CLOUDFLARE_TUNNEL_TOKEN= CLOUDFLARE_TUNNEL_TOKEN=
# AppFlowy AI # AppFlowy AI
APPFLOWY_AI_OPENAI_API_KEY= AI_OPENAI_API_KEY=
APPFLOWY_AI_SERVER_PORT=5001 AI_SERVER_PORT=5001
APPFLOWY_AI_SERVER_HOST=localhost AI_SERVER_HOST=localhost
APPFLOWY_AI_DATABASE_URL=postgresql+psycopg://postgres:password@localhost:5432/postgres AI_DATABASE_URL=postgresql+psycopg://postgres:password@localhost:5432/postgres
APPFLOWY_AI_REDIS_URL=redis://redis:6379 AI_REDIS_URL=redis://redis:6379
APPFLOWY_LOCAL_AI_TEST_ENABLED=false LOCAL_AI_TEST_ENABLED=false
AI_APPFLOWY_BUCKET_NAME=appflowy
AI_APPFLOWY_HOST=http://localhost:8000
AI_AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY}
AI_AWS_SECRET_ACCESS_KEY=${AWS_SECRET}
# AppFlowy Indexer # AppFlowy Indexer
APPFLOWY_INDEXER_ENABLED=true APPFLOWY_INDEXER_ENABLED=true

View File

@ -131,15 +131,15 @@ services:
- APPFLOWY_ACCESS_CONTROL=${APPFLOWY_ACCESS_CONTROL} - APPFLOWY_ACCESS_CONTROL=${APPFLOWY_ACCESS_CONTROL}
# For the CI testing, we set the database connection to 20. The default value is 40. # For the CI testing, we set the database connection to 20. The default value is 40.
- APPFLOWY_DATABASE_MAX_CONNECTIONS=20 - APPFLOWY_DATABASE_MAX_CONNECTIONS=20
- APPFLOWY_AI_SERVER_HOST=${APPFLOWY_AI_SERVER_HOST} - AI_SERVER_HOST=${AI_SERVER_HOST}
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT} - AI_SERVER_PORT=${AI_SERVER_PORT}
- APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL} - APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL}
- APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST} - APPFLOWY_MAILER_SMTP_HOST=${APPFLOWY_MAILER_SMTP_HOST}
- APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT} - APPFLOWY_MAILER_SMTP_PORT=${APPFLOWY_MAILER_SMTP_PORT}
- APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME} - APPFLOWY_MAILER_SMTP_USERNAME=${APPFLOWY_MAILER_SMTP_USERNAME}
- APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL} - APPFLOWY_MAILER_SMTP_EMAIL=${APPFLOWY_MAILER_SMTP_EMAIL}
- APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD} - APPFLOWY_MAILER_SMTP_PASSWORD=${APPFLOWY_MAILER_SMTP_PASSWORD}
- APPFLOWY_AI_OPENAI_API_KEY=${APPFLOWY_AI_OPENAI_API_KEY} - AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY}
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@ -171,12 +171,14 @@ services:
ports: ports:
- "5001:5001" - "5001:5001"
environment: environment:
- OPENAI_API_KEY=${APPFLOWY_AI_OPENAI_API_KEY} - OPENAI_API_KEY=${AI_OPENAI_API_KEY}
- LOCAL_AI_AWS_ACCESS_KEY_ID=${LOCAL_AI_AWS_ACCESS_KEY_ID} - AI_AWS_ACCESS_KEY_ID=${AI_AWS_ACCESS_KEY_ID}
- LOCAL_AI_AWS_SECRET_ACCESS_KEY=${LOCAL_AI_AWS_SECRET_ACCESS_KEY} - AI_AWS_SECRET_ACCESS_KEY=${AI_AWS_SECRET_ACCESS_KEY}
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT} - AI_SERVER_PORT=${AI_SERVER_PORT}
- APPFLOWY_AI_DATABASE_URL=${APPFLOWY_AI_DATABASE_URL} - AI_DATABASE_URL=${AI_DATABASE_URL}
- APPFLOWY_AI_REDIS_URL=${APPFLOWY_AI_REDIS_URL} - AI_REDIS_URL=${AI_REDIS_URL}
- AI_APPFLOWY_BUCKET_NAME=${AI_APPFLOWY_BUCKET_NAME}
- AI_APPFLOWY_HOST=${AI_APPFLOWY_HOST}
appflowy_worker: appflowy_worker:
restart: on-failure restart: on-failure

View File

@ -133,11 +133,10 @@ services:
- APPFLOWY_MAILER_SMTP_TLS_KIND=${APPFLOWY_MAILER_SMTP_TLS_KIND} - APPFLOWY_MAILER_SMTP_TLS_KIND=${APPFLOWY_MAILER_SMTP_TLS_KIND}
- APPFLOWY_ACCESS_CONTROL=${APPFLOWY_ACCESS_CONTROL} - APPFLOWY_ACCESS_CONTROL=${APPFLOWY_ACCESS_CONTROL}
- APPFLOWY_DATABASE_MAX_CONNECTIONS=${APPFLOWY_DATABASE_MAX_CONNECTIONS} - APPFLOWY_DATABASE_MAX_CONNECTIONS=${APPFLOWY_DATABASE_MAX_CONNECTIONS}
- APPFLOWY_AI_SERVER_HOST=${APPFLOWY_AI_SERVER_HOST} - AI_SERVER_HOST=${AI_SERVER_HOST}
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT} - AI_SERVER_PORT=${AI_SERVER_PORT}
- APPFLOWY_AI_OPENAI_API_KEY=${APPFLOWY_AI_OPENAI_API_KEY} - AI_OPENAI_API_KEY=${AI_OPENAI_API_KEY}
# Uncomment this line if AppFlowy Web has been deployed # Uncomment this line if AppFlowy Web has been deployed
# - APPFLOWY_WEB_URL=${APPFLOWY_WEB_URL}
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@ -161,10 +160,10 @@ services:
restart: on-failure restart: on-failure
image: appflowyinc/appflowy_ai:${APPFLOWY_AI_VERSION:-latest} image: appflowyinc/appflowy_ai:${APPFLOWY_AI_VERSION:-latest}
environment: environment:
- OPENAI_API_KEY=${APPFLOWY_AI_OPENAI_API_KEY} - OPENAI_API_KEY=${AI_OPENAI_API_KEY}
- APPFLOWY_AI_SERVER_PORT=${APPFLOWY_AI_SERVER_PORT} - APPFLOWY_AI_SERVER_PORT=${AI_SERVER_PORT}
- APPFLOWY_AI_DATABASE_URL=${APPFLOWY_AI_DATABASE_URL} - APPFLOWY_AI_DATABASE_URL=${AI_DATABASE_URL}
- APPFLOWY_AI_REDIS_URL=${APPFLOWY_AI_REDIS_URL} - APPFLOWY_AI_REDIS_URL=${AI_REDIS_URL}
appflowy_worker: appflowy_worker:
restart: on-failure restart: on-failure

View File

@ -1,8 +1,9 @@
use crate::dto::{ use crate::dto::{
AIModel, CalculateSimilarityParams, ChatAnswer, ChatQuestion, CompleteTextResponse, AIModel, CalculateSimilarityParams, ChatAnswer, ChatQuestion, CompleteTextResponse,
CompletionType, CreateChatContext, CustomPrompt, Document, LocalAIConfig, MessageData, CompletionType, CreateChatContext, CustomPrompt, Document, LocalAIConfig, MessageData,
RepeatedLocalAIPackage, RepeatedRelatedQuestion, ResponseFormat, SearchDocumentsRequest, QuestionMetadata, RepeatedLocalAIPackage, RepeatedRelatedQuestion, ResponseFormat,
SimilarityResponse, SummarizeRowResponse, TranslateRowData, TranslateRowResponse, SearchDocumentsRequest, SimilarityResponse, SummarizeRowResponse, TranslateRowData,
TranslateRowResponse,
}; };
use crate::error::AIError; use crate::error::AIError;
@ -173,6 +174,7 @@ impl AppFlowyAIClient {
pub async fn send_question( pub async fn send_question(
&self, &self,
workspace_id: &str,
chat_id: &str, chat_id: &str,
question_id: i64, question_id: i64,
content: &str, content: &str,
@ -184,10 +186,13 @@ impl AppFlowyAIClient {
data: MessageData { data: MessageData {
content: content.to_string(), content: content.to_string(),
metadata, metadata,
rag_ids: vec![],
message_id: Some(question_id.to_string()), message_id: Some(question_id.to_string()),
}, },
format: Default::default(), format: Default::default(),
metadata: QuestionMetadata {
workspace_id: workspace_id.to_string(),
rag_ids: vec![],
},
}; };
let url = format!("{}/chat/message", self.url); let url = format!("{}/chat/message", self.url);
let resp = self let resp = self
@ -203,6 +208,7 @@ impl AppFlowyAIClient {
pub async fn stream_question( pub async fn stream_question(
&self, &self,
workspace_id: String,
chat_id: &str, chat_id: &str,
content: &str, content: &str,
metadata: Option<Value>, metadata: Option<Value>,
@ -214,10 +220,13 @@ impl AppFlowyAIClient {
data: MessageData { data: MessageData {
content: content.to_string(), content: content.to_string(),
metadata, metadata,
rag_ids,
message_id: None, message_id: None,
}, },
format: Default::default(), format: Default::default(),
metadata: QuestionMetadata {
workspace_id,
rag_ids,
},
}; };
let url = format!("{}/chat/message/stream", self.url); let url = format!("{}/chat/message/stream", self.url);
let resp = self let resp = self
@ -230,8 +239,10 @@ impl AppFlowyAIClient {
AIResponse::<()>::stream_response(resp).await AIResponse::<()>::stream_response(resp).await
} }
#[allow(clippy::too_many_arguments)]
pub async fn stream_question_v2( pub async fn stream_question_v2(
&self, &self,
workspace_id: String,
chat_id: &str, chat_id: &str,
question_id: i64, question_id: i64,
content: &str, content: &str,
@ -244,10 +255,13 @@ impl AppFlowyAIClient {
data: MessageData { data: MessageData {
content: content.to_string(), content: content.to_string(),
metadata, metadata,
rag_ids,
message_id: Some(question_id.to_string()), message_id: Some(question_id.to_string()),
}, },
format: ResponseFormat::default(), format: ResponseFormat::default(),
metadata: QuestionMetadata {
workspace_id,
rag_ids,
},
}; };
self.stream_question_v3(model, json).await self.stream_question_v3(model, json).await
} }

View File

@ -26,6 +26,13 @@ pub struct ChatQuestion {
pub data: MessageData, pub data: MessageData,
#[serde(default)] #[serde(default)]
pub format: ResponseFormat, pub format: ResponseFormat,
pub metadata: QuestionMetadata,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QuestionMetadata {
pub workspace_id: String,
pub rag_ids: Vec<String>,
} }
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
@ -60,7 +67,7 @@ pub struct OutputContentMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_image_prompt: Option<String>, pub custom_image_prompt: Option<String>,
/// The image model to use for generation (default: "dall-e-2"). /// The image model to use for generation (default: "dall-e-3").
#[serde(default = "default_image_model")] #[serde(default = "default_image_model")]
pub image_model: String, pub image_model: String,
@ -81,7 +88,7 @@ pub struct OutputContentMetadata {
// Default values for the fields // Default values for the fields
fn default_image_model() -> String { fn default_image_model() -> String {
"dall-e-2".to_string() "dall-e-3".to_string()
} }
fn default_image_size() -> Option<String> { fn default_image_size() -> Option<String> {
@ -98,8 +105,6 @@ pub struct MessageData {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>, pub metadata: Option<serde_json::Value>,
#[serde(default)] #[serde(default)]
pub rag_ids: Vec<String>,
#[serde(default)]
pub message_id: Option<String>, pub message_id: Option<String>,
} }

View File

@ -14,7 +14,14 @@ async fn create_chat_context_test() {
}; };
client.create_chat_text_context(context).await.unwrap(); client.create_chat_text_context(context).await.unwrap();
let resp = client let resp = client
.send_question(&chat_id, 1, "Where I live?", &AIModel::GPT4oMini, None) .send_question(
&uuid::Uuid::new_v4().to_string(),
&chat_id,
1,
"Where I live?",
&AIModel::GPT4oMini,
None,
)
.await .await
.unwrap(); .unwrap();
// response will be something like: // response will be something like:

View File

@ -1,9 +1,6 @@
use crate::appflowy_ai_client; use crate::appflowy_ai_client;
use appflowy_ai_client::dto::{AIModel, STREAM_ANSWER_KEY}; use appflowy_ai_client::dto::AIModel;
use appflowy_ai_client::error::AIError;
use futures::stream::StreamExt;
use infra::reqwest::JsonStream;
#[tokio::test] #[tokio::test]
async fn qa_test() { async fn qa_test() {
@ -11,7 +8,14 @@ async fn qa_test() {
client.health_check().await.unwrap(); client.health_check().await.unwrap();
let chat_id = uuid::Uuid::new_v4().to_string(); let chat_id = uuid::Uuid::new_v4().to_string();
let resp = client let resp = client
.send_question(&chat_id, 1, "I feel hungry", &AIModel::GPT4o, None) .send_question(
&uuid::Uuid::new_v4().to_string(),
&chat_id,
1,
"I feel hungry",
&AIModel::GPT4o,
None,
)
.await .await
.unwrap(); .unwrap();
assert!(!resp.content.is_empty()); assert!(!resp.content.is_empty());

View File

@ -35,7 +35,7 @@ pub fn ai_test_enabled() -> bool {
load_env(); load_env();
// local ai test is disable by default // local ai test is disable by default
let enabled = get_bool_from_env_var("APPFLOWY_LOCAL_AI_TEST_ENABLED"); let enabled = get_bool_from_env_var("LOCAL_AI_TEST_ENABLED");
if enabled { if enabled {
trace!("Local AI test is enabled"); trace!("Local AI test is enabled");
} }

View File

@ -67,7 +67,7 @@ pub trait BucketClient {
pub trait BlobKey: Send + Sync { pub trait BlobKey: Send + Sync {
fn workspace_id(&self) -> &Uuid; fn workspace_id(&self) -> &Uuid;
fn object_key(&self) -> String; fn object_key(&self) -> String;
fn meta_key(&self) -> String; fn blob_metadata_key(&self) -> String;
fn e_tag(&self) -> &str; fn e_tag(&self) -> &str;
} }
@ -99,11 +99,11 @@ where
file_type: String, file_type: String,
file_size: usize, file_size: usize,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
if is_blob_metadata_exists(&self.pg_pool, key.workspace_id(), &key.meta_key()).await? { if is_blob_metadata_exists(&self.pg_pool, key.workspace_id(), &key.blob_metadata_key()).await? {
warn!( warn!(
"file already exists, workspace_id: {}, meta_key: {}", "file already exists, workspace_id: {}, blob_metadata_key: {}",
key.workspace_id(), key.workspace_id(),
key.meta_key() key.blob_metadata_key()
); );
return Ok(()); return Ok(());
} }
@ -114,7 +114,7 @@ where
.await?; .await?;
insert_blob_metadata( insert_blob_metadata(
&self.pg_pool, &self.pg_pool,
&key.meta_key(), &key.blob_metadata_key(),
key.workspace_id(), key.workspace_id(),
&file_type, &file_type,
file_size, file_size,
@ -127,7 +127,7 @@ where
self.client.delete_blob(&key.object_key()).await?; self.client.delete_blob(&key.object_key()).await?;
let mut tx = self.pg_pool.begin().await?; let mut tx = self.pg_pool.begin().await?;
delete_blob_metadata(&mut tx, key.workspace_id(), &key.meta_key()).await?; delete_blob_metadata(&mut tx, key.workspace_id(), &key.blob_metadata_key()).await?;
tx.commit().await?; tx.commit().await?;
Ok(()) Ok(())
} }
@ -135,9 +135,9 @@ where
pub async fn get_blob_metadata( pub async fn get_blob_metadata(
&self, &self,
workspace_id: &Uuid, workspace_id: &Uuid,
meta_key: &str, store_key: &str,
) -> Result<AFBlobMetadataRow, AppError> { ) -> Result<AFBlobMetadataRow, AppError> {
let metadata = get_blob_metadata(&self.pg_pool, workspace_id, meta_key).await?; let metadata = get_blob_metadata(&self.pg_pool, workspace_id, store_key).await?;
Ok(metadata) Ok(metadata)
} }
@ -180,7 +180,7 @@ where
self.client.complete_upload(&key.object_key(), req).await?; self.client.complete_upload(&key.object_key(), req).await?;
insert_blob_metadata( insert_blob_metadata(
&self.pg_pool, &self.pg_pool,
&key.meta_key(), &key.blob_metadata_key(),
key.workspace_id(), key.workspace_id(),
&content_type, &content_type,
content_length, content_length,

View File

@ -163,7 +163,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
enable: get_env_var("APPFLOWY_INDEXER_ENABLED", "true") enable: get_env_var("APPFLOWY_INDEXER_ENABLED", "true")
.parse::<bool>() .parse::<bool>()
.unwrap_or(true), .unwrap_or(true),
openai_api_key: Secret::new(get_env_var("APPFLOWY_AI_OPENAI_API_KEY", "")), openai_api_key: Secret::new(get_env_var("AI_OPENAI_API_KEY", "")),
embedding_buffer_size: get_env_var("APPFLOWY_INDEXER_EMBEDDING_BUFFER_SIZE", "2000") embedding_buffer_size: get_env_var("APPFLOWY_INDEXER_EMBEDDING_BUFFER_SIZE", "2000")
.parse::<usize>() .parse::<usize>()
.unwrap_or(2000), .unwrap_or(2000),

View File

@ -210,7 +210,7 @@ pub fn get_configuration() -> Result<Config, anyhow::Error> {
redis_worker_count: get_env_var("APPFLOWY_REDIS_WORKERS", "60").parse()?, redis_worker_count: get_env_var("APPFLOWY_REDIS_WORKERS", "60").parse()?,
ai: AISettings { ai: AISettings {
port: get_env_var("APPFLOWY_AI_SERVER_PORT", "5001").parse()?, port: get_env_var("APPFLOWY_AI_SERVER_PORT", "5001").parse()?,
host: get_env_var("APPFLOWY_AI_SERVER_HOST", "localhost"), host: get_env_var("AI_SERVER_HOST", "localhost"),
}, },
}; };
Ok(config) Ok(config)

View File

@ -145,7 +145,7 @@ pub async fn create_app(listener: TcpListener, config: Config) -> Result<(), Err
.parse::<bool>() .parse::<bool>()
.unwrap_or(true), .unwrap_or(true),
open_api_key: Secret::new(appflowy_collaborate::config::get_env_var( open_api_key: Secret::new(appflowy_collaborate::config::get_env_var(
"APPFLOWY_AI_OPENAI_API_KEY", "AI_OPENAI_API_KEY",
"", "",
)), )),
tick_interval_secs: 10, tick_interval_secs: 10,

View File

@ -10,7 +10,8 @@ use serde::Deserialize;
use crate::api::util::ai_model_from_header; use crate::api::util::ai_model_from_header;
use app_error::AppError; use app_error::AppError;
use appflowy_ai_client::dto::{ use appflowy_ai_client::dto::{
ChatQuestion, ChatQuestionQuery, CreateChatContext, MessageData, RepeatedRelatedQuestion, ChatQuestion, ChatQuestionQuery, CreateChatContext, MessageData, QuestionMetadata,
RepeatedRelatedQuestion,
}; };
use authentication::jwt::UserUuid; use authentication::jwt::UserUuid;
use bytes::Bytes; use bytes::Bytes;
@ -88,7 +89,7 @@ pub fn chat_scope() -> Scope {
) )
.service( .service(
web::resource("/{chat_id}/{message_id}/v2/answer/stream") web::resource("/{chat_id}/{message_id}/v2/answer/stream")
.route(web::get().to(answer_stream_v2_handler)) .route(web::get().to(answer_stream_v2_handler)) // Deprecated since 0.9.2
) )
.service( .service(
web::resource("/{chat_id}/answer/stream") web::resource("/{chat_id}/answer/stream")
@ -140,13 +141,22 @@ async fn create_chat_context_handler(
} }
async fn update_question_handler( async fn update_question_handler(
path: web::Path<(String, String)>,
state: Data<AppState>, state: Data<AppState>,
payload: Json<UpdateChatMessageContentParams>, payload: Json<UpdateChatMessageContentParams>,
req: HttpRequest, req: HttpRequest,
) -> actix_web::Result<JsonAppResponse<()>> { ) -> actix_web::Result<JsonAppResponse<()>> {
let (workspace_id, _chat_id) = path.into_inner();
let params = payload.into_inner(); let params = payload.into_inner();
let ai_model = ai_model_from_header(&req); let ai_model = ai_model_from_header(&req);
update_chat_message(&state.pg_pool, params, state.ai_client.clone(), ai_model).await?; update_chat_message(
workspace_id,
&state.pg_pool,
params,
state.ai_client.clone(),
ai_model,
)
.await?;
Ok(AppResponse::Ok().into()) Ok(AppResponse::Ok().into())
} }
@ -236,9 +246,10 @@ async fn answer_handler(
state: Data<AppState>, state: Data<AppState>,
req: HttpRequest, req: HttpRequest,
) -> actix_web::Result<JsonAppResponse<ChatMessage>> { ) -> actix_web::Result<JsonAppResponse<ChatMessage>> {
let (_workspace_id, chat_id, message_id) = path.into_inner(); let (workspace_id, chat_id, message_id) = path.into_inner();
let ai_model = ai_model_from_header(&req); let ai_model = ai_model_from_header(&req);
let message = generate_chat_message_answer( let message = generate_chat_message_answer(
workspace_id,
&state.pg_pool, &state.pg_pool,
state.ai_client.clone(), state.ai_client.clone(),
message_id, message_id,
@ -255,14 +266,21 @@ async fn answer_stream_handler(
state: Data<AppState>, state: Data<AppState>,
req: HttpRequest, req: HttpRequest,
) -> actix_web::Result<HttpResponse> { ) -> actix_web::Result<HttpResponse> {
let (_workspace_id, chat_id, question_id) = path.into_inner(); let (workspace_id, chat_id, question_id) = path.into_inner();
let (content, metadata) = let (content, metadata) =
chat::chat_ops::select_chat_message_content(&state.pg_pool, question_id).await?; chat::chat_ops::select_chat_message_content(&state.pg_pool, question_id).await?;
let rag_ids = chat::chat_ops::select_chat_rag_ids(&state.pg_pool, &chat_id).await?; let rag_ids = chat::chat_ops::select_chat_rag_ids(&state.pg_pool, &chat_id).await?;
let ai_model = ai_model_from_header(&req); let ai_model = ai_model_from_header(&req);
match state match state
.ai_client .ai_client
.stream_question(&chat_id, &content, Some(metadata), rag_ids, &ai_model) .stream_question(
workspace_id,
&chat_id,
&content,
Some(metadata),
rag_ids,
&ai_model,
)
.await .await
{ {
Ok(answer_stream) => { Ok(answer_stream) => {
@ -289,7 +307,7 @@ async fn answer_stream_v2_handler(
state: Data<AppState>, state: Data<AppState>,
req: HttpRequest, req: HttpRequest,
) -> actix_web::Result<HttpResponse> { ) -> actix_web::Result<HttpResponse> {
let (_workspace_id, chat_id, question_id) = path.into_inner(); let (workspace_id, chat_id, question_id) = path.into_inner();
let (content, metadata) = let (content, metadata) =
chat::chat_ops::select_chat_message_content(&state.pg_pool, question_id).await?; chat::chat_ops::select_chat_message_content(&state.pg_pool, question_id).await?;
let rag_ids = chat::chat_ops::select_chat_rag_ids(&state.pg_pool, &chat_id).await?; let rag_ids = chat::chat_ops::select_chat_rag_ids(&state.pg_pool, &chat_id).await?;
@ -304,6 +322,7 @@ async fn answer_stream_v2_handler(
match state match state
.ai_client .ai_client
.stream_question_v2( .stream_question_v2(
workspace_id,
&chat_id, &chat_id,
question_id, question_id,
&content, &content,
@ -333,10 +352,12 @@ async fn answer_stream_v2_handler(
#[instrument(level = "debug", skip_all, err)] #[instrument(level = "debug", skip_all, err)]
async fn answer_stream_v3_handler( async fn answer_stream_v3_handler(
path: web::Path<(String, String)>,
payload: Json<ChatQuestionQuery>, payload: Json<ChatQuestionQuery>,
state: Data<AppState>, state: Data<AppState>,
req: HttpRequest, req: HttpRequest,
) -> actix_web::Result<HttpResponse> { ) -> actix_web::Result<HttpResponse> {
let (workspace_id, _) = path.into_inner();
let payload = payload.into_inner(); let payload = payload.into_inner();
let (content, metadata) = let (content, metadata) =
chat::chat_ops::select_chat_message_content(&state.pg_pool, payload.question_id).await?; chat::chat_ops::select_chat_message_content(&state.pg_pool, payload.question_id).await?;
@ -348,10 +369,13 @@ async fn answer_stream_v3_handler(
data: MessageData { data: MessageData {
content: content.to_string(), content: content.to_string(),
metadata: Some(metadata), metadata: Some(metadata),
rag_ids,
message_id: Some(payload.question_id.to_string()), message_id: Some(payload.question_id.to_string()),
}, },
format: payload.format, format: payload.format,
metadata: QuestionMetadata {
workspace_id,
rag_ids,
},
}; };
trace!("[Chat] stream v3 {:?}", question); trace!("[Chat] stream v3 {:?}", question);
match state match state

View File

@ -357,7 +357,7 @@ async fn get_blob_by_object_key(
// Get the metadata // Get the metadata
let result = state let result = state
.bucket_storage .bucket_storage
.get_blob_metadata(key.workspace_id(), &key.meta_key()) .get_blob_metadata(key.workspace_id(), &key.blob_metadata_key())
.await; .await;
if let Err(err) = result.as_ref() { if let Err(err) = result.as_ref() {
@ -424,7 +424,7 @@ async fn get_blob_metadata_handler(
// Get the metadata // Get the metadata
let metadata = state let metadata = state
.bucket_storage .bucket_storage
.get_blob_metadata(&path.workspace_id, &path.meta_key()) .get_blob_metadata(&path.workspace_id, &path.blob_metadata_key())
.await .await
.map(|meta| BlobMetadata { .map(|meta| BlobMetadata {
workspace_id: meta.workspace_id, workspace_id: meta.workspace_id,
@ -448,7 +448,7 @@ async fn get_blob_metadata_v1_handler(
// Get the metadata // Get the metadata
let metadata = state let metadata = state
.bucket_storage .bucket_storage
.get_blob_metadata(&path.workspace_id, &path.meta_key()) .get_blob_metadata(&path.workspace_id, &path.blob_metadata_key())
.await .await
.map(|meta| BlobMetadata { .map(|meta| BlobMetadata {
workspace_id: meta.workspace_id, workspace_id: meta.workspace_id,
@ -589,7 +589,7 @@ impl BlobKey for BlobPathV0 {
format!("{}/{}", self.workspace_id, self.file_id) format!("{}/{}", self.workspace_id, self.file_id)
} }
fn meta_key(&self) -> String { fn blob_metadata_key(&self) -> String {
self.file_id.clone() self.file_id.clone()
} }
@ -615,7 +615,7 @@ impl BlobKey for BlobPathV1 {
format!("{}/{}/{}", self.workspace_id, self.parent_dir, self.file_id) format!("{}/{}/{}", self.workspace_id, self.parent_dir, self.file_id)
} }
fn meta_key(&self) -> String { fn blob_metadata_key(&self) -> String {
format!("{}_{}", self.parent_dir, self.file_id) format!("{}_{}", self.parent_dir, self.file_id)
} }

View File

@ -332,7 +332,7 @@ pub async fn init_state(config: &Config, rt_cmd_tx: CLCommandSender) -> Result<A
enable: get_env_var("APPFLOWY_INDEXER_ENABLED", "true") enable: get_env_var("APPFLOWY_INDEXER_ENABLED", "true")
.parse::<bool>() .parse::<bool>()
.unwrap_or(true), .unwrap_or(true),
openai_api_key: Secret::new(get_env_var("APPFLOWY_AI_OPENAI_API_KEY", "")), openai_api_key: Secret::new(get_env_var("AI_OPENAI_API_KEY", "")),
embedding_buffer_size: appflowy_collaborate::config::get_env_var( embedding_buffer_size: appflowy_collaborate::config::get_env_var(
"APPFLOWY_INDEXER_EMBEDDING_BUFFER_SIZE", "APPFLOWY_INDEXER_EMBEDDING_BUFFER_SIZE",
"5000", "5000",

View File

@ -42,6 +42,7 @@ pub(crate) async fn delete_chat(pg_pool: &PgPool, chat_id: &str) -> Result<(), A
} }
pub async fn update_chat_message( pub async fn update_chat_message(
workspace_id: String,
pg_pool: &PgPool, pg_pool: &PgPool,
params: UpdateChatMessageContentParams, params: UpdateChatMessageContentParams,
ai_client: AppFlowyAIClient, ai_client: AppFlowyAIClient,
@ -60,6 +61,7 @@ pub async fn update_chat_message(
// TODO(nathan): query the metadata from the database // TODO(nathan): query the metadata from the database
let new_answer = ai_client let new_answer = ai_client
.send_question( .send_question(
&workspace_id,
&params.chat_id, &params.chat_id,
params.message_id, params.message_id,
&params.content, &params.content,
@ -81,6 +83,7 @@ pub async fn update_chat_message(
} }
pub async fn generate_chat_message_answer( pub async fn generate_chat_message_answer(
workspace_id: String,
pg_pool: &PgPool, pg_pool: &PgPool,
ai_client: AppFlowyAIClient, ai_client: AppFlowyAIClient,
question_message_id: i64, question_message_id: i64,
@ -91,6 +94,7 @@ pub async fn generate_chat_message_answer(
chat::chat_ops::select_chat_message_content(pg_pool, question_message_id).await?; chat::chat_ops::select_chat_message_content(pg_pool, question_message_id).await?;
let new_answer = ai_client let new_answer = ai_client
.send_question( .send_question(
&workspace_id,
chat_id, chat_id,
question_message_id, question_message_id,
&content, &content,
@ -145,6 +149,7 @@ pub async fn create_chat_message(
pub async fn create_chat_message_stream( pub async fn create_chat_message_stream(
pg_pool: &PgPool, pg_pool: &PgPool,
uid: i64, uid: i64,
workspace_id: String,
chat_id: String, chat_id: String,
params: CreateChatMessageParams, params: CreateChatMessageParams,
ai_client: AppFlowyAIClient, ai_client: AppFlowyAIClient,
@ -186,7 +191,7 @@ pub async fn create_chat_message_stream(
match params.message_type { match params.message_type {
ChatMessageType::System => {} ChatMessageType::System => {}
ChatMessageType::User => { ChatMessageType::User => {
let answer = match ai_client.send_question(&chat_id,question_id, &params.content, &ai_model, Some(json!(params.metadata))).await { let answer = match ai_client.send_question(&workspace_id, &chat_id,question_id, &params.content, &ai_model, Some(json!(params.metadata))).await {
Ok(response) => response, Ok(response) => response,
Err(err) => { Err(err) => {
error!("Failed to send question to AI: {}", err); error!("Failed to send question to AI: {}", err);

View File

@ -241,8 +241,8 @@ pub fn get_configuration() -> Result<Config, anyhow::Error> {
region: get_env_var("APPFLOWY_S3_REGION", ""), region: get_env_var("APPFLOWY_S3_REGION", ""),
}, },
appflowy_ai: AppFlowyAISetting { appflowy_ai: AppFlowyAISetting {
port: get_env_var("APPFLOWY_AI_SERVER_PORT", "5001").into(), port: get_env_var("AI_SERVER_PORT", "5001").into(),
host: get_env_var("APPFLOWY_AI_SERVER_HOST", "localhost").into(), host: get_env_var("AI_SERVER_HOST", "localhost").into(),
}, },
grpc_history: GrpcHistorySetting { grpc_history: GrpcHistorySetting {
addrs: get_env_var("APPFLOWY_GRPC_HISTORY_ADDRS", "http://localhost:50051"), addrs: get_env_var("APPFLOWY_GRPC_HISTORY_ADDRS", "http://localhost:50051"),