mempal 0.5.4

Project memory for coding agents. Single binary, hybrid search, knowledge graph.
Documentation
use std::collections::BTreeSet;

use crate::core::types::{RouteDecision, TaxonomyEntry};

pub fn route_query(query: &str, taxonomy: &[TaxonomyEntry]) -> RouteDecision {
    let normalized_query = query.to_lowercase();
    let query_terms = query_terms(&normalized_query);

    let Some((entry, matched_keywords)) = taxonomy
        .iter()
        .filter_map(|entry| {
            let matched_keywords = matched_keywords(&normalized_query, &query_terms, entry);
            (!matched_keywords.is_empty()).then_some((entry, matched_keywords))
        })
        .max_by(|(left_entry, left_matches), (right_entry, right_matches)| {
            left_matches
                .len()
                .cmp(&right_matches.len())
                .then_with(|| {
                    left_matches
                        .iter()
                        .map(String::len)
                        .sum::<usize>()
                        .cmp(&right_matches.iter().map(String::len).sum::<usize>())
                })
                .then_with(|| left_entry.keywords.len().cmp(&right_entry.keywords.len()))
        })
    else {
        return RouteDecision {
            wing: None,
            room: None,
            confidence: 0.0,
            reason: "global search: no taxonomy keyword match".to_string(),
        };
    };

    let room = (!entry.room.is_empty()).then_some(entry.room.clone());
    let confidence = match matched_keywords.len() {
        0 => 0.0,
        1 => 0.6,
        2 => 0.8,
        _ => 1.0,
    };
    let reason = format!(
        "taxonomy match: {} / {} via keywords [{}]",
        entry.wing,
        room.as_deref().unwrap_or("default"),
        matched_keywords.join(", ")
    );

    RouteDecision {
        wing: Some(entry.wing.clone()),
        room,
        confidence,
        reason,
    }
}

fn matched_keywords(
    normalized_query: &str,
    query_terms: &BTreeSet<String>,
    entry: &TaxonomyEntry,
) -> Vec<String> {
    entry
        .keywords
        .iter()
        .map(|keyword| keyword.trim().to_lowercase())
        .filter(|keyword| {
            !keyword.is_empty()
                && (query_terms.contains(keyword) || normalized_query.contains(keyword.as_str()))
        })
        .collect()
}

fn query_terms(query: &str) -> BTreeSet<String> {
    query
        .split(|ch: char| !ch.is_alphanumeric())
        .filter(|term| !term.is_empty())
        .map(ToOwned::to_owned)
        .collect()
}