1use anyhow::Result;
11use serde::{Deserialize, Serialize};
12use smallvec::SmallVec;
13use std::collections::HashMap;
14use tracing::{debug, instrument, span, Level};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct QueryClassification {
19 pub intent: QueryIntent,
21 pub confidence: f32,
23 pub characteristics: Vec<QueryCharacteristic>,
25 pub naturalness_score: f32,
27 pub complexity_score: f32,
29 pub language_hints: Vec<String>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum QueryIntent {
36 Definition,
38 References,
40 Symbol,
42 Structural,
44 Lexical,
46 NaturalLanguage,
48 SymbolSearch,
50 StructuralSearch,
52}
53
54impl std::fmt::Display for QueryIntent {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 QueryIntent::Definition => write!(f, "def"),
58 QueryIntent::References => write!(f, "refs"),
59 QueryIntent::Symbol => write!(f, "symbol"),
60 QueryIntent::Structural => write!(f, "struct"),
61 QueryIntent::Lexical => write!(f, "lexical"),
62 QueryIntent::NaturalLanguage => write!(f, "NL"),
63 QueryIntent::SymbolSearch => write!(f, "symbol_search"),
64 QueryIntent::StructuralSearch => write!(f, "structural_search"),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71pub enum QueryCharacteristic {
72 HasArticles,
74 HasPrepositions,
75 HasDescriptiveWords,
76 HasQuestions,
77 HasMultipleWords,
78
79 HasOperators,
81 HasSymbols,
82 HasProgrammingSyntax,
83 HasFunctionCalls,
84 HasBrackets,
85
86 HasDefinitionPattern,
88 HasReferencePattern,
89 HasSymbolPrefix,
90 HasStructuralChars,
91
92 PythonSyntax,
94 JavaScriptSyntax,
95 RustSyntax,
96 CppSyntax,
97 SqlSyntax,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ClassifierConfig {
103 pub nl_threshold: f32,
105 pub intent_confidence_threshold: f32,
107 pub enable_language_detection: bool,
109 pub feature_weights: HashMap<String, f32>,
111 pub custom_patterns: Vec<CustomPattern>,
113}
114
115impl Default for ClassifierConfig {
116 fn default() -> Self {
117 Self {
118 nl_threshold: 0.6,
119 intent_confidence_threshold: 0.7,
120 enable_language_detection: true,
121 feature_weights: HashMap::new(),
122 custom_patterns: Vec::new(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CustomPattern {
130 pub name: String,
131 pub pattern: String,
132 pub intent: QueryIntent,
133 pub confidence_boost: f32,
134}
135
136pub struct QueryClassifier {
138 config: ClassifierConfig,
139 definition_patterns: Vec<regex::Regex>,
141 reference_patterns: Vec<regex::Regex>,
142 language_patterns: HashMap<String, Vec<regex::Regex>>,
143 nl_indicators: HashSet<String>,
145 code_keywords: HashSet<String>,
146 metrics: parking_lot::RwLock<ClassifierMetrics>,
148}
149
150use std::collections::HashSet;
151
152impl QueryClassifier {
153 pub fn new(config: ClassifierConfig) -> Result<Self> {
155 let definition_patterns = Self::compile_definition_patterns()?;
156 let reference_patterns = Self::compile_reference_patterns()?;
157 let language_patterns = Self::compile_language_patterns()?;
158
159 let nl_indicators = Self::build_nl_vocabulary();
160 let code_keywords = Self::build_code_vocabulary();
161
162 Ok(Self {
163 config,
164 definition_patterns,
165 reference_patterns,
166 language_patterns,
167 nl_indicators,
168 code_keywords,
169 metrics: parking_lot::RwLock::new(ClassifierMetrics::default()),
170 })
171 }
172
173 #[instrument(skip(self), fields(query_len = query.len()))]
175 pub fn classify(&self, query: &str) -> QueryClassification {
176 let start = std::time::Instant::now();
177
178 let features = self.extract_features(query);
180
181 let intent_scores = self.calculate_intent_scores(query, &features);
183
184 let (intent, confidence) = self.select_primary_intent(&intent_scores);
186
187 let naturalness_score = self.calculate_naturalness(&features);
189 let complexity_score = self.calculate_complexity(query, &features);
190
191 let language_hints = if self.config.enable_language_detection {
193 self.detect_languages(query)
194 } else {
195 Vec::new()
196 };
197
198 let classification = QueryClassification {
199 intent,
200 confidence,
201 characteristics: features.into_vec(),
202 naturalness_score,
203 complexity_score,
204 language_hints,
205 };
206
207 let latency = start.elapsed();
209 self.record_classification(latency, &classification);
210
211 debug!("Classified query: intent={}, confidence={:.3}, naturalness={:.3}",
212 intent, confidence, naturalness_score);
213
214 classification
215 }
216
217 #[instrument(skip(self))]
219 pub fn classify_fast(&self, query: &str) -> (QueryIntent, f32) {
220 if self.has_definition_pattern_fast(query) {
224 return (QueryIntent::Definition, 0.9);
225 }
226
227 if self.has_reference_pattern_fast(query) {
229 return (QueryIntent::References, 0.9);
230 }
231
232 if self.has_structural_chars_fast(query) {
234 return (QueryIntent::Structural, 0.8);
235 }
236
237 let nl_score = self.calculate_naturalness_fast(query);
239 if nl_score > self.config.nl_threshold {
240 return (QueryIntent::NaturalLanguage, nl_score);
241 }
242
243 if self.has_symbol_pattern_fast(query) {
245 return (QueryIntent::Symbol, 0.7);
246 }
247
248 (QueryIntent::Lexical, 0.6)
250 }
251
252 fn extract_features(&self, query: &str) -> SmallVec<[QueryCharacteristic; 8]> {
254 let mut features = SmallVec::new();
255 let query_lower = query.to_lowercase();
256 let words: Vec<&str> = query_lower.split_whitespace().collect();
257
258 if self.has_articles(&words) {
260 features.push(QueryCharacteristic::HasArticles);
261 }
262
263 if self.has_prepositions(&words) {
264 features.push(QueryCharacteristic::HasPrepositions);
265 }
266
267 if self.has_descriptive_words(&words) {
268 features.push(QueryCharacteristic::HasDescriptiveWords);
269 }
270
271 if self.has_question_words(&words) {
272 features.push(QueryCharacteristic::HasQuestions);
273 }
274
275 if words.len() > 3 {
276 features.push(QueryCharacteristic::HasMultipleWords);
277 }
278
279 if self.has_programming_operators(query) {
281 features.push(QueryCharacteristic::HasOperators);
282 }
283
284 if self.has_special_symbols(query) {
285 features.push(QueryCharacteristic::HasSymbols);
286 }
287
288 if self.has_programming_syntax(query) {
289 features.push(QueryCharacteristic::HasProgrammingSyntax);
290 }
291
292 if self.has_function_calls(query) {
293 features.push(QueryCharacteristic::HasFunctionCalls);
294 }
295
296 if self.has_brackets(query) {
297 features.push(QueryCharacteristic::HasBrackets);
298 }
299
300 if self.has_definition_pattern(query) {
302 features.push(QueryCharacteristic::HasDefinitionPattern);
303 }
304
305 if self.has_reference_pattern(query) {
306 features.push(QueryCharacteristic::HasReferencePattern);
307 }
308
309 if self.has_symbol_prefix(query) {
310 features.push(QueryCharacteristic::HasSymbolPrefix);
311 }
312
313 if self.has_structural_chars(query) {
314 features.push(QueryCharacteristic::HasStructuralChars);
315 }
316
317 if self.has_python_syntax(query) {
319 features.push(QueryCharacteristic::PythonSyntax);
320 }
321
322 if self.has_javascript_syntax(query) {
323 features.push(QueryCharacteristic::JavaScriptSyntax);
324 }
325
326 if self.has_rust_syntax(query) {
327 features.push(QueryCharacteristic::RustSyntax);
328 }
329
330 if self.has_cpp_syntax(query) {
331 features.push(QueryCharacteristic::CppSyntax);
332 }
333
334 if self.has_sql_syntax(query) {
335 features.push(QueryCharacteristic::SqlSyntax);
336 }
337
338 features
339 }
340
341 fn calculate_intent_scores(&self, query: &str, features: &[QueryCharacteristic]) -> HashMap<QueryIntent, f32> {
343 let mut scores = HashMap::new();
344
345 scores.insert(QueryIntent::Definition, 0.1);
347 scores.insert(QueryIntent::References, 0.1);
348 scores.insert(QueryIntent::Symbol, 0.2);
349 scores.insert(QueryIntent::Structural, 0.15);
350 scores.insert(QueryIntent::Lexical, 0.3);
351 scores.insert(QueryIntent::NaturalLanguage, 0.2);
352
353 for &feature in features {
355 match feature {
356 QueryCharacteristic::HasDefinitionPattern => {
357 *scores.entry(QueryIntent::Definition).or_insert(0.0) += 0.8;
358 }
359 QueryCharacteristic::HasReferencePattern => {
360 *scores.entry(QueryIntent::References).or_insert(0.0) += 0.8;
361 }
362 QueryCharacteristic::HasSymbolPrefix => {
363 *scores.entry(QueryIntent::Symbol).or_insert(0.0) += 0.6;
364 }
365 QueryCharacteristic::HasStructuralChars => {
366 *scores.entry(QueryIntent::Structural).or_insert(0.0) += 0.5;
367 }
368 QueryCharacteristic::HasArticles |
369 QueryCharacteristic::HasPrepositions |
370 QueryCharacteristic::HasDescriptiveWords |
371 QueryCharacteristic::HasQuestions => {
372 *scores.entry(QueryIntent::NaturalLanguage).or_insert(0.0) += 0.3;
373 }
374 QueryCharacteristic::HasOperators |
375 QueryCharacteristic::HasProgrammingSyntax => {
376 *scores.entry(QueryIntent::Structural).or_insert(0.0) += 0.4;
377 *scores.entry(QueryIntent::NaturalLanguage).or_insert(0.0) -= 0.2;
378 }
379 _ => {} }
381 }
382
383 for pattern in &self.config.custom_patterns {
385 if query.contains(&pattern.pattern) {
386 *scores.entry(pattern.intent).or_insert(0.0) += pattern.confidence_boost;
387 }
388 }
389
390 for score in scores.values_mut() {
392 *score = score.clamp(0.0, 1.0);
393 }
394
395 scores
396 }
397
398 fn select_primary_intent(&self, scores: &HashMap<QueryIntent, f32>) -> (QueryIntent, f32) {
400 scores.iter()
401 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
402 .map(|(&intent, &score)| (intent, score))
403 .unwrap_or((QueryIntent::Lexical, 0.5))
404 }
405
406 fn calculate_naturalness(&self, features: &[QueryCharacteristic]) -> f32 {
408 let mut score = 0.5; for &feature in features {
411 match feature {
412 QueryCharacteristic::HasArticles => score += 0.15,
413 QueryCharacteristic::HasPrepositions => score += 0.12,
414 QueryCharacteristic::HasDescriptiveWords => score += 0.18,
415 QueryCharacteristic::HasQuestions => score += 0.15,
416 QueryCharacteristic::HasMultipleWords => score += 0.08,
417
418 QueryCharacteristic::HasOperators => score -= 0.15,
419 QueryCharacteristic::HasProgrammingSyntax => score -= 0.20,
420 QueryCharacteristic::HasFunctionCalls => score -= 0.12,
421 QueryCharacteristic::HasBrackets => score -= 0.08,
422
423 _ => {} }
425 }
426
427 (score as f32).clamp(0.0, 1.0)
428 }
429
430 fn calculate_complexity(&self, query: &str, features: &[QueryCharacteristic]) -> f32 {
432 let mut complexity = 0.0;
433
434 complexity += (query.len() as f32 / 100.0).min(0.3);
436 complexity += (query.split_whitespace().count() as f32 / 20.0).min(0.2);
437
438 for &feature in features {
440 match feature {
441 QueryCharacteristic::HasStructuralChars |
442 QueryCharacteristic::HasProgrammingSyntax => complexity += 0.15,
443 QueryCharacteristic::HasFunctionCalls => complexity += 0.1,
444 QueryCharacteristic::HasMultipleWords => complexity += 0.05,
445 _ => {}
446 }
447 }
448
449 complexity.clamp(0.0, 1.0)
450 }
451
452 fn detect_languages(&self, query: &str) -> Vec<String> {
454 let mut languages = Vec::new();
455
456 for (lang, patterns) in &self.language_patterns {
457 if patterns.iter().any(|pattern| pattern.is_match(query)) {
458 languages.push(lang.clone());
459 }
460 }
461
462 languages
463 }
464
465 fn has_definition_pattern_fast(&self, query: &str) -> bool {
467 let lower = query.to_lowercase();
468 lower.starts_with("def ") ||
469 lower.starts_with("define ") ||
470 lower.starts_with("definition ") ||
471 lower.contains(" definition") ||
472 lower.starts_with("class ") ||
473 lower.starts_with("function ") ||
474 lower.starts_with("interface ")
475 }
476
477 fn has_reference_pattern_fast(&self, query: &str) -> bool {
478 let lower = query.to_lowercase();
479 lower.starts_with("refs ") ||
480 lower.starts_with("references ") ||
481 lower.starts_with("usages ") ||
482 lower.starts_with("uses ") ||
483 lower.contains("references of") ||
484 lower.contains("usages of")
485 }
486
487 fn has_structural_chars_fast(&self, query: &str) -> bool {
488 let structural_count = query.chars()
489 .filter(|&c| "{}[]()<>=!&|+\\-*/%^~".contains(c))
490 .count();
491 structural_count >= 2
492 }
493
494 fn has_symbol_pattern_fast(&self, query: &str) -> bool {
495 let has_camel = query.chars().any(|c| c.is_uppercase());
497 let has_function_call = query.contains("()");
498 let has_member_access = query.contains('.');
499
500 has_camel || has_function_call || has_member_access
501 }
502
503 fn calculate_naturalness_fast(&self, query: &str) -> f32 {
504 let words: Vec<&str> = query.split_whitespace().collect();
505 let mut score = 0.0;
506
507 let has_articles = words.iter().any(|&w| matches!(w, "the" | "a" | "an"));
509 let has_prepositions = words.iter().any(|&w| matches!(w, "in" | "on" | "at" | "for" | "with" | "by"));
510 let has_questions = words.iter().any(|&w| matches!(w, "what" | "how" | "where" | "when" | "why" | "who"));
511 let has_descriptive = words.iter().any(|&w| matches!(w, "find" | "search" | "get" | "show" | "list"));
512
513 if has_articles { score += 0.25; }
514 if has_prepositions { score += 0.2; }
515 if has_questions { score += 0.25; }
516 if has_descriptive { score += 0.2; }
517 if words.len() > 3 { score += 0.1; }
518
519 (score as f32).min(1.0)
520 }
521
522 fn has_articles(&self, words: &[&str]) -> bool {
524 words.iter().any(|&word| matches!(word, "the" | "a" | "an"))
525 }
526
527 fn has_prepositions(&self, words: &[&str]) -> bool {
528 const PREPOSITIONS: &[&str] = &[
529 "for", "in", "with", "to", "of", "from", "by", "at", "on",
530 "about", "against", "between", "into", "through", "during",
531 "before", "after", "above", "below", "under", "over"
532 ];
533 words.iter().any(|&word| PREPOSITIONS.contains(&word))
534 }
535
536 fn has_descriptive_words(&self, words: &[&str]) -> bool {
537 const DESCRIPTIVE: &[&str] = &[
538 "find", "search", "show", "get", "fetch", "retrieve", "locate",
539 "display", "list", "identify", "discover", "look", "grab",
540 "obtain", "extract", "collect", "gather"
541 ];
542 words.iter().any(|&word| DESCRIPTIVE.contains(&word))
543 }
544
545 fn has_question_words(&self, words: &[&str]) -> bool {
546 const QUESTIONS: &[&str] = &["what", "how", "where", "when", "why", "which", "who"];
547 words.iter().any(|&word| QUESTIONS.contains(&word))
548 }
549
550 fn has_programming_operators(&self, query: &str) -> bool {
551 let operator_chars = "=<>!&|+\\-*/%^~";
552 query.chars().filter(|&c| operator_chars.contains(c)).count() >= 1
553 }
554
555 fn has_special_symbols(&self, query: &str) -> bool {
556 let symbols = "{}[]();,.:@#$";
557 query.chars().any(|c| symbols.contains(c))
558 }
559
560 fn has_programming_syntax(&self, query: &str) -> bool {
561 regex::Regex::new(r"[a-z][A-Z]").unwrap().is_match(query) || query.contains('_') || regex::Regex::new(r"\w+\.\w+").unwrap().is_match(query) || regex::Regex::new(r"^\w+\s*\(").unwrap().is_match(query) }
567
568 fn has_function_calls(&self, query: &str) -> bool {
569 query.contains("()") || regex::Regex::new(r"\w+\s*\(").unwrap().is_match(query)
570 }
571
572 fn has_brackets(&self, query: &str) -> bool {
573 query.contains("[]") || query.contains("{}") || query.contains("()")
574 }
575
576 fn has_definition_pattern(&self, query: &str) -> bool {
577 self.definition_patterns.iter().any(|pattern| pattern.is_match(query))
578 }
579
580 fn has_reference_pattern(&self, query: &str) -> bool {
581 self.reference_patterns.iter().any(|pattern| pattern.is_match(query))
582 }
583
584 fn has_symbol_prefix(&self, query: &str) -> bool {
585 let symbol_patterns = [
586 regex::Regex::new(r"^(class|function|method|var|const|let|type|interface|enum)\s+").unwrap(),
587 regex::Regex::new(r"^[A-Z][a-zA-Z0-9_]*$").unwrap(), regex::Regex::new(r"^[a-z][a-zA-Z0-9_]*\(\)$").unwrap(), regex::Regex::new(r"^\w+\.\w+").unwrap(), regex::Regex::new(r"^@\w+").unwrap(), ];
592
593 symbol_patterns.iter().any(|pattern| pattern.is_match(query))
594 }
595
596 fn has_structural_chars(&self, query: &str) -> bool {
597 let structural_chars = "{}[]()<>=!&|+\\-*/%^~";
598 let count = query.chars().filter(|&c| structural_chars.contains(c)).count();
599 count >= 2
600 }
601
602 fn has_python_syntax(&self, query: &str) -> bool {
604 query.contains("def ") || query.contains("import ") ||
605 query.contains("from ") || query.contains("class ") ||
606 query.contains("__") || query.contains("self.")
607 }
608
609 fn has_javascript_syntax(&self, query: &str) -> bool {
610 query.contains("function ") || query.contains("const ") ||
611 query.contains("let ") || query.contains("var ") ||
612 query.contains("=>") || query.contains("async ")
613 }
614
615 fn has_rust_syntax(&self, query: &str) -> bool {
616 query.contains("fn ") || query.contains("impl ") ||
617 query.contains("struct ") || query.contains("enum ") ||
618 query.contains("::") || query.contains("&mut ")
619 }
620
621 fn has_cpp_syntax(&self, query: &str) -> bool {
622 query.contains("#include") || query.contains("std::") ||
623 query.contains("namespace ") || query.contains("template<") ||
624 query.contains("::") || query.contains("->")
625 }
626
627 fn has_sql_syntax(&self, query: &str) -> bool {
628 let lower = query.to_lowercase();
629 lower.contains("select ") || lower.contains("from ") ||
630 lower.contains("where ") || lower.contains("insert ") ||
631 lower.contains("update ") || lower.contains("delete ")
632 }
633
634 fn compile_definition_patterns() -> Result<Vec<regex::Regex>> {
636 let patterns = [
637 r"^(def|define|definition|declare)\s+",
638 r"^(what is|where is|find definition)\s+",
639 r"^(class|function|interface|type)\s+\w+$",
640 r"^go to definition",
641 r"^\w+\s+(definition|declaration)$",
642 ];
643
644 patterns.iter()
645 .map(|&pattern| regex::Regex::new(pattern))
646 .collect::<Result<Vec<_>, _>>()
647 .map_err(|e| anyhow::anyhow!("Failed to compile definition patterns: {}", e))
648 }
649
650 fn compile_reference_patterns() -> Result<Vec<regex::Regex>> {
651 let patterns = [
652 r"^(refs|references|usages|uses)\s+",
653 r"^(find|show|list)\s+(references|usages|uses)",
654 r"^(where|who)\s+(uses|calls|references)",
655 r"^\w+\s+(references|usages|calls)$",
656 ];
657
658 patterns.iter()
659 .map(|&pattern| regex::Regex::new(pattern))
660 .collect::<Result<Vec<_>, _>>()
661 .map_err(|e| anyhow::anyhow!("Failed to compile reference patterns: {}", e))
662 }
663
664 fn compile_language_patterns() -> Result<HashMap<String, Vec<regex::Regex>>> {
665 let mut patterns = HashMap::new();
666
667 let python_patterns = vec![
669 regex::Regex::new(r"\bdef\s+\w+")?,
670 regex::Regex::new(r"\bimport\s+\w+")?,
671 regex::Regex::new(r"\bfrom\s+\w+\s+import")?,
672 regex::Regex::new(r"__\w+__")?,
673 regex::Regex::new(r"\bself\.")?,
674 ];
675 patterns.insert("python".to_string(), python_patterns);
676
677 let js_patterns = vec![
679 regex::Regex::new(r"\bfunction\s+\w+")?,
680 regex::Regex::new(r"\bconst\s+\w+")?,
681 regex::Regex::new(r"\blet\s+\w+")?,
682 regex::Regex::new(r"=>\s*\{")?,
683 regex::Regex::new(r"\basync\s+\w+")?,
684 ];
685 patterns.insert("javascript".to_string(), js_patterns);
686
687 let rust_patterns = vec![
689 regex::Regex::new(r"\bfn\s+\w+")?,
690 regex::Regex::new(r"\bimpl\s+\w+")?,
691 regex::Regex::new(r"\bstruct\s+\w+")?,
692 regex::Regex::new(r"\benum\s+\w+")?,
693 regex::Regex::new(r"::")?,
694 ];
695 patterns.insert("rust".to_string(), rust_patterns);
696
697 Ok(patterns)
698 }
699
700 fn build_nl_vocabulary() -> HashSet<String> {
701 let words = [
702 "the", "a", "an",
704 "in", "on", "at", "for", "with", "by", "to", "from", "of", "about",
706 "what", "how", "where", "when", "why", "who", "which",
708 "find", "search", "get", "show", "list", "identify", "discover",
710 "look", "grab", "obtain", "extract", "collect", "gather",
711 "good", "bad", "big", "small", "fast", "slow", "easy", "hard",
713 "simple", "complex", "new", "old", "best", "better", "worst",
714 ];
715
716 words.iter().map(|&s| s.to_string()).collect()
717 }
718
719 fn build_code_vocabulary() -> HashSet<String> {
720 let words = [
721 "def", "class", "function", "const", "let", "var", "import", "export",
723 "if", "else", "for", "while", "try", "catch", "async", "await",
724 "return", "yield", "break", "continue", "struct", "enum", "impl",
725 "fn", "pub", "mod", "use", "namespace", "template", "typedef",
726 "and", "or", "not", "true", "false", "null", "undefined", "void",
728 "int", "str", "bool", "float", "char", "string", "array", "list",
730 "dict", "map", "set", "vector", "option", "result",
731 ];
732
733 words.iter().map(|&s| s.to_string()).collect()
734 }
735
736 fn record_classification(&self, latency: std::time::Duration, classification: &QueryClassification) {
738 let mut metrics = self.metrics.write();
739 metrics.total_classifications += 1;
740 metrics.total_latency += latency;
741
742 let latency_ms = latency.as_millis() as f64;
743 if latency_ms < metrics.min_latency_ms || metrics.min_latency_ms == 0.0 {
744 metrics.min_latency_ms = latency_ms;
745 }
746 if latency_ms > metrics.max_latency_ms {
747 metrics.max_latency_ms = latency_ms;
748 }
749
750 *metrics.intent_counts.entry(classification.intent).or_insert(0) += 1;
752
753 let confidence_bucket = (classification.confidence * 10.0) as usize;
755 metrics.confidence_histogram[confidence_bucket.min(9)] += 1;
756 }
757
758 pub fn get_metrics(&self) -> ClassifierMetrics {
760 self.metrics.read().clone()
761 }
762}
763
764#[derive(Debug, Clone, Default)]
766pub struct ClassifierMetrics {
767 pub total_classifications: u64,
768 pub total_latency: std::time::Duration,
769 pub min_latency_ms: f64,
770 pub max_latency_ms: f64,
771 pub intent_counts: HashMap<QueryIntent, u64>,
772 pub confidence_histogram: [u64; 10], }
774
775impl ClassifierMetrics {
776 pub fn avg_latency_ms(&self) -> f64 {
777 if self.total_classifications == 0 {
778 0.0
779 } else {
780 self.total_latency.as_millis() as f64 / self.total_classifications as f64
781 }
782 }
783
784 pub fn intent_distribution(&self) -> HashMap<QueryIntent, f64> {
785 let total = self.total_classifications as f64;
786 if total == 0.0 {
787 return HashMap::new();
788 }
789
790 self.intent_counts.iter()
791 .map(|(&intent, &count)| (intent, count as f64 / total))
792 .collect()
793 }
794
795 pub fn avg_confidence(&self) -> f64 {
796 let total_weight: u64 = self.confidence_histogram.iter().sum();
797 if total_weight == 0 {
798 return 0.0;
799 }
800
801 let weighted_sum: f64 = self.confidence_histogram.iter()
802 .enumerate()
803 .map(|(i, &count)| (i as f64 + 0.5) * 0.1 * count as f64)
804 .sum();
805
806 weighted_sum / total_weight as f64
807 }
808}
809
810pub fn should_apply_semantic_reranking(
812 classification: &QueryClassification,
813 candidate_count: usize,
814 mode: &str,
815 config: &ClassifierConfig,
816) -> bool {
817 if mode != "hybrid" {
819 return false;
820 }
821
822 let min_candidates = 10;
824 let max_candidates = 200;
825 if candidate_count < min_candidates || candidate_count > max_candidates {
826 return false;
827 }
828
829 classification.naturalness_score >= config.nl_threshold
831}
832
833pub fn explain_classification_decision(
835 classification: &QueryClassification,
836 candidate_count: usize,
837 mode: &str,
838) -> String {
839 if mode != "hybrid" {
840 return format!("Classification: mode is '{}', requires 'hybrid'", mode);
841 }
842
843 if candidate_count < 10 {
844 return format!("Classification: only {} candidates, need ≥10", candidate_count);
845 }
846
847 if candidate_count > 200 {
848 return format!("Classification: {} candidates exceed limit (200)", candidate_count);
849 }
850
851 let nl_indicators: Vec<String> = classification.characteristics
852 .iter()
853 .filter_map(|&c| match c {
854 QueryCharacteristic::HasArticles => Some("articles".to_string()),
855 QueryCharacteristic::HasPrepositions => Some("prepositions".to_string()),
856 QueryCharacteristic::HasDescriptiveWords => Some("descriptive_words".to_string()),
857 QueryCharacteristic::HasQuestions => Some("questions".to_string()),
858 _ => None,
859 })
860 .collect();
861
862 let code_indicators: Vec<String> = classification.characteristics
863 .iter()
864 .filter_map(|&c| match c {
865 QueryCharacteristic::HasOperators => Some("operators".to_string()),
866 QueryCharacteristic::HasProgrammingSyntax => Some("programming_syntax".to_string()),
867 QueryCharacteristic::HasSymbols => Some("symbols".to_string()),
868 _ => None,
869 })
870 .collect();
871
872 format!(
873 "Classification: intent={}, confidence={:.3}, naturalness={:.3} (NL: {}, Code: {})",
874 classification.intent,
875 classification.confidence,
876 classification.naturalness_score,
877 nl_indicators.join(", "),
878 code_indicators.join(", ")
879 )
880}
881
882pub async fn initialize_classifier(config: &ClassifierConfig) -> Result<()> {
884 tracing::info!("Initializing query classifier module");
885 tracing::info!("NL threshold: {}", config.nl_threshold);
886 tracing::info!("Intent confidence threshold: {}", config.intent_confidence_threshold);
887 tracing::info!("Language detection: {}", config.enable_language_detection);
888 tracing::info!("Custom patterns: {}", config.custom_patterns.len());
889
890 if config.nl_threshold < 0.0 || config.nl_threshold > 1.0 {
892 anyhow::bail!("NL threshold must be in range [0.0, 1.0]");
893 }
894
895 if config.intent_confidence_threshold < 0.0 || config.intent_confidence_threshold > 1.0 {
896 anyhow::bail!("Intent confidence threshold must be in range [0.0, 1.0]");
897 }
898
899 tracing::info!("Query classifier module initialized successfully");
900 Ok(())
901}
902
903#[cfg(test)]
904mod tests {
905 use super::*;
906
907 #[tokio::test]
908 async fn test_classification_natural_language() {
909 let config = ClassifierConfig::default();
910 let classifier = QueryClassifier::new(config).unwrap();
911
912 let query = "how to find a function that calculates the sum of two numbers";
913 let classification = classifier.classify(query);
914
915 assert_eq!(classification.intent, QueryIntent::NaturalLanguage);
916 assert!(classification.naturalness_score > 0.6);
917 assert!(classification.characteristics.contains(&QueryCharacteristic::HasArticles));
918 assert!(classification.characteristics.contains(&QueryCharacteristic::HasDescriptiveWords));
919 }
920
921 #[tokio::test]
922 async fn test_classification_definition() {
923 let config = ClassifierConfig::default();
924 let classifier = QueryClassifier::new(config).unwrap();
925
926 let query = "def calculateSum";
927 let classification = classifier.classify(query);
928
929 assert_eq!(classification.intent, QueryIntent::Definition);
930 assert!(classification.confidence > 0.8);
931 assert!(classification.characteristics.contains(&QueryCharacteristic::HasDefinitionPattern));
932 }
933
934 #[tokio::test]
935 async fn test_classification_references() {
936 let config = ClassifierConfig::default();
937 let classifier = QueryClassifier::new(config).unwrap();
938
939 let query = "refs MyFunction";
940 let classification = classifier.classify(query);
941
942 assert_eq!(classification.intent, QueryIntent::References);
943 assert!(classification.confidence > 0.8);
944 assert!(classification.characteristics.contains(&QueryCharacteristic::HasReferencePattern));
945 }
946
947 #[tokio::test]
948 async fn test_classification_structural() {
949 let config = ClassifierConfig::default();
950 let classifier = QueryClassifier::new(config).unwrap();
951
952 let query = "for (let i = 0; i < length; i++)";
953 let classification = classifier.classify(query);
954
955 assert_eq!(classification.intent, QueryIntent::Structural);
956 assert!(classification.characteristics.contains(&QueryCharacteristic::HasStructuralChars));
957 assert!(classification.characteristics.contains(&QueryCharacteristic::HasOperators));
958 }
959
960 #[tokio::test]
961 async fn test_fast_classification() {
962 let config = ClassifierConfig::default();
963 let classifier = QueryClassifier::new(config).unwrap();
964
965 let (intent, confidence) = classifier.classify_fast("def myFunction");
966 assert_eq!(intent, QueryIntent::Definition);
967 assert!(confidence > 0.8);
968
969 let (intent, confidence) = classifier.classify_fast("how to sort array");
970 assert!(matches!(intent, QueryIntent::Lexical | QueryIntent::NaturalLanguage));
972 assert!(confidence > 0.5);
973 }
974
975 #[tokio::test]
976 async fn test_language_detection() {
977 let config = ClassifierConfig::default();
978 let classifier = QueryClassifier::new(config).unwrap();
979
980 let query = "def calculate_sum(a, b): return a + b";
981 let classification = classifier.classify(query);
982
983 assert!(classification.language_hints.contains(&"python".to_string()));
984 assert!(classification.characteristics.contains(&QueryCharacteristic::PythonSyntax));
985 }
986
987 #[tokio::test]
988 async fn test_semantic_reranking_decision() {
989 let config = ClassifierConfig::default();
990 let classifier = QueryClassifier::new(config.clone()).unwrap();
991
992 let query = "find a function to sort an array";
993 let classification = classifier.classify(query);
994
995 assert!(should_apply_semantic_reranking(&classification, 50, "hybrid", &config));
997
998 assert!(!should_apply_semantic_reranking(&classification, 50, "lexical", &config));
1000
1001 assert!(!should_apply_semantic_reranking(&classification, 5, "hybrid", &config));
1003 }
1004
1005 #[test]
1006 fn test_metrics_calculation() {
1007 let mut metrics = ClassifierMetrics::default();
1008 metrics.total_classifications = 100;
1009 metrics.intent_counts.insert(QueryIntent::NaturalLanguage, 30);
1010 metrics.intent_counts.insert(QueryIntent::Definition, 20);
1011 metrics.intent_counts.insert(QueryIntent::Lexical, 50);
1012
1013 let distribution = metrics.intent_distribution();
1014 assert_eq!(distribution[&QueryIntent::NaturalLanguage], 0.3);
1015 assert_eq!(distribution[&QueryIntent::Definition], 0.2);
1016 assert_eq!(distribution[&QueryIntent::Lexical], 0.5);
1017 }
1018}