knowledge_runtime/query/
route.rs1use crate::ids::Scope;
4use crate::query::classify::QueryMode;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RoutePlan {
13 pub query: String,
15 pub scope: Scope,
17 pub legs: Vec<RouteLeg>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct RouteLeg {
24 pub strategy: RetrievalStrategy,
26 pub limit: usize,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub filter: Option<LegFilter>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum RetrievalStrategy {
37 HybridSearch,
39 EntitySearch {
41 mention: String,
43 },
44 TemporalSearch {
46 temporal_expr: String,
48 },
49}
50
51impl RetrievalStrategy {
52 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#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct LegFilter {
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub source_types: Option<Vec<String>>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub namespace: Option<String>,
71}
72
73pub 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}