1use anyhow::{Context, Result};
10use rusqlite::OptionalExtension;
11use serde::Serialize;
12
13use crate::inspect::now_unix;
14use crate::search::{
15 ConfidenceBreakdown, compute_confidence_breakdown, format_confidence_breakdown_token,
16};
17use crate::store::Store;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Feedback {
25 Up,
26 Down,
27 Custom(i64),
28}
29
30impl Feedback {
31 pub fn delta(self) -> i64 {
32 match self {
33 Feedback::Up => 1,
34 Feedback::Down => -1,
35 Feedback::Custom(n) => n,
36 }
37 }
38}
39
40pub fn record_feedback(store: &Store, chunk_id: &str, feedback: Feedback) -> Result<i64> {
46 let delta = feedback.delta();
47 let conn = store.conn();
48 let rows = conn
49 .execute(
50 "UPDATE chunks SET feedback_score = feedback_score + ?1 WHERE id = ?2",
51 rusqlite::params![delta, chunk_id],
52 )
53 .with_context(|| format!("applying feedback to chunk {chunk_id}"))?;
54 if rows == 0 {
55 anyhow::bail!("no chunk with id {chunk_id}");
56 }
57 get_feedback_score(store, chunk_id)?
58 .ok_or_else(|| anyhow::anyhow!("chunk {chunk_id} disappeared after feedback update"))
59}
60
61pub fn get_feedback_score(store: &Store, chunk_id: &str) -> Result<Option<i64>> {
64 let conn = store.conn();
65 let score = conn
66 .query_row(
67 "SELECT feedback_score FROM chunks WHERE id = ?1",
68 rusqlite::params![chunk_id],
69 |row| row.get::<_, i64>(0),
70 )
71 .optional()?;
72 Ok(score)
73}
74
75#[derive(Debug, Clone, Serialize)]
81pub struct FeedbackReport {
82 pub chunk_id: String,
83 pub delta: i64,
84 pub score: i64,
85 pub confidence: f64,
86 pub confidence_breakdown: ConfidenceBreakdown,
87 pub access_decay_at: Option<i64>,
88}
89
90fn current_confidence(
91 store: &Store,
92 chunk_id: &str,
93 at_unix: i64,
94) -> Result<(f64, ConfidenceBreakdown, Option<i64>)> {
95 let conn = store.conn();
96 let (access_decay_at, last_accessed_at, timestamp_unix, access_count, feedback_score, query_success_count) =
97 conn.query_row(
98 "SELECT access_decay_at, last_accessed_at, timestamp_unix, access_count, feedback_score, query_success_count FROM chunks WHERE id = ?1",
99 rusqlite::params![chunk_id],
100 |row| {
101 Ok((
102 row.get(0)?,
103 row.get(1)?,
104 row.get(2)?,
105 row.get(3)?,
106 row.get(4)?,
107 row.get(5)?,
108 ))
109 },
110 )?;
111 let (confidence, confidence_breakdown) = compute_confidence_breakdown(
112 at_unix,
113 last_accessed_at,
114 timestamp_unix,
115 access_count,
116 feedback_score,
117 query_success_count,
118 );
119 Ok((confidence, confidence_breakdown, access_decay_at))
120}
121
122pub fn apply_feedback(store: &Store, chunk_id: &str, feedback: Feedback) -> Result<FeedbackReport> {
126 let delta = feedback.delta();
127 let score = record_feedback(store, chunk_id, feedback)?;
128 let (confidence, confidence_breakdown, access_decay_at) =
129 current_confidence(store, chunk_id, now_unix())?;
130 Ok(FeedbackReport {
131 chunk_id: chunk_id.to_string(),
132 delta,
133 score,
134 confidence,
135 confidence_breakdown,
136 access_decay_at,
137 })
138}
139
140pub(crate) fn format_text(report: &FeedbackReport) -> String {
141 let mut text = format!(
142 "feedback recorded: chunk={} delta={:+} score={} confidence={:.3} freshness_source={} {}",
143 report.chunk_id,
144 report.delta,
145 report.score,
146 report.confidence,
147 report.confidence_breakdown.freshness_source.as_str(),
148 format_confidence_breakdown_token(&report.confidence_breakdown),
149 );
150 if let Some(access_decay_at) = report.access_decay_at {
151 text.push_str(&format!(" access_decay_at={access_decay_at}"));
152 }
153 text
154}
155
156pub fn print_text(report: &FeedbackReport) {
157 println!("{}", format_text(report));
158}
159
160pub fn print_json(report: &FeedbackReport) -> Result<()> {
161 println!("{}", serde_json::to_string_pretty(report)?);
162 Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn sample_report() -> FeedbackReport {
170 FeedbackReport {
171 chunk_id: "chunk-1".into(),
172 delta: 1,
173 score: 3,
174 confidence: 0.732,
175 confidence_breakdown: ConfidenceBreakdown {
176 freshness: 0.5,
177 freshness_source: crate::search::FreshnessSource::TimestampUnix,
178 access_boost: 0.5,
179 base: 0.5,
180 feedback_factor: 0.0,
181 query_success_factor: 0.0,
182 },
183 access_decay_at: Some(1_700_000_800),
184 }
185 }
186
187 #[test]
188 fn format_text_surfaces_freshness_source_token() {
189 let text = format_text(&sample_report());
190 assert!(text.contains("freshness_source=timestamp_unix"), "{text}");
191 assert!(text.contains("confidence=0.732"), "{text}");
192 assert!(text.contains("access_decay_at=1700000800"), "{text}");
193 }
194}