use std::sync::Arc;
use crate::error::AmpError;
use crate::store::MemoryStore;
use crate::wire::{AmpEnvelope, AmpHit, AmpOp, AmpResult};
#[derive(Clone)]
pub struct AmpRouter {
stores: Vec<Arc<dyn MemoryStore>>,
rrf_k: f32,
}
impl AmpRouter {
pub fn new(store: Arc<dyn MemoryStore>) -> Self {
Self {
stores: vec![store],
rrf_k: 60.0,
}
}
pub fn fan_out(stores: Vec<Arc<dyn MemoryStore>>) -> Self {
Self {
stores,
rrf_k: 60.0,
}
}
pub fn with_rrf_k(mut self, k: f32) -> Self {
self.rrf_k = k;
self
}
pub async fn route(&self, env: &AmpEnvelope) -> Result<AmpResult, AmpError> {
match env.op {
AmpOp::Recall => self.route_recall(env).await,
_ => {
let mut first: Option<AmpResult> = None;
for store in &self.stores {
let r = store.dispatch(env).await?;
if first.is_none() {
first = Some(r);
}
}
first.ok_or_else(|| AmpError::Validation("router has no backends".into()))
}
}
}
async fn route_recall(&self, env: &AmpEnvelope) -> Result<AmpResult, AmpError> {
if self.stores.len() == 1 {
return self.stores[0].recall(env).await;
}
let mut lists: Vec<Vec<AmpHit>> = Vec::with_capacity(self.stores.len());
for store in &self.stores {
lists.push(store.recall(env).await?.hits);
}
let fused = rrf_fuse(&lists, self.rrf_k);
let mut out = AmpResult::ok(AmpOp::Recall);
out.hits = fused;
Ok(out)
}
}
pub fn rrf_fuse(lists: &[Vec<AmpHit>], k: f32) -> Vec<AmpHit> {
use std::collections::HashMap;
let mut score: HashMap<String, f32> = HashMap::new();
let mut repr: HashMap<String, AmpHit> = HashMap::new();
for list in lists {
for (rank, hit) in list.iter().enumerate() {
*score.entry(hit.id.clone()).or_insert(0.0) += 1.0 / (k + rank as f32);
repr.entry(hit.id.clone()).or_insert_with(|| hit.clone());
}
}
sort_by_fused(score, repr)
}
pub fn max_fuse(lists: &[Vec<AmpHit>]) -> Vec<AmpHit> {
use std::collections::HashMap;
let mut score: HashMap<String, f32> = HashMap::new();
let mut repr: HashMap<String, AmpHit> = HashMap::new();
for list in lists {
for hit in list {
let e = score.entry(hit.id.clone()).or_insert(f32::MIN);
if hit.score > *e {
*e = hit.score;
}
repr.entry(hit.id.clone()).or_insert_with(|| hit.clone());
}
}
sort_by_fused(score, repr)
}
fn sort_by_fused(
score: std::collections::HashMap<String, f32>,
repr: std::collections::HashMap<String, AmpHit>,
) -> Vec<AmpHit> {
let mut fused: Vec<AmpHit> = repr
.into_iter()
.map(|(id, mut hit)| {
hit.score = score.get(&id).copied().unwrap_or(0.0);
hit
})
.collect();
fused.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.id.cmp(&b.id))
});
fused
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::AmpMemoryType;
fn hit(id: &str, score: f32) -> AmpHit {
AmpHit {
id: id.to_string(),
content: format!("content-{id}"),
memory_type: AmpMemoryType::Semantic,
score,
tags: vec![],
}
}
#[test]
fn rrf_holds_under_rank0_injection_but_max_is_fooled() {
let list_a = vec![
hit("ADV", 999.0), hit("TRUE", 0.9), hit("x1", 0.5),
];
let list_b = vec![
hit("TRUE", 0.95), hit("y1", 0.6),
hit("y2", 0.4),
];
let rrf = rrf_fuse(&[list_a.clone(), list_b.clone()], 60.0);
assert_eq!(rrf[0].id, "TRUE", "RRF must rank the true item first");
let max = max_fuse(&[list_a, list_b]);
assert_eq!(
max[0].id, "ADV",
"max-fusion is fooled by the rank-0 injection"
);
}
#[test]
fn rrf_is_deterministic() {
let a = vec![hit("a", 0.9), hit("b", 0.8)];
let b = vec![hit("b", 0.7), hit("a", 0.6)];
let r1 = rrf_fuse(&[a.clone(), b.clone()], 60.0);
let r2 = rrf_fuse(&[a, b], 60.0);
assert_eq!(r1, r2);
}
}