use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelevanceScore {
pub path: String,
pub score: f64,
pub reason: String,
}
#[derive(Debug, Clone, Default)]
pub struct AttentionWindow {
pub ranked_files: Vec<RelevanceScore>,
pub max_files: usize,
pub max_tokens: usize,
}
impl AttentionWindow {
pub fn new(max_files: usize, max_tokens: usize) -> Self {
Self {
ranked_files: Vec::new(),
max_files,
max_tokens,
}
}
pub fn top_files(&self, n: usize) -> Vec<&RelevanceScore> {
self.ranked_files
.iter()
.take(n.min(self.max_files))
.collect()
}
pub fn add(&mut self, score: RelevanceScore) {
let pos = self
.ranked_files
.binary_search_by(|s| {
s.score
.partial_cmp(&score.score)
.unwrap_or(std::cmp::Ordering::Equal)
.reverse()
})
.unwrap_or_else(|p| p);
self.ranked_files.insert(pos, score);
}
}
pub struct AttentionMechanism {
cache: HashMap<String, AttentionWindow>,
default_max_files: usize,
default_max_tokens: usize,
}
impl AttentionMechanism {
pub fn new(max_files: usize, max_tokens: usize) -> Self {
Self {
cache: HashMap::new(),
default_max_files: max_files,
default_max_tokens: max_tokens,
}
}
pub fn from_search_results(
&mut self,
task_id: &str,
results: Vec<(String, f64, String)>,
) -> &AttentionWindow {
let mut window = AttentionWindow::new(self.default_max_files, self.default_max_tokens);
for (path, score, reason) in results {
window.add(RelevanceScore {
path,
score,
reason,
});
}
self.cache.insert(task_id.to_string(), window);
self.cache.get(task_id).expect("just inserted")
}
pub fn get(&self, task_id: &str) -> Option<&AttentionWindow> {
self.cache.get(task_id)
}
pub fn clear(&mut self) {
self.cache.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attention_window_new_starts_empty() {
let w = AttentionWindow::new(10, 5000);
assert!(w.ranked_files.is_empty());
assert_eq!(w.max_files, 10);
assert_eq!(w.max_tokens, 5000);
}
#[test]
fn add_maintains_descending_score_order() {
let mut w = AttentionWindow::new(10, 5000);
w.add(RelevanceScore {
path: "a.rs".into(),
score: 0.5,
reason: "".into(),
});
w.add(RelevanceScore {
path: "b.rs".into(),
score: 0.9,
reason: "".into(),
});
w.add(RelevanceScore {
path: "c.rs".into(),
score: 0.7,
reason: "".into(),
});
assert_eq!(w.ranked_files[0].path, "b.rs");
assert_eq!(w.ranked_files[1].path, "c.rs");
assert_eq!(w.ranked_files[2].path, "a.rs");
}
#[test]
fn top_files_returns_correct_count() {
let mut w = AttentionWindow::new(5, 5000);
for i in 0..10 {
w.add(RelevanceScore {
path: format!("file{i}.rs"),
score: i as f64 / 10.0,
reason: "".into(),
});
}
assert_eq!(w.top_files(3).len(), 3);
assert_eq!(w.top_files(10).len(), 5); }
#[test]
fn attention_mechanism_from_search_results_and_get() {
let mut mech = AttentionMechanism::new(10, 5000);
let results = vec![
("src/main.rs".to_string(), 0.9, "entry point".to_string()),
("src/lib.rs".to_string(), 0.7, "library".to_string()),
];
let window = mech.from_search_results("task-1", results);
assert_eq!(window.ranked_files.len(), 2);
assert_eq!(window.ranked_files[0].path, "src/main.rs");
let cached = mech.get("task-1").expect("should be cached");
assert_eq!(cached.ranked_files.len(), 2);
assert!(mech.get("nonexistent").is_none());
}
}