1use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tokio::sync::RwLock;
15use tracing::{debug, info, warn};
16
17pub struct CrossEncoder {
19 config: CrossEncoderConfig,
20 model: Arc<RwLock<Option<CrossEncoderModel>>>,
22 complexity_analyzer: QueryComplexityAnalyzer,
24 performance_tracker: Arc<RwLock<PerformanceTracker>>,
26 budget_manager: Arc<RwLock<BudgetManager>>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CrossEncoderConfig {
32 pub enabled: bool,
34 pub max_inference_ms: u64,
36 pub complexity_threshold: f32,
38 pub top_k: usize,
40 pub model_type: String,
42 pub max_batch_size: usize,
44 pub budget_strategy: BudgetStrategy,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub enum BudgetStrategy {
50 FixedPerQuery,
52 DynamicImportance,
54 AdaptiveHistorical,
56}
57
58#[derive(Debug)]
60pub struct CrossEncoderModel {
61 model_type: String,
62 initialized: bool,
63 inference_time_ms: u64,
64}
65
66#[derive(Debug)]
68pub struct QueryComplexityAnalyzer {
69 nl_classifier: NLClassifier,
71 complexity_cache: HashMap<String, f32>,
73}
74
75#[derive(Debug)]
77pub struct NLClassifier {
78 nl_indicators: Vec<String>,
80 code_patterns: Vec<String>,
82}
83
84#[derive(Debug, Default)]
86pub struct PerformanceTracker {
87 pub latency_p50_ms: f64,
89 pub latency_p95_ms: f64,
90 pub latency_p99_ms: f64,
91 pub queries_activated: u64,
93 pub queries_skipped: u64,
94 pub precision_improvement: f32,
96 pub ndcg_improvement: f32,
97 pub budget_utilization: f32,
99 latency_samples: Vec<u64>,
101}
102
103#[derive(Debug)]
105pub struct BudgetManager {
106 pub current_budget_ms: u64,
108 pub budget_per_window_ms: u64,
110 pub window_duration: Duration,
112 pub last_reset: Instant,
114 pub query_priorities: HashMap<String, f32>,
116}
117
118#[derive(Debug, Clone)]
120pub struct QueryAnalysis {
121 pub query: String,
122 pub complexity_score: f32,
123 pub is_natural_language: bool,
124 pub should_activate_cross_encoder: bool,
125 pub priority_score: f32,
126 pub estimated_benefit: f32,
127}
128
129#[derive(Debug, Clone)]
131pub struct CrossEncoderPair {
132 pub query: String,
133 pub candidate: String,
134 pub initial_score: f32,
135 pub metadata: HashMap<String, String>,
136}
137
138#[derive(Debug, Clone)]
140pub struct CrossEncoderResult {
141 pub query: String,
142 pub candidate: String,
143 pub relevance_score: f32,
144 pub confidence: f32,
145 pub inference_time_ms: u64,
146 pub model_version: String,
147}
148
149impl CrossEncoder {
150 pub async fn new(config: CrossEncoderConfig) -> Result<Self> {
152 info!("Creating cross-encoder with config: {:?}", config);
153
154 if !config.enabled {
155 info!("Cross-encoder is disabled");
156 }
157
158 let complexity_analyzer = QueryComplexityAnalyzer::new();
159 let performance_tracker = Arc::new(RwLock::new(PerformanceTracker::default()));
160 let budget_manager = Arc::new(RwLock::new(BudgetManager::new(config.max_inference_ms)));
161
162 Ok(Self {
163 config,
164 model: Arc::new(RwLock::new(None)),
165 complexity_analyzer,
166 performance_tracker,
167 budget_manager,
168 })
169 }
170
171 pub async fn initialize(&self) -> Result<()> {
173 if !self.config.enabled {
174 info!("Cross-encoder disabled, skipping initialization");
175 return Ok(());
176 }
177
178 info!("Initializing cross-encoder model: {}", self.config.model_type);
179
180 let model = CrossEncoderModel {
182 model_type: self.config.model_type.clone(),
183 initialized: true,
184 inference_time_ms: 25, };
186
187 *self.model.write().await = Some(model);
188
189 info!("Cross-encoder initialized successfully");
190 Ok(())
191 }
192
193 pub async fn analyze_query(&self, query: &str) -> Result<QueryAnalysis> {
195 let complexity_score = self.complexity_analyzer.calculate_complexity(query).await?;
197
198 let is_natural_language = self.complexity_analyzer.is_natural_language(query);
200
201 let priority_score = self.calculate_priority_score(query, complexity_score, is_natural_language).await?;
203
204 let budget_available = self.check_budget_availability(priority_score).await?;
206
207 let should_activate = self.config.enabled &&
209 complexity_score >= self.config.complexity_threshold &&
210 is_natural_language &&
211 budget_available;
212
213 let estimated_benefit = if should_activate {
215 self.estimate_benefit(complexity_score, is_natural_language)
216 } else {
217 0.0
218 };
219
220 let analysis = QueryAnalysis {
221 query: query.to_string(),
222 complexity_score,
223 is_natural_language,
224 should_activate_cross_encoder: should_activate,
225 priority_score,
226 estimated_benefit,
227 };
228
229 debug!("Query analysis: complexity={:.3}, NL={}, activate={}, benefit={:.3}",
230 complexity_score, is_natural_language, should_activate, estimated_benefit);
231
232 Ok(analysis)
233 }
234
235 pub async fn cross_encode(&self, pairs: Vec<CrossEncoderPair>) -> Result<Vec<CrossEncoderResult>> {
237 if !self.config.enabled || pairs.is_empty() {
238 return Ok(Vec::new());
239 }
240
241 let start_time = Instant::now();
242
243 let query = &pairs[0].query;
245 let priority = self.calculate_query_priority(query).await?;
246
247 if !self.allocate_budget(priority).await? {
248 debug!("Budget exhausted, skipping cross-encoder for query");
249 self.update_skip_statistics().await;
250 return Ok(Vec::new());
251 }
252
253 let candidates_to_process = pairs.into_iter()
255 .take(self.config.top_k)
256 .collect::<Vec<_>>();
257
258 let results = self.batch_cross_encode(candidates_to_process).await
260 .context("Cross-encoder inference failed")?;
261
262 let inference_time = start_time.elapsed().as_millis() as u64;
263
264 self.update_performance_metrics(inference_time, &results).await;
266
267 if inference_time > self.config.max_inference_ms {
269 warn!("Cross-encoder exceeded budget: {}ms > {}ms",
270 inference_time, self.config.max_inference_ms);
271 }
272
273 debug!("Cross-encoded {} pairs in {}ms", results.len(), inference_time);
274
275 Ok(results)
276 }
277
278 pub async fn get_metrics(&self) -> CrossEncoderMetrics {
280 let tracker = self.performance_tracker.read().await;
281 let budget = self.budget_manager.read().await;
282
283 CrossEncoderMetrics {
284 enabled: self.config.enabled,
285 queries_activated: tracker.queries_activated,
286 queries_skipped: tracker.queries_skipped,
287 activation_rate: if tracker.queries_activated + tracker.queries_skipped > 0 {
288 tracker.queries_activated as f32 / (tracker.queries_activated + tracker.queries_skipped) as f32
289 } else {
290 0.0
291 },
292 latency_p50_ms: tracker.latency_p50_ms,
293 latency_p95_ms: tracker.latency_p95_ms,
294 latency_p99_ms: tracker.latency_p99_ms,
295 budget_utilization: budget.budget_utilization(),
296 precision_improvement: tracker.precision_improvement,
297 ndcg_improvement: tracker.ndcg_improvement,
298 meets_latency_target: tracker.latency_p95_ms <= self.config.max_inference_ms as f64,
299 }
300 }
301
302 pub async fn update_performance_feedback(&self, query: &str, actual_improvement: f32) -> Result<()> {
304 let mut tracker = self.performance_tracker.write().await;
306
307 let alpha = 0.1; if actual_improvement > 0.0 {
310 tracker.precision_improvement = tracker.precision_improvement * (1.0 - alpha) + actual_improvement * alpha;
311 }
312
313 let mut budget = self.budget_manager.write().await;
315 let current_priority = budget.query_priorities.get(query).cloned().unwrap_or(0.5);
316 let new_priority = (current_priority + actual_improvement * 0.5).clamp(0.0, 1.0);
317 budget.query_priorities.insert(query.to_string(), new_priority);
318
319 debug!("Updated performance feedback for query '{}': improvement={:.3}, new_priority={:.3}",
320 query, actual_improvement, new_priority);
321
322 Ok(())
323 }
324
325 async fn calculate_priority_score(&self, query: &str, complexity: f32, is_nl: bool) -> Result<f32> {
328 let mut score = complexity;
329
330 if is_nl {
332 score += 0.2;
333 }
334
335 let budget = self.budget_manager.read().await;
337 if let Some(historical_priority) = budget.query_priorities.get(query) {
338 score = (score + historical_priority) / 2.0;
339 }
340
341 Ok(score.clamp(0.0, 1.0))
342 }
343
344 async fn check_budget_availability(&self, priority: f32) -> Result<bool> {
345 let budget = self.budget_manager.read().await;
346
347 let has_budget = budget.current_budget_ms > 0;
349
350 let priority_threshold = match budget.current_budget_ms {
352 0..=10 => 0.9, 11..=25 => 0.7, _ => priority, };
356
357 Ok(has_budget && priority >= priority_threshold)
358 }
359
360 async fn allocate_budget(&self, priority: f32) -> Result<bool> {
361 let mut budget = self.budget_manager.write().await;
362
363 if budget.last_reset.elapsed() >= budget.window_duration {
365 budget.current_budget_ms = budget.budget_per_window_ms;
366 budget.last_reset = Instant::now();
367 debug!("Budget reset: {}ms available", budget.current_budget_ms);
368 }
369
370 let estimated_cost = match self.config.budget_strategy {
372 BudgetStrategy::FixedPerQuery => 30, BudgetStrategy::DynamicImportance => (20.0 + priority * 30.0) as u64,
374 BudgetStrategy::AdaptiveHistorical => {
375 let tracker = self.performance_tracker.read().await;
376 tracker.latency_p95_ms as u64
377 }
378 };
379
380 if budget.current_budget_ms >= estimated_cost {
381 budget.current_budget_ms -= estimated_cost;
382 debug!("Budget allocated: {}ms, remaining: {}ms", estimated_cost, budget.current_budget_ms);
385 Ok(true)
386 } else {
387 debug!("Budget allocation failed: need {}ms, have {}ms", estimated_cost, budget.current_budget_ms);
388 Ok(false)
389 }
390 }
391
392 async fn batch_cross_encode(&self, pairs: Vec<CrossEncoderPair>) -> Result<Vec<CrossEncoderResult>> {
393 let model_guard = self.model.read().await;
394 let model = model_guard.as_ref()
395 .ok_or_else(|| anyhow::anyhow!("Cross-encoder model not initialized"))?;
396
397 let mut results = Vec::with_capacity(pairs.len());
398
399 for chunk in pairs.chunks(self.config.max_batch_size) {
401 let batch_start = Instant::now();
402
403 for pair in chunk {
404 let result = self.mock_cross_encode(pair, model).await?;
406 results.push(result);
407 }
408
409 let batch_time = batch_start.elapsed().as_millis() as u64;
410 debug!("Processed batch of {} pairs in {}ms", chunk.len(), batch_time);
411
412 if batch_time > self.config.max_inference_ms / 2 {
414 warn!("Batch processing time {}ms approaching budget limit", batch_time);
415 }
416 }
417
418 Ok(results)
419 }
420
421 async fn mock_cross_encode(&self, pair: &CrossEncoderPair, model: &CrossEncoderModel) -> Result<CrossEncoderResult> {
422 tokio::time::sleep(Duration::from_millis(model.inference_time_ms / 4)).await;
427
428 let query_lower = pair.query.to_lowercase();
430 let candidate_lower = pair.candidate.to_lowercase();
431
432 let query_terms: Vec<&str> = query_lower.split_whitespace().collect();
434 let matches = query_terms.iter()
435 .filter(|term| candidate_lower.contains(*term))
436 .count();
437
438 let term_overlap = matches as f32 / query_terms.len().max(1) as f32;
439
440 let relevance_score = (pair.initial_score * 0.6 + term_overlap * 0.4).clamp(0.0, 1.0);
442
443 let confidence = if relevance_score > 0.8 || relevance_score < 0.2 {
445 0.9 } else {
447 0.6 };
449
450 Ok(CrossEncoderResult {
451 query: pair.query.clone(),
452 candidate: pair.candidate.clone(),
453 relevance_score,
454 confidence,
455 inference_time_ms: model.inference_time_ms,
456 model_version: model.model_type.clone(),
457 })
458 }
459
460 fn estimate_benefit(&self, complexity: f32, is_nl: bool) -> f32 {
461 let mut benefit = complexity * 0.02; if is_nl {
464 benefit += 0.015; }
466
467 benefit.clamp(0.0, 0.025) }
469
470 async fn calculate_query_priority(&self, query: &str) -> Result<f32> {
471 let budget = self.budget_manager.read().await;
472 Ok(budget.query_priorities.get(query).cloned().unwrap_or(0.5))
473 }
474
475 async fn update_skip_statistics(&self) {
476 let mut tracker = self.performance_tracker.write().await;
477 tracker.queries_skipped += 1;
478 }
479
480 async fn update_performance_metrics(&self, inference_time: u64, _results: &[CrossEncoderResult]) {
481 let mut tracker = self.performance_tracker.write().await;
482
483 tracker.queries_activated += 1;
484 tracker.latency_samples.push(inference_time);
485
486 if tracker.latency_samples.len() > 1000 {
488 tracker.latency_samples.drain(0..500);
489 }
490
491 if !tracker.latency_samples.is_empty() {
493 let mut sorted_samples = tracker.latency_samples.clone();
494 sorted_samples.sort_unstable();
495
496 let len = sorted_samples.len();
497 tracker.latency_p50_ms = sorted_samples[len / 2] as f64;
498 tracker.latency_p95_ms = sorted_samples[(len * 95) / 100] as f64;
499 tracker.latency_p99_ms = sorted_samples[(len * 99) / 100] as f64;
500 }
501 }
502}
503
504impl QueryComplexityAnalyzer {
505 pub fn new() -> Self {
506 Self {
507 nl_classifier: NLClassifier::new(),
508 complexity_cache: HashMap::new(),
509 }
510 }
511
512 pub async fn calculate_complexity(&self, query: &str) -> Result<f32> {
513 let mut complexity = 0.0;
515
516 let length_factor = (query.len() as f32 / 20.0).min(1.0);
518 complexity += length_factor * 0.3;
519
520 let word_count = query.split_whitespace().count();
522 let word_factor = (word_count as f32 / 5.0).min(1.0);
523 complexity += word_factor * 0.3;
524
525 if self.nl_classifier.is_natural_language_query(query) {
527 complexity += 0.4;
528 }
529
530 Ok(complexity.clamp(0.0, 1.0))
531 }
532
533 pub fn is_natural_language(&self, query: &str) -> bool {
534 self.nl_classifier.is_natural_language_query(query)
535 }
536}
537
538impl Default for NLClassifier {
539 fn default() -> Self {
540 Self::new()
541 }
542}
543
544impl NLClassifier {
545 pub fn new() -> Self {
546 let nl_indicators = vec![
547 "find".to_string(),
548 "show".to_string(),
549 "get".to_string(),
550 "how".to_string(),
551 "what".to_string(),
552 "where".to_string(),
553 "functions".to_string(),
554 "methods".to_string(),
555 "classes".to_string(),
556 ];
557
558 let code_patterns = vec![
559 "def ".to_string(),
560 "function ".to_string(), "class ".to_string(),
562 "import ".to_string(),
563 "const ".to_string(),
564 "let ".to_string(),
565 "fn ".to_string(),
566 ];
567
568 Self {
569 nl_indicators,
570 code_patterns,
571 }
572 }
573 pub fn is_natural_language_query(&self, query: &str) -> bool {
574 let query_lower = query.to_lowercase();
575
576 let has_nl_indicators = self.nl_indicators.iter()
578 .any(|indicator| query_lower.contains(indicator));
579
580 let has_code_patterns = self.code_patterns.iter()
582 .any(|pattern| query_lower.contains(pattern));
583
584
585 has_nl_indicators && !has_code_patterns
587 }
588}
589
590impl BudgetManager {
591 pub fn new(max_inference_ms: u64) -> Self {
592 Self {
593 current_budget_ms: max_inference_ms * 60, budget_per_window_ms: max_inference_ms * 60,
595 window_duration: Duration::from_secs(60), last_reset: Instant::now(),
597 query_priorities: HashMap::new(),
598 }
599 }
600
601 pub fn budget_utilization(&self) -> f32 {
602 1.0 - (self.current_budget_ms as f32 / self.budget_per_window_ms as f32)
603 }
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct CrossEncoderMetrics {
608 pub enabled: bool,
609 pub queries_activated: u64,
610 pub queries_skipped: u64,
611 pub activation_rate: f32,
612 pub latency_p50_ms: f64,
613 pub latency_p95_ms: f64,
614 pub latency_p99_ms: f64,
615 pub budget_utilization: f32,
616 pub precision_improvement: f32,
617 pub ndcg_improvement: f32,
618 pub meets_latency_target: bool,
619}
620
621pub async fn initialize_cross_encoder(config: &CrossEncoderConfig) -> Result<()> {
623 info!("Initializing cross-encoder");
624 info!("Enabled: {}, max inference: {}ms, complexity threshold: {}",
625 config.enabled, config.max_inference_ms, config.complexity_threshold);
626
627 if config.enabled {
628 if config.max_inference_ms > 50 {
630 warn!("Max inference time {}ms > 50ms target", config.max_inference_ms);
631 }
632
633 if config.complexity_threshold < 0.5 {
634 warn!("Complexity threshold {} may activate too frequently", config.complexity_threshold);
635 }
636
637 info!("Cross-encoder will target +1-2pp improvement on complex NL queries");
638 }
639
640 info!("Cross-encoder initialization complete");
641 Ok(())
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647
648 #[tokio::test]
649 async fn test_cross_encoder_creation() {
650 let config = CrossEncoderConfig {
651 enabled: true,
652 max_inference_ms: 50,
653 complexity_threshold: 0.7,
654 top_k: 10,
655 model_type: "cross-encoder-ms-marco-MiniLM-L-6-v2".to_string(),
656 max_batch_size: 8,
657 budget_strategy: BudgetStrategy::FixedPerQuery,
658 };
659
660 let encoder = CrossEncoder::new(config).await.unwrap();
661 let metrics = encoder.get_metrics().await;
662 assert_eq!(metrics.queries_activated, 0);
663 assert!(metrics.enabled);
664 }
665
666 #[tokio::test]
667 async fn test_query_analysis() {
668 let config = CrossEncoderConfig {
669 enabled: true,
670 max_inference_ms: 50,
671 complexity_threshold: 0.5,
672 top_k: 10,
673 model_type: "test".to_string(),
674 max_batch_size: 8,
675 budget_strategy: BudgetStrategy::FixedPerQuery,
676 };
677
678 let encoder = CrossEncoder::new(config).await.unwrap();
679
680 let nl_query = "find all functions that handle user authentication";
681 let analysis = encoder.analyze_query(nl_query).await.unwrap();
682
683 assert!(analysis.is_natural_language);
684 assert!(analysis.complexity_score > 0.0);
685 }
686
687 #[test]
688 fn test_nl_classifier() {
689 let classifier = NLClassifier::default();
690
691 assert!(classifier.is_natural_language_query("find all functions that process user data"));
693 assert!(classifier.is_natural_language_query("show me methods for error handling"));
694
695 assert!(!classifier.is_natural_language_query("def process_user_data():"));
697 assert!(!classifier.is_natural_language_query("function authenticate(user)"));
698 }
699
700 #[tokio::test]
701 async fn test_complexity_calculation() {
702 let analyzer = QueryComplexityAnalyzer::new();
703
704 let simple_query = "test";
705 let complex_query = "find all functions that handle user authentication and error processing";
706
707 let simple_complexity = analyzer.calculate_complexity(simple_query).await.unwrap();
708 let complex_complexity = analyzer.calculate_complexity(complex_query).await.unwrap();
709
710 assert!(complex_complexity > simple_complexity);
711 assert!(simple_complexity >= 0.0 && simple_complexity <= 1.0);
712 assert!(complex_complexity >= 0.0 && complex_complexity <= 1.0);
713 }
714
715 #[tokio::test]
716 async fn test_budget_management() {
717 let mut budget = BudgetManager::new(50);
718
719 assert_eq!(budget.current_budget_ms, 3000); assert!(budget.current_budget_ms > 0);
721
722 budget.current_budget_ms = 1500; assert!((budget.budget_utilization() - 0.5).abs() < 0.01);
725 }
726}