1use chrono::{DateTime, Utc};
2use lean_ctx_protocol::knowledge::{ClassificationLevel, KnowledgeObjectV1};
3
4use super::ranking::sort_fact_for_output;
5use super::types::{KnowledgeFact, ProjectKnowledge};
6
7#[derive(Debug, Clone, Default)]
9pub struct KnowledgeQuery {
10 pub source: Option<String>,
11 pub classification: Option<ClassificationLevel>,
12 pub valid_at: Option<DateTime<Utc>>,
13 pub tags: Vec<String>,
14}
15
16impl KnowledgeQuery {
17 pub fn valid_at(timestamp: DateTime<Utc>) -> Self {
19 Self {
20 valid_at: Some(timestamp),
21 ..Self::default()
22 }
23 }
24
25 pub fn matches(&self, object: &KnowledgeObjectV1) -> bool {
27 if self.source.as_ref().is_some_and(|source| {
28 object
29 .source_ref
30 .as_ref()
31 .is_none_or(|reference| reference.uri != *source)
32 }) {
33 return false;
34 }
35 if self.classification.is_some_and(|level| {
36 object
37 .classification
38 .as_ref()
39 .is_none_or(|classification| classification.level != level)
40 }) {
41 return false;
42 }
43 if let Some(timestamp) = self.valid_at {
44 let Some(validity) = &object.validity else {
45 return false;
46 };
47 if validity.superseded_by.is_some() {
48 return false;
49 }
50 let Ok(valid_from) = DateTime::parse_from_rfc3339(&validity.valid_from) else {
51 return false;
52 };
53 if timestamp < valid_from.with_timezone(&Utc) {
54 return false;
55 }
56 if let Some(valid_until) = &validity.valid_until {
57 let Ok(valid_until) = DateTime::parse_from_rfc3339(valid_until) else {
58 return false;
59 };
60 if timestamp > valid_until.with_timezone(&Utc) {
61 return false;
62 }
63 }
64 }
65 let object_tags = object
66 .extra
67 .get("tags")
68 .and_then(serde_json::Value::as_array);
69 self.tags.iter().all(|tag| {
70 object_tags.is_some_and(|tags| tags.iter().any(|value| value.as_str() == Some(tag)))
71 })
72 }
73}
74
75const TASK_FILLER_WORDS: &[&str] = &[
78 "a",
79 "an",
80 "and",
81 "are",
82 "as",
83 "at",
84 "be",
85 "by",
86 "for",
87 "from",
88 "how",
89 "i",
90 "in",
91 "into",
92 "is",
93 "it",
94 "its",
95 "me",
96 "my",
97 "of",
98 "on",
99 "or",
100 "our",
101 "please",
102 "so",
103 "that",
104 "the",
105 "then",
106 "this",
107 "to",
108 "up",
109 "we",
110 "what",
111 "when",
112 "where",
113 "which",
114 "why",
115 "with",
116 "add",
119 "analyse",
120 "analyze",
121 "change",
122 "check",
123 "create",
124 "debug",
125 "explain",
126 "explore",
127 "find",
128 "fix",
129 "implement",
130 "improve",
131 "inspect",
132 "investigate",
133 "look",
134 "make",
135 "refactor",
136 "remove",
137 "rename",
138 "review",
139 "see",
140 "show",
141 "understand",
142 "update",
143 "verify",
144 "write",
145];
146
147fn query_terms(query: &str) -> Vec<String> {
153 let mut terms: Vec<String> = Vec::new();
154 for token in super::ranking::tokenize_lower(query) {
155 if !terms.contains(&token) {
156 terms.push(token);
157 }
158 }
159 terms
160}
161
162fn task_content_terms(task: &str) -> Vec<String> {
166 let mut terms: Vec<String> = Vec::new();
167 for token in super::ranking::tokenize_lower(task) {
168 let term = token.trim_matches(|c: char| !c.is_alphanumeric());
169 if term.is_empty() || TASK_FILLER_WORDS.contains(&term) {
170 continue;
171 }
172 if !terms.iter().any(|t| t == term) {
173 terms.push(term.to_string());
174 }
175 }
176 terms
177}
178
179impl ProjectKnowledge {
180 fn matching_indices(&self, term: &str, include_session: bool) -> Vec<usize> {
181 let Some(indices) = self.index.token_positions.get(term) else {
182 return if include_session {
183 self.index
184 .session_token_positions
185 .get(term)
186 .cloned()
187 .unwrap_or_default()
188 } else {
189 Vec::new()
190 };
191 };
192
193 if !include_session {
194 return indices.clone();
195 }
196
197 let Some(session_indices) = self.index.session_token_positions.get(term) else {
198 return indices.clone();
199 };
200 let mut merged = indices.clone();
201 merged.extend(
202 session_indices
203 .iter()
204 .copied()
205 .filter(|idx| indices.binary_search(idx).is_err()),
206 );
207 merged
208 }
209
210 pub fn recall(&self, query: &str) -> Vec<&KnowledgeFact> {
211 let terms = query_terms(query);
212 if terms.is_empty() {
213 return Vec::new();
214 }
215
216 let mut match_counts: std::collections::HashMap<usize, usize> =
217 std::collections::HashMap::new();
218 for term in &terms {
219 for idx in self.matching_indices(term, true) {
220 if self.facts[idx].is_current() {
221 *match_counts.entry(idx).or_insert(0) += 1;
222 }
223 }
224 }
225
226 let mut results: Vec<(&KnowledgeFact, f32)> = match_counts
227 .into_iter()
228 .map(|(idx, count)| {
229 let f = &self.facts[idx];
230 let relevance = (count as f32 / terms.len() as f32) * f.quality_score();
231 (f, relevance)
232 })
233 .collect();
234
235 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
236 results.into_iter().map(|(f, _)| f).collect()
237 }
238
239 pub fn recall_for_task(&self, task: &str) -> Vec<&KnowledgeFact> {
249 let terms = task_content_terms(task);
250 if terms.is_empty() {
251 return Vec::new();
252 }
253 self.recall(&terms.join(" "))
254 }
255
256 pub fn recall_by_category(&self, category: &str) -> Vec<&KnowledgeFact> {
257 self.index
258 .category_positions
259 .get(category)
260 .into_iter()
261 .flatten()
262 .filter_map(|&idx| self.facts.get(idx))
263 .filter(|f| f.is_current())
264 .collect()
265 }
266
267 pub fn recall_at_time(&self, query: &str, at: DateTime<Utc>) -> Vec<&KnowledgeFact> {
268 let terms = query_terms(query);
269 if terms.is_empty() {
270 return Vec::new();
271 }
272
273 let mut match_counts: std::collections::HashMap<usize, usize> =
274 std::collections::HashMap::new();
275 for term in &terms {
276 for idx in self.matching_indices(term, false) {
277 if self.facts[idx].was_valid_at(at) {
278 *match_counts.entry(idx).or_insert(0) += 1;
279 }
280 }
281 }
282
283 let mut results: Vec<(&KnowledgeFact, f32)> = match_counts
284 .into_iter()
285 .map(|(idx, count)| {
286 let f = &self.facts[idx];
287 (f, count as f32 / terms.len() as f32)
288 })
289 .collect();
290
291 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
292 results.into_iter().map(|(f, _)| f).collect()
293 }
294
295 pub fn timeline(&self, category: &str) -> Vec<&KnowledgeFact> {
296 let mut facts: Vec<&KnowledgeFact> = self
297 .index
298 .category_positions
299 .get(category)
300 .into_iter()
301 .flatten()
302 .filter_map(|&idx| self.facts.get(idx))
303 .collect();
304 facts.sort_by_key(|x| x.created_at);
305 facts
306 }
307
308 pub fn list_rooms(&self) -> Vec<(String, usize)> {
309 let mut categories: std::collections::BTreeMap<String, usize> =
310 std::collections::BTreeMap::new();
311 for f in &self.facts {
312 if f.is_current() {
313 *categories.entry(f.category.clone()).or_insert(0) += 1;
314 }
315 }
316 categories.into_iter().collect()
317 }
318
319 pub fn recall_for_output(&mut self, query: &str, limit: usize) -> (Vec<KnowledgeFact>, usize) {
320 let q = query.to_lowercase();
321 let terms = query_terms(query);
322 if terms.is_empty() {
323 return (Vec::new(), 0);
324 }
325
326 let mut match_counts: std::collections::HashMap<usize, usize> =
327 std::collections::HashMap::new();
328 for term in &terms {
329 for idx in self.matching_indices(term, true) {
330 if self.facts[idx].is_current() {
331 *match_counts.entry(idx).or_insert(0) += 1;
332 }
333 }
334 }
335
336 struct Scored {
337 idx: usize,
338 relevance: f32,
339 }
340
341 let mut scored: Vec<Scored> = match_counts
342 .into_iter()
343 .map(|(idx, count)| {
344 let f = &self.facts[idx];
345 let mut relevance = (count as f32 / terms.len() as f32) * f.confidence;
346 let key_lower = f.key.to_lowercase();
350 if key_lower == q {
351 relevance += 1.0;
352 } else if f.category.to_lowercase() == q {
353 relevance += 0.5;
354 }
355 if f.is_synthesized_observation() {
360 relevance += 0.4;
361 }
362 Scored { idx, relevance }
363 })
364 .collect();
365
366 scored.sort_by(|a, b| {
367 b.relevance
368 .partial_cmp(&a.relevance)
369 .unwrap_or(std::cmp::Ordering::Equal)
370 .then_with(|| sort_fact_for_output(&self.facts[a.idx], &self.facts[b.idx]))
371 });
372
373 let total = scored.len();
374 scored.truncate(limit);
375
376 let now = Utc::now();
377 let mut out: Vec<KnowledgeFact> = Vec::new();
378 for s in scored {
379 if let Some(f) = self.facts.get_mut(s.idx) {
380 f.retrieval_count = f.retrieval_count.saturating_add(1);
381 f.last_retrieved = Some(now);
382 out.push(f.clone());
383 }
384 }
385
386 (out, total)
387 }
388
389 pub fn recall_by_category_for_output(
390 &mut self,
391 category: &str,
392 limit: usize,
393 ) -> (Vec<KnowledgeFact>, usize) {
394 let mut idxs: Vec<usize> = self
395 .index
396 .category_positions
397 .get(category)
398 .into_iter()
399 .flatten()
400 .copied()
401 .filter(|&idx| self.facts[idx].is_current())
402 .collect();
403
404 idxs.sort_by(|a, b| {
407 let (fa, fb) = (&self.facts[*a], &self.facts[*b]);
408 fb.is_synthesized_observation()
409 .cmp(&fa.is_synthesized_observation())
410 .then_with(|| sort_fact_for_output(fa, fb))
411 });
412
413 let total = idxs.len();
414 idxs.truncate(limit);
415
416 let now = Utc::now();
417 let mut out = Vec::new();
418 for idx in idxs {
419 if let Some(f) = self.facts.get_mut(idx) {
420 f.retrieval_count = f.retrieval_count.saturating_add(1);
421 f.last_retrieved = Some(now);
422 out.push(f.clone());
423 }
424 }
425
426 (out, total)
427 }
428}