Move RelevanceInputs into core's models.rs, wire up api/models.rs rename
All checks were successful
CI / check (pull_request) Successful in 2m5s
CI / test (pull_request) Successful in 2m43s
CI / audit (pull_request) Successful in 10s

The previous commit only carried the dto.rs -> models.rs file rename;
this carries the rest: mod.rs's import/re-export update, and
RelevanceInputs moving out of scoring.rs into models.rs (built by
score_article's callers, not score_article itself), plus an unrelated
import-order fmt fix in db/src/articles.rs.
This commit is contained in:
Austin Schaefer 2026-09-03 23:24:48 +02:00
parent 7ccc36955a
commit 893e0ed251
5 changed files with 25 additions and 23 deletions

View file

@ -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;

View file

@ -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,
}

View file

@ -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

View file

@ -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.

View file

@ -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};