1use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, VecDeque};
11use std::time::Duration;
12use uuid::Uuid;
13
14use super::config::CachingConfig;
15use super::error::{DecisionError, DecisionResult};
16use super::traits::OptimizationStrategy;
17use super::types::{
18 ComparisonOperator, ConfigChange, ConfigType, Decision, DecisionCriteria, DecisionInput,
19 DecisionOutcome, DecisionType, ExpectedImpact, RollbackCondition, RollbackPlan, SafetyCheck,
20};
21
22pub struct CachingStrategy {
27 config: CachingConfig,
29
30 request_patterns: VecDeque<RequestPattern>,
32
33 hit_rate_history: VecDeque<HitRateRecord>,
35
36 similarity_cache: HashMap<String, Vec<SimilarityScore>>,
38
39 stats: CachingStats,
41
42 last_evaluation: Option<DateTime<Utc>>,
44
45 current_config: Option<CacheConfiguration>,
47
48 learning_data: LearningData,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54struct RequestPattern {
55 id: String,
57
58 timestamp: DateTime<Utc>,
60
61 fingerprint: String,
63
64 metadata: HashMap<String, String>,
66
67 tokens: u32,
69
70 model: String,
72
73 count: u64,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79struct HitRateRecord {
80 timestamp: DateTime<Utc>,
82
83 hit_rate_pct: f64,
85
86 total_requests: u64,
88
89 cache_hits: u64,
91
92 cache_size: usize,
94
95 avg_latency_saved_ms: f64,
97
98 cost_saved_usd: f64,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104struct SimilarityScore {
105 pattern_id: String,
107
108 score: f64,
110
111 timestamp: DateTime<Utc>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117struct CacheConfiguration {
118 enabled: bool,
120
121 ttl: Duration,
123
124 max_size: usize,
126
127 similarity_threshold: f64,
129
130 strategy_type: CacheStrategyType,
132
133 last_updated: DateTime<Utc>,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139enum CacheStrategyType {
140 Exact,
142
143 Semantic,
145
146 Hybrid,
148}
149
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152struct CachingStats {
153 total_decisions: u64,
155
156 caching_enabled: u64,
158
159 caching_disabled: u64,
161
162 config_updates: u64,
164
165 avg_predicted_hit_rate: f64,
167
168 avg_actual_hit_rate: f64,
170
171 total_cost_savings_usd: f64,
173
174 total_latency_savings_ms: f64,
176
177 prediction_accuracy: f64,
179
180 patterns_analyzed: u64,
182
183 learning_iterations: u64,
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189struct LearningData {
190 hit_rate_predictions: VecDeque<HitRatePrediction>,
192
193 cost_accuracy: VecDeque<CostAccuracy>,
195
196 threshold_effectiveness: HashMap<String, f64>,
198
199 hit_rate_adjustment_factor: f64,
201
202 cost_adjustment_factor: f64,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208struct HitRatePrediction {
209 decision_id: String,
211
212 predicted: f64,
214
215 actual: f64,
217
218 variance: f64,
220
221 timestamp: DateTime<Utc>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227struct CostAccuracy {
228 decision_id: String,
230
231 predicted_savings_usd: f64,
233
234 actual_savings_usd: f64,
236
237 variance: f64,
239
240 timestamp: DateTime<Utc>,
242}
243
244#[derive(Debug, Clone)]
246struct RequestCluster {
247 id: String,
249
250 representative: RequestPattern,
252
253 members: Vec<RequestPattern>,
255
256 total_count: u64,
258
259 avg_similarity: f64,
261
262 frequency: f64,
264}
265
266#[derive(Debug, Clone)]
268struct CacheOpportunity {
269 cluster: RequestCluster,
271
272 expected_hit_rate_pct: f64,
274
275 expected_cost_savings_usd: f64,
277
278 expected_latency_savings_ms: f64,
280
281 recommended_cache_size: usize,
283
284 recommended_ttl: Duration,
286
287 recommended_similarity_threshold: f64,
289
290 confidence: f64,
292}
293
294impl CachingStrategy {
295 pub fn new(config: CachingConfig) -> Self {
297 Self {
298 config,
299 request_patterns: VecDeque::new(),
300 hit_rate_history: VecDeque::new(),
301 similarity_cache: HashMap::new(),
302 stats: CachingStats::default(),
303 last_evaluation: None,
304 current_config: None,
305 learning_data: LearningData {
306 hit_rate_adjustment_factor: 1.0,
307 cost_adjustment_factor: 1.0,
308 ..Default::default()
309 },
310 }
311 }
312
313 fn analyze_request_patterns(&mut self, input: &DecisionInput) -> DecisionResult<()> {
315 for report in &input.analysis_reports {
317 for insight in &report.insights {
318 if let Some(pattern_data) = insight.metrics.get("request_pattern") {
320 let pattern = RequestPattern {
321 id: Uuid::new_v4().to_string(),
322 timestamp: insight.timestamp,
323 fingerprint: format!("fp_{}", pattern_data),
324 metadata: HashMap::new(),
325 tokens: *insight.metrics.get("tokens").unwrap_or(&0.0) as u32,
326 model: insight
327 .metrics
328 .get("model")
329 .and_then(|v| Some(format!("model_{}", v)))
330 .unwrap_or_else(|| "unknown".to_string()),
331 count: *insight.metrics.get("count").unwrap_or(&1.0) as u64,
332 };
333
334 self.request_patterns.push_back(pattern);
335 }
336 }
337 }
338
339 while self.request_patterns.len() > 10000 {
341 self.request_patterns.pop_front();
342 }
343
344 self.stats.patterns_analyzed += 1;
345 Ok(())
346 }
347
348 fn cluster_requests(&self) -> Vec<RequestCluster> {
350 let mut clusters = Vec::new();
351 let mut processed = std::collections::HashSet::new();
352
353 for pattern in self.request_patterns.iter() {
354 if processed.contains(&pattern.id) {
355 continue;
356 }
357
358 let similar = self.find_similar_patterns(pattern);
360 let total_count: u64 = similar.iter().map(|p| p.count).sum();
361
362 let cluster = RequestCluster {
363 id: Uuid::new_v4().to_string(),
364 representative: pattern.clone(),
365 members: similar.clone(),
366 total_count,
367 avg_similarity: self.calculate_avg_similarity(&similar),
368 frequency: self.calculate_frequency(&similar),
369 };
370
371 for member in &similar {
372 processed.insert(member.id.clone());
373 }
374
375 clusters.push(cluster);
376 }
377
378 clusters
379 }
380
381 fn find_similar_patterns(&self, pattern: &RequestPattern) -> Vec<RequestPattern> {
383 let threshold = self.config.min_similarity_threshold;
384 let mut similar = vec![pattern.clone()];
385
386 for other in self.request_patterns.iter() {
387 if other.id == pattern.id {
388 continue;
389 }
390
391 let similarity = self.calculate_similarity(pattern, other);
392 if similarity >= threshold {
393 similar.push(other.clone());
394 }
395 }
396
397 similar
398 }
399
400 fn calculate_similarity(&self, a: &RequestPattern, b: &RequestPattern) -> f64 {
402 if let Some(scores) = self.similarity_cache.get(&a.id) {
404 if let Some(score) = scores.iter().find(|s| s.pattern_id == b.id) {
405 return score.score;
406 }
407 }
408
409 let mut similarity = 0.0;
411
412 if a.fingerprint == b.fingerprint {
414 similarity += 0.6;
415 }
416
417 if a.model == b.model {
419 similarity += 0.2;
420 }
421
422 let token_diff = (a.tokens as f64 - b.tokens as f64).abs() / a.tokens.max(1) as f64;
424 if token_diff < 0.1 {
425 similarity += 0.2;
426 } else if token_diff < 0.3 {
427 similarity += 0.1;
428 }
429
430 similarity
431 }
432
433 fn calculate_avg_similarity(&self, patterns: &[RequestPattern]) -> f64 {
435 if patterns.len() <= 1 {
436 return 1.0;
437 }
438
439 let mut total_similarity = 0.0;
440 let mut count = 0;
441
442 for i in 0..patterns.len() {
443 for j in (i + 1)..patterns.len() {
444 total_similarity += self.calculate_similarity(&patterns[i], &patterns[j]);
445 count += 1;
446 }
447 }
448
449 if count > 0 {
450 total_similarity / count as f64
451 } else {
452 1.0
453 }
454 }
455
456 fn calculate_frequency(&self, patterns: &[RequestPattern]) -> f64 {
458 if patterns.is_empty() {
459 return 0.0;
460 }
461
462 let timestamps: Vec<_> = patterns.iter().map(|p| p.timestamp).collect();
464 let min_time = timestamps.iter().min().unwrap();
465 let max_time = timestamps.iter().max().unwrap();
466
467 let duration_mins = (*max_time - *min_time).num_minutes().max(1) as f64;
468 let total_requests: u64 = patterns.iter().map(|p| p.count).sum();
469
470 total_requests as f64 / duration_mins
471 }
472
473 fn identify_opportunities(&self, clusters: &[RequestCluster]) -> Vec<CacheOpportunity> {
475 let mut opportunities = Vec::new();
476
477 for cluster in clusters {
478 if cluster.frequency < self.config.min_request_frequency {
480 continue;
481 }
482
483 let expected_hit_rate = self.calculate_expected_hit_rate(cluster);
485
486 if expected_hit_rate < self.config.min_expected_hit_rate_pct {
488 continue;
489 }
490
491 let cost_savings = self.calculate_cost_savings(cluster, expected_hit_rate);
493 let latency_savings = self.calculate_latency_savings(cluster, expected_hit_rate);
494
495 let cache_size = self.recommend_cache_size(cluster);
497 let ttl = self.recommend_ttl(cluster);
498 let similarity_threshold = self.recommend_similarity_threshold(cluster);
499
500 let confidence = self.calculate_opportunity_confidence(cluster, expected_hit_rate);
502
503 opportunities.push(CacheOpportunity {
504 cluster: cluster.clone(),
505 expected_hit_rate_pct: expected_hit_rate,
506 expected_cost_savings_usd: cost_savings,
507 expected_latency_savings_ms: latency_savings,
508 recommended_cache_size: cache_size,
509 recommended_ttl: ttl,
510 recommended_similarity_threshold: similarity_threshold,
511 confidence,
512 });
513 }
514
515 opportunities.sort_by(|a, b| {
517 b.expected_cost_savings_usd
518 .partial_cmp(&a.expected_cost_savings_usd)
519 .unwrap_or(std::cmp::Ordering::Equal)
520 });
521
522 opportunities
523 }
524
525 fn calculate_expected_hit_rate(&self, cluster: &RequestCluster) -> f64 {
527 let mut base_rate = 0.0;
529
530 base_rate += cluster.avg_similarity * 50.0;
532
533 let freq_factor = (cluster.frequency / 100.0).min(1.0);
535 base_rate += freq_factor * 30.0;
536
537 let size_factor = (cluster.members.len() as f64 / 10.0).min(1.0);
539 base_rate += size_factor * 20.0;
540
541 base_rate *= self.learning_data.hit_rate_adjustment_factor;
543
544 base_rate.min(95.0)
546 }
547
548 fn calculate_cost_savings(&self, cluster: &RequestCluster, hit_rate_pct: f64) -> f64 {
550 let daily_requests = cluster.frequency * 60.0 * 24.0;
552
553 let cost_per_request = 0.001;
555
556 let savings = daily_requests * (hit_rate_pct / 100.0) * cost_per_request;
558
559 savings * self.learning_data.cost_adjustment_factor
561 }
562
563 fn calculate_latency_savings(&self, cluster: &RequestCluster, hit_rate_pct: f64) -> f64 {
565 let latency_saved_per_hit = 200.0;
567
568 (hit_rate_pct / 100.0) * latency_saved_per_hit
570 }
571
572 fn recommend_cache_size(&self, cluster: &RequestCluster) -> usize {
574 let base_size = cluster.members.len() * 10;
576
577 base_size.min(self.config.max_cache_size)
579 }
580
581 fn recommend_ttl(&self, cluster: &RequestCluster) -> Duration {
583 let base_ttl = if cluster.frequency > 50.0 {
585 Duration::from_secs(1800) } else if cluster.frequency > 20.0 {
588 Duration::from_secs(3600) } else {
591 Duration::from_secs(7200) };
594
595 if base_ttl > self.config.cache_ttl {
597 self.config.cache_ttl
598 } else {
599 base_ttl
600 }
601 }
602
603 fn recommend_similarity_threshold(&self, cluster: &RequestCluster) -> f64 {
605 let base_threshold: f64 = if cluster.avg_similarity > 0.9 {
607 0.80
609 } else if cluster.avg_similarity > 0.8 {
610 0.85
612 } else {
613 0.90
615 };
616
617 base_threshold.max(self.config.min_similarity_threshold)
619 }
620
621 fn calculate_opportunity_confidence(&self, cluster: &RequestCluster, hit_rate: f64) -> f64 {
623 let mut confidence = 0.5; let size_factor = (cluster.members.len() as f64 / 50.0).min(1.0);
627 confidence += size_factor * 0.2;
628
629 confidence += cluster.avg_similarity * 0.15;
631
632 let freq_factor = (cluster.frequency / 100.0).min(1.0);
634 confidence += freq_factor * 0.1;
635
636 confidence += self.stats.prediction_accuracy * 0.05;
638
639 confidence.min(1.0)
640 }
641
642 fn create_decision(
644 &self,
645 opportunity: &CacheOpportunity,
646 criteria: &DecisionCriteria,
647 ) -> DecisionResult<Decision> {
648 let decision_id = Uuid::new_v4().to_string();
649 let now = Utc::now();
650
651 let mut config_changes = Vec::new();
653
654 config_changes.push(ConfigChange {
656 config_type: ConfigType::Cache,
657 path: "cache.enabled".to_string(),
658 old_value: Some(serde_json::json!(false)),
659 new_value: serde_json::json!(true),
660 description: "Enable semantic caching".to_string(),
661 });
662
663 config_changes.push(ConfigChange {
665 config_type: ConfigType::Cache,
666 path: "cache.max_size".to_string(),
667 old_value: self
668 .current_config
669 .as_ref()
670 .map(|c| serde_json::json!(c.max_size)),
671 new_value: serde_json::json!(opportunity.recommended_cache_size),
672 description: format!("Set cache size to {}", opportunity.recommended_cache_size),
673 });
674
675 config_changes.push(ConfigChange {
677 config_type: ConfigType::Cache,
678 path: "cache.ttl_seconds".to_string(),
679 old_value: self
680 .current_config
681 .as_ref()
682 .map(|c| serde_json::json!(c.ttl.as_secs())),
683 new_value: serde_json::json!(opportunity.recommended_ttl.as_secs()),
684 description: format!(
685 "Set cache TTL to {} seconds",
686 opportunity.recommended_ttl.as_secs()
687 ),
688 });
689
690 config_changes.push(ConfigChange {
692 config_type: ConfigType::Cache,
693 path: "cache.similarity_threshold".to_string(),
694 old_value: self
695 .current_config
696 .as_ref()
697 .map(|c| serde_json::json!(c.similarity_threshold)),
698 new_value: serde_json::json!(opportunity.recommended_similarity_threshold),
699 description: format!(
700 "Set similarity threshold to {:.2}",
701 opportunity.recommended_similarity_threshold
702 ),
703 });
704
705 let mut safety_checks = Vec::new();
707
708 let storage_check = self.check_storage_limits(opportunity)?;
710 safety_checks.push(storage_check);
711
712 let latency_check = self.check_latency_impact(opportunity, criteria)?;
714 safety_checks.push(latency_check);
715
716 let cost_check = self.check_cost_impact(opportunity, criteria)?;
718 safety_checks.push(cost_check);
719
720 if !safety_checks.iter().all(|c| c.passed) {
722 return Err(DecisionError::ValidationFailed(
723 "Safety checks failed for caching decision".to_string(),
724 ));
725 }
726
727 let expected_impact = ExpectedImpact {
729 cost_change_usd: Some(-opportunity.expected_cost_savings_usd),
730 latency_change_ms: Some(-opportunity.expected_latency_savings_ms),
731 quality_change: None,
732 throughput_change_rps: None,
733 success_rate_change_pct: None,
734 time_to_impact: Duration::from_secs(300), confidence: opportunity.confidence,
736 };
737
738 let rollback_plan = RollbackPlan {
740 description: "Disable caching if hit rate is too low or costs increase".to_string(),
741 revert_changes: config_changes
742 .iter()
743 .map(|c| ConfigChange {
744 config_type: c.config_type,
745 path: c.path.clone(),
746 old_value: Some(c.new_value.clone()),
747 new_value: c.old_value.clone().unwrap_or(serde_json::json!(null)),
748 description: format!("Revert: {}", c.description),
749 })
750 .collect(),
751 trigger_conditions: vec![
752 RollbackCondition {
753 metric: "cache_hit_rate_pct".to_string(),
754 operator: ComparisonOperator::LessThan,
755 threshold: self.config.min_expected_hit_rate_pct * 0.5,
756 description: "Hit rate fell below 50% of expected".to_string(),
757 },
758 RollbackCondition {
759 metric: "cost_change_usd".to_string(),
760 operator: ComparisonOperator::GreaterThan,
761 threshold: 0.0,
762 description: "Cost increased instead of decreased".to_string(),
763 },
764 ],
765 max_duration: Duration::from_secs(3600), };
767
768 let justification = format!(
770 "Detected {} requests with {:.1}% similarity occurring at {:.1} req/min. \
771 Expected {:.1}% cache hit rate would save ${:.2}/day and {:.1}ms latency. \
772 Confidence: {:.1}%",
773 opportunity.cluster.total_count,
774 opportunity.cluster.avg_similarity * 100.0,
775 opportunity.cluster.frequency,
776 opportunity.expected_hit_rate_pct,
777 opportunity.expected_cost_savings_usd,
778 opportunity.expected_latency_savings_ms,
779 opportunity.confidence * 100.0
780 );
781
782 Ok(Decision {
783 id: decision_id,
784 timestamp: now,
785 strategy: "caching".to_string(),
786 decision_type: DecisionType::EnableCaching,
787 confidence: opportunity.confidence,
788 expected_impact,
789 config_changes,
790 justification,
791 related_insights: Vec::new(),
792 related_recommendations: Vec::new(),
793 priority: if opportunity.expected_cost_savings_usd > 10.0 {
794 100
795 } else if opportunity.expected_cost_savings_usd > 5.0 {
796 80
797 } else {
798 60
799 },
800 requires_approval: opportunity.confidence < 0.8,
801 safety_checks,
802 rollback_plan: Some(rollback_plan),
803 metadata: {
804 let mut meta = HashMap::new();
805 meta.insert(
806 "cluster_id".to_string(),
807 serde_json::json!(opportunity.cluster.id),
808 );
809 meta.insert(
810 "expected_hit_rate".to_string(),
811 serde_json::json!(opportunity.expected_hit_rate_pct),
812 );
813 meta.insert(
814 "request_frequency".to_string(),
815 serde_json::json!(opportunity.cluster.frequency),
816 );
817 meta
818 },
819 })
820 }
821
822 fn check_storage_limits(&self, opportunity: &CacheOpportunity) -> DecisionResult<SafetyCheck> {
824 let passed = opportunity.recommended_cache_size <= self.config.max_cache_size;
825
826 Ok(SafetyCheck {
827 name: "storage_limits".to_string(),
828 passed,
829 details: format!(
830 "Recommended cache size {} is {} configured limit {}",
831 opportunity.recommended_cache_size,
832 if passed { "within" } else { "exceeds" },
833 self.config.max_cache_size
834 ),
835 timestamp: Utc::now(),
836 })
837 }
838
839 fn check_latency_impact(
841 &self,
842 opportunity: &CacheOpportunity,
843 criteria: &DecisionCriteria,
844 ) -> DecisionResult<SafetyCheck> {
845 let cache_overhead_ms = 7.5;
847
848 let net_latency_change = cache_overhead_ms - opportunity.expected_latency_savings_ms;
850
851 let passed = if let Some(max_increase) = criteria.max_latency_increase_ms {
852 net_latency_change <= max_increase
853 } else {
854 net_latency_change <= 0.0 };
856
857 Ok(SafetyCheck {
858 name: "latency_impact".to_string(),
859 passed,
860 details: format!(
861 "Net latency change: {:.2}ms (overhead: {:.2}ms, savings: {:.2}ms)",
862 net_latency_change, cache_overhead_ms, opportunity.expected_latency_savings_ms
863 ),
864 timestamp: Utc::now(),
865 })
866 }
867
868 fn check_cost_impact(
870 &self,
871 opportunity: &CacheOpportunity,
872 criteria: &DecisionCriteria,
873 ) -> DecisionResult<SafetyCheck> {
874 let storage_cost_per_day =
876 (opportunity.recommended_cache_size as f64 / 1000.0) * 0.001 * 0.1 / 30.0;
877
878 let net_cost_change = -opportunity.expected_cost_savings_usd + storage_cost_per_day;
880
881 let passed = if let Some(max_increase) = criteria.max_cost_increase_usd {
882 net_cost_change <= max_increase
883 } else {
884 net_cost_change <= 0.0 };
886
887 Ok(SafetyCheck {
888 name: "cost_impact".to_string(),
889 passed,
890 details: format!(
891 "Net cost change: ${:.4}/day (savings: ${:.4}, storage: ${:.4})",
892 net_cost_change, opportunity.expected_cost_savings_usd, storage_cost_per_day
893 ),
894 timestamp: Utc::now(),
895 })
896 }
897
898 fn update_learning_model(&mut self, decision: &Decision, outcome: &DecisionOutcome) {
900 if outcome.success && outcome.actual_impact.is_some() {
901 let actual = outcome.actual_impact.as_ref().unwrap();
902
903 if let Some(predicted_hit_rate) = decision.metadata.get("expected_hit_rate") {
905 if let Some(predicted_rate) = predicted_hit_rate.as_f64() {
906 if let Some(actual_rate) = outcome
908 .metrics_after
909 .as_ref()
910 .and_then(|m| m.cache_hit_rate_pct)
911 {
912 let variance = (predicted_rate - actual_rate).abs() / predicted_rate.max(1.0);
913
914 self.learning_data.hit_rate_predictions.push_back(HitRatePrediction {
915 decision_id: decision.id.clone(),
916 predicted: predicted_rate,
917 actual: actual_rate,
918 variance,
919 timestamp: outcome.execution_time,
920 });
921
922 if !self.learning_data.hit_rate_predictions.is_empty() {
924 let avg_variance: f64 = self
925 .learning_data
926 .hit_rate_predictions
927 .iter()
928 .map(|p| p.variance)
929 .sum::<f64>()
930 / self.learning_data.hit_rate_predictions.len() as f64;
931
932 if avg_variance > 0.2 {
934 let avg_ratio: f64 = self
935 .learning_data
936 .hit_rate_predictions
937 .iter()
938 .map(|p| p.actual / p.predicted.max(0.01))
939 .sum::<f64>()
940 / self.learning_data.hit_rate_predictions.len() as f64;
941
942 self.learning_data.hit_rate_adjustment_factor = avg_ratio;
943 }
944 }
945 }
946 }
947 }
948
949 if let Some(expected_cost) = decision.expected_impact.cost_change_usd {
951 let actual_cost = actual.cost_change_usd;
952 let variance = (expected_cost - actual_cost).abs() / expected_cost.abs().max(0.01);
953
954 self.learning_data.cost_accuracy.push_back(CostAccuracy {
955 decision_id: decision.id.clone(),
956 predicted_savings_usd: -expected_cost,
957 actual_savings_usd: -actual_cost,
958 variance,
959 timestamp: outcome.execution_time,
960 });
961
962 if !self.learning_data.cost_accuracy.is_empty() {
964 let avg_ratio: f64 = self
965 .learning_data
966 .cost_accuracy
967 .iter()
968 .map(|c| c.actual_savings_usd / c.predicted_savings_usd.max(0.01))
969 .sum::<f64>()
970 / self.learning_data.cost_accuracy.len() as f64;
971
972 self.learning_data.cost_adjustment_factor = avg_ratio;
973 }
974 }
975
976 let hit_rate_accuracy = if !self.learning_data.hit_rate_predictions.is_empty() {
978 1.0 - self
979 .learning_data
980 .hit_rate_predictions
981 .iter()
982 .map(|p| p.variance)
983 .sum::<f64>()
984 / self.learning_data.hit_rate_predictions.len() as f64
985 } else {
986 0.5
987 };
988
989 let cost_accuracy = if !self.learning_data.cost_accuracy.is_empty() {
990 1.0 - self
991 .learning_data
992 .cost_accuracy
993 .iter()
994 .map(|c| c.variance)
995 .sum::<f64>()
996 / self.learning_data.cost_accuracy.len() as f64
997 } else {
998 0.5
999 };
1000
1001 self.stats.prediction_accuracy = (hit_rate_accuracy + cost_accuracy) / 2.0;
1002 }
1003
1004 while self.learning_data.hit_rate_predictions.len() > 100 {
1006 self.learning_data.hit_rate_predictions.pop_front();
1007 }
1008 while self.learning_data.cost_accuracy.len() > 100 {
1009 self.learning_data.cost_accuracy.pop_front();
1010 }
1011
1012 self.stats.learning_iterations += 1;
1013 }
1014}
1015
1016#[async_trait]
1017impl OptimizationStrategy for CachingStrategy {
1018 fn name(&self) -> &str {
1019 "caching"
1020 }
1021
1022 fn priority(&self) -> u32 {
1023 self.config.priority
1024 }
1025
1026 fn is_applicable(&self, input: &DecisionInput) -> bool {
1027 if !self.config.enabled {
1033 return false;
1034 }
1035
1036 if input.analysis_reports.is_empty() {
1037 return false;
1038 }
1039
1040 input.analysis_reports.iter().any(|report| {
1042 !report.insights.is_empty()
1043 || report.metrics.contains_key("request_count")
1044 || report.metrics.contains_key("cache_hit_rate")
1045 })
1046 }
1047
1048 async fn evaluate(
1049 &self,
1050 input: &DecisionInput,
1051 criteria: &DecisionCriteria,
1052 ) -> DecisionResult<Vec<Decision>> {
1053 let mut decisions = Vec::new();
1054
1055 let mut strategy = self.clone();
1057 strategy.analyze_request_patterns(input)?;
1058
1059 let clusters = strategy.cluster_requests();
1061
1062 if clusters.is_empty() {
1063 return Ok(decisions);
1064 }
1065
1066 let opportunities = strategy.identify_opportunities(&clusters);
1068
1069 for opportunity in opportunities.iter().take(3) {
1071 if opportunity.confidence < self.config.min_confidence {
1074 continue;
1075 }
1076
1077 match strategy.create_decision(opportunity, criteria) {
1079 Ok(decision) => decisions.push(decision),
1080 Err(e) => {
1081 tracing::warn!("Failed to create caching decision: {}", e);
1082 }
1083 }
1084 }
1085
1086 Ok(decisions)
1087 }
1088
1089 async fn validate(&self, decision: &Decision, criteria: &DecisionCriteria) -> DecisionResult<()> {
1090 if decision.decision_type != DecisionType::EnableCaching {
1092 return Err(DecisionError::ValidationFailed(
1093 "Invalid decision type for caching strategy".to_string(),
1094 ));
1095 }
1096
1097 if decision.confidence < criteria.min_confidence {
1099 return Err(DecisionError::ValidationFailed(format!(
1100 "Confidence {:.2} below minimum {:.2}",
1101 decision.confidence, criteria.min_confidence
1102 )));
1103 }
1104
1105 if let Some(cost_change) = decision.expected_impact.cost_change_usd {
1107 if let Some(max_increase) = criteria.max_cost_increase_usd {
1108 if cost_change > max_increase {
1109 return Err(DecisionError::ConstraintViolation(format!(
1110 "Cost increase ${:.2} exceeds maximum ${:.2}",
1111 cost_change, max_increase
1112 )));
1113 }
1114 }
1115 }
1116
1117 if !decision.safety_checks.iter().all(|c| c.passed) {
1119 return Err(DecisionError::ValidationFailed(
1120 "Not all safety checks passed".to_string(),
1121 ));
1122 }
1123
1124 Ok(())
1125 }
1126
1127 async fn learn(&mut self, decision: &Decision, outcome: &DecisionOutcome) -> DecisionResult<()> {
1128 if decision.decision_type != DecisionType::EnableCaching {
1130 return Ok(());
1131 }
1132
1133 self.update_learning_model(decision, outcome);
1135
1136 if outcome.success {
1138 self.stats.caching_enabled += 1;
1139
1140 if let Some(actual) = &outcome.actual_impact {
1141 self.stats.total_cost_savings_usd += -actual.cost_change_usd;
1142 self.stats.total_latency_savings_ms += -actual.latency_change_ms;
1143 }
1144 }
1145
1146 self.stats.total_decisions += 1;
1147
1148 Ok(())
1149 }
1150
1151 fn get_stats(&self) -> serde_json::Value {
1152 serde_json::json!({
1153 "total_decisions": self.stats.total_decisions,
1154 "caching_enabled": self.stats.caching_enabled,
1155 "caching_disabled": self.stats.caching_disabled,
1156 "config_updates": self.stats.config_updates,
1157 "avg_predicted_hit_rate": self.stats.avg_predicted_hit_rate,
1158 "avg_actual_hit_rate": self.stats.avg_actual_hit_rate,
1159 "total_cost_savings_usd": self.stats.total_cost_savings_usd,
1160 "total_latency_savings_ms": self.stats.total_latency_savings_ms,
1161 "prediction_accuracy": self.stats.prediction_accuracy,
1162 "patterns_analyzed": self.stats.patterns_analyzed,
1163 "learning_iterations": self.stats.learning_iterations,
1164 "hit_rate_adjustment_factor": self.learning_data.hit_rate_adjustment_factor,
1165 "cost_adjustment_factor": self.learning_data.cost_adjustment_factor,
1166 "request_patterns_count": self.request_patterns.len(),
1167 "hit_rate_history_count": self.hit_rate_history.len(),
1168 })
1169 }
1170}
1171
1172impl Clone for CachingStrategy {
1174 fn clone(&self) -> Self {
1175 Self {
1176 config: self.config.clone(),
1177 request_patterns: self.request_patterns.clone(),
1178 hit_rate_history: self.hit_rate_history.clone(),
1179 similarity_cache: self.similarity_cache.clone(),
1180 stats: self.stats.clone(),
1181 last_evaluation: self.last_evaluation,
1182 current_config: self.current_config.clone(),
1183 learning_data: self.learning_data.clone(),
1184 }
1185 }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191 use crate::analyzer::{AnalysisReport, Insight, ReportSummary};
1192 use crate::decision::types::SystemMetrics;
1193
1194 fn create_test_config() -> CachingConfig {
1195 CachingConfig {
1196 enabled: true,
1197 priority: 90,
1198 min_confidence: 0.6,
1199 min_request_frequency: 5.0,
1200 min_similarity_threshold: 0.85,
1201 cache_ttl: Duration::from_secs(3600),
1202 max_cache_size: 1000,
1203 min_expected_hit_rate_pct: 20.0,
1204 }
1205 }
1206
1207 fn create_test_input() -> DecisionInput {
1208 let now = Utc::now();
1209
1210 DecisionInput {
1211 timestamp: now,
1212 analysis_reports: vec![AnalysisReport {
1213 analyzer: "test".to_string(),
1214 timestamp: now,
1215 period_start: now - chrono::Duration::hours(1),
1216 period_end: now,
1217 summary: ReportSummary {
1218 events_processed: 100,
1219 events_per_second: 1.0,
1220 insights_count: 1,
1221 recommendations_count: 0,
1222 alerts_count: 0,
1223 analysis_duration_ms: 100,
1224 highlights: vec![],
1225 },
1226 insights: vec![Insight {
1227 id: "test".to_string(),
1228 analyzer: "test".to_string(),
1229 timestamp: now,
1230 severity: crate::analyzer::Severity::Info,
1231 confidence: crate::analyzer::Confidence::new(0.8),
1232 title: "Test".to_string(),
1233 description: "Test".to_string(),
1234 category: crate::analyzer::InsightCategory::Pattern,
1235 evidence: vec![],
1236 metrics: {
1237 let mut m = HashMap::new();
1238 m.insert("request_pattern".to_string(), 1.0);
1239 m.insert("count".to_string(), 100.0);
1240 m.insert("tokens".to_string(), 500.0);
1241 m
1242 },
1243 tags: vec![],
1244 }],
1245 recommendations: vec![],
1246 alerts: vec![],
1247 metrics: {
1248 let mut m = HashMap::new();
1249 m.insert("request_count".to_string(), 100.0);
1250 m
1251 },
1252 }],
1253 insights: vec![],
1254 recommendations: vec![],
1255 current_metrics: SystemMetrics {
1256 avg_latency_ms: 100.0,
1257 p95_latency_ms: 200.0,
1258 p99_latency_ms: 300.0,
1259 success_rate_pct: 99.0,
1260 error_rate_pct: 1.0,
1261 throughput_rps: 10.0,
1262 total_cost_usd: 100.0,
1263 daily_cost_usd: 10.0,
1264 avg_rating: Some(4.0),
1265 cache_hit_rate_pct: None,
1266 request_count: 1000,
1267 timestamp: now,
1268 },
1269 context: HashMap::new(),
1270 }
1271 }
1272
1273 #[test]
1274 fn test_strategy_creation() {
1275 let config = create_test_config();
1276 let strategy = CachingStrategy::new(config.clone());
1277
1278 assert_eq!(strategy.name(), "caching");
1279 assert_eq!(strategy.priority(), config.priority);
1280 assert_eq!(strategy.request_patterns.len(), 0);
1281 }
1282
1283 #[test]
1284 fn test_is_applicable() {
1285 let config = create_test_config();
1286 let strategy = CachingStrategy::new(config);
1287 let input = create_test_input();
1288
1289 assert!(strategy.is_applicable(&input));
1290 }
1291
1292 #[test]
1293 fn test_is_not_applicable_when_disabled() {
1294 let mut config = create_test_config();
1295 config.enabled = false;
1296 let strategy = CachingStrategy::new(config);
1297 let input = create_test_input();
1298
1299 assert!(!strategy.is_applicable(&input));
1300 }
1301
1302 #[test]
1303 fn test_is_not_applicable_with_empty_reports() {
1304 let config = create_test_config();
1305 let strategy = CachingStrategy::new(config);
1306 let mut input = create_test_input();
1307 input.analysis_reports.clear();
1308
1309 assert!(!strategy.is_applicable(&input));
1310 }
1311
1312 #[test]
1313 fn test_pattern_analysis() {
1314 let config = create_test_config();
1315 let mut strategy = CachingStrategy::new(config);
1316 let input = create_test_input();
1317
1318 strategy.analyze_request_patterns(&input).unwrap();
1319
1320 assert!(!strategy.request_patterns.is_empty());
1321 }
1322
1323 #[test]
1324 fn test_similarity_calculation_exact_match() {
1325 let config = create_test_config();
1326 let strategy = CachingStrategy::new(config);
1327
1328 let pattern1 = RequestPattern {
1329 id: "1".to_string(),
1330 timestamp: Utc::now(),
1331 fingerprint: "fp1".to_string(),
1332 metadata: HashMap::new(),
1333 tokens: 100,
1334 model: "model1".to_string(),
1335 count: 1,
1336 };
1337
1338 let pattern2 = pattern1.clone();
1339
1340 let similarity = strategy.calculate_similarity(&pattern1, &pattern2);
1341 assert_eq!(similarity, 1.0);
1342 }
1343
1344 #[test]
1345 fn test_similarity_calculation_different() {
1346 let config = create_test_config();
1347 let strategy = CachingStrategy::new(config);
1348
1349 let pattern1 = RequestPattern {
1350 id: "1".to_string(),
1351 timestamp: Utc::now(),
1352 fingerprint: "fp1".to_string(),
1353 metadata: HashMap::new(),
1354 tokens: 100,
1355 model: "model1".to_string(),
1356 count: 1,
1357 };
1358
1359 let pattern2 = RequestPattern {
1360 id: "2".to_string(),
1361 timestamp: Utc::now(),
1362 fingerprint: "fp2".to_string(),
1363 metadata: HashMap::new(),
1364 tokens: 500,
1365 model: "model2".to_string(),
1366 count: 1,
1367 };
1368
1369 let similarity = strategy.calculate_similarity(&pattern1, &pattern2);
1370 assert!(similarity < 0.5);
1371 }
1372
1373 #[test]
1374 fn test_hit_rate_calculation() {
1375 let config = create_test_config();
1376 let strategy = CachingStrategy::new(config);
1377
1378 let cluster = RequestCluster {
1379 id: "c1".to_string(),
1380 representative: RequestPattern {
1381 id: "1".to_string(),
1382 timestamp: Utc::now(),
1383 fingerprint: "fp1".to_string(),
1384 metadata: HashMap::new(),
1385 tokens: 100,
1386 model: "model1".to_string(),
1387 count: 10,
1388 },
1389 members: vec![],
1390 total_count: 100,
1391 avg_similarity: 0.9,
1392 frequency: 50.0,
1393 };
1394
1395 let hit_rate = strategy.calculate_expected_hit_rate(&cluster);
1396 assert!(hit_rate > 0.0);
1397 assert!(hit_rate <= 95.0);
1398 }
1399
1400 #[test]
1401 fn test_cost_savings_calculation() {
1402 let config = create_test_config();
1403 let strategy = CachingStrategy::new(config);
1404
1405 let cluster = RequestCluster {
1406 id: "c1".to_string(),
1407 representative: RequestPattern {
1408 id: "1".to_string(),
1409 timestamp: Utc::now(),
1410 fingerprint: "fp1".to_string(),
1411 metadata: HashMap::new(),
1412 tokens: 100,
1413 model: "model1".to_string(),
1414 count: 10,
1415 },
1416 members: vec![],
1417 total_count: 100,
1418 avg_similarity: 0.9,
1419 frequency: 50.0,
1420 };
1421
1422 let savings = strategy.calculate_cost_savings(&cluster, 50.0);
1423 assert!(savings > 0.0);
1424 }
1425
1426 #[test]
1427 fn test_cache_size_recommendation() {
1428 let config = create_test_config();
1429 let strategy = CachingStrategy::new(config.clone());
1430
1431 let cluster = RequestCluster {
1432 id: "c1".to_string(),
1433 representative: RequestPattern {
1434 id: "1".to_string(),
1435 timestamp: Utc::now(),
1436 fingerprint: "fp1".to_string(),
1437 metadata: HashMap::new(),
1438 tokens: 100,
1439 model: "model1".to_string(),
1440 count: 10,
1441 },
1442 members: vec![],
1443 total_count: 100,
1444 avg_similarity: 0.9,
1445 frequency: 50.0,
1446 };
1447
1448 let size = strategy.recommend_cache_size(&cluster);
1449 assert!(size > 0);
1450 assert!(size <= config.max_cache_size);
1451 }
1452
1453 #[test]
1454 fn test_ttl_recommendation() {
1455 let config = create_test_config();
1456 let strategy = CachingStrategy::new(config.clone());
1457
1458 let cluster = RequestCluster {
1459 id: "c1".to_string(),
1460 representative: RequestPattern {
1461 id: "1".to_string(),
1462 timestamp: Utc::now(),
1463 fingerprint: "fp1".to_string(),
1464 metadata: HashMap::new(),
1465 tokens: 100,
1466 model: "model1".to_string(),
1467 count: 10,
1468 },
1469 members: vec![],
1470 total_count: 100,
1471 avg_similarity: 0.9,
1472 frequency: 50.0,
1473 };
1474
1475 let ttl = strategy.recommend_ttl(&cluster);
1476 assert!(ttl > Duration::from_secs(0));
1477 assert!(ttl <= config.cache_ttl);
1478 }
1479
1480 #[tokio::test]
1481 async fn test_evaluate_generates_decisions() {
1482 let config = create_test_config();
1483 let strategy = CachingStrategy::new(config);
1484 let input = create_test_input();
1485 let criteria = DecisionCriteria::default();
1486
1487 let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1488
1489 assert!(decisions.len() <= 3);
1491 }
1492
1493 #[tokio::test]
1494 async fn test_validate_correct_decision() {
1495 let config = create_test_config();
1496 let strategy = CachingStrategy::new(config);
1497
1498 let decision = Decision {
1499 id: "test".to_string(),
1500 timestamp: Utc::now(),
1501 strategy: "caching".to_string(),
1502 decision_type: DecisionType::EnableCaching,
1503 confidence: 0.8,
1504 expected_impact: ExpectedImpact {
1505 cost_change_usd: Some(-5.0),
1506 latency_change_ms: Some(-100.0),
1507 quality_change: None,
1508 throughput_change_rps: None,
1509 success_rate_change_pct: None,
1510 time_to_impact: Duration::from_secs(300),
1511 confidence: 0.8,
1512 },
1513 config_changes: vec![],
1514 justification: "Test".to_string(),
1515 related_insights: vec![],
1516 related_recommendations: vec![],
1517 priority: 90,
1518 requires_approval: false,
1519 safety_checks: vec![SafetyCheck {
1520 name: "test".to_string(),
1521 passed: true,
1522 details: "test".to_string(),
1523 timestamp: Utc::now(),
1524 }],
1525 rollback_plan: None,
1526 metadata: HashMap::new(),
1527 };
1528
1529 let criteria = DecisionCriteria::default();
1530 assert!(strategy.validate(&decision, &criteria).await.is_ok());
1531 }
1532
1533 #[tokio::test]
1534 async fn test_validate_wrong_decision_type() {
1535 let config = create_test_config();
1536 let strategy = CachingStrategy::new(config);
1537
1538 let decision = Decision {
1539 id: "test".to_string(),
1540 timestamp: Utc::now(),
1541 strategy: "caching".to_string(),
1542 decision_type: DecisionType::ModelSwitch,
1543 confidence: 0.8,
1544 expected_impact: ExpectedImpact {
1545 cost_change_usd: Some(-5.0),
1546 latency_change_ms: Some(-100.0),
1547 quality_change: None,
1548 throughput_change_rps: None,
1549 success_rate_change_pct: None,
1550 time_to_impact: Duration::from_secs(300),
1551 confidence: 0.8,
1552 },
1553 config_changes: vec![],
1554 justification: "Test".to_string(),
1555 related_insights: vec![],
1556 related_recommendations: vec![],
1557 priority: 90,
1558 requires_approval: false,
1559 safety_checks: vec![],
1560 rollback_plan: None,
1561 metadata: HashMap::new(),
1562 };
1563
1564 let criteria = DecisionCriteria::default();
1565 assert!(strategy.validate(&decision, &criteria).await.is_err());
1566 }
1567
1568 #[tokio::test]
1569 async fn test_validate_low_confidence() {
1570 let config = create_test_config();
1571 let strategy = CachingStrategy::new(config);
1572
1573 let decision = Decision {
1574 id: "test".to_string(),
1575 timestamp: Utc::now(),
1576 strategy: "caching".to_string(),
1577 decision_type: DecisionType::EnableCaching,
1578 confidence: 0.5,
1579 expected_impact: ExpectedImpact {
1580 cost_change_usd: Some(-5.0),
1581 latency_change_ms: Some(-100.0),
1582 quality_change: None,
1583 throughput_change_rps: None,
1584 success_rate_change_pct: None,
1585 time_to_impact: Duration::from_secs(300),
1586 confidence: 0.5,
1587 },
1588 config_changes: vec![],
1589 justification: "Test".to_string(),
1590 related_insights: vec![],
1591 related_recommendations: vec![],
1592 priority: 90,
1593 requires_approval: false,
1594 safety_checks: vec![],
1595 rollback_plan: None,
1596 metadata: HashMap::new(),
1597 };
1598
1599 let criteria = DecisionCriteria::default();
1600 assert!(strategy.validate(&decision, &criteria).await.is_err());
1601 }
1602
1603 #[tokio::test]
1604 async fn test_learn_updates_statistics() {
1605 let config = create_test_config();
1606 let mut strategy = CachingStrategy::new(config);
1607
1608 let decision = Decision {
1609 id: "test".to_string(),
1610 timestamp: Utc::now(),
1611 strategy: "caching".to_string(),
1612 decision_type: DecisionType::EnableCaching,
1613 confidence: 0.8,
1614 expected_impact: ExpectedImpact {
1615 cost_change_usd: Some(-5.0),
1616 latency_change_ms: Some(-100.0),
1617 quality_change: None,
1618 throughput_change_rps: None,
1619 success_rate_change_pct: None,
1620 time_to_impact: Duration::from_secs(300),
1621 confidence: 0.8,
1622 },
1623 config_changes: vec![],
1624 justification: "Test".to_string(),
1625 related_insights: vec![],
1626 related_recommendations: vec![],
1627 priority: 90,
1628 requires_approval: false,
1629 safety_checks: vec![],
1630 rollback_plan: None,
1631 metadata: {
1632 let mut m = HashMap::new();
1633 m.insert("expected_hit_rate".to_string(), serde_json::json!(50.0));
1634 m
1635 },
1636 };
1637
1638 let outcome = DecisionOutcome {
1639 decision_id: "test".to_string(),
1640 execution_time: Utc::now(),
1641 success: true,
1642 error: None,
1643 actual_impact: Some(crate::decision::types::ActualImpact {
1644 cost_change_usd: -4.5,
1645 latency_change_ms: -95.0,
1646 quality_change: 0.0,
1647 throughput_change_rps: 0.0,
1648 success_rate_change_pct: 0.0,
1649 time_to_impact: Duration::from_secs(300),
1650 variance_from_expected: 0.1,
1651 }),
1652 rolled_back: false,
1653 rollback_reason: None,
1654 metrics_before: create_test_input().current_metrics,
1655 metrics_after: Some(SystemMetrics {
1656 cache_hit_rate_pct: Some(48.0),
1657 ..create_test_input().current_metrics
1658 }),
1659 duration: Duration::from_secs(3600),
1660 };
1661
1662 let initial_decisions = strategy.stats.total_decisions;
1663 strategy.learn(&decision, &outcome).await.unwrap();
1664
1665 assert_eq!(strategy.stats.total_decisions, initial_decisions + 1);
1666 assert_eq!(strategy.stats.caching_enabled, 1);
1667 }
1668
1669 #[test]
1670 fn test_get_stats() {
1671 let config = create_test_config();
1672 let strategy = CachingStrategy::new(config);
1673
1674 let stats = strategy.get_stats();
1675 assert!(stats.is_object());
1676 assert!(stats.get("total_decisions").is_some());
1677 assert!(stats.get("prediction_accuracy").is_some());
1678 }
1679}