weavatrix-memory 0.3.1

Event-sourced, bitemporal context compiler for coding agents
Documentation
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use weavatrix_memory::{EvaluationCase, RankedPrediction};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
    pub id: String,
    pub text: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreparedCase {
    pub id: String,
    pub group_id: String,
    pub category: String,
    pub query: String,
    pub relevant_ids: BTreeSet<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreparedBenchmark {
    pub name: String,
    pub groups: BTreeMap<String, Vec<Document>>,
    pub cases: Vec<PreparedCase>,
}

impl PreparedBenchmark {
    pub fn evaluation_cases(&self) -> Vec<EvaluationCase> {
        self.cases
            .iter()
            .map(|case| EvaluationCase {
                id: case.id.clone(),
                category: case.category.clone(),
                relevant_ids: case.relevant_ids.clone(),
            })
            .collect()
    }

    pub fn literal_predictions(&self, limit: usize) -> Vec<RankedPrediction> {
        self.cases
            .iter()
            .map(|case| {
                let query = tokens(&case.query);
                let mut ranked = self.groups[&case.group_id]
                    .iter()
                    .map(|document| {
                        let terms = tokens(&document.text);
                        let score = query.intersection(&terms).count();
                        (score, document.id.as_str())
                    })
                    .collect::<Vec<_>>();
                ranked
                    .sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(right.1)));
                RankedPrediction {
                    case_id: case.id.clone(),
                    ranked_ids: ranked
                        .into_iter()
                        .take(limit)
                        .map(|(_, id)| id.to_owned())
                        .collect(),
                }
            })
            .collect()
    }

    pub fn validate(&self) -> Result<(), String> {
        let mut ids = BTreeSet::new();
        for case in &self.cases {
            let Some(documents) = self.groups.get(&case.group_id) else {
                return Err(format!("case {} references a missing group", case.id));
            };
            let document_ids = documents
                .iter()
                .map(|document| document.id.as_str())
                .collect::<BTreeSet<_>>();
            if case.id.is_empty()
                || case.query.is_empty()
                || case.relevant_ids.is_empty()
                || !ids.insert(case.id.as_str())
                || !case
                    .relevant_ids
                    .iter()
                    .all(|id| document_ids.contains(id.as_str()))
            {
                return Err(format!("invalid prepared case {}", case.id));
            }
        }
        Ok(())
    }
}

fn tokens(text: &str) -> BTreeSet<String> {
    text.split(|character: char| !character.is_alphanumeric())
        .filter(|token| token.len() > 1)
        .map(str::to_lowercase)
        .collect()
}