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