1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use rusqlite::params;
use crate::error::Result;
use crate::types::LearnedWeights;
use super::{now, YantrikDB};
impl YantrikDB {
/// Record feedback on a recall result.
///
/// The AI agent calls this when it knows a result was relevant or irrelevant.
/// Accumulated feedback powers the adaptive learning loop.
pub fn recall_feedback(
&self,
query_text: Option<&str>,
query_embedding: Option<&[f32]>,
rid: &str,
feedback: &str, // "relevant" | "irrelevant"
score_at_retrieval: Option<f64>,
rank_at_retrieval: Option<i32>,
) -> Result<()> {
let ts = now();
let emb_blob = query_embedding.map(|e| crate::serde_helpers::serialize_f32(e));
let conn = self.conn.lock();
conn.execute(
"INSERT INTO recall_feedback (query_text, query_embedding, rid, feedback, \
score_at_retrieval, rank_at_retrieval, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
query_text,
emb_blob,
rid,
feedback,
score_at_retrieval,
rank_at_retrieval,
ts,
],
)?;
// Update feedback count in learned_weights
conn.execute(
"UPDATE learned_weights SET feedback_count = feedback_count + 1 WHERE id = 1",
[],
)?;
drop(conn);
// v0.10 Item 2: explicit feedback is the gold label source —
// bind it to the rid's most recent impression so the learner
// trains on the features that were actually served (no
// impression in the horizon → legacy row only, no label).
let polarity = if feedback == "irrelevant" { -1 } else { 1 };
let _ = self.record_ranking_label(
rid,
"explicit",
polarity,
super::impressions::WEIGHT_EXPLICIT,
);
Ok(())
}
/// Load the current learned weights from the database.
pub fn load_learned_weights(&self) -> Result<LearnedWeights> {
let conn = self.conn.lock();
let result = conn.query_row(
"SELECT w_sim, w_decay, w_recency, gate_tau, alpha_imp, keyword_boost, generation \
FROM learned_weights WHERE id = 1",
[],
|row| {
Ok(LearnedWeights {
w_sim: row.get(0)?,
w_decay: row.get(1)?,
w_recency: row.get(2)?,
gate_tau: row.get(3)?,
alpha_imp: row.get(4)?,
keyword_boost: row.get(5)?,
generation: row.get(6)?,
})
},
);
match result {
// Clamped on the way OUT of the database, not on the way in:
// rows predating the clamp, hand-edited rows and restored
// backups all flow through here, and none of them were
// validated when written.
Ok(w) => Ok(w.clamped()),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(LearnedWeights::default()),
Err(e) => Err(e.into()),
}
}
/// Get the current feedback count.
pub fn feedback_count(&self) -> Result<i64> {
let conn = self.conn.lock();
let count: i64 = conn
.query_row(
"SELECT COALESCE(feedback_count, 0) FROM learned_weights WHERE id = 1",
[],
|row| row.get(0),
)
.unwrap_or(0);
Ok(count)
}
}