Skip to main content

lens_core/semantic/
intent_router.rs

1//! # High-Performance Intent Router for Query Classification
2//!
3//! Production-ready intent routing system with:
4//! - Zero-allocation pattern matching for hot paths
5//! - LSP integration for symbol-aware routing
6//! - Fallback strategies with performance monitoring
7//! - Confidence-based routing decisions
8//! - Extensible routing rules and custom handlers
9
10use 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/// LSP capability types for routing
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum LSPCapability {
26    GotoDefinition,
27    FindReferences,
28    DocumentSymbol,
29}
30
31/// Intent routing configuration
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct IntentRouterConfig {
34    /// Confidence threshold for specialized routing
35    pub confidence_threshold: f32,
36    /// Maximum results for primary routing
37    pub max_primary_results: usize,
38    /// Enable LSP integration
39    pub enable_lsp_routing: bool,
40    /// Fallback timeout in milliseconds
41    pub fallback_timeout_ms: u64,
42    /// Cache size for routing decisions
43    pub decision_cache_size: usize,
44    /// Custom routing rules
45    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/// Custom routing rule for domain-specific behavior
62#[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/// Intent routing result with comprehensive metadata
72#[derive(Debug, Clone)]
73pub struct IntentRoutingResult {
74    /// Original query classification
75    pub classification: QueryClassification,
76    /// Primary candidates from specialized routing
77    pub primary_candidates: Vec<SearchResult>,
78    /// Whether fallback was triggered
79    pub fallback_triggered: bool,
80    /// Routing path taken
81    pub routing_path: SmallVec<[String; 4]>,
82    /// Confidence threshold met
83    pub confidence_threshold_met: bool,
84    /// LSP routing decision if applicable
85    pub lsp_routing_decision: Option<LSPRoutingDecision>,
86    /// Performance metrics for this routing
87    pub performance_metrics: RoutingMetrics,
88}
89
90/// LSP-based routing decision
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct LSPRoutingDecision {
93    /// Should route through LSP
94    pub should_route: bool,
95    /// Target LSP capability
96    pub capability: LSPCapability,
97    /// Routing confidence
98    pub confidence: f32,
99    /// Reasoning for routing decision
100    pub reasoning: String,
101    /// Expected LSP results to use
102    pub expected_hints: Vec<LspSearchResult>,
103}
104
105/// Performance metrics for routing operations
106#[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/// Search context for routing decisions
118#[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/// File context for LSP-aware routing
128#[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/// User preferences for routing behavior
138#[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
145/// Result from a search handler
146// SearchResult is already imported from crate::search at the top
147
148/// Handler function type for different intents
149pub type IntentHandler = Box<dyn for<'a> Fn(&'a str, &'a SearchContext) -> futures::future::BoxFuture<'a, Result<Vec<SearchResult>>> + Send + Sync>;
150
151/// High-performance intent router with LSP integration
152pub struct IntentRouter {
153    config: IntentRouterConfig,
154    classifier: QueryClassifier,
155    lsp_manager: Option<Arc<LspManager>>,
156    
157    // Routing handlers
158    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    // Performance optimization
166    decision_cache: Arc<DashMap<String, CachedRoutingDecision>>,
167    hot_patterns: Arc<RwLock<Vec<HotPattern>>>,
168    
169    // Metrics
170    metrics: Arc<parking_lot::RwLock<IntentRouterMetrics>>,
171}
172
173/// Cached routing decision for performance
174#[derive(Debug, Clone)]
175struct CachedRoutingDecision {
176    classification: QueryClassification,
177    lsp_decision: Option<LSPRoutingDecision>,
178    cached_at: Instant,
179    hit_count: u32,
180}
181
182/// Hot pattern for zero-allocation matching
183#[derive(Debug, Clone)]
184struct HotPattern {
185    pattern: String,
186    intent: QueryIntent,
187    confidence: f32,
188    hit_count: u64,
189}
190
191impl IntentRouter {
192    /// Create new intent router with configuration
193    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    /// Register handler for specific intent
224    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    /// Route query with full intent analysis and LSP integration
240    #[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        // Check cache first
246        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        // Step 1: Classify query intent
252        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        // Step 2: LSP routing decision (if enabled and available)
257        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        // Cache the decision
265        self.cache_routing_decision(&cache_key, &classification, &lsp_decision);
266        
267        // Step 3: Execute routing strategy
268        let routing_result = self.execute_routing_strategy(
269            context,
270            &classification,
271            lsp_decision,
272            metrics,
273            start_time,
274        ).await?;
275        
276        // Step 4: Record metrics and update hot patterns
277        self.record_routing_metrics(&routing_result);
278        self.update_hot_patterns(&context.query, &classification).await;
279        
280        Ok(routing_result)
281    }
282    
283    /// Fast path routing for hot queries
284    #[instrument(skip(self, context))]
285    pub async fn route_query_fast(&self, context: &SearchContext) -> Result<(QueryIntent, Vec<SearchResult>)> {
286        // Check hot patterns first
287        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        // Fast classification
295        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            // Fallback to default handler
302            let results = self.execute_fallback_handler(context).await.unwrap_or_default();
303            Ok((QueryIntent::Lexical, results))
304        }
305    }
306    
307    /// Make LSP routing decision based on context and classification
308    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        // Determine if LSP routing is beneficial
319        let should_route = match classification.intent {
320            QueryIntent::Definition | QueryIntent::References => {
321                // Check if we have file context for precise LSP queries
322                context.file_context.is_some() && classification.confidence > 0.8
323            }
324            QueryIntent::Symbol => {
325                // Symbol queries benefit from LSP when we have project context
326                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        // Select appropriate LSP capability
336        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        // Get LSP search results
344        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 // Reduce confidence if no hints available
349        } else {
350            classification.confidence * 1.1 // Boost confidence with LSP support
351        }.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    /// Execute routing strategy based on classification and LSP decision
364    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        // Check confidence threshold
377        let confidence_threshold_met = classification.confidence >= self.config.confidence_threshold;
378        
379        // Execute LSP routing if decided
380        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                    // Convert LSP results to SearchResult format
392                    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        // Execute intent-specific routing if LSP didn't provide results
402        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        // Fallback if no results from specialized routing
412        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        // Truncate results to configured maximum
425        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    /// Execute handler for specific intent
442    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    /// Execute fallback handler
473    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    /// Match query against hot patterns for fast routing
488    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    /// Update hot patterns based on routing frequency
501    async fn update_hot_patterns(&self, query: &str, classification: &QueryClassification) {
502        if classification.confidence < 0.8 {
503            return; // Only track high-confidence patterns
504        }
505        
506        // Extract pattern from query (simplified)
507        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        // Update existing pattern or add new one
516        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 { // Limit hot patterns
520            patterns.push(HotPattern {
521                pattern,
522                intent: classification.intent,
523                confidence: classification.confidence,
524                hit_count: 1,
525            });
526        }
527        
528        // Sort by hit count to prioritize frequent patterns
529        patterns.sort_by(|a, b| b.hit_count.cmp(&a.hit_count));
530    }
531    
532    /// Generate cache key for routing decision
533    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    /// Cache routing decision
548    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            // Simple LRU: remove oldest entry
556            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    /// Get cached routing decision if available and valid
574    fn get_cached_decision(&self, cache_key: &str) -> Option<CachedRoutingDecision> {
575        if let Some(mut cached) = self.decision_cache.get_mut(cache_key) {
576            // Check if cache entry is still valid (5 minutes TTL)
577            if cached.cached_at.elapsed() < Duration::from_secs(300) {
578                cached.hit_count += 1;
579                return Some(cached.clone());
580            } else {
581                // Remove expired entry
582                drop(cached);
583                self.decision_cache.remove(cache_key);
584            }
585        }
586        None
587    }
588    
589    /// Execute cached routing decision
590    async fn execute_cached_routing(
591        &self,
592        context: &SearchContext,
593        cached: CachedRoutingDecision,
594        start_time: Instant,
595    ) -> Result<IntentRoutingResult> {
596        // Execute the same routing strategy as the cached decision
597        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    /// Record routing metrics
617    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        // Record intent distribution
623        *metrics.intent_counts.entry(result.classification.intent).or_insert(0) += 1;
624        
625        // Record routing path statistics
626        for path_component in &result.routing_path {
627            *metrics.path_counts.entry(path_component.clone()).or_insert(0) += 1;
628        }
629        
630        // Record performance metrics
631        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    /// Get router performance metrics
644    pub fn get_metrics(&self) -> IntentRouterMetrics {
645        self.metrics.read().clone()
646    }
647    
648    /// Clear routing caches
649    pub fn clear_caches(&self) {
650        self.decision_cache.clear();
651        info!("Cleared intent router caches");
652    }
653}
654
655/// Performance metrics for intent router
656#[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
702/// Initialize intent router module
703pub 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    // Validate configuration
713    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        // Test cache miss
763        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        // Add a hot pattern
775        {
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        // Without LSP manager, should return None
818        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        // Register a simple fallback handler
829        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; // Invalid
882        
883        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; // Valid
888        config.max_primary_results = 0; // Invalid
889        
890        let result = initialize_router(&config).await;
891        assert!(result.is_err());
892        assert!(result.unwrap_err().to_string().contains("Max primary results"));
893    }
894}