lean_ctx/core/knowledge/
query.rs1use chrono::{DateTime, Utc};
2
3use super::ranking::sort_fact_for_output;
4use super::types::{KnowledgeFact, ProjectKnowledge};
5
6impl ProjectKnowledge {
7 fn matching_indices(&self, term: &str, include_session: bool) -> Vec<usize> {
8 let Some(indices) = self.index.token_positions.get(term) else {
9 return if include_session {
10 self.index
11 .session_token_positions
12 .get(term)
13 .cloned()
14 .unwrap_or_default()
15 } else {
16 Vec::new()
17 };
18 };
19
20 if !include_session {
21 return indices.clone();
22 }
23
24 let Some(session_indices) = self.index.session_token_positions.get(term) else {
25 return indices.clone();
26 };
27 let mut merged = indices.clone();
28 merged.extend(
29 session_indices
30 .iter()
31 .copied()
32 .filter(|idx| indices.binary_search(idx).is_err()),
33 );
34 merged
35 }
36
37 pub fn recall(&self, query: &str) -> Vec<&KnowledgeFact> {
38 let q = query.to_lowercase();
39 let terms: Vec<&str> = q.split_whitespace().collect();
40 if terms.is_empty() {
41 return Vec::new();
42 }
43
44 let mut match_counts: std::collections::HashMap<usize, usize> =
45 std::collections::HashMap::new();
46 for term in &terms {
47 for idx in self.matching_indices(term, true) {
48 if self.facts[idx].is_current() {
49 *match_counts.entry(idx).or_insert(0) += 1;
50 }
51 }
52 }
53
54 let mut results: Vec<(&KnowledgeFact, f32)> = match_counts
55 .into_iter()
56 .map(|(idx, count)| {
57 let f = &self.facts[idx];
58 let relevance = (count as f32 / terms.len() as f32) * f.quality_score();
59 (f, relevance)
60 })
61 .collect();
62
63 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
64 results.into_iter().map(|(f, _)| f).collect()
65 }
66
67 pub fn recall_by_category(&self, category: &str) -> Vec<&KnowledgeFact> {
68 self.index
69 .category_positions
70 .get(category)
71 .into_iter()
72 .flatten()
73 .filter_map(|&idx| self.facts.get(idx))
74 .filter(|f| f.is_current())
75 .collect()
76 }
77
78 pub fn recall_at_time(&self, query: &str, at: DateTime<Utc>) -> Vec<&KnowledgeFact> {
79 let q = query.to_lowercase();
80 let terms: Vec<&str> = q.split_whitespace().collect();
81 if terms.is_empty() {
82 return Vec::new();
83 }
84
85 let mut match_counts: std::collections::HashMap<usize, usize> =
86 std::collections::HashMap::new();
87 for term in &terms {
88 for idx in self.matching_indices(term, false) {
89 if self.facts[idx].was_valid_at(at) {
90 *match_counts.entry(idx).or_insert(0) += 1;
91 }
92 }
93 }
94
95 let mut results: Vec<(&KnowledgeFact, f32)> = match_counts
96 .into_iter()
97 .map(|(idx, count)| {
98 let f = &self.facts[idx];
99 (f, count as f32 / terms.len() as f32)
100 })
101 .collect();
102
103 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
104 results.into_iter().map(|(f, _)| f).collect()
105 }
106
107 pub fn timeline(&self, category: &str) -> Vec<&KnowledgeFact> {
108 let mut facts: Vec<&KnowledgeFact> = self
109 .index
110 .category_positions
111 .get(category)
112 .into_iter()
113 .flatten()
114 .filter_map(|&idx| self.facts.get(idx))
115 .collect();
116 facts.sort_by_key(|x| x.created_at);
117 facts
118 }
119
120 pub fn list_rooms(&self) -> Vec<(String, usize)> {
121 let mut categories: std::collections::BTreeMap<String, usize> =
122 std::collections::BTreeMap::new();
123 for f in &self.facts {
124 if f.is_current() {
125 *categories.entry(f.category.clone()).or_insert(0) += 1;
126 }
127 }
128 categories.into_iter().collect()
129 }
130
131 pub fn recall_for_output(&mut self, query: &str, limit: usize) -> (Vec<KnowledgeFact>, usize) {
132 let q = query.to_lowercase();
133 let terms: Vec<&str> = q.split_whitespace().filter(|t| !t.is_empty()).collect();
134 if terms.is_empty() {
135 return (Vec::new(), 0);
136 }
137
138 let mut match_counts: std::collections::HashMap<usize, usize> =
139 std::collections::HashMap::new();
140 for term in &terms {
141 for idx in self.matching_indices(term, true) {
142 if self.facts[idx].is_current() {
143 *match_counts.entry(idx).or_insert(0) += 1;
144 }
145 }
146 }
147
148 struct Scored {
149 idx: usize,
150 relevance: f32,
151 }
152
153 let mut scored: Vec<Scored> = match_counts
154 .into_iter()
155 .map(|(idx, count)| {
156 let f = &self.facts[idx];
157 let mut relevance = (count as f32 / terms.len() as f32) * f.confidence;
158 let key_lower = f.key.to_lowercase();
162 if key_lower == q {
163 relevance += 1.0;
164 } else if f.category.to_lowercase() == q {
165 relevance += 0.5;
166 }
167 if f.is_synthesized_observation() {
172 relevance += 0.4;
173 }
174 Scored { idx, relevance }
175 })
176 .collect();
177
178 scored.sort_by(|a, b| {
179 b.relevance
180 .partial_cmp(&a.relevance)
181 .unwrap_or(std::cmp::Ordering::Equal)
182 .then_with(|| sort_fact_for_output(&self.facts[a.idx], &self.facts[b.idx]))
183 });
184
185 let total = scored.len();
186 scored.truncate(limit);
187
188 let now = Utc::now();
189 let mut out: Vec<KnowledgeFact> = Vec::new();
190 for s in scored {
191 if let Some(f) = self.facts.get_mut(s.idx) {
192 f.retrieval_count = f.retrieval_count.saturating_add(1);
193 f.last_retrieved = Some(now);
194 out.push(f.clone());
195 }
196 }
197
198 (out, total)
199 }
200
201 pub fn recall_by_category_for_output(
202 &mut self,
203 category: &str,
204 limit: usize,
205 ) -> (Vec<KnowledgeFact>, usize) {
206 let mut idxs: Vec<usize> = self
207 .index
208 .category_positions
209 .get(category)
210 .into_iter()
211 .flatten()
212 .copied()
213 .filter(|&idx| self.facts[idx].is_current())
214 .collect();
215
216 idxs.sort_by(|a, b| {
219 let (fa, fb) = (&self.facts[*a], &self.facts[*b]);
220 fb.is_synthesized_observation()
221 .cmp(&fa.is_synthesized_observation())
222 .then_with(|| sort_fact_for_output(fa, fb))
223 });
224
225 let total = idxs.len();
226 idxs.truncate(limit);
227
228 let now = Utc::now();
229 let mut out = Vec::new();
230 for idx in idxs {
231 if let Some(f) = self.facts.get_mut(idx) {
232 f.retrieval_count = f.retrieval_count.saturating_add(1);
233 f.last_retrieved = Some(now);
234 out.push(f.clone());
235 }
236 }
237
238 (out, total)
239 }
240}