Add feed sidebar navigation #8
5 changed files with 25 additions and 23 deletions
|
|
@ -3,5 +3,5 @@ pub mod models;
|
||||||
pub mod scoring;
|
pub mod scoring;
|
||||||
|
|
||||||
pub use affinity::TopicAffinities;
|
pub use affinity::TopicAffinities;
|
||||||
pub use models::{Article, Feed, ReadingEvent, ReadingOutcome};
|
pub use models::{Article, Feed, ReadingEvent, ReadingOutcome, RelevanceInputs};
|
||||||
pub use scoring::{score_article, RelevanceInputs};
|
pub use scoring::score_article;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::affinity::TopicAffinities;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -58,3 +59,16 @@ pub enum ReadingOutcome {
|
||||||
/// Explicitly marked not relevant, independent of whether it was opened.
|
/// Explicitly marked not relevant, independent of whether it was opened.
|
||||||
Dismissed,
|
Dismissed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inputs to the final blended relevance score for one article, see
|
||||||
|
/// `scoring::score_article`.
|
||||||
|
pub struct RelevanceInputs<'a> {
|
||||||
|
/// Cosine similarity (0.0-1.0, already renormalized from [-1,1] if
|
||||||
|
/// needed) between article and preference-profile embeddings.
|
||||||
|
pub embedding_score: f32,
|
||||||
|
/// LLM judgment (0.0-1.0), `None` if the article didn't clear the
|
||||||
|
/// embedding shortlist threshold and so was never sent to the LLM.
|
||||||
|
pub llm_score: Option<f32>,
|
||||||
|
pub topics: &'a [String],
|
||||||
|
pub affinities: &'a TopicAffinities,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,4 @@
|
||||||
use crate::affinity::TopicAffinities;
|
use crate::models::RelevanceInputs;
|
||||||
|
|
||||||
/// Inputs to the final blended relevance score for one article.
|
|
||||||
pub struct RelevanceInputs<'a> {
|
|
||||||
/// Cosine similarity (0.0-1.0, already renormalized from [-1,1] if
|
|
||||||
/// needed) between article and preference-profile embeddings.
|
|
||||||
pub embedding_score: f32,
|
|
||||||
/// LLM judgment (0.0-1.0), `None` if the article didn't clear the
|
|
||||||
/// embedding shortlist threshold and so was never sent to the LLM.
|
|
||||||
pub llm_score: Option<f32>,
|
|
||||||
pub topics: &'a [String],
|
|
||||||
pub affinities: &'a TopicAffinities,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Weights are deliberately conservative: the LLM judgment dominates when
|
/// Weights are deliberately conservative: the LLM judgment dominates when
|
||||||
/// present (it has read the actual content), the embedding score is a
|
/// present (it has read the actual content), the embedding score is a
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ use chrono::Utc;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use feedsignal_core::Article;
|
use feedsignal_core::Article;
|
||||||
|
use schema::articles::dsl;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
pub async fn insert_article(&self, article: &Article) -> Result<()> {
|
pub async fn insert_article(&self, article: &Article) -> Result<()> {
|
||||||
use schema::articles::dsl;
|
|
||||||
let mut conn = self.pool.get().await?;
|
let mut conn = self.pool.get().await?;
|
||||||
let topics = serde_json::to_string(&article.topics)?;
|
let topics = serde_json::to_string(&article.topics)?;
|
||||||
diesel::insert_into(dsl::articles)
|
diesel::insert_into(dsl::articles)
|
||||||
|
|
@ -36,7 +36,6 @@ impl Db {
|
||||||
/// Articles above the embedding-similarity threshold that haven't been
|
/// Articles above the embedding-similarity threshold that haven't been
|
||||||
/// through the (slower) LLM scoring stage yet.
|
/// through the (slower) LLM scoring stage yet.
|
||||||
pub async fn shortlist_for_llm_scoring(&self, threshold: f32, limit: i64) -> Result<Vec<Uuid>> {
|
pub async fn shortlist_for_llm_scoring(&self, threshold: f32, limit: i64) -> Result<Vec<Uuid>> {
|
||||||
use schema::articles::dsl;
|
|
||||||
let mut conn = self.pool.get().await?;
|
let mut conn = self.pool.get().await?;
|
||||||
let ids: Vec<String> = dsl::articles
|
let ids: Vec<String> = dsl::articles
|
||||||
.filter(dsl::embedding_score.ge(threshold))
|
.filter(dsl::embedding_score.ge(threshold))
|
||||||
|
|
@ -55,7 +54,6 @@ impl Db {
|
||||||
&self,
|
&self,
|
||||||
article_id: Uuid,
|
article_id: Uuid,
|
||||||
) -> Result<Option<(String, String, Vec<String>, f32)>> {
|
) -> Result<Option<(String, String, Vec<String>, f32)>> {
|
||||||
use schema::articles::dsl;
|
|
||||||
let mut conn = self.pool.get().await?;
|
let mut conn = self.pool.get().await?;
|
||||||
let row: Option<(String, String, String, Option<f32>)> = dsl::articles
|
let row: Option<(String, String, String, Option<f32>)> = dsl::articles
|
||||||
.filter(dsl::id.eq(article_id.to_string()))
|
.filter(dsl::id.eq(article_id.to_string()))
|
||||||
|
|
@ -63,13 +61,16 @@ impl Db {
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(match row {
|
|
||||||
|
let result = match row {
|
||||||
Some((title, summary, topics_json, score)) => {
|
Some((title, summary, topics_json, score)) => {
|
||||||
let topics: Vec<String> = serde_json::from_str(&topics_json).unwrap_or_default();
|
let topics: Vec<String> = serde_json::from_str(&topics_json).unwrap_or_default();
|
||||||
Some((title, summary, topics, score.unwrap_or(0.0)))
|
Some((title, summary, topics, score.unwrap_or(0.0)))
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
})
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn store_llm_result(
|
pub async fn store_llm_result(
|
||||||
|
|
@ -100,7 +101,6 @@ impl Db {
|
||||||
feed_id: Option<Uuid>,
|
feed_id: Option<Uuid>,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<Vec<RankedArticleRow>> {
|
) -> Result<Vec<RankedArticleRow>> {
|
||||||
use schema::articles::dsl;
|
|
||||||
let mut conn = self.pool.get().await?;
|
let mut conn = self.pool.get().await?;
|
||||||
// SQLite sorts NULL before any value, so `DESC` already puts NULL
|
// SQLite sorts NULL before any value, so `DESC` already puts NULL
|
||||||
// `final_score`s last — no separate NULLS LAST clause needed here.
|
// `final_score`s last — no separate NULLS LAST clause needed here.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pub mod articles;
|
pub mod articles;
|
||||||
mod dto;
|
|
||||||
pub mod feeds;
|
pub mod feeds;
|
||||||
|
mod models;
|
||||||
|
|
||||||
pub use dto::{ArticleView, FeedView};
|
pub use models::{ArticleView, FeedView};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue