1use crate::memory::{MemoryKind, MemoryRecord};
2use crate::store::MemoryStore;
3
4pub fn compute_importance(mem: &MemoryRecord) -> f64 {
14 let mut score = 0.0;
15 let mut weights = 0.0;
16
17 let access_factor = if mem.access_count > 0 {
20 ((mem.access_count as f64 + 1.0).log10() / 2.0_f64.log10()).min(1.0)
21 } else {
22 0.0
23 };
24 score += 0.35 * access_factor;
25 weights += 0.35;
26
27 let tag_factor = (mem.tags.len() as f64 / 3.0).min(1.0);
30 score += 0.20 * tag_factor;
31 weights += 0.20;
32
33 let source_factor = if mem.source.is_some() { 1.0 } else { 0.0 };
35 score += 0.10 * source_factor;
36 weights += 0.10;
37
38 let content_factor = match &mem.kind {
40 MemoryKind::Fact(_) => {
41 0.5
43 }
44 MemoryKind::Episode(e) => {
45 let len = e.text.len() as f64;
48 (len / 200.0).min(1.0)
49 }
50 };
51 score += 0.35 * content_factor;
52 weights += 0.35;
53
54 (score / weights).clamp(0.0, 1.0)
56}
57
58pub fn score_all(store: &MemoryStore) -> Result<usize, rusqlite::Error> {
61 let memories = store.all_memories()?;
62 let mut count = 0;
63 for mem in &memories {
64 let importance = compute_importance(mem);
65 if (importance - mem.importance).abs() > 1e-6 {
66 store.update_importance(mem.id, importance)?;
67 count += 1;
68 }
69 }
70 Ok(count)
71}
72
73#[derive(Debug, Clone, serde::Serialize)]
75pub struct ImportanceInfo {
76 pub id: i64,
77 pub content: String,
78 pub importance: f64,
79 pub access_count: i64,
80 pub tag_count: usize,
81 pub has_source: bool,
82}
83
84pub fn list_importance(store: &MemoryStore) -> Result<Vec<ImportanceInfo>, rusqlite::Error> {
86 let memories = store.all_memories()?;
87 let mut infos: Vec<ImportanceInfo> = memories
88 .iter()
89 .map(|mem| {
90 let content = mem.text_for_embedding();
91 ImportanceInfo {
92 id: mem.id,
93 content,
94 importance: mem.importance,
95 access_count: mem.access_count,
96 tag_count: mem.tags.len(),
97 has_source: mem.source.is_some(),
98 }
99 })
100 .collect();
101 infos.sort_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
102 Ok(infos)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use chrono::Utc;
109 use crate::memory::{Episode, Fact, MemoryKind};
110
111 fn make_record(kind: MemoryKind, access_count: i64, tags: Vec<String>, source: Option<String>) -> MemoryRecord {
112 MemoryRecord {
113 id: 1,
114 kind,
115 strength: 1.0,
116 embedding: None,
117 created_at: Utc::now(),
118 last_accessed_at: Utc::now(),
119 access_count,
120 tags,
121 source,
122 session_id: None,
123 channel: None,
124 importance: 0.5,
125 namespace: "default".to_string(),
126 checksum: None,
127 }
128 }
129
130 #[test]
131 fn high_access_count_increases_importance() {
132 let low = make_record(
133 MemoryKind::Fact(Fact { subject: "A".into(), relation: "is".into(), object: "B".into() }),
134 0, vec![], None,
135 );
136 let high = make_record(
137 MemoryKind::Fact(Fact { subject: "A".into(), relation: "is".into(), object: "B".into() }),
138 50, vec![], None,
139 );
140 assert!(compute_importance(&high) > compute_importance(&low));
141 }
142
143 #[test]
144 fn more_tags_increases_importance() {
145 let no_tags = make_record(
146 MemoryKind::Fact(Fact { subject: "A".into(), relation: "is".into(), object: "B".into() }),
147 0, vec![], None,
148 );
149 let with_tags = make_record(
150 MemoryKind::Fact(Fact { subject: "A".into(), relation: "is".into(), object: "B".into() }),
151 0, vec!["a".into(), "b".into(), "c".into()], None,
152 );
153 assert!(compute_importance(&with_tags) > compute_importance(&no_tags));
154 }
155
156 #[test]
157 fn source_increases_importance() {
158 let no_source = make_record(
159 MemoryKind::Fact(Fact { subject: "A".into(), relation: "is".into(), object: "B".into() }),
160 0, vec![], None,
161 );
162 let with_source = make_record(
163 MemoryKind::Fact(Fact { subject: "A".into(), relation: "is".into(), object: "B".into() }),
164 0, vec![], Some("cli".into()),
165 );
166 assert!(compute_importance(&with_source) > compute_importance(&no_source));
167 }
168
169 #[test]
170 fn longer_episodes_more_important() {
171 let short = make_record(
172 MemoryKind::Episode(Episode { text: "hi".into() }),
173 0, vec![], None,
174 );
175 let long = make_record(
176 MemoryKind::Episode(Episode { text: "This is a very detailed episode describing a complex technical decision about database architecture and schema migration patterns that was made after careful deliberation.".into() }),
177 0, vec![], None,
178 );
179 assert!(compute_importance(&long) > compute_importance(&short));
180 }
181
182 #[test]
183 fn importance_is_bounded() {
184 let maxed = make_record(
186 MemoryKind::Episode(Episode { text: "x".repeat(500) }),
187 1000, vec!["a".into(), "b".into(), "c".into(), "d".into()], Some("cli".into()),
188 );
189 let imp = compute_importance(&maxed);
190 assert!(imp >= 0.0 && imp <= 1.0, "importance should be in [0, 1], got {imp}");
191 }
192
193 #[test]
194 fn score_all_updates_importance_in_store() {
195 let store = MemoryStore::open_in_memory().unwrap();
196 let id = store.remember_fact("A", "is", "B", None).unwrap();
197
198 store.conn().execute(
200 "UPDATE memories SET access_count = 50 WHERE id = ?1",
201 rusqlite::params![id],
202 ).unwrap();
203
204 let count = score_all(&store).unwrap();
205 assert!(count > 0, "should have updated at least 1 memory");
206
207 let mem = store.get_memory(id).unwrap().unwrap();
208 assert!(mem.importance != 0.5, "importance should have changed from default");
209 }
210}