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 task_content_terms(task: &str) -> Vec<String> {
151 let mut terms: Vec<String> = Vec::new();
152 for token in super::ranking::tokenize_lower(task) {
153 let term = token.trim_matches(|c: char| !c.is_alphanumeric());
154 if term.is_empty() || TASK_FILLER_WORDS.contains(&term) {
155 continue;
156 }
157 if !terms.iter().any(|t| t == term) {
158 terms.push(term.to_string());
159 }
160 }
161 terms
162}
163
164impl ProjectKnowledge {
165 fn matching_indices(&self, term: &str, include_session: bool) -> Vec<usize> {
166 let Some(indices) = self.index.token_positions.get(term) else {
167 return if include_session {
168 self.index
169 .session_token_positions
170 .get(term)
171 .cloned()
172 .unwrap_or_default()
173 } else {
174 Vec::new()
175 };
176 };
177
178 if !include_session {
179 return indices.clone();
180 }
181
182 let Some(session_indices) = self.index.session_token_positions.get(term) else {
183 return indices.clone();
184 };
185 let mut merged = indices.clone();
186 merged.extend(
187 session_indices
188 .iter()
189 .copied()
190 .filter(|idx| indices.binary_search(idx).is_err()),
191 );
192 merged
193 }
194
195 pub fn recall(&self, query: &str) -> Vec<&KnowledgeFact> {
196 let q = query.to_lowercase();
197 let terms: Vec<&str> = q.split_whitespace().collect();
198 if terms.is_empty() {
199 return Vec::new();
200 }
201
202 let mut match_counts: std::collections::HashMap<usize, usize> =
203 std::collections::HashMap::new();
204 for term in &terms {
205 for idx in self.matching_indices(term, true) {
206 if self.facts[idx].is_current() {
207 *match_counts.entry(idx).or_insert(0) += 1;
208 }
209 }
210 }
211
212 let mut results: Vec<(&KnowledgeFact, f32)> = match_counts
213 .into_iter()
214 .map(|(idx, count)| {
215 let f = &self.facts[idx];
216 let relevance = (count as f32 / terms.len() as f32) * f.quality_score();
217 (f, relevance)
218 })
219 .collect();
220
221 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
222 results.into_iter().map(|(f, _)| f).collect()
223 }
224
225 pub fn recall_for_task(&self, task: &str) -> Vec<&KnowledgeFact> {
235 let terms = task_content_terms(task);
236 if terms.is_empty() {
237 return Vec::new();
238 }
239 self.recall(&terms.join(" "))
240 }
241
242 pub fn recall_by_category(&self, category: &str) -> Vec<&KnowledgeFact> {
243 self.index
244 .category_positions
245 .get(category)
246 .into_iter()
247 .flatten()
248 .filter_map(|&idx| self.facts.get(idx))
249 .filter(|f| f.is_current())
250 .collect()
251 }
252
253 pub fn recall_at_time(&self, query: &str, at: DateTime<Utc>) -> Vec<&KnowledgeFact> {
254 let q = query.to_lowercase();
255 let terms: Vec<&str> = q.split_whitespace().collect();
256 if terms.is_empty() {
257 return Vec::new();
258 }
259
260 let mut match_counts: std::collections::HashMap<usize, usize> =
261 std::collections::HashMap::new();
262 for term in &terms {
263 for idx in self.matching_indices(term, false) {
264 if self.facts[idx].was_valid_at(at) {
265 *match_counts.entry(idx).or_insert(0) += 1;
266 }
267 }
268 }
269
270 let mut results: Vec<(&KnowledgeFact, f32)> = match_counts
271 .into_iter()
272 .map(|(idx, count)| {
273 let f = &self.facts[idx];
274 (f, count as f32 / terms.len() as f32)
275 })
276 .collect();
277
278 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
279 results.into_iter().map(|(f, _)| f).collect()
280 }
281
282 pub fn timeline(&self, category: &str) -> Vec<&KnowledgeFact> {
283 let mut facts: Vec<&KnowledgeFact> = self
284 .index
285 .category_positions
286 .get(category)
287 .into_iter()
288 .flatten()
289 .filter_map(|&idx| self.facts.get(idx))
290 .collect();
291 facts.sort_by_key(|x| x.created_at);
292 facts
293 }
294
295 pub fn list_rooms(&self) -> Vec<(String, usize)> {
296 let mut categories: std::collections::BTreeMap<String, usize> =
297 std::collections::BTreeMap::new();
298 for f in &self.facts {
299 if f.is_current() {
300 *categories.entry(f.category.clone()).or_insert(0) += 1;
301 }
302 }
303 categories.into_iter().collect()
304 }
305
306 pub fn recall_for_output(&mut self, query: &str, limit: usize) -> (Vec<KnowledgeFact>, usize) {
307 let q = query.to_lowercase();
308 let terms: Vec<&str> = q.split_whitespace().filter(|t| !t.is_empty()).collect();
309 if terms.is_empty() {
310 return (Vec::new(), 0);
311 }
312
313 let mut match_counts: std::collections::HashMap<usize, usize> =
314 std::collections::HashMap::new();
315 for term in &terms {
316 for idx in self.matching_indices(term, true) {
317 if self.facts[idx].is_current() {
318 *match_counts.entry(idx).or_insert(0) += 1;
319 }
320 }
321 }
322
323 struct Scored {
324 idx: usize,
325 relevance: f32,
326 }
327
328 let mut scored: Vec<Scored> = match_counts
329 .into_iter()
330 .map(|(idx, count)| {
331 let f = &self.facts[idx];
332 let mut relevance = (count as f32 / terms.len() as f32) * f.confidence;
333 let key_lower = f.key.to_lowercase();
337 if key_lower == q {
338 relevance += 1.0;
339 } else if f.category.to_lowercase() == q {
340 relevance += 0.5;
341 }
342 if f.is_synthesized_observation() {
347 relevance += 0.4;
348 }
349 Scored { idx, relevance }
350 })
351 .collect();
352
353 scored.sort_by(|a, b| {
354 b.relevance
355 .partial_cmp(&a.relevance)
356 .unwrap_or(std::cmp::Ordering::Equal)
357 .then_with(|| sort_fact_for_output(&self.facts[a.idx], &self.facts[b.idx]))
358 });
359
360 let total = scored.len();
361 scored.truncate(limit);
362
363 let now = Utc::now();
364 let mut out: Vec<KnowledgeFact> = Vec::new();
365 for s in scored {
366 if let Some(f) = self.facts.get_mut(s.idx) {
367 f.retrieval_count = f.retrieval_count.saturating_add(1);
368 f.last_retrieved = Some(now);
369 out.push(f.clone());
370 }
371 }
372
373 (out, total)
374 }
375
376 pub fn recall_by_category_for_output(
377 &mut self,
378 category: &str,
379 limit: usize,
380 ) -> (Vec<KnowledgeFact>, usize) {
381 let mut idxs: Vec<usize> = self
382 .index
383 .category_positions
384 .get(category)
385 .into_iter()
386 .flatten()
387 .copied()
388 .filter(|&idx| self.facts[idx].is_current())
389 .collect();
390
391 idxs.sort_by(|a, b| {
394 let (fa, fb) = (&self.facts[*a], &self.facts[*b]);
395 fb.is_synthesized_observation()
396 .cmp(&fa.is_synthesized_observation())
397 .then_with(|| sort_fact_for_output(fa, fb))
398 });
399
400 let total = idxs.len();
401 idxs.truncate(limit);
402
403 let now = Utc::now();
404 let mut out = Vec::new();
405 for idx in idxs {
406 if let Some(f) = self.facts.get_mut(idx) {
407 f.retrieval_count = f.retrieval_count.saturating_add(1);
408 f.last_retrieved = Some(now);
409 out.push(f.clone());
410 }
411 }
412
413 (out, total)
414 }
415}