Skip to main content

knowledge_runtime/query/
route.rs

1//! Route planning: translates classified query modes into retrieval legs.
2
3use crate::ids::Scope;
4use crate::query::classify::QueryMode;
5use serde::{Deserialize, Serialize};
6
7/// A typed, inspectable query plan.
8///
9/// The planner translates a classified `QueryMode` into a sequence of
10/// `RouteLeg`s, each of which targets a specific retrieval strategy.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RoutePlan {
13    /// Original query text.
14    pub query: String,
15    /// Scope for all legs.
16    pub scope: Scope,
17    /// Ordered retrieval legs.
18    pub legs: Vec<RouteLeg>,
19}
20
21/// A single retrieval leg within a route plan.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RouteLeg {
24    /// What this leg retrieves.
25    pub strategy: RetrievalStrategy,
26    /// Maximum results for this leg.
27    pub limit: usize,
28    /// Optional filter applied to this leg.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub filter: Option<LegFilter>,
31}
32
33/// Retrieval strategy for a route leg.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum RetrievalStrategy {
37    /// Hybrid BM25 + vector search via `semantic-memory`.
38    HybridSearch,
39    /// Entity registry lookup followed by targeted search.
40    EntitySearch {
41        /// The entity mention to resolve.
42        mention: String,
43    },
44    /// Time-bounded search.
45    TemporalSearch {
46        /// Raw temporal expression (resolved at execution time).
47        temporal_expr: String,
48    },
49}
50
51impl RetrievalStrategy {
52    /// Stable discriminant string for logging and provenance records.
53    pub fn kind(&self) -> &'static str {
54        match self {
55            Self::HybridSearch => "hybrid",
56            Self::EntitySearch { .. } => "entity",
57            Self::TemporalSearch { .. } => "temporal",
58        }
59    }
60}
61
62/// Optional filter narrowing a route leg.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct LegFilter {
65    /// Restrict to specific source types.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub source_types: Option<Vec<String>>,
68    /// Restrict to a specific namespace.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub namespace: Option<String>,
71}
72
73/// Plan a route from a classified query mode.
74pub fn plan(query: &str, mode: &QueryMode, scope: &Scope, default_limit: usize) -> RoutePlan {
75    let legs = match mode {
76        QueryMode::SemanticLookup => vec![RouteLeg {
77            strategy: RetrievalStrategy::HybridSearch,
78            limit: default_limit,
79            filter: None,
80        }],
81        QueryMode::EntityLookup { mention } => vec![RouteLeg {
82            strategy: RetrievalStrategy::EntitySearch {
83                mention: mention.clone(),
84            },
85            limit: default_limit,
86            filter: None,
87        }],
88        QueryMode::TemporalLookup { temporal_expr } => vec![RouteLeg {
89            strategy: RetrievalStrategy::TemporalSearch {
90                temporal_expr: temporal_expr.clone(),
91            },
92            limit: default_limit,
93            filter: None,
94        }],
95        QueryMode::Mixed { components } => components
96            .iter()
97            .map(|c| match c {
98                QueryMode::SemanticLookup => RouteLeg {
99                    strategy: RetrievalStrategy::HybridSearch,
100                    limit: default_limit,
101                    filter: None,
102                },
103                QueryMode::EntityLookup { mention } => RouteLeg {
104                    strategy: RetrievalStrategy::EntitySearch {
105                        mention: mention.clone(),
106                    },
107                    limit: default_limit,
108                    filter: None,
109                },
110                QueryMode::TemporalLookup { temporal_expr } => RouteLeg {
111                    strategy: RetrievalStrategy::TemporalSearch {
112                        temporal_expr: temporal_expr.clone(),
113                    },
114                    limit: default_limit,
115                    filter: None,
116                },
117                QueryMode::Mixed { .. } => RouteLeg {
118                    strategy: RetrievalStrategy::HybridSearch,
119                    limit: default_limit,
120                    filter: None,
121                },
122            })
123            .collect(),
124    };
125
126    RoutePlan {
127        query: query.to_string(),
128        scope: scope.clone(),
129        legs,
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn semantic_produces_one_hybrid_leg() {
139        let scope = Scope::new("test");
140        let plan = plan("hello", &QueryMode::SemanticLookup, &scope, 10);
141        assert_eq!(plan.legs.len(), 1);
142        assert_eq!(plan.legs[0].strategy.kind(), "hybrid");
143    }
144
145    #[test]
146    fn mixed_produces_multiple_legs() {
147        let scope = Scope::new("test");
148        let mode = QueryMode::Mixed {
149            components: vec![
150                QueryMode::EntityLookup {
151                    mention: "foo".into(),
152                },
153                QueryMode::TemporalLookup {
154                    temporal_expr: "last week".into(),
155                },
156            ],
157        };
158        let plan = plan("find foo from last week", &mode, &scope, 10);
159        assert_eq!(plan.legs.len(), 2);
160        assert_eq!(plan.legs[0].strategy.kind(), "entity");
161        assert_eq!(plan.legs[1].strategy.kind(), "temporal");
162    }
163}