1use hirn_core::types::Layer;
15use hirn_core::{DerivedArtifactKind, EvidenceRole, HirnResult, HydrationMode, ModalityProfile};
16
17use crate::ActivationMode;
18use crate::db::HirnDB;
19
20use super::ast::*;
21use super::direct_support;
22use super::planner;
23use super::results::QueryResult;
24
25use super::context::ContextConfig;
26
27pub struct QueryBuilder<'a> {
29 db: &'a HirnDB,
30 layers: Vec<Layer>,
31 about: Option<String>,
32 involving: Option<Vec<String>>,
33 temporal: Option<TemporalClause>,
34 expand: Option<ExpandClause>,
35 follow_causes: Option<usize>,
36 where_clauses: Vec<WhereCondition>,
37 modalities: Option<Vec<String>>,
38 resource_roles: Option<Vec<String>>,
39 hydration_modes: Option<Vec<String>>,
40 artifact_kinds: Option<Vec<String>>,
41 output_format: Option<OutputFormat>,
42 budget: Option<usize>,
43 namespace: Option<String>,
44 consistency: Option<ConsistencyLevel>,
45 limit: Option<usize>,
46 context_config: Option<ContextConfig>,
47}
48
49impl<'a> QueryBuilder<'a> {
50 pub fn new(db: &'a HirnDB) -> Self {
52 Self {
53 db,
54 layers: vec![],
55 about: None,
56 involving: None,
57 temporal: None,
58 expand: None,
59 follow_causes: None,
60 where_clauses: vec![],
61 modalities: None,
62 resource_roles: None,
63 hydration_modes: None,
64 artifact_kinds: None,
65 output_format: None,
66 budget: None,
67 namespace: None,
68 consistency: None,
69 limit: None,
70 context_config: None,
71 }
72 }
73
74 pub fn recall(mut self, layers: &[Layer]) -> Self {
76 self.layers = layers.to_vec();
77 self
78 }
79
80 pub fn about(mut self, query: &str) -> Self {
82 self.about = Some(query.to_string());
83 self
84 }
85
86 pub fn involving(mut self, entities: &[&str]) -> Self {
88 self.involving = Some(entities.iter().map(|s| (*s).to_string()).collect());
89 self
90 }
91
92 pub fn modalities(mut self, modalities: &[ModalityProfile]) -> Self {
94 self.modalities = Some(
95 modalities
96 .iter()
97 .map(|modality| modality.as_str().to_string())
98 .collect(),
99 );
100 self
101 }
102
103 pub fn resource_roles(mut self, roles: &[EvidenceRole]) -> Self {
105 self.resource_roles = Some(roles.iter().map(|role| role.as_str().to_string()).collect());
106 self
107 }
108
109 pub fn hydration_modes(mut self, modes: &[HydrationMode]) -> Self {
111 self.hydration_modes = Some(modes.iter().map(|mode| mode.as_str().to_string()).collect());
112 self
113 }
114
115 pub fn artifact_kinds(mut self, kinds: &[DerivedArtifactKind]) -> Self {
117 self.artifact_kinds = Some(kinds.iter().map(|kind| kind.as_str().to_string()).collect());
118 self
119 }
120
121 pub fn after(mut self, ts: &str) -> Self {
123 self.temporal = Some(TemporalClause::After(ts.to_string()));
124 self
125 }
126
127 pub fn before(mut self, ts: &str) -> Self {
129 self.temporal = Some(TemporalClause::Before(ts.to_string()));
130 self
131 }
132
133 pub fn between(mut self, start: &str, end: &str) -> Self {
135 self.temporal = Some(TemporalClause::Between {
136 start: start.to_string(),
137 end: end.to_string(),
138 });
139 self
140 }
141
142 pub fn expand_graph(mut self, depth: usize) -> Self {
144 let ex = self.expand.get_or_insert(ExpandClause {
145 depth: 1,
146 min_weight: None,
147 activation: None,
148 });
149 ex.depth = depth;
150 self
151 }
152
153 pub fn min_weight(mut self, w: f32) -> Self {
155 let ex = self.expand.get_or_insert(ExpandClause {
156 depth: 2,
157 min_weight: None,
158 activation: None,
159 });
160 ex.min_weight = Some(w);
161 self
162 }
163
164 pub fn activation(mut self, mode: ActivationMode) -> Self {
166 let ast_mode = match mode {
167 ActivationMode::None => ActivationModeAst::None,
168 ActivationMode::Static => ActivationModeAst::Static,
169 ActivationMode::Spreading => ActivationModeAst::Spreading,
170 ActivationMode::PersonalizedPageRank(_) => ActivationModeAst::Ppr,
171 };
172 let ex = self.expand.get_or_insert(ExpandClause {
173 depth: 2,
174 min_weight: None,
175 activation: None,
176 });
177 ex.activation = Some(ast_mode);
178 self
179 }
180
181 pub fn follow_causes(mut self, depth: usize) -> Self {
183 self.follow_causes = Some(depth);
184 self
185 }
186
187 pub fn min_importance(mut self, threshold: f64) -> Self {
189 self.where_clauses.push(WhereCondition {
190 field: "importance".into(),
191 op: ComparisonOp::Gt,
192 value: ConditionValue::Float(threshold),
193 });
194 self
195 }
196
197 pub fn min_confidence(mut self, threshold: f64) -> Self {
199 self.where_clauses.push(WhereCondition {
200 field: "confidence".into(),
201 op: ComparisonOp::Gt,
202 value: ConditionValue::Float(threshold),
203 });
204 self
205 }
206
207 pub fn format(mut self, fmt: OutputFormat) -> Self {
209 self.output_format = Some(fmt);
210 self
211 }
212
213 pub fn budget(mut self, tokens: usize) -> Self {
215 self.budget = Some(tokens);
216 self
217 }
218
219 pub fn namespace(mut self, ns: &str) -> Self {
221 self.namespace = Some(ns.to_string());
222 self
223 }
224
225 pub fn consistency(mut self, level: ConsistencyLevel) -> Self {
227 self.consistency = Some(level);
228 self
229 }
230
231 pub fn limit(mut self, n: usize) -> Self {
233 self.limit = Some(n);
234 self
235 }
236
237 pub fn context_config(mut self, config: ContextConfig) -> Self {
239 self.context_config = Some(config);
240 self
241 }
242
243 pub fn build_recall_stmt(&self) -> Statement {
245 Statement::Recall(Box::new(RecallStmt {
246 layers: if self.layers.is_empty() {
247 vec![Layer::Episodic, Layer::Semantic]
248 } else {
249 self.layers.clone()
250 },
251 about: self.about.clone().unwrap_or_default(),
252 involving: self.involving.clone(),
253 temporal: self.temporal.clone(),
254 expand: self.expand.clone(),
255 follow_causes: self.follow_causes,
256 where_clauses: self.where_clauses.clone(),
257 modality: self.modalities.clone(),
258 resource_roles: self.resource_roles.clone(),
259 hydration_modes: self.hydration_modes.clone(),
260 artifact_kinds: self.artifact_kinds.clone(),
261 group_by: None,
262 projection: None,
263 output_format: self.output_format,
264 result_format: None,
265 as_of: None,
266 subquery_filters: vec![],
267 budget: self.budget,
268 namespace: self.namespace.clone(),
269 consistency: self.consistency,
270 limit: self.limit,
271 hybrid: false,
272 depth_mode: None,
273 with_prospective: None,
274 with_mcfa: None,
275 with_conflicts: false,
276 provenance_depth: None,
277 topic: None,
278 from_realms: None,
279 }))
280 }
281
282 pub fn build_think_stmt(&self) -> Statement {
284 Statement::Think(Box::new(ThinkStmt {
285 about: self.about.clone().unwrap_or_default(),
286 involving: self.involving.clone(),
287 temporal: self.temporal.clone(),
288 expand: self.expand.clone(),
289 follow_causes: self.follow_causes,
290 where_clauses: self.where_clauses.clone(),
291 output_format: self.output_format,
292 budget: self.budget,
293 namespace: self.namespace.clone(),
294 consistency: self.consistency,
295 limit: self.limit,
296 hybrid: false,
297 mode: RetrievalMode::Local,
298 community_depth: None,
299 depth_mode: None,
300 with_prospective: None,
301 with_mcfa: None,
302 provenance_depth: None,
303 max_hops: None,
304 }))
305 }
306
307 pub fn plan(&self) -> planner::QueryPlan {
309 let stmt = self.build_recall_stmt();
310 planner::plan(&stmt, None)
311 }
312
313 pub async fn execute(self) -> HirnResult<QueryResult> {
315 let stmt = self.build_recall_stmt();
316 let query = stmt.to_string();
317 self.db.execute_ql(&query).await
318 }
319
320 pub async fn think(self) -> HirnResult<QueryResult> {
322 let stmt = self.build_think_stmt();
323
324 if self.context_config.is_none() {
325 let query = stmt.to_string();
326 return self.db.execute_ql(&query).await;
327 }
328
329 let Statement::Think(stmt) = stmt else {
330 unreachable!("build_think_stmt always returns Statement::Think")
331 };
332
333 direct_support::execute_think_with_config(self.db, &stmt, self.context_config).await
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn builder_produces_recall_stmt() {
343 let stmt = RecallStmt {
350 layers: vec![Layer::Episodic, Layer::Semantic],
351 about: "test".into(),
352 involving: None,
353 temporal: None,
354 expand: Some(ExpandClause {
355 depth: 2,
356 min_weight: Some(0.3),
357 activation: Some(ActivationModeAst::Spreading),
358 }),
359 follow_causes: None,
360 where_clauses: vec![WhereCondition {
361 field: "importance".into(),
362 op: ComparisonOp::Gt,
363 value: ConditionValue::Float(0.4),
364 }],
365 modality: None,
366 resource_roles: None,
367 hydration_modes: None,
368 artifact_kinds: None,
369 group_by: None,
370 projection: None,
371 output_format: None,
372 result_format: None,
373 as_of: None,
374 subquery_filters: vec![],
375 budget: Some(4096),
376 namespace: None,
377 consistency: None,
378 limit: Some(20),
379 hybrid: false,
380 depth_mode: None,
381 with_prospective: None,
382 with_mcfa: None,
383 with_conflicts: false,
384 topic: None,
385 provenance_depth: None,
386 from_realms: None,
387 };
388
389 let ql_stmt = crate::ql::parser::parse(
391 r#"RECALL episodic, semantic ABOUT "test" EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading WHERE importance > 0.4 BUDGET 4096 LIMIT 20"#,
392 )
393 .unwrap();
394
395 match ql_stmt {
396 Statement::Recall(ql_recall) => {
397 assert_eq!(stmt.layers, ql_recall.layers);
398 assert_eq!(stmt.about, ql_recall.about);
399 assert_eq!(stmt.expand, ql_recall.expand);
400 assert_eq!(stmt.budget, ql_recall.budget);
401 assert_eq!(stmt.limit, ql_recall.limit);
402 }
403 _ => panic!("expected Recall"),
404 }
405 }
406
407 #[test]
408 fn builder_plan_matches_ql_plan() {
409 let builder_stmt = Statement::Recall(Box::new(RecallStmt {
411 layers: vec![Layer::Episodic],
412 about: "test query".into(),
413 involving: None,
414 temporal: None,
415 expand: None,
416 follow_causes: None,
417 where_clauses: vec![],
418 modality: None,
419 resource_roles: None,
420 hydration_modes: None,
421 artifact_kinds: None,
422 group_by: None,
423 projection: None,
424 output_format: None,
425 result_format: None,
426 as_of: None,
427 subquery_filters: vec![],
428 budget: None,
429 namespace: None,
430 consistency: None,
431 limit: Some(10),
432 hybrid: false,
433 depth_mode: None,
434 with_prospective: None,
435 with_mcfa: None,
436 with_conflicts: false,
437 topic: None,
438 provenance_depth: None,
439 from_realms: None,
440 }));
441
442 let ql_stmt =
443 crate::ql::parser::parse(r#"RECALL episodic ABOUT "test query" LIMIT 10"#).unwrap();
444
445 let plan1 = planner::plan(&builder_stmt, None);
446 let plan2 = planner::plan(&ql_stmt, None);
447
448 assert_eq!(plan1, plan2);
449 }
450}