1use anyhow::{Context, Result};
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13use smallvec::SmallVec;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16use tokio::sync::RwLock;
17use tracing::{debug, info, instrument, span, Level};
18
19use super::query_classifier::{QueryClassification, QueryClassifier, QueryIntent};
20use crate::lsp::{LspManager, LspSearchResponse, LspSearchResult, HintType, LspServerType, SymbolHint};
21use crate::search::{SearchResult, SearchResultType};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum LSPCapability {
26 GotoDefinition,
27 FindReferences,
28 DocumentSymbol,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct IntentRouterConfig {
34 pub confidence_threshold: f32,
36 pub max_primary_results: usize,
38 pub enable_lsp_routing: bool,
40 pub fallback_timeout_ms: u64,
42 pub decision_cache_size: usize,
44 pub custom_rules: Vec<CustomRoutingRule>,
46}
47
48impl Default for IntentRouterConfig {
49 fn default() -> Self {
50 Self {
51 confidence_threshold: 0.7,
52 max_primary_results: 20,
53 enable_lsp_routing: true,
54 fallback_timeout_ms: 100,
55 decision_cache_size: 1000,
56 custom_rules: Vec::new(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CustomRoutingRule {
64 pub name: String,
65 pub pattern: String,
66 pub target_intent: QueryIntent,
67 pub confidence_boost: f32,
68 pub custom_handler: Option<String>,
69}
70
71#[derive(Debug, Clone)]
73pub struct IntentRoutingResult {
74 pub classification: QueryClassification,
76 pub primary_candidates: Vec<SearchResult>,
78 pub fallback_triggered: bool,
80 pub routing_path: SmallVec<[String; 4]>,
82 pub confidence_threshold_met: bool,
84 pub lsp_routing_decision: Option<LSPRoutingDecision>,
86 pub performance_metrics: RoutingMetrics,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct LSPRoutingDecision {
93 pub should_route: bool,
95 pub capability: LSPCapability,
97 pub confidence: f32,
99 pub reasoning: String,
101 pub expected_hints: Vec<LspSearchResult>,
103}
104
105#[derive(Debug, Clone, Default)]
107pub struct RoutingMetrics {
108 pub total_latency_ms: f64,
109 pub lsp_routing_latency_ms: f64,
110 pub primary_search_latency_ms: f64,
111 pub fallback_search_latency_ms: f64,
112 pub classification_latency_ms: f64,
113 pub candidates_from_primary: usize,
114 pub candidates_from_fallback: usize,
115}
116
117#[derive(Debug, Clone)]
119pub struct SearchContext {
120 pub query: String,
121 pub mode: String,
122 pub repo_path: Option<String>,
123 pub file_context: Option<FileContext>,
124 pub user_preferences: Option<UserPreferences>,
125}
126
127#[derive(Debug, Clone)]
129pub struct FileContext {
130 pub current_file: String,
131 pub current_line: usize,
132 pub current_column: usize,
133 pub language: String,
134 pub project_root: String,
135}
136
137#[derive(Debug, Clone)]
139pub struct UserPreferences {
140 pub prefer_local_results: bool,
141 pub language_preferences: Vec<String>,
142 pub max_results: usize,
143}
144
145pub type IntentHandler = Box<dyn for<'a> Fn(&'a str, &'a SearchContext) -> futures::future::BoxFuture<'a, Result<Vec<SearchResult>>> + Send + Sync>;
150
151pub struct IntentRouter {
153 config: IntentRouterConfig,
154 classifier: QueryClassifier,
155 lsp_manager: Option<Arc<LspManager>>,
156
157 definition_handler: Option<IntentHandler>,
159 references_handler: Option<IntentHandler>,
160 symbol_handler: Option<IntentHandler>,
161 structural_handler: Option<IntentHandler>,
162 natural_language_handler: Option<IntentHandler>,
163 fallback_handler: Option<IntentHandler>,
164
165 decision_cache: Arc<DashMap<String, CachedRoutingDecision>>,
167 hot_patterns: Arc<RwLock<Vec<HotPattern>>>,
168
169 metrics: Arc<parking_lot::RwLock<IntentRouterMetrics>>,
171}
172
173#[derive(Debug, Clone)]
175struct CachedRoutingDecision {
176 classification: QueryClassification,
177 lsp_decision: Option<LSPRoutingDecision>,
178 cached_at: Instant,
179 hit_count: u32,
180}
181
182#[derive(Debug, Clone)]
184struct HotPattern {
185 pattern: String,
186 intent: QueryIntent,
187 confidence: f32,
188 hit_count: u64,
189}
190
191impl IntentRouter {
192 pub async fn new(
194 config: IntentRouterConfig,
195 classifier: QueryClassifier,
196 lsp_manager: Option<Arc<LspManager>>,
197 ) -> Result<Self> {
198 let decision_cache = Arc::new(DashMap::with_capacity(config.decision_cache_size));
199 let hot_patterns = Arc::new(RwLock::new(Vec::new()));
200
201 info!(
202 "Initializing IntentRouter: confidence_threshold={}, lsp_enabled={}",
203 config.confidence_threshold,
204 config.enable_lsp_routing
205 );
206
207 Ok(Self {
208 config,
209 classifier,
210 lsp_manager,
211 definition_handler: None,
212 references_handler: None,
213 symbol_handler: None,
214 structural_handler: None,
215 natural_language_handler: None,
216 fallback_handler: None,
217 decision_cache,
218 hot_patterns,
219 metrics: Arc::new(parking_lot::RwLock::new(IntentRouterMetrics::default())),
220 })
221 }
222
223 pub fn register_handler(&mut self, intent: QueryIntent, handler: IntentHandler) {
225 match intent {
226 QueryIntent::Definition => self.definition_handler = Some(handler),
227 QueryIntent::References => self.references_handler = Some(handler),
228 QueryIntent::Symbol => self.symbol_handler = Some(handler),
229 QueryIntent::Structural => self.structural_handler = Some(handler),
230 QueryIntent::NaturalLanguage => self.natural_language_handler = Some(handler),
231 QueryIntent::Lexical => self.fallback_handler = Some(handler),
232 QueryIntent::SymbolSearch => self.symbol_handler = Some(handler),
233 QueryIntent::StructuralSearch => self.structural_handler = Some(handler),
234 }
235
236 debug!("Registered handler for intent: {:?}", intent);
237 }
238
239 #[instrument(skip(self, context), fields(query = %context.query, mode = %context.mode))]
241 pub async fn route_query(&self, context: &SearchContext) -> Result<IntentRoutingResult> {
242 let start_time = Instant::now();
243 let mut metrics = RoutingMetrics::default();
244
245 let cache_key = self.generate_cache_key(context);
247 if let Some(cached) = self.get_cached_decision(&cache_key) {
248 return self.execute_cached_routing(context, cached, start_time).await;
249 }
250
251 let classification_start = Instant::now();
253 let classification = self.classifier.classify(&context.query);
254 metrics.classification_latency_ms = classification_start.elapsed().as_millis() as f64;
255
256 let mut lsp_decision = None;
258 if self.config.enable_lsp_routing && self.lsp_manager.is_some() {
259 let lsp_start = Instant::now();
260 lsp_decision = self.make_lsp_routing_decision(context, &classification).await?;
261 metrics.lsp_routing_latency_ms = lsp_start.elapsed().as_millis() as f64;
262 }
263
264 self.cache_routing_decision(&cache_key, &classification, &lsp_decision);
266
267 let routing_result = self.execute_routing_strategy(
269 context,
270 &classification,
271 lsp_decision,
272 metrics,
273 start_time,
274 ).await?;
275
276 self.record_routing_metrics(&routing_result);
278 self.update_hot_patterns(&context.query, &classification).await;
279
280 Ok(routing_result)
281 }
282
283 #[instrument(skip(self, context))]
285 pub async fn route_query_fast(&self, context: &SearchContext) -> Result<(QueryIntent, Vec<SearchResult>)> {
286 if let Some((intent, confidence)) = self.match_hot_patterns(&context.query).await {
288 if confidence > self.config.confidence_threshold {
289 let results = self.execute_intent_handler(intent, context).await.unwrap_or_default();
290 return Ok((intent, results));
291 }
292 }
293
294 let (intent, confidence) = self.classifier.classify_fast(&context.query);
296
297 if confidence > self.config.confidence_threshold {
298 let results = self.execute_intent_handler(intent, context).await.unwrap_or_default();
299 Ok((intent, results))
300 } else {
301 let results = self.execute_fallback_handler(context).await.unwrap_or_default();
303 Ok((QueryIntent::Lexical, results))
304 }
305 }
306
307 async fn make_lsp_routing_decision(
309 &self,
310 context: &SearchContext,
311 classification: &QueryClassification,
312 ) -> Result<Option<LSPRoutingDecision>> {
313 let lsp_manager = match &self.lsp_manager {
314 Some(manager) => manager,
315 None => return Ok(None),
316 };
317
318 let should_route = match classification.intent {
320 QueryIntent::Definition | QueryIntent::References => {
321 context.file_context.is_some() && classification.confidence > 0.8
323 }
324 QueryIntent::Symbol => {
325 context.repo_path.is_some() && classification.confidence > 0.7
327 }
328 _ => false,
329 };
330
331 if !should_route {
332 return Ok(None);
333 }
334
335 let capability = match classification.intent {
337 QueryIntent::Definition => LSPCapability::GotoDefinition,
338 QueryIntent::References => LSPCapability::FindReferences,
339 QueryIntent::Symbol => LSPCapability::DocumentSymbol,
340 _ => return Ok(None),
341 };
342
343 let lsp_response = lsp_manager.search(&context.query, None).await
345 .unwrap_or_else(|_| LspSearchResponse::default());
346
347 let confidence = if lsp_response.lsp_results.is_empty() {
348 classification.confidence * 0.7 } else {
350 classification.confidence * 1.1 }.min(1.0);
352
353 Ok(Some(LSPRoutingDecision {
354 should_route: true,
355 capability,
356 confidence,
357 reasoning: format!("LSP routing for {} with {} results",
358 classification.intent, lsp_response.lsp_results.len()),
359 expected_hints: lsp_response.lsp_results,
360 }))
361 }
362
363 async fn execute_routing_strategy(
365 &self,
366 context: &SearchContext,
367 classification: &QueryClassification,
368 lsp_decision: Option<LSPRoutingDecision>,
369 mut metrics: RoutingMetrics,
370 start_time: Instant,
371 ) -> Result<IntentRoutingResult> {
372 let mut routing_path = SmallVec::new();
373 let mut primary_candidates = Vec::new();
374 let mut fallback_triggered = false;
375
376 let confidence_threshold_met = classification.confidence >= self.config.confidence_threshold;
378
379 if let Some(ref lsp_dec) = lsp_decision {
381 if lsp_dec.should_route {
382 routing_path.push("lsp_routing".to_string());
383
384 if let Some(lsp_manager) = &self.lsp_manager {
385 let lsp_start = Instant::now();
386 let lsp_response = lsp_manager.search(
387 &context.query,
388 context.file_context.as_ref().map(|fc| fc.current_file.as_str()),
389 ).await.unwrap_or_default();
390
391 primary_candidates = lsp_response.fallback_results;
393
394 metrics.primary_search_latency_ms += lsp_start.elapsed().as_millis() as f64;
395 }
396
397 routing_path.push(format!("lsp_{:?}", lsp_dec.capability));
398 }
399 }
400
401 if primary_candidates.is_empty() && confidence_threshold_met {
403 let intent_start = Instant::now();
404 primary_candidates = self.execute_intent_handler(classification.intent, context).await
405 .unwrap_or_default();
406 metrics.primary_search_latency_ms += intent_start.elapsed().as_millis() as f64;
407
408 routing_path.push(format!("intent_{}", classification.intent));
409 }
410
411 if primary_candidates.is_empty() &&
413 matches!(classification.intent, QueryIntent::Definition | QueryIntent::References | QueryIntent::Symbol) {
414
415 let fallback_start = Instant::now();
416 primary_candidates = self.execute_fallback_handler(context).await
417 .unwrap_or_default();
418 metrics.fallback_search_latency_ms = fallback_start.elapsed().as_millis() as f64;
419
420 routing_path.push("fallback_triggered".to_string());
421 fallback_triggered = true;
422 }
423
424 primary_candidates.truncate(self.config.max_primary_results);
426
427 metrics.total_latency_ms = start_time.elapsed().as_millis() as f64;
428 metrics.candidates_from_primary = primary_candidates.len();
429
430 Ok(IntentRoutingResult {
431 classification: classification.clone(),
432 primary_candidates,
433 fallback_triggered,
434 routing_path,
435 confidence_threshold_met,
436 lsp_routing_decision: lsp_decision,
437 performance_metrics: metrics,
438 })
439 }
440
441 async fn execute_intent_handler(
443 &self,
444 intent: QueryIntent,
445 context: &SearchContext,
446 ) -> Option<Vec<SearchResult>> {
447 let handler = match intent {
448 QueryIntent::Definition => &self.definition_handler,
449 QueryIntent::References => &self.references_handler,
450 QueryIntent::Symbol => &self.symbol_handler,
451 QueryIntent::Structural => &self.structural_handler,
452 QueryIntent::NaturalLanguage => &self.natural_language_handler,
453 QueryIntent::Lexical => &self.fallback_handler,
454 QueryIntent::SymbolSearch => &self.symbol_handler,
455 QueryIntent::StructuralSearch => &self.structural_handler,
456 };
457
458 if let Some(handler) = handler {
459 match handler(&context.query, context).await {
460 Ok(results) => Some(results),
461 Err(e) => {
462 debug!("Intent handler failed for {:?}: {}", intent, e);
463 None
464 }
465 }
466 } else {
467 debug!("No handler registered for intent: {:?}", intent);
468 None
469 }
470 }
471
472 async fn execute_fallback_handler(&self, context: &SearchContext) -> Option<Vec<SearchResult>> {
474 if let Some(handler) = &self.fallback_handler {
475 match handler(&context.query, context).await {
476 Ok(results) => Some(results),
477 Err(e) => {
478 debug!("Fallback handler failed: {}", e);
479 None
480 }
481 }
482 } else {
483 None
484 }
485 }
486
487 async fn match_hot_patterns(&self, query: &str) -> Option<(QueryIntent, f32)> {
489 let patterns = self.hot_patterns.read().await;
490
491 for pattern in patterns.iter() {
492 if query.starts_with(&pattern.pattern) || query.contains(&pattern.pattern) {
493 return Some((pattern.intent, pattern.confidence));
494 }
495 }
496
497 None
498 }
499
500 async fn update_hot_patterns(&self, query: &str, classification: &QueryClassification) {
502 if classification.confidence < 0.8 {
503 return; }
505
506 let pattern = if query.len() > 20 {
508 query[..20].to_string()
509 } else {
510 query.to_string()
511 };
512
513 let mut patterns = self.hot_patterns.write().await;
514
515 if let Some(hot_pattern) = patterns.iter_mut().find(|p| p.pattern == pattern) {
517 hot_pattern.hit_count += 1;
518 hot_pattern.confidence = (hot_pattern.confidence + classification.confidence) / 2.0;
519 } else if patterns.len() < 100 { patterns.push(HotPattern {
521 pattern,
522 intent: classification.intent,
523 confidence: classification.confidence,
524 hit_count: 1,
525 });
526 }
527
528 patterns.sort_by(|a, b| b.hit_count.cmp(&a.hit_count));
530 }
531
532 fn generate_cache_key(&self, context: &SearchContext) -> String {
534 use std::hash::{Hash, Hasher};
535
536 let mut hasher = std::collections::hash_map::DefaultHasher::new();
537 context.query.hash(&mut hasher);
538 context.mode.hash(&mut hasher);
539 if let Some(ref file_ctx) = context.file_context {
540 file_ctx.current_file.hash(&mut hasher);
541 file_ctx.language.hash(&mut hasher);
542 }
543
544 format!("route_{}", hasher.finish())
545 }
546
547 fn cache_routing_decision(
549 &self,
550 cache_key: &str,
551 classification: &QueryClassification,
552 lsp_decision: &Option<LSPRoutingDecision>,
553 ) {
554 if self.decision_cache.len() >= self.config.decision_cache_size {
555 if let Some(entry) = self.decision_cache.iter().next() {
557 let key = entry.key().clone();
558 self.decision_cache.remove(&key);
559 }
560 }
561
562 self.decision_cache.insert(
563 cache_key.to_string(),
564 CachedRoutingDecision {
565 classification: classification.clone(),
566 lsp_decision: lsp_decision.clone(),
567 cached_at: Instant::now(),
568 hit_count: 0,
569 },
570 );
571 }
572
573 fn get_cached_decision(&self, cache_key: &str) -> Option<CachedRoutingDecision> {
575 if let Some(mut cached) = self.decision_cache.get_mut(cache_key) {
576 if cached.cached_at.elapsed() < Duration::from_secs(300) {
578 cached.hit_count += 1;
579 return Some(cached.clone());
580 } else {
581 drop(cached);
583 self.decision_cache.remove(cache_key);
584 }
585 }
586 None
587 }
588
589 async fn execute_cached_routing(
591 &self,
592 context: &SearchContext,
593 cached: CachedRoutingDecision,
594 start_time: Instant,
595 ) -> Result<IntentRoutingResult> {
596 let mut metrics = RoutingMetrics::default();
598 metrics.total_latency_ms = start_time.elapsed().as_millis() as f64;
599
600 let primary_candidates = self.execute_intent_handler(
601 cached.classification.intent,
602 context,
603 ).await.unwrap_or_default();
604
605 Ok(IntentRoutingResult {
606 classification: cached.classification,
607 primary_candidates,
608 fallback_triggered: false,
609 routing_path: smallvec::smallvec!["cached".to_string()],
610 confidence_threshold_met: true,
611 lsp_routing_decision: cached.lsp_decision,
612 performance_metrics: metrics,
613 })
614 }
615
616 fn record_routing_metrics(&self, result: &IntentRoutingResult) {
618 let mut metrics = self.metrics.write();
619 metrics.total_routes += 1;
620 metrics.total_latency += Duration::from_millis(result.performance_metrics.total_latency_ms as u64);
621
622 *metrics.intent_counts.entry(result.classification.intent).or_insert(0) += 1;
624
625 for path_component in &result.routing_path {
627 *metrics.path_counts.entry(path_component.clone()).or_insert(0) += 1;
628 }
629
630 if result.performance_metrics.lsp_routing_latency_ms > 0.0 {
632 metrics.lsp_routes += 1;
633 metrics.lsp_latency += Duration::from_millis(result.performance_metrics.lsp_routing_latency_ms as u64);
634 }
635
636 if result.fallback_triggered {
637 metrics.fallback_routes += 1;
638 }
639
640 metrics.cache_hits += if result.routing_path.contains(&"cached".to_string()) { 1 } else { 0 };
641 }
642
643 pub fn get_metrics(&self) -> IntentRouterMetrics {
645 self.metrics.read().clone()
646 }
647
648 pub fn clear_caches(&self) {
650 self.decision_cache.clear();
651 info!("Cleared intent router caches");
652 }
653}
654
655#[derive(Debug, Clone, Default)]
657pub struct IntentRouterMetrics {
658 pub total_routes: u64,
659 pub total_latency: Duration,
660 pub lsp_routes: u64,
661 pub lsp_latency: Duration,
662 pub fallback_routes: u64,
663 pub cache_hits: u64,
664 pub intent_counts: std::collections::HashMap<QueryIntent, u64>,
665 pub path_counts: std::collections::HashMap<String, u64>,
666}
667
668impl IntentRouterMetrics {
669 pub fn avg_latency_ms(&self) -> f64 {
670 if self.total_routes == 0 {
671 0.0
672 } else {
673 self.total_latency.as_millis() as f64 / self.total_routes as f64
674 }
675 }
676
677 pub fn lsp_route_percentage(&self) -> f64 {
678 if self.total_routes == 0 {
679 0.0
680 } else {
681 (self.lsp_routes as f64 / self.total_routes as f64) * 100.0
682 }
683 }
684
685 pub fn fallback_rate(&self) -> f64 {
686 if self.total_routes == 0 {
687 0.0
688 } else {
689 (self.fallback_routes as f64 / self.total_routes as f64) * 100.0
690 }
691 }
692
693 pub fn cache_hit_rate(&self) -> f64 {
694 if self.total_routes == 0 {
695 0.0
696 } else {
697 (self.cache_hits as f64 / self.total_routes as f64) * 100.0
698 }
699 }
700}
701
702pub async fn initialize_router(config: &IntentRouterConfig) -> Result<()> {
704 tracing::info!("Initializing intent router module");
705 tracing::info!("Confidence threshold: {}", config.confidence_threshold);
706 tracing::info!("Max primary results: {}", config.max_primary_results);
707 tracing::info!("LSP routing enabled: {}", config.enable_lsp_routing);
708 tracing::info!("Fallback timeout: {}ms", config.fallback_timeout_ms);
709 tracing::info!("Decision cache size: {}", config.decision_cache_size);
710 tracing::info!("Custom rules: {}", config.custom_rules.len());
711
712 if config.confidence_threshold < 0.0 || config.confidence_threshold > 1.0 {
714 anyhow::bail!("Confidence threshold must be in range [0.0, 1.0]");
715 }
716
717 if config.max_primary_results == 0 {
718 anyhow::bail!("Max primary results must be greater than 0");
719 }
720
721 if config.decision_cache_size == 0 {
722 anyhow::bail!("Decision cache size must be greater than 0");
723 }
724
725 tracing::info!("Intent router module initialized successfully");
726 Ok(())
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use crate::semantic::query_classifier::{ClassifierConfig, QueryClassifier};
733
734 #[tokio::test]
735 async fn test_intent_router_creation() {
736 let config = IntentRouterConfig::default();
737 let classifier_config = ClassifierConfig::default();
738 let classifier = QueryClassifier::new(classifier_config).unwrap();
739
740 let router = IntentRouter::new(config, classifier, None).await;
741 assert!(router.is_ok());
742 }
743
744 #[tokio::test]
745 async fn test_routing_decision_caching() {
746 let config = IntentRouterConfig::default();
747 let classifier_config = ClassifierConfig::default();
748 let classifier = QueryClassifier::new(classifier_config).unwrap();
749 let router = IntentRouter::new(config, classifier, None).await.unwrap();
750
751 let context = SearchContext {
752 query: "def calculateSum".to_string(),
753 mode: "hybrid".to_string(),
754 repo_path: None,
755 file_context: None,
756 user_preferences: None,
757 };
758
759 let cache_key = router.generate_cache_key(&context);
760 assert!(!cache_key.is_empty());
761
762 let cached = router.get_cached_decision(&cache_key);
764 assert!(cached.is_none());
765 }
766
767 #[tokio::test]
768 async fn test_hot_pattern_matching() {
769 let config = IntentRouterConfig::default();
770 let classifier_config = ClassifierConfig::default();
771 let classifier = QueryClassifier::new(classifier_config).unwrap();
772 let router = IntentRouter::new(config, classifier, None).await.unwrap();
773
774 {
776 let mut patterns = router.hot_patterns.write().await;
777 patterns.push(HotPattern {
778 pattern: "def ".to_string(),
779 intent: QueryIntent::Definition,
780 confidence: 0.9,
781 hit_count: 10,
782 });
783 }
784
785 let result = router.match_hot_patterns("def calculateSum").await;
786 assert!(result.is_some());
787 let (intent, confidence) = result.unwrap();
788 assert_eq!(intent, QueryIntent::Definition);
789 assert_eq!(confidence, 0.9);
790 }
791
792 #[tokio::test]
793 async fn test_lsp_routing_decision() {
794 let config = IntentRouterConfig::default();
795 let classifier_config = ClassifierConfig::default();
796 let classifier = QueryClassifier::new(classifier_config).unwrap();
797 let router = IntentRouter::new(config, classifier, None).await.unwrap();
798
799 let context = SearchContext {
800 query: "def calculateSum".to_string(),
801 mode: "hybrid".to_string(),
802 repo_path: Some("/path/to/repo".to_string()),
803 file_context: Some(FileContext {
804 current_file: "main.py".to_string(),
805 current_line: 10,
806 current_column: 5,
807 language: "python".to_string(),
808 project_root: "/path/to/repo".to_string(),
809 }),
810 user_preferences: None,
811 };
812
813 let classification = router.classifier.classify(&context.query);
814 let decision = router.make_lsp_routing_decision(&context, &classification).await;
815
816 assert!(decision.is_ok());
817 assert!(decision.unwrap().is_none());
819 }
820
821 #[tokio::test]
822 async fn test_fast_routing() {
823 let config = IntentRouterConfig::default();
824 let classifier_config = ClassifierConfig::default();
825 let classifier = QueryClassifier::new(classifier_config).unwrap();
826 let mut router = IntentRouter::new(config, classifier, None).await.unwrap();
827
828 router.register_handler(
830 QueryIntent::Lexical,
831 Box::new(|query, _context| {
832 Box::pin(async move {
833 Ok(vec![SearchResult {
834 file_path: "test.py".to_string(),
835 line_number: 1,
836 column: 1,
837 content: query.to_string(),
838 score: 0.8,
839 result_type: SearchResultType::TextMatch,
840 language: Some("python".to_string()),
841 context_lines: None,
842 lsp_metadata: None,
843 }])
844 })
845 })
846 );
847
848 let context = SearchContext {
849 query: "simple query".to_string(),
850 mode: "hybrid".to_string(),
851 repo_path: None,
852 file_context: None,
853 user_preferences: None,
854 };
855
856 let result = router.route_query_fast(&context).await;
857 assert!(result.is_ok());
858
859 let (intent, results) = result.unwrap();
860 assert!(!results.is_empty());
861 }
862
863 #[test]
864 fn test_metrics_calculation() {
865 let mut metrics = IntentRouterMetrics::default();
866 metrics.total_routes = 100;
867 metrics.lsp_routes = 30;
868 metrics.fallback_routes = 20;
869 metrics.cache_hits = 15;
870 metrics.total_latency = Duration::from_millis(5000);
871
872 assert_eq!(metrics.lsp_route_percentage(), 30.0);
873 assert_eq!(metrics.fallback_rate(), 20.0);
874 assert_eq!(metrics.cache_hit_rate(), 15.0);
875 assert_eq!(metrics.avg_latency_ms(), 50.0);
876 }
877
878 #[tokio::test]
879 async fn test_configuration_validation() {
880 let mut config = IntentRouterConfig::default();
881 config.confidence_threshold = 1.5; let result = initialize_router(&config).await;
884 assert!(result.is_err());
885 assert!(result.unwrap_err().to_string().contains("Confidence threshold"));
886
887 config.confidence_threshold = 0.7; config.max_primary_results = 0; let result = initialize_router(&config).await;
891 assert!(result.is_err());
892 assert!(result.unwrap_err().to_string().contains("Max primary results"));
893 }
894}