From 101e0f77a748c9e6b42b04d722910df1b93c05a4 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 21:29:11 +0200 Subject: [PATCH 01/24] feat: add Recommender with async recommendation generation --- paper/src/api/mod.rs | 2 ++ paper/src/api/recommender.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 paper/src/api/recommender.rs diff --git a/paper/src/api/mod.rs b/paper/src/api/mod.rs index 6538136..6865d3e 100644 --- a/paper/src/api/mod.rs +++ b/paper/src/api/mod.rs @@ -1,3 +1,5 @@ pub use self::api_client::APIClient; +pub use self::recommender::Recommender; mod api_client; +mod recommender; diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs new file mode 100644 index 0000000..deaafde --- /dev/null +++ b/paper/src/api/recommender.rs @@ -0,0 +1,30 @@ +use crate::error::PaperError; +use futures::future; + +#[derive(uniffi::Object)] +pub struct Recommender { + client: reqwest::Client, +} + +#[uniffi::export] +impl Recommender { + #[uniffi::constructor] + pub fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + pub async fn get_recommendations(&self, titles: Vec) -> Result, PaperError> { + // Simulate async recommendation generation + let recommendations = future::join_all(titles.into_iter().map(|title| async move { + // Here you would make actual API calls to get recommendations + // For now just return a mock recommendation + Ok(format!("Recommendation based on: {}", title)) + })) + .await; + + // Collect results + recommendations.into_iter().collect() + } +} From 5253340efa7b1bbc43b314950cfc12d336971e01 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 7 Jun 2025 21:35:17 +0200 Subject: [PATCH 02/24] remove legacy renewal token parser --- paper/src/authenticators/public_hamburg_authenticator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper/src/authenticators/public_hamburg_authenticator.rs b/paper/src/authenticators/public_hamburg_authenticator.rs index 98cd949..f5a5525 100644 --- a/paper/src/authenticators/public_hamburg_authenticator.rs +++ b/paper/src/authenticators/public_hamburg_authenticator.rs @@ -1,9 +1,9 @@ use super::{LoginResult, RawLoansPage}; +use crate::configuration::Configuration; use crate::error::PaperError; use crate::model::Loans; use crate::scrapers::public_hamburg::LoansScraper; use crate::token_scraper::TokenScraper; -use crate::{configuration::Configuration}; use reqwest::{ header::{HeaderMap, HeaderValue}, Client, From 7bb6c18b31aa3be889d7cdbf718173c4d938d91f Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 22:09:54 +0200 Subject: [PATCH 03/24] feat: add async-openai dependency to paper Cargo.toml --- paper/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/paper/Cargo.toml b/paper/Cargo.toml index 513936e..d8a6170 100644 --- a/paper/Cargo.toml +++ b/paper/Cargo.toml @@ -32,6 +32,7 @@ uniffi_bindgen = { version = "=0.28.0" } chrono = "0.4.38" uuid = { version = "1.4", features = ["v4"] } roxmltree = "0.20.0" +async-openai = "0.16.1" [build-dependencies] uniffi = { workspace = true, features = ["build"] } From dce23b583ba2ead6d5e126bd94770e5a93ff93a6 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 22:15:20 +0200 Subject: [PATCH 04/24] feat: integrate async-openai for book recommendations in recommender.rs --- paper/src/api/recommender.rs | 50 ++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index deaafde..b175bae 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -1,9 +1,13 @@ +use async_openai::{ + types::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs, Role}, + Client, +}; use crate::error::PaperError; use futures::future; #[derive(uniffi::Object)] pub struct Recommender { - client: reqwest::Client, + client: Client, } #[uniffi::export] @@ -11,20 +15,50 @@ impl Recommender { #[uniffi::constructor] pub fn new() -> Self { Self { - client: reqwest::Client::new(), + client: Client::new(), } } pub async fn get_recommendations(&self, titles: Vec) -> Result, PaperError> { - // Simulate async recommendation generation - let recommendations = future::join_all(titles.into_iter().map(|title| async move { - // Here you would make actual API calls to get recommendations - // For now just return a mock recommendation - Ok(format!("Recommendation based on: {}", title)) + let recommendations = future::join_all(titles.into_iter().map(|title| async { + let messages = vec![ + ChatCompletionRequestMessage { + role: Role::System, + content: "You are a helpful librarian making book recommendations.".into(), + name: None, + function_call: None, + }, + ChatCompletionRequestMessage { + role: Role::User, + content: format!("Suggest one similar book to '{}' and return just the title.", title), + name: None, + function_call: None, + }, + ]; + + let request = CreateChatCompletionRequestArgs::default() + .model("gpt-3.5-turbo") + .messages(messages) + .max_tokens(50_u16) + .build()?; + + match self.client.chat().create(request).await { + Ok(response) => { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.message.content { + Ok(content.trim().to_string()) + } else { + Err(PaperError::GeneralError) + } + } else { + Err(PaperError::GeneralError) + } + } + Err(_) => Err(PaperError::GeneralError) + } })) .await; - // Collect results recommendations.into_iter().collect() } } From 02002e2bf267b95bc39658a3a0c447ce5f5feee3 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 22:16:34 +0200 Subject: [PATCH 05/24] feat: add API key parameter to get_recommendations function --- paper/src/api/recommender.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index b175bae..8629d7d 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -19,7 +19,8 @@ impl Recommender { } } - pub async fn get_recommendations(&self, titles: Vec) -> Result, PaperError> { + pub async fn get_recommendations(&self, titles: Vec, api_key: String) -> Result, PaperError> { + std::env::set_var("OPENAI_API_KEY", api_key); let recommendations = future::join_all(titles.into_iter().map(|title| async { let messages = vec![ ChatCompletionRequestMessage { From 4fe939e01a6dcc3dc6d5b00ee61391548a5cf774 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 22:18:02 +0200 Subject: [PATCH 06/24] refactor: modify API key configuration to use Client config instead of environment variable --- paper/src/api/recommender.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 8629d7d..915f33e 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -20,7 +20,7 @@ impl Recommender { } pub async fn get_recommendations(&self, titles: Vec, api_key: String) -> Result, PaperError> { - std::env::set_var("OPENAI_API_KEY", api_key); + let client = Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); let recommendations = future::join_all(titles.into_iter().map(|title| async { let messages = vec![ ChatCompletionRequestMessage { From becc0ce63bd5a14622d21e4667efb80f904e1242 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 7 Jun 2025 22:36:10 +0200 Subject: [PATCH 07/24] refactor: update Recommender to use dynamic client and GPT-4o model --- paper/src/api/recommender.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 915f33e..7f8b49b 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -1,26 +1,27 @@ +use crate::error::PaperError; use async_openai::{ types::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs, Role}, Client, }; -use crate::error::PaperError; use futures::future; #[derive(uniffi::Object)] -pub struct Recommender { - client: Client, -} +pub struct Recommender {} #[uniffi::export] impl Recommender { #[uniffi::constructor] pub fn new() -> Self { - Self { - client: Client::new(), - } + Self {} } - pub async fn get_recommendations(&self, titles: Vec, api_key: String) -> Result, PaperError> { - let client = Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); + pub async fn get_recommendations( + &self, + titles: Vec, + api_key: String, + ) -> Result, PaperError> { + let client = + Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); let recommendations = future::join_all(titles.into_iter().map(|title| async { let messages = vec![ ChatCompletionRequestMessage { @@ -31,19 +32,24 @@ impl Recommender { }, ChatCompletionRequestMessage { role: Role::User, - content: format!("Suggest one similar book to '{}' and return just the title.", title), + content: format!( + "Suggest one similar book to '{}' and return just the title.", + title + ), name: None, function_call: None, }, ]; let request = CreateChatCompletionRequestArgs::default() - .model("gpt-3.5-turbo") + .model("gpt-4o") + .store(false) .messages(messages) .max_tokens(50_u16) - .build()?; + .build() + .expect("do it"); - match self.client.chat().create(request).await { + match client.chat().create(request).await { Ok(response) => { if let Some(choice) = response.choices.first() { if let Some(content) = &choice.message.content { @@ -55,7 +61,7 @@ impl Recommender { Err(PaperError::GeneralError) } } - Err(_) => Err(PaperError::GeneralError) + Err(_) => Err(PaperError::GeneralError), } })) .await; From a4a35abf766e42aab95b396b6d1a497dcabe1d76 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 22:36:12 +0200 Subject: [PATCH 08/24] fix: Update async-openai message types and error handling --- paper/src/api/recommender.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 7f8b49b..23eba1c 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -24,30 +24,25 @@ impl Recommender { Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); let recommendations = future::join_all(titles.into_iter().map(|title| async { let messages = vec![ - ChatCompletionRequestMessage { - role: Role::System, + async_openai::types::ChatCompletionRequestSystemMessage { content: "You are a helpful librarian making book recommendations.".into(), name: None, - function_call: None, + role: Role::System, }, - ChatCompletionRequestMessage { - role: Role::User, - content: format!( - "Suggest one similar book to '{}' and return just the title.", - title + async_openai::types::ChatCompletionRequestUserMessage { + content: async_openai::types::ChatCompletionRequestUserMessageContent::Text( + format!("Suggest one similar book to '{}' and return just the title.", title) ), name: None, - function_call: None, + role: Role::User, }, ]; let request = CreateChatCompletionRequestArgs::default() - .model("gpt-4o") - .store(false) + .model("gpt-3.5-turbo") .messages(messages) .max_tokens(50_u16) - .build() - .expect("do it"); + .build()?; match client.chat().create(request).await { Ok(response) => { From e8fd3c8bbb88a32c50337b54dc0865132756fe3b Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 7 Jun 2025 22:58:08 +0200 Subject: [PATCH 09/24] refactor: update OpenAI API call with improved type handling and model selection --- paper/src/api/recommender.rs | 63 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 23eba1c..e6ad888 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -1,6 +1,9 @@ use crate::error::PaperError; use async_openai::{ - types::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs, Role}, + types::{ + ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageArgs, + ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs, + }, Client, }; use futures::future; @@ -22,41 +25,41 @@ impl Recommender { ) -> Result, PaperError> { let client = Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); - let recommendations = future::join_all(titles.into_iter().map(|title| async { - let messages = vec![ - async_openai::types::ChatCompletionRequestSystemMessage { - content: "You are a helpful librarian making book recommendations.".into(), - name: None, - role: Role::System, - }, - async_openai::types::ChatCompletionRequestUserMessage { - content: async_openai::types::ChatCompletionRequestUserMessageContent::Text( - format!("Suggest one similar book to '{}' and return just the title.", title) - ), - name: None, - role: Role::User, - }, - ]; + let recommendations = future::join_all(titles.into_iter().map(|title| { + let value = client.clone(); + async move { + let request = CreateChatCompletionRequestArgs::default() + .model("gpt-4o") + .max_tokens(50_u16) + .messages([ + ChatCompletionRequestSystemMessageArgs::default() + .content("You are a helpful librarian making book recommendations.") + .build() + .expect("msg") + .into(), + ChatCompletionRequestUserMessageArgs::default() + .content(format!("Recommend books similar to '{}'.", title)) + .build() + .expect("msg") + .into(), + ]) + .build() + .expect("msg"); - let request = CreateChatCompletionRequestArgs::default() - .model("gpt-3.5-turbo") - .messages(messages) - .max_tokens(50_u16) - .build()?; - - match client.chat().create(request).await { - Ok(response) => { - if let Some(choice) = response.choices.first() { - if let Some(content) = &choice.message.content { - Ok(content.trim().to_string()) + match value.chat().create(request).await { + Ok(response) => { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.message.content { + Ok(content.trim().to_string()) + } else { + Err(PaperError::GeneralError) + } } else { Err(PaperError::GeneralError) } - } else { - Err(PaperError::GeneralError) } + Err(_) => Err(PaperError::GeneralError), } - Err(_) => Err(PaperError::GeneralError), } })) .await; From 90830d66e869989c07e28346d15eb2303001405b Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 22:58:10 +0200 Subject: [PATCH 10/24] feat: add Tokio runtime to handle async recommendations in recommender --- paper/src/api/recommender.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index e6ad888..d224e9d 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -23,8 +23,16 @@ impl Recommender { titles: Vec, api_key: String, ) -> Result, PaperError> { - let client = - Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .thread_name("recommendations") + .enable_io() + .enable_time() + .build()?; + + runtime.block_on(async { + let client = + Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); let recommendations = future::join_all(titles.into_iter().map(|title| { let value = client.clone(); async move { @@ -65,5 +73,6 @@ impl Recommender { .await; recommendations.into_iter().collect() + }) } } From a696ca9ad22fb5afac152b7f918c621dba3f254d Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 7 Jun 2025 23:07:40 +0200 Subject: [PATCH 11/24] feat: add OpenRouter API base URL for book recommendations --- paper/src/api/recommender.rs | 74 ++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index d224e9d..a127a55 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -31,48 +31,56 @@ impl Recommender { .build()?; runtime.block_on(async { - let client = - Client::with_config(async_openai::config::OpenAIConfig::new().with_api_key(api_key)); - let recommendations = future::join_all(titles.into_iter().map(|title| { - let value = client.clone(); - async move { - let request = CreateChatCompletionRequestArgs::default() - .model("gpt-4o") - .max_tokens(50_u16) - .messages([ - ChatCompletionRequestSystemMessageArgs::default() - .content("You are a helpful librarian making book recommendations.") - .build() - .expect("msg") - .into(), - ChatCompletionRequestUserMessageArgs::default() - .content(format!("Recommend books similar to '{}'.", title)) - .build() - .expect("msg") - .into(), - ]) - .build() - .expect("msg"); + let client = Client::with_config( + async_openai::config::OpenAIConfig::new() + .with_api_key(api_key) + .with_api_base("https://openrouter.ai/api/v1"), + ); + let recommendations = future::join_all(titles.into_iter().map(|title| { + let value = client.clone(); + async move { + let request = CreateChatCompletionRequestArgs::default() + .model("gpt-4o") + .max_tokens(50_u16) + .messages([ + ChatCompletionRequestSystemMessageArgs::default() + .content("You are a helpful librarian making book recommendations.") + .build() + .expect("msg") + .into(), + ChatCompletionRequestUserMessageArgs::default() + .content(format!("Recommend books similar to '{}'.", title)) + .build() + .expect("msg") + .into(), + ]) + .build() + .expect("msg"); - match value.chat().create(request).await { - Ok(response) => { - if let Some(choice) = response.choices.first() { - if let Some(content) = &choice.message.content { - Ok(content.trim().to_string()) + match value.chat().create(request).await { + Ok(response) => { + if let Some(choice) = response.choices.first() { + if let Some(content) = &choice.message.content { + Ok(content.trim().to_string()) + } else { + println!("&choice.message.content"); + Err(PaperError::GeneralError) + } } else { + println!("response.choices.first()"); Err(PaperError::GeneralError) } - } else { + } + Err(e) => { + println!("not ok {}", e); Err(PaperError::GeneralError) } } - Err(_) => Err(PaperError::GeneralError), } - } - })) - .await; + })) + .await; - recommendations.into_iter().collect() + recommendations.into_iter().collect() }) } } From 4273b423c14bd62bf451de23f9c22a63dd961a92 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Sat, 7 Jun 2025 23:07:46 +0200 Subject: [PATCH 12/24] feat: modify recommender to collect and return all OpenAI API response choices --- paper/src/api/recommender.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index a127a55..bc25713 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -59,20 +59,21 @@ impl Recommender { match value.chat().create(request).await { Ok(response) => { - if let Some(choice) = response.choices.first() { - if let Some(content) = &choice.message.content { - Ok(content.trim().to_string()) - } else { - println!("&choice.message.content"); - Err(PaperError::GeneralError) - } - } else { - println!("response.choices.first()"); + let choices: Vec = response.choices + .into_iter() + .filter_map(|choice| choice.message.content) + .map(|content| content.trim().to_string()) + .collect(); + + if choices.is_empty() { + println!("No valid choices returned"); Err(PaperError::GeneralError) + } else { + Ok(choices.join("\n")) } } Err(e) => { - println!("not ok {}", e); + println!("API error: {}", e); Err(PaperError::GeneralError) } } From 4de2894b546acd4546f3ac20a4081906d85852fa Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Fri, 13 Jun 2025 23:24:35 +0200 Subject: [PATCH 13/24] feat: Add Recommendation type and JSON parsing for book recommendations --- paper/src/api/recommender.rs | 46 +++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index bc25713..1702a63 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -7,6 +7,12 @@ use async_openai::{ Client, }; use futures::future; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Recommendation { + pub book_titles: Vec, +} #[derive(uniffi::Object)] pub struct Recommender {} @@ -22,7 +28,7 @@ impl Recommender { &self, titles: Vec, api_key: String, - ) -> Result, PaperError> { + ) -> Result { let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(4) .thread_name("recommendations") @@ -44,12 +50,12 @@ impl Recommender { .max_tokens(50_u16) .messages([ ChatCompletionRequestSystemMessageArgs::default() - .content("You are a helpful librarian making book recommendations.") + .content("You are a helpful librarian making book recommendations. Always respond with valid JSON in the format: {\"book_titles\": [\"Title 1\", \"Title 2\", \"Title 3\"]}") .build() .expect("msg") .into(), ChatCompletionRequestUserMessageArgs::default() - .content(format!("Recommend books similar to '{}'.", title)) + .content(format!("Recommend 3-5 books similar to '{}'. Return only JSON with book_titles array.", title)) .build() .expect("msg") .into(), @@ -59,17 +65,18 @@ impl Recommender { match value.chat().create(request).await { Ok(response) => { - let choices: Vec = response.choices - .into_iter() - .filter_map(|choice| choice.message.content) - .map(|content| content.trim().to_string()) - .collect(); - - if choices.is_empty() { - println!("No valid choices returned"); - Err(PaperError::GeneralError) + if let Some(content) = response.choices.first().and_then(|choice| choice.message.content.as_ref()) { + match serde_json::from_str::(content.trim()) { + Ok(recommendation) => Ok(recommendation), + Err(e) => { + println!("JSON parsing error: {}", e); + println!("Raw content: {}", content); + Err(PaperError::GeneralError) + } + } } else { - Ok(choices.join("\n")) + println!("No content in response"); + Err(PaperError::GeneralError) } } Err(e) => { @@ -81,7 +88,18 @@ impl Recommender { })) .await; - recommendations.into_iter().collect() + // Combine all recommendations into a single Recommendation struct + let mut all_book_titles = Vec::new(); + for result in recommendations { + match result { + Ok(recommendation) => all_book_titles.extend(recommendation.book_titles), + Err(e) => return Err(e), + } + } + + Ok(Recommendation { + book_titles: all_book_titles, + }) }) } } From 56f0978178e027f5d3a64416e3b9d0d91e2ae9b5 Mon Sep 17 00:00:00 2001 From: "Martin Kim Dung-Pham (aider)" Date: Fri, 13 Jun 2025 23:27:31 +0200 Subject: [PATCH 14/24] feat: add uniffi::Record derive for Recommendation struct --- paper/src/api/recommender.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 1702a63..60b9036 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -9,7 +9,7 @@ use async_openai::{ use futures::future; use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct Recommendation { pub book_titles: Vec, } From ee59ccab7099467f9e46b5404f3fb6e20edf7ce2 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Thu, 19 Jun 2025 09:30:41 +0200 Subject: [PATCH 15/24] Make 3 to 5 recommendations per selected item --- paper/src/api/recommender.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 60b9036..b4ea062 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -1,8 +1,8 @@ use crate::error::PaperError; use async_openai::{ types::{ - ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageArgs, - ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs, + ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs, + CreateChatCompletionRequestArgs, }, Client, }; @@ -45,17 +45,19 @@ impl Recommender { let recommendations = future::join_all(titles.into_iter().map(|title| { let value = client.clone(); async move { + let x = r#"{"book_titles": ["Title 1", "Title 2", "Title 3"]}"#; let request = CreateChatCompletionRequestArgs::default() .model("gpt-4o") .max_tokens(50_u16) .messages([ - ChatCompletionRequestSystemMessageArgs::default() - .content("You are a helpful librarian making book recommendations. Always respond with valid JSON in the format: {\"book_titles\": [\"Title 1\", \"Title 2\", \"Title 3\"]}") - .build() - .expect("msg") - .into(), ChatCompletionRequestUserMessageArgs::default() - .content(format!("Recommend 3-5 books similar to '{}'. Return only JSON with book_titles array.", title)) + .content(format!(r#" + You are a helpful librarian making book recommendations. + Recommend 3 books similar to '{}'. + The books should have the same language as the examples. + Always respond with valid JSON in the format: {}. + The response itself should be valid json. + Please do not include any additional text like markdown or explanations."#, title, x)) .build() .expect("msg") .into(), @@ -96,7 +98,7 @@ impl Recommender { Err(e) => return Err(e), } } - + Ok(Recommendation { book_titles: all_book_titles, }) From 0949f52d2694183356cfc8cb3e633e7b70062b32 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 28 Jun 2025 13:56:38 +0200 Subject: [PATCH 16/24] refactor: Optimize book recommendation API call with single request and bullet-formatted titles Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) --- paper/src/api/recommender.rs | 91 ++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index b4ea062..a40f48f 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -42,66 +42,55 @@ impl Recommender { .with_api_key(api_key) .with_api_base("https://openrouter.ai/api/v1"), ); - let recommendations = future::join_all(titles.into_iter().map(|title| { - let value = client.clone(); - async move { - let x = r#"{"book_titles": ["Title 1", "Title 2", "Title 3"]}"#; - let request = CreateChatCompletionRequestArgs::default() - .model("gpt-4o") - .max_tokens(50_u16) - .messages([ - ChatCompletionRequestUserMessageArgs::default() - .content(format!(r#" - You are a helpful librarian making book recommendations. - Recommend 3 books similar to '{}'. - The books should have the same language as the examples. - Always respond with valid JSON in the format: {}. - The response itself should be valid json. - Please do not include any additional text like markdown or explanations."#, title, x)) - .build() - .expect("msg") - .into(), - ]) + + // Format all titles as bullet points + let titles_bullets = titles.iter() + .map(|title| format!("• {}", title)) + .collect::>() + .join("\n"); + + let x = r#"{"book_titles": ["Title 1", "Title 2", "Title 3"]}"#; + let request = CreateChatCompletionRequestArgs::default() + .model("gpt-4o") + .max_tokens(50_u16) + .messages([ + ChatCompletionRequestUserMessageArgs::default() + .content(format!(r#" + You are a helpful librarian making book recommendations. + Recommend 3 books similar to these titles: + {} + The books should have the same language as the examples. + Always respond with valid JSON in the format: {}. + The response itself should be valid json. + Please do not include any additional text like markdown or explanations."#, titles_bullets, x)) .build() - .expect("msg"); + .expect("msg") + .into(), + ]) + .build() + .expect("msg"); - match value.chat().create(request).await { - Ok(response) => { - if let Some(content) = response.choices.first().and_then(|choice| choice.message.content.as_ref()) { - match serde_json::from_str::(content.trim()) { - Ok(recommendation) => Ok(recommendation), - Err(e) => { - println!("JSON parsing error: {}", e); - println!("Raw content: {}", content); - Err(PaperError::GeneralError) - } - } - } else { - println!("No content in response"); + match client.chat().create(request).await { + Ok(response) => { + if let Some(content) = response.choices.first().and_then(|choice| choice.message.content.as_ref()) { + match serde_json::from_str::(content.trim()) { + Ok(recommendation) => Ok(recommendation), + Err(e) => { + println!("JSON parsing error: {}", e); + println!("Raw content: {}", content); Err(PaperError::GeneralError) } } - Err(e) => { - println!("API error: {}", e); - Err(PaperError::GeneralError) - } + } else { + println!("No content in response"); + Err(PaperError::GeneralError) } } - })) - .await; - - // Combine all recommendations into a single Recommendation struct - let mut all_book_titles = Vec::new(); - for result in recommendations { - match result { - Ok(recommendation) => all_book_titles.extend(recommendation.book_titles), - Err(e) => return Err(e), + Err(e) => { + println!("API error: {}", e); + Err(PaperError::GeneralError) } } - - Ok(Recommendation { - book_titles: all_book_titles, - }) }) } } From 12c098c3926eb41ea7641765f17e5fbe19b414de Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 28 Jun 2025 13:58:35 +0200 Subject: [PATCH 17/24] refactor: extract content into variable and add debug print Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) --- paper/src/api/recommender.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index a40f48f..0b93421 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -50,19 +50,23 @@ impl Recommender { .join("\n"); let x = r#"{"book_titles": ["Title 1", "Title 2", "Title 3"]}"#; - let request = CreateChatCompletionRequestArgs::default() - .model("gpt-4o") - .max_tokens(50_u16) - .messages([ - ChatCompletionRequestUserMessageArgs::default() - .content(format!(r#" + let content = format!(r#" You are a helpful librarian making book recommendations. Recommend 3 books similar to these titles: {} The books should have the same language as the examples. Always respond with valid JSON in the format: {}. The response itself should be valid json. - Please do not include any additional text like markdown or explanations."#, titles_bullets, x)) + Please do not include any additional text like markdown or explanations."#, titles_bullets, x); + + println!("Request content: {}", content); + + let request = CreateChatCompletionRequestArgs::default() + .model("gpt-4o") + .max_tokens(50_u16) + .messages([ + ChatCompletionRequestUserMessageArgs::default() + .content(content) .build() .expect("msg") .into(), From 927562d725e9a2a38ed5f6703a0a561f7277c4b3 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 28 Jun 2025 18:45:17 +0200 Subject: [PATCH 18/24] refactor: update book recommendation JSON structure and prompt --- paper/src/api/recommender.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 0b93421..0484437 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -42,14 +42,21 @@ impl Recommender { .with_api_key(api_key) .with_api_base("https://openrouter.ai/api/v1"), ); - + // Format all titles as bullet points let titles_bullets = titles.iter() .map(|title| format!("• {}", title)) .collect::>() .join("\n"); - - let x = r#"{"book_titles": ["Title 1", "Title 2", "Title 3"]}"#; + + let x = r#" + { + "recommendations": [ + {"title": "Title 1", "author": "Author 1", "isbn": "1234567890"}, + {"title": "Title 2", "author": "Author 2", "isbn": "1234567890"}, + {"title": "Title 3", "author": "Author 3", "isbn": "1234567890"} + ] + }"#; let content = format!(r#" You are a helpful librarian making book recommendations. Recommend 3 books similar to these titles: @@ -58,9 +65,9 @@ impl Recommender { Always respond with valid JSON in the format: {}. The response itself should be valid json. Please do not include any additional text like markdown or explanations."#, titles_bullets, x); - + println!("Request content: {}", content); - + let request = CreateChatCompletionRequestArgs::default() .model("gpt-4o") .max_tokens(50_u16) From 8362dfbb0c1aff522236dbd566fc7b414a7408b2 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Sat, 28 Jun 2025 18:46:14 +0200 Subject: [PATCH 19/24] feat: update Recommendation struct to support book details with title, author, and ISBN Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) --- paper/src/api/recommender.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 0484437..294a263 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -9,9 +9,16 @@ use async_openai::{ use futures::future; use serde::{Deserialize, Serialize}; +#[derive(Debug, Serialize, Deserialize, uniffi::Record)] +pub struct BookRecommendation { + pub title: String, + pub author: String, + pub isbn: String, +} + #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct Recommendation { - pub book_titles: Vec, + pub recommendations: Vec, } #[derive(uniffi::Object)] From 80ebb29158a45f80151093197731302763708a09 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Mon, 30 Jun 2025 10:23:49 +0200 Subject: [PATCH 20/24] Recommend books for multiple samples at once instead of each sample --- paper/src/api/recommender.rs | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 294a263..707238c 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -1,19 +1,14 @@ use crate::error::PaperError; use async_openai::{ - types::{ - ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs, - CreateChatCompletionRequestArgs, - }, + types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs}, Client, }; -use futures::future; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, uniffi::Record)] pub struct BookRecommendation { pub title: String, pub author: String, - pub isbn: String, } #[derive(Debug, Serialize, Deserialize, uniffi::Record)] @@ -56,37 +51,37 @@ impl Recommender { .collect::>() .join("\n"); - let x = r#" + let json_format = r#" { "recommendations": [ - {"title": "Title 1", "author": "Author 1", "isbn": "1234567890"}, - {"title": "Title 2", "author": "Author 2", "isbn": "1234567890"}, - {"title": "Title 3", "author": "Author 3", "isbn": "1234567890"} + {"title": "Title 1", "author": "Author 1"}, + {"title": "Title 2", "author": "Author 2"}, + {"title": "Title 3", "author": "Author 3"} ] }"#; let content = format!(r#" You are a helpful librarian making book recommendations. Recommend 3 books similar to these titles: {} - The books should have the same language as the examples. - Always respond with valid JSON in the format: {}. + The books should be localized in the same language as the samples. + Always respond with valid JSON in the format: `{}`. The response itself should be valid json. - Please do not include any additional text like markdown or explanations."#, titles_bullets, x); + Please do not include any additional text like markdown or explanations."#, titles_bullets, json_format); println!("Request content: {}", content); let request = CreateChatCompletionRequestArgs::default() - .model("gpt-4o") - .max_tokens(50_u16) + .model("openai/gpt-4.1") + .max_tokens(500_u16) .messages([ ChatCompletionRequestUserMessageArgs::default() .content(content) .build() - .expect("msg") + .expect("Should be able to create ChatCompletionRequestUserMessageArgs") .into(), ]) .build() - .expect("msg"); + .expect("Should be able to create CreateChatCompletionRequestArgs"); match client.chat().create(request).await { Ok(response) => { From 988cbb969ff48a9978c53dbc6a52733868df26d6 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Mon, 30 Jun 2025 10:31:46 +0200 Subject: [PATCH 21/24] Undo unwanted change --- paper/src/authenticators/public_hamburg_authenticator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paper/src/authenticators/public_hamburg_authenticator.rs b/paper/src/authenticators/public_hamburg_authenticator.rs index f5a5525..98cd949 100644 --- a/paper/src/authenticators/public_hamburg_authenticator.rs +++ b/paper/src/authenticators/public_hamburg_authenticator.rs @@ -1,9 +1,9 @@ use super::{LoginResult, RawLoansPage}; -use crate::configuration::Configuration; use crate::error::PaperError; use crate::model::Loans; use crate::scrapers::public_hamburg::LoansScraper; use crate::token_scraper::TokenScraper; +use crate::{configuration::Configuration}; use reqwest::{ header::{HeaderMap, HeaderValue}, Client, From 202237c30c0168ca58e48dae8292b1dfd5ec5e06 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Fri, 4 Jul 2025 16:54:59 +0200 Subject: [PATCH 22/24] Update cargo dependencies --- Cargo.lock | 329 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 325 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be082d2..84e0fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-convert" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" +dependencies = [ + "async-trait", +] + [[package]] name = "async-executor" version = "1.11.0" @@ -293,6 +302,31 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-openai" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49befca6fce02518292854f986151b70bb1c0a32e402c5efe52c3aa75f2e183" +dependencies = [ + "async-convert", + "backoff", + "base64", + "bytes", + "derive_builder", + "futures", + "rand", + "reqwest", + "reqwest-eventsource", + "secrecy", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + [[package]] name = "async-std" version = "1.12.0" @@ -326,6 +360,17 @@ version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.59", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -349,6 +394,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom", + "instant", + "pin-project-lite", + "rand", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -468,9 +527,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.94" +version = "1.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "4ad45f4f74e4e20eaa392913b7b33a7091c87e59628f4dd27888205ad888843c" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -693,6 +755,41 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "deranged" version = "0.3.11" @@ -702,6 +799,37 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -825,6 +953,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -988,6 +1127,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -1203,6 +1348,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1239,6 +1398,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -1339,9 +1504,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "linux-raw-sys" @@ -1602,6 +1767,7 @@ name = "paper" version = "0.1.0" dependencies = [ "anyhow", + "async-openai", "async-std", "chrono", "clap 2.34.0", @@ -1994,15 +2160,19 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", @@ -2011,14 +2181,47 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] +[[package]] +name = "reqwest-eventsource" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -2058,6 +2261,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2067,6 +2294,16 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.17" @@ -2124,6 +2361,26 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "2.10.0" @@ -2227,6 +2484,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.1.17" @@ -2338,6 +2601,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -2568,6 +2837,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2604,9 +2894,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.59", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2876,6 +3178,12 @@ dependencies = [ "weedle2", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -3019,6 +3327,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" From 2781e2105f1c886f487dde0879415a98fe42fad8 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Mon, 7 Jul 2025 19:02:13 +0200 Subject: [PATCH 23/24] Adjust recommender implementation according to review feedback --- paper/src/api/recommender.rs | 129 +++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index 707238c..b90842b 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -1,6 +1,9 @@ use crate::error::PaperError; use async_openai::{ - types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs}, + types::{ + ChatCompletionRequestUserMessageArgs, ChatCompletionResponseFormat, + ChatCompletionResponseFormatType, CreateChatCompletionRequestArgs, + }, Client, }; use serde::{Deserialize, Serialize}; @@ -19,7 +22,27 @@ pub struct Recommendation { #[derive(uniffi::Object)] pub struct Recommender {} -#[uniffi::export] +#[derive(uniffi::Record)] +pub struct RecommenderConfig { + pub api_key: String, + pub api_base: String, + pub model: Model, +} + +#[derive(uniffi::Enum)] +pub enum Model { + GPT4_1, +} + +impl Model { + pub fn identifier(&self) -> String { + match self { + Model::GPT4_1 => "openai/gpt-4.1".to_string(), + } + } +} + +#[uniffi::export(async_runtime = "tokio")] impl Recommender { #[uniffi::constructor] pub fn new() -> Self { @@ -29,29 +52,22 @@ impl Recommender { pub async fn get_recommendations( &self, titles: Vec, - api_key: String, + config: RecommenderConfig, ) -> Result { - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(4) - .thread_name("recommendations") - .enable_io() - .enable_time() - .build()?; - - runtime.block_on(async { - let client = Client::with_config( - async_openai::config::OpenAIConfig::new() - .with_api_key(api_key) - .with_api_base("https://openrouter.ai/api/v1"), - ); + let client = Client::with_config( + async_openai::config::OpenAIConfig::new() + .with_api_key(config.api_key) + .with_api_base(config.api_base), + ); - // Format all titles as bullet points - let titles_bullets = titles.iter() - .map(|title| format!("• {}", title)) - .collect::>() - .join("\n"); + // Format all titles as bullet points + let titles_bullets = titles + .iter() + .map(|title| format!("• {}", title)) + .collect::>() + .join("\n"); - let json_format = r#" + let json_format = r#" { "recommendations": [ {"title": "Title 1", "author": "Author 1"}, @@ -59,51 +75,58 @@ impl Recommender { {"title": "Title 3", "author": "Author 3"} ] }"#; - let content = format!(r#" + let content = format!( + r#" You are a helpful librarian making book recommendations. Recommend 3 books similar to these titles: {} The books should be localized in the same language as the samples. Always respond with valid JSON in the format: `{}`. The response itself should be valid json. - Please do not include any additional text like markdown or explanations."#, titles_bullets, json_format); + Please do not include any additional text like markdown or explanations."#, + titles_bullets, json_format + ); - println!("Request content: {}", content); + println!("Request content: {}", content); - let request = CreateChatCompletionRequestArgs::default() - .model("openai/gpt-4.1") - .max_tokens(500_u16) - .messages([ - ChatCompletionRequestUserMessageArgs::default() - .content(content) - .build() - .expect("Should be able to create ChatCompletionRequestUserMessageArgs") - .into(), - ]) + let request = CreateChatCompletionRequestArgs::default() + .model(config.model.identifier()) + .max_tokens(500_u16) + .response_format(ChatCompletionResponseFormat { + r#type: ChatCompletionResponseFormatType::JsonObject, + }) + .messages([ChatCompletionRequestUserMessageArgs::default() + .content(content) .build() - .expect("Should be able to create CreateChatCompletionRequestArgs"); + .expect("Should be able to create ChatCompletionRequestUserMessageArgs") + .into()]) + .build() + .expect("Should be able to create CreateChatCompletionRequestArgs"); - match client.chat().create(request).await { - Ok(response) => { - if let Some(content) = response.choices.first().and_then(|choice| choice.message.content.as_ref()) { - match serde_json::from_str::(content.trim()) { - Ok(recommendation) => Ok(recommendation), - Err(e) => { - println!("JSON parsing error: {}", e); - println!("Raw content: {}", content); - Err(PaperError::GeneralError) - } + match client.chat().create(request).await { + Ok(response) => { + if let Some(content) = response + .choices + .first() + .and_then(|choice| choice.message.content.as_ref()) + { + match serde_json::from_str::(content.trim()) { + Ok(recommendation) => Ok(recommendation), + Err(e) => { + println!("JSON parsing error: {}", e); + println!("Raw content: {}", content); + Err(PaperError::GeneralError) } - } else { - println!("No content in response"); - Err(PaperError::GeneralError) } - } - Err(e) => { - println!("API error: {}", e); + } else { + println!("No content in response"); Err(PaperError::GeneralError) } } - }) + Err(e) => { + println!("API error: {}", e); + Err(PaperError::GeneralError) + } + } } } From 7d55e55f08d5713590cd2ab5c06152f733baa611 Mon Sep 17 00:00:00 2001 From: Martin Kim Dung-Pham Date: Mon, 7 Jul 2025 19:05:39 +0200 Subject: [PATCH 24/24] Removed dev print statements --- paper/src/api/recommender.rs | 2 -- paper/src/authenticators/opac_authenticator.rs | 1 - 2 files changed, 3 deletions(-) diff --git a/paper/src/api/recommender.rs b/paper/src/api/recommender.rs index b90842b..cc26a9b 100644 --- a/paper/src/api/recommender.rs +++ b/paper/src/api/recommender.rs @@ -87,8 +87,6 @@ impl Recommender { titles_bullets, json_format ); - println!("Request content: {}", content); - let request = CreateChatCompletionRequestArgs::default() .model(config.model.identifier()) .max_tokens(500_u16) diff --git a/paper/src/authenticators/opac_authenticator.rs b/paper/src/authenticators/opac_authenticator.rs index f4b1040..57ff94e 100644 --- a/paper/src/authenticators/opac_authenticator.rs +++ b/paper/src/authenticators/opac_authenticator.rs @@ -12,7 +12,6 @@ impl OpacAuthenticator { let username = self.configuration.username.clone().unwrap(); let password = self.configuration.password.clone().unwrap(); let login_url = self.configuration.login_url(); - // println!("{:?}, {:?}, {:?}", username, password, login_url); let html_string = client .post(login_url) .query(&[