feat: custom completion prompt (#906)

* feat: custom completion prompt

* chore: custom prompt
This commit is contained in:
Nathan.fooo 2024-10-20 14:27:41 +08:00 committed by GitHub
parent 57c44818e2
commit 2f715c3136
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 75 additions and 33 deletions

View File

@ -1,6 +1,6 @@
use crate::dto::{ use crate::dto::{
AIModel, ChatAnswer, ChatQuestion, CompleteTextResponse, CompletionType, CreateTextChatContext, AIModel, ChatAnswer, ChatQuestion, CompleteTextResponse, CompletionType, CreateTextChatContext,
Document, EmbeddingRequest, EmbeddingResponse, LocalAIConfig, MessageData, CustomPrompt, Document, EmbeddingRequest, EmbeddingResponse, LocalAIConfig, MessageData,
RepeatedLocalAIPackage, RepeatedRelatedQuestion, SearchDocumentsRequest, SummarizeRowResponse, RepeatedLocalAIPackage, RepeatedRelatedQuestion, SearchDocumentsRequest, SummarizeRowResponse,
TranslateRowData, TranslateRowResponse, TranslateRowData, TranslateRowResponse,
}; };
@ -49,19 +49,29 @@ impl AppFlowyAIClient {
Ok(()) Ok(())
} }
pub async fn completion_text( pub async fn completion_text<T: Into<Option<CompletionType>>>(
&self, &self,
text: &str, text: &str,
completion_type: CompletionType, completion_type: T,
custom_prompt: Option<CustomPrompt>,
model: AIModel, model: AIModel,
) -> Result<CompleteTextResponse, AIError> { ) -> Result<CompleteTextResponse, AIError> {
let completion_type = completion_type.into();
if completion_type.is_some() && custom_prompt.is_some() {
return Err(AIError::InvalidRequest(
"Cannot specify both completion_type and custom_prompt".to_string(),
));
}
if text.is_empty() { if text.is_empty() {
return Err(AIError::InvalidRequest("Empty text".to_string())); return Err(AIError::InvalidRequest("Empty text".to_string()));
} }
let params = json!({ let params = json!({
"text": text, "text": text,
"type": completion_type as u8, "type": completion_type.map(|t| t as u8),
"custom_prompt": custom_prompt,
}); });
let url = format!("{}/completion", self.url); let url = format!("{}/completion", self.url);
@ -76,19 +86,22 @@ impl AppFlowyAIClient {
.into_data() .into_data()
} }
pub async fn stream_completion_text( pub async fn stream_completion_text<T: Into<Option<CompletionType>>>(
&self, &self,
text: &str, text: &str,
completion_type: CompletionType, completion_type: T,
custom_prompt: Option<CustomPrompt>,
model: AIModel, model: AIModel,
) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> { ) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> {
let completion_type = completion_type.into();
if text.is_empty() { if text.is_empty() {
return Err(AIError::InvalidRequest("Empty text".to_string())); return Err(AIError::InvalidRequest("Empty text".to_string()));
} }
let params = json!({ let params = json!({
"text": text, "text": text,
"type": completion_type as u8, "type": completion_type.map(|t| t as u8),
"custom_prompt": custom_prompt,
}); });
let url = format!("{}/completion/stream", self.url); let url = format!("{}/completion/stream", self.url);
@ -201,7 +214,7 @@ impl AppFlowyAIClient {
chat_id: &str, chat_id: &str,
content: &str, content: &str,
model: &AIModel, model: &AIModel,
metadata: Option<serde_json::Value>, metadata: Option<Value>,
) -> Result<ChatAnswer, AIError> { ) -> Result<ChatAnswer, AIError> {
let json = ChatQuestion { let json = ChatQuestion {
chat_id: chat_id.to_string(), chat_id: chat_id.to_string(),
@ -226,7 +239,7 @@ impl AppFlowyAIClient {
&self, &self,
chat_id: &str, chat_id: &str,
content: &str, content: &str,
metadata: Option<serde_json::Value>, metadata: Option<Value>,
model: &AIModel, model: &AIModel,
) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> { ) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> {
let json = ChatQuestion { let json = ChatQuestion {
@ -251,7 +264,7 @@ impl AppFlowyAIClient {
&self, &self,
chat_id: &str, chat_id: &str,
content: &str, content: &str,
metadata: Option<serde_json::Value>, metadata: Option<Value>,
model: &AIModel, model: &AIModel,
) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> { ) -> Result<impl Stream<Item = Result<Bytes, AIError>>, AIError> {
let json = ChatQuestion { let json = ChatQuestion {
@ -337,7 +350,7 @@ pub struct AIResponse<T> {
impl<T> AIResponse<T> impl<T> AIResponse<T>
where where
T: serde::de::DeserializeOwned + 'static, T: DeserializeOwned + 'static,
{ {
pub async fn from_response(resp: reqwest::Response) -> Result<Self, anyhow::Error> { pub async fn from_response(resp: reqwest::Response) -> Result<Self, anyhow::Error> {
let status_code = resp.status(); let status_code = resp.status();

View File

@ -205,7 +205,7 @@ impl Display for EmbeddingsModel {
pub enum AIModel { pub enum AIModel {
#[default] #[default]
DefaultModel = 0, DefaultModel = 0,
GPT35 = 1, GPT4oMini = 1,
GPT4o = 2, GPT4o = 2,
Claude3Sonnet = 3, Claude3Sonnet = 3,
Claude3Opus = 4, Claude3Opus = 4,
@ -215,7 +215,7 @@ impl AIModel {
pub fn to_str(&self) -> &str { pub fn to_str(&self) -> &str {
match self { match self {
AIModel::DefaultModel => "default-model", AIModel::DefaultModel => "default-model",
AIModel::GPT35 => "gpt-3.5-turbo", AIModel::GPT4oMini => "gpt-4o-mini",
AIModel::GPT4o => "gpt-4o", AIModel::GPT4o => "gpt-4o",
AIModel::Claude3Sonnet => "claude-3-sonnet", AIModel::Claude3Sonnet => "claude-3-sonnet",
AIModel::Claude3Opus => "claude-3-opus", AIModel::Claude3Opus => "claude-3-opus",
@ -228,7 +228,8 @@ impl FromStr for AIModel {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"gpt-3.5-turbo" => Ok(AIModel::GPT35), "gpt-3.5-turbo" => Ok(AIModel::GPT4oMini),
"gpt-4o-mini" => Ok(AIModel::GPT4oMini),
"gpt-4o" => Ok(AIModel::GPT4o), "gpt-4o" => Ok(AIModel::GPT4o),
"claude-3-sonnet" => Ok(AIModel::Claude3Sonnet), "claude-3-sonnet" => Ok(AIModel::Claude3Sonnet),
"claude-3-opus" => Ok(AIModel::Claude3Opus), "claude-3-opus" => Ok(AIModel::Claude3Opus),
@ -364,3 +365,9 @@ impl Display for CreateTextChatContext {
)) ))
} }
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CustomPrompt {
pub system: String,
pub user: Option<String>,
}

View File

@ -8,6 +8,7 @@ async fn continue_writing_test() {
.completion_text( .completion_text(
"I feel hungry", "I feel hungry",
CompletionType::ContinueWriting, CompletionType::ContinueWriting,
None,
AIModel::Claude3Sonnet, AIModel::Claude3Sonnet,
) )
.await .await
@ -23,7 +24,8 @@ async fn improve_writing_test() {
.completion_text( .completion_text(
"I fell tired because i sleep not very well last night", "I fell tired because i sleep not very well last night",
CompletionType::ImproveWriting, CompletionType::ImproveWriting,
AIModel::GPT35, None,
AIModel::GPT4oMini,
) )
.await .await
.unwrap(); .unwrap();
@ -39,7 +41,8 @@ async fn make_text_shorter_text() {
.stream_completion_text( .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", "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, CompletionType::MakeShorter,
AIModel::GPT35 None,
AIModel::GPT4oMini
) )
.await .await
.unwrap(); .unwrap();

View File

@ -14,7 +14,7 @@ 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, "Where I live?", &AIModel::GPT35, None) .send_question(&chat_id, "Where I live?", &AIModel::GPT4oMini, None)
.await .await
.unwrap(); .unwrap();
// response will be something like: // response will be something like:

View File

@ -10,13 +10,13 @@ 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, "I feel hungry", &AIModel::GPT35, None) .send_question(&chat_id, "I feel hungry", &AIModel::GPT4o, None)
.await .await
.unwrap(); .unwrap();
assert!(!resp.content.is_empty()); assert!(!resp.content.is_empty());
let questions = client let questions = client
.get_related_question(&chat_id, &1, &AIModel::GPT35) .get_related_question(&chat_id, &1, &AIModel::GPT4oMini)
.await .await
.unwrap() .unwrap()
.items; .items;
@ -29,7 +29,7 @@ async fn stop_stream_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 mut stream = client let mut stream = client
.stream_question(&chat_id, "I feel hungry", None, &AIModel::GPT35) .stream_question(&chat_id, "I feel hungry", None, &AIModel::GPT4oMini)
.await .await
.unwrap(); .unwrap();
@ -51,7 +51,7 @@ async fn stream_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 stream = client let stream = client
.stream_question_v2(&chat_id, "I feel hungry", None, &AIModel::GPT35) .stream_question_v2(&chat_id, "I feel hungry", None, &AIModel::GPT4oMini)
.await .await
.unwrap(); .unwrap();
let json_stream = JsonStream::<serde_json::Value>::new(stream); let json_stream = JsonStream::<serde_json::Value>::new(stream);

View File

@ -9,7 +9,7 @@ async fn summarize_row_test() {
let json = json!({"name": "Jack", "age": 25, "city": "New York"}); let json = json!({"name": "Jack", "age": 25, "city": "New York"});
let result = client let result = client
.summarize_row(json.as_object().unwrap(), AIModel::GPT35) .summarize_row(json.as_object().unwrap(), AIModel::GPT4oMini)
.await .await
.unwrap(); .unwrap();
result.text.contains("Jack"); result.text.contains("Jack");

View File

@ -20,6 +20,9 @@ async fn translate_row_test() {
include_header: false, include_header: false,
}; };
let result = client.translate_row(data, AIModel::GPT35).await.unwrap(); let result = client
.translate_row(data, AIModel::GPT4oMini)
.await
.unwrap();
assert_eq!(result.items.len(), 2); assert_eq!(result.items.len(), 2);
} }

View File

@ -172,7 +172,7 @@ impl Client {
); );
} }
let ai_model = Arc::new(RwLock::new(AIModel::GPT35)); let ai_model = Arc::new(RwLock::new(AIModel::GPT4oMini));
Self { Self {
base_url: base_url.to_string(), base_url: base_url.to_string(),

View File

@ -34,7 +34,18 @@ pub struct SummarizeRowResponse {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompleteTextParams { pub struct CompleteTextParams {
pub text: String, pub text: String,
pub completion_type: CompletionType, 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)] #[derive(Debug)]

View File

@ -36,7 +36,7 @@ async fn complete_text_handler(
let params = payload.into_inner(); let params = payload.into_inner();
let resp = state let resp = state
.ai_client .ai_client
.completion_text(&params.text, params.completion_type, ai_model) .completion_text(&params.text, params.completion_type, None, ai_model)
.await .await
.map_err(|err| AppError::Internal(err.into()))?; .map_err(|err| AppError::Internal(err.into()))?;
Ok(AppResponse::Ok().with_data(resp).into()) Ok(AppResponse::Ok().with_data(resp).into())
@ -51,7 +51,12 @@ async fn stream_complete_text_handler(
let params = payload.into_inner(); let params = payload.into_inner();
match state match state
.ai_client .ai_client
.stream_completion_text(&params.text, params.completion_type, ai_model) .stream_completion_text(
&params.text,
params.completion_type,
params.custom_prompt,
ai_model,
)
.await .await
{ {
Ok(stream) => Ok( Ok(stream) => Ok(

View File

@ -179,5 +179,5 @@ pub(crate) fn ai_model_from_header(req: &HttpRequest) -> AIModel {
let header = header.to_str().ok()?; let header = header.to_str().ok()?;
AIModel::from_str(header).ok() AIModel::from_str(header).ok()
}) })
.unwrap_or(AIModel::GPT35) .unwrap_or(AIModel::GPT4oMini)
} }

View File

@ -8,13 +8,13 @@ async fn improve_writing_test() {
return; return;
} }
let test_client = TestClient::new_user().await; let test_client = TestClient::new_user().await;
test_client.api_client.set_ai_model(AIModel::GPT4o); test_client.api_client.set_ai_model(AIModel::GPT4oMini);
let workspace_id = test_client.workspace_id().await; let workspace_id = test_client.workspace_id().await;
let params = CompleteTextParams { let params = CompleteTextParams::new_with_completion_type(
text: "I feel hungry".to_string(), "I feel hungry".to_string(),
completion_type: CompletionType::ImproveWriting, CompletionType::ImproveWriting,
}; );
let resp = test_client let resp = test_client
.api_client .api_client