use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryProfile {
pub token_count: usize,
pub has_entities: bool,
pub is_question: bool,
pub specificity: f64,
pub needs_provenance: bool,
pub needs_temporal: bool,
pub contradiction_risk: bool,
pub latency_budget_ms: u64,
pub token_budget: usize,
}
impl QueryProfile {
pub fn from_query(query: &str) -> Self {
let lower = query.to_lowercase();
let token_count = query.split_whitespace().count();
let has_entities = query
.split_whitespace()
.any(|w| w.chars().next().map(|c| c.is_uppercase()).unwrap_or(false));
let question_words = ["what", "why", "how", "when", "where", "who", "which", "is", "are", "can", "does"];
let is_question = query.contains('?') || question_words.iter().any(|w| lower.starts_with(w));
let specificity = (token_count as f64 / 20.0).min(1.0);
let needs_provenance = ["source", "evidence", "cite", "citation", "reference", "proof"]
.iter().any(|w| lower.contains(w));
let needs_temporal = ["recent", "latest", "before", "after", "since", "until", "current", "new", "old", "updated"]
.iter().any(|w| lower.contains(w));
let contradiction_risk = ["vs", "compare", "conflict", "contradict", "versus", "difference", "disagree"]
.iter().any(|w| lower.contains(w));
Self {
token_count,
has_entities,
is_question,
specificity,
needs_provenance,
needs_temporal,
contradiction_risk,
latency_budget_ms: 0,
token_budget: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingDecision {
pub bm25_coarse: bool,
pub vector_medium: bool,
pub rerank_fine: bool,
pub graph_expansion: bool,
pub decoder: bool,
pub discord: bool,
pub no_retrieval: bool,
pub reasoning: String,
}
pub struct RetrievalRouter {
pub min_query_length: usize,
pub broad_query_threshold: f64,
pub decoder_enabled: bool,
pub discord_enabled: bool,
pub corpus_density: f64,
}
impl Default for RetrievalRouter {
fn default() -> Self {
Self {
min_query_length: 3,
broad_query_threshold: 0.15,
decoder_enabled: false,
discord_enabled: false,
corpus_density: 0.5,
}
}
}
impl RetrievalRouter {
pub fn route(&self, profile: &QueryProfile) -> RoutingDecision {
let mut reasoning_parts = Vec::new();
let no_retrieval = profile.token_count < self.min_query_length
&& !profile.needs_provenance
&& !profile.needs_temporal;
if no_retrieval {
reasoning_parts.push(format!(
"query too short ({} tokens < {}), no provenance/temporal need → skip retrieval",
profile.token_count, self.min_query_length
));
return RoutingDecision {
bm25_coarse: false,
vector_medium: false,
rerank_fine: false,
graph_expansion: false,
decoder: false,
discord: false,
no_retrieval: true,
reasoning: reasoning_parts.join("; "),
};
}
let bm25_coarse = true;
reasoning_parts.push("BM25 always on (cheap keyword match)".to_string());
let vector_medium = profile.specificity >= self.broad_query_threshold;
if !vector_medium {
reasoning_parts.push(format!(
"vector search skipped (specificity {:.2} < {:.2} → too broad)",
profile.specificity, self.broad_query_threshold
));
} else {
reasoning_parts.push("vector search on (sufficient specificity)".to_string());
}
let rerank_fine = vector_medium;
if rerank_fine {
reasoning_parts.push("rerank on (follows vector search)".to_string());
}
let graph_expansion = self.corpus_density > 0.3 && profile.has_entities;
if graph_expansion {
reasoning_parts.push(format!(
"graph expansion on (corpus density {:.2} > 0.3, entities detected)",
self.corpus_density
));
} else {
reasoning_parts.push("graph expansion off (sparse corpus or no entities)".to_string());
}
let decoder = profile.contradiction_risk && self.decoder_enabled;
if decoder {
reasoning_parts.push("decoder on (contradiction risk detected)".to_string());
} else if profile.contradiction_risk {
reasoning_parts.push("decoder off (contradiction risk but feature not enabled)".to_string());
}
let discord = profile.has_entities && self.discord_enabled;
if discord {
reasoning_parts.push("discord on (entities detected, second-order discovery)".to_string());
}
if profile.needs_temporal {
reasoning_parts.push("temporal boost recommended (temporal query)".to_string());
}
if profile.needs_provenance {
reasoning_parts.push("provenance filter recommended (provenance query)".to_string());
}
RoutingDecision {
bm25_coarse,
vector_medium,
rerank_fine,
graph_expansion,
decoder,
discord,
no_retrieval: false,
reasoning: reasoning_parts.join("; "),
}
}
pub fn route_query(&self, query: &str) -> RoutingDecision {
let profile = QueryProfile::from_query(query);
self.route(&profile)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingMetrics {
pub routing_correct: bool,
pub quality_per_token: f64,
pub quality_per_latency: f64,
pub retrieval_overuse: bool,
pub retrieval_underuse: bool,
pub contradiction_aware: bool,
pub provenance_aware: bool,
pub routing_deterministic: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn route_short_query_skips_retrieval() {
let router = RetrievalRouter::default();
let decision = router.route_query("hi");
assert!(decision.no_retrieval, "short query should skip retrieval");
assert!(!decision.bm25_coarse);
}
#[test]
fn route_normal_query_uses_bm25_and_vector() {
let router = RetrievalRouter::default();
let decision = router.route_query("what is the architecture of semantic memory");
assert!(decision.bm25_coarse, "normal query should use BM25");
assert!(decision.vector_medium, "normal query should use vector search");
assert!(!decision.no_retrieval);
}
#[test]
fn route_broad_query_skips_vector() {
let router = RetrievalRouter {
broad_query_threshold: 0.15,
..Default::default()
};
let decision = router.route_query("a b c");
assert!(decision.bm25_coarse, "BM25 always on");
let router2 = RetrievalRouter {
broad_query_threshold: 0.2,
..Default::default()
};
let decision2 = router2.route_query("a b c");
assert!(decision2.bm25_coarse, "BM25 always on");
assert!(!decision2.vector_medium, "broad query should skip vector with threshold 0.2");
}
#[test]
fn route_contradiction_query_enables_decoder() {
let router = RetrievalRouter {
decoder_enabled: true,
..Default::default()
};
let decision = router.route_query("compare rust vs python performance differences");
assert!(decision.decoder, "contradiction query should enable decoder");
}
#[test]
fn route_contradiction_query_decoder_disabled_when_feature_off() {
let router = RetrievalRouter {
decoder_enabled: false,
..Default::default()
};
let decision = router.route_query("compare rust vs python performance differences");
assert!(!decision.decoder, "decoder should be off when feature not enabled");
}
#[test]
fn route_provenance_query_noted_in_reasoning() {
let router = RetrievalRouter::default();
let decision = router.route_query("what is the source of the turbo-quant compression algorithm");
assert!(decision.reasoning.contains("provenance"), "reasoning should mention provenance");
}
#[test]
fn route_temporal_query_noted_in_reasoning() {
let router = RetrievalRouter::default();
let decision = router.route_query("what are the latest developments in vector search");
assert!(decision.reasoning.contains("temporal"), "reasoning should mention temporal");
}
#[test]
fn route_entity_query_enables_graph_expansion() {
let router = RetrievalRouter {
corpus_density: 0.7,
..Default::default()
};
let decision = router.route_query("how does Semantic-Memory integrate with Turbo-Quant");
assert!(decision.graph_expansion, "entity query with dense corpus should enable graph expansion");
}
#[test]
fn route_entity_query_skips_graph_if_sparse_corpus() {
let router = RetrievalRouter {
corpus_density: 0.1, ..Default::default()
};
let decision = router.route_query("how does Semantic-Memory integrate with Turbo-Quant");
assert!(!decision.graph_expansion, "entity query with sparse corpus should skip graph expansion");
}
#[test]
fn route_discord_enabled_with_entities() {
let router = RetrievalRouter {
discord_enabled: true,
..Default::default()
};
let decision = router.route_query("how does AiDENs work with Recall");
assert!(decision.discord, "entity query with discord enabled should activate discord");
}
#[test]
fn route_is_deterministic() {
let router = RetrievalRouter::default();
let query = "what is the architecture of semantic memory";
let d1 = router.route_query(query);
let d2 = router.route_query(query);
assert_eq!(d1.bm25_coarse, d2.bm25_coarse);
assert_eq!(d1.vector_medium, d2.vector_medium);
assert_eq!(d1.decoder, d2.decoder);
assert_eq!(d1.no_retrieval, d2.no_retrieval);
}
#[test]
fn route_reasoning_is_populated() {
let router = RetrievalRouter::default();
let decision = router.route_query("compare the latest source evidence for rust vs python");
assert!(!decision.reasoning.is_empty(), "reasoning should be populated");
assert!(decision.reasoning.contains(";"), "reasoning should have multiple parts");
}
#[test]
fn query_profile_detects_characteristics() {
let profile = QueryProfile::from_query("What is the latest source of evidence for Rust vs Python?");
assert!(profile.is_question);
assert!(profile.needs_provenance);
assert!(profile.needs_temporal);
assert!(profile.contradiction_risk);
assert!(profile.has_entities);
}
}