Skip to main content

conch_core/
importance.rs

1use crate::memory::{MemoryKind, MemoryRecord};
2use crate::store::MemoryStore;
3
4/// Compute importance score for a memory based on heuristics.
5///
6/// Heuristics:
7/// - Facts with high access_count → higher importance
8/// - Memories with more tags → higher importance
9/// - Memories with source tracking → slightly higher importance
10/// - Episode length (longer = more context = more important)
11///
12/// Returns a score in [0.0, 1.0].
13pub fn compute_importance(mem: &MemoryRecord) -> f64 {
14    let mut score = 0.0;
15    let mut weights = 0.0;
16
17    // Access count: log-scaled contribution (weight: 0.35)
18    // access_count=0 → 0.0, access_count=1 → ~0.3, access_count=10 → ~0.72, access_count=100 → 1.0
19    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    // Tag count: more tags = more contextualized = more important (weight: 0.20)
28    // 0 tags → 0.0, 1 tag → 0.33, 2 tags → 0.67, 3+ tags → 1.0
29    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    // Source tracking: having a source = slightly more important (weight: 0.10)
34    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    // Content richness based on memory kind (weight: 0.35)
39    let content_factor = match &mem.kind {
40        MemoryKind::Fact(_) => {
41            // Facts are inherently structured, give a base score
42            0.5
43        }
44        MemoryKind::Episode(e) => {
45            // Longer episodes contain more context
46            // <50 chars → low, 50-200 → medium, 200+ → high
47            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    // Normalize to [0.0, 1.0]
55    (score / weights).clamp(0.0, 1.0)
56}
57
58/// Compute and store importance scores for all memories.
59/// Returns the number of memories updated.
60pub 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/// Result for displaying importance info.
74#[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
84/// Get importance info for all memories, sorted by importance descending.
85pub 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        // Max everything out
185        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        // Bump access count so importance changes from default
199        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}