Skip to main content

llm_optimizer_processor/decision/
caching.rs

1//! Caching Strategy for the Decision Engine
2//!
3//! This strategy analyzes request patterns to determine when to enable or update
4//! caching configurations to reduce costs and latency. It supports semantic similarity
5//! caching and provides comprehensive cost-benefit analysis.
6
7use 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
22/// Caching optimization strategy
23///
24/// Analyzes request patterns to identify caching opportunities and generates
25/// decisions to enable or configure caching with semantic similarity support.
26pub struct CachingStrategy {
27    /// Strategy configuration
28    config: CachingConfig,
29
30    /// Request pattern history for analysis
31    request_patterns: VecDeque<RequestPattern>,
32
33    /// Cache hit rate tracking
34    hit_rate_history: VecDeque<HitRateRecord>,
35
36    /// Similarity matrix for request clustering
37    similarity_cache: HashMap<String, Vec<SimilarityScore>>,
38
39    /// Strategy statistics
40    stats: CachingStats,
41
42    /// Last evaluation timestamp
43    last_evaluation: Option<DateTime<Utc>>,
44
45    /// Current cache configuration state
46    current_config: Option<CacheConfiguration>,
47
48    /// Learning data from outcomes
49    learning_data: LearningData,
50}
51
52/// A request pattern captured for analysis
53#[derive(Debug, Clone, Serialize, Deserialize)]
54struct RequestPattern {
55    /// Pattern identifier
56    id: String,
57
58    /// Timestamp
59    timestamp: DateTime<Utc>,
60
61    /// Request fingerprint (hash of normalized request)
62    fingerprint: String,
63
64    /// Request metadata
65    metadata: HashMap<String, String>,
66
67    /// Prompt tokens
68    tokens: u32,
69
70    /// Model used
71    model: String,
72
73    /// Request count with this pattern
74    count: u64,
75}
76
77/// Cache hit rate record
78#[derive(Debug, Clone, Serialize, Deserialize)]
79struct HitRateRecord {
80    /// Timestamp
81    timestamp: DateTime<Utc>,
82
83    /// Hit rate (0-100)
84    hit_rate_pct: f64,
85
86    /// Total requests
87    total_requests: u64,
88
89    /// Cache hits
90    cache_hits: u64,
91
92    /// Cache size
93    cache_size: usize,
94
95    /// Average latency saved (ms)
96    avg_latency_saved_ms: f64,
97
98    /// Cost saved (USD)
99    cost_saved_usd: f64,
100}
101
102/// Similarity score between two requests
103#[derive(Debug, Clone, Serialize, Deserialize)]
104struct SimilarityScore {
105    /// Request pattern ID
106    pattern_id: String,
107
108    /// Similarity score (0-1)
109    score: f64,
110
111    /// Timestamp
112    timestamp: DateTime<Utc>,
113}
114
115/// Current cache configuration
116#[derive(Debug, Clone, Serialize, Deserialize)]
117struct CacheConfiguration {
118    /// Whether caching is enabled
119    enabled: bool,
120
121    /// Cache TTL
122    ttl: Duration,
123
124    /// Maximum cache size
125    max_size: usize,
126
127    /// Similarity threshold
128    similarity_threshold: f64,
129
130    /// Cache strategy type
131    strategy_type: CacheStrategyType,
132
133    /// Last updated
134    last_updated: DateTime<Utc>,
135}
136
137/// Type of cache strategy
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139enum CacheStrategyType {
140    /// Exact match only
141    Exact,
142
143    /// Semantic similarity based
144    Semantic,
145
146    /// Hybrid (exact + semantic)
147    Hybrid,
148}
149
150/// Statistics for the caching strategy
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
152struct CachingStats {
153    /// Total decisions made
154    total_decisions: u64,
155
156    /// Caching enabled decisions
157    caching_enabled: u64,
158
159    /// Caching disabled decisions
160    caching_disabled: u64,
161
162    /// Configuration updates
163    config_updates: u64,
164
165    /// Average predicted hit rate
166    avg_predicted_hit_rate: f64,
167
168    /// Average actual hit rate
169    avg_actual_hit_rate: f64,
170
171    /// Total cost savings (USD)
172    total_cost_savings_usd: f64,
173
174    /// Total latency savings (ms)
175    total_latency_savings_ms: f64,
176
177    /// Prediction accuracy
178    prediction_accuracy: f64,
179
180    /// Patterns analyzed
181    patterns_analyzed: u64,
182
183    /// Learning iterations
184    learning_iterations: u64,
185}
186
187/// Learning data for strategy improvement
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189struct LearningData {
190    /// Predicted vs actual hit rates
191    hit_rate_predictions: VecDeque<HitRatePrediction>,
192
193    /// Cost benefit accuracy
194    cost_accuracy: VecDeque<CostAccuracy>,
195
196    /// Similarity threshold effectiveness
197    threshold_effectiveness: HashMap<String, f64>,
198
199    /// Model for hit rate adjustment
200    hit_rate_adjustment_factor: f64,
201
202    /// Model for cost estimation adjustment
203    cost_adjustment_factor: f64,
204}
205
206/// Hit rate prediction record
207#[derive(Debug, Clone, Serialize, Deserialize)]
208struct HitRatePrediction {
209    /// Decision ID
210    decision_id: String,
211
212    /// Predicted hit rate
213    predicted: f64,
214
215    /// Actual hit rate
216    actual: f64,
217
218    /// Variance
219    variance: f64,
220
221    /// Timestamp
222    timestamp: DateTime<Utc>,
223}
224
225/// Cost accuracy record
226#[derive(Debug, Clone, Serialize, Deserialize)]
227struct CostAccuracy {
228    /// Decision ID
229    decision_id: String,
230
231    /// Predicted savings
232    predicted_savings_usd: f64,
233
234    /// Actual savings
235    actual_savings_usd: f64,
236
237    /// Variance
238    variance: f64,
239
240    /// Timestamp
241    timestamp: DateTime<Utc>,
242}
243
244/// Request cluster for pattern analysis
245#[derive(Debug, Clone)]
246struct RequestCluster {
247    /// Cluster ID
248    id: String,
249
250    /// Representative pattern
251    representative: RequestPattern,
252
253    /// Member patterns
254    members: Vec<RequestPattern>,
255
256    /// Total request count
257    total_count: u64,
258
259    /// Average similarity
260    avg_similarity: f64,
261
262    /// Frequency (requests per minute)
263    frequency: f64,
264}
265
266/// Cache opportunity analysis result
267#[derive(Debug, Clone)]
268struct CacheOpportunity {
269    /// Cluster being analyzed
270    cluster: RequestCluster,
271
272    /// Expected hit rate
273    expected_hit_rate_pct: f64,
274
275    /// Expected cost savings (USD/day)
276    expected_cost_savings_usd: f64,
277
278    /// Expected latency savings (ms)
279    expected_latency_savings_ms: f64,
280
281    /// Recommended cache size
282    recommended_cache_size: usize,
283
284    /// Recommended TTL
285    recommended_ttl: Duration,
286
287    /// Recommended similarity threshold
288    recommended_similarity_threshold: f64,
289
290    /// Confidence in recommendation
291    confidence: f64,
292}
293
294impl CachingStrategy {
295    /// Create a new caching strategy
296    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    /// Analyze request patterns from the input
314    fn analyze_request_patterns(&mut self, input: &DecisionInput) -> DecisionResult<()> {
315        // Extract request patterns from analysis reports
316        for report in &input.analysis_reports {
317            for insight in &report.insights {
318                // Look for request patterns in insights
319                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        // Limit history size
340        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    /// Cluster similar requests for pattern analysis
349    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            // Find similar patterns
359            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    /// Find patterns similar to the given pattern
382    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    /// Calculate similarity between two patterns
401    fn calculate_similarity(&self, a: &RequestPattern, b: &RequestPattern) -> f64 {
402        // Check cache first
403        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        // Simple similarity based on fingerprint, model, and token count
410        let mut similarity = 0.0;
411
412        // Fingerprint similarity (exact match for now, could use embeddings)
413        if a.fingerprint == b.fingerprint {
414            similarity += 0.6;
415        }
416
417        // Model similarity
418        if a.model == b.model {
419            similarity += 0.2;
420        }
421
422        // Token count similarity (within 10%)
423        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    /// Calculate average similarity within a cluster
434    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    /// Calculate request frequency for a cluster
457    fn calculate_frequency(&self, patterns: &[RequestPattern]) -> f64 {
458        if patterns.is_empty() {
459            return 0.0;
460        }
461
462        // Calculate time span
463        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    /// Identify caching opportunities from clusters
474    fn identify_opportunities(&self, clusters: &[RequestCluster]) -> Vec<CacheOpportunity> {
475        let mut opportunities = Vec::new();
476
477        for cluster in clusters {
478            // Only consider clusters with sufficient frequency
479            if cluster.frequency < self.config.min_request_frequency {
480                continue;
481            }
482
483            // Calculate expected hit rate based on cluster characteristics
484            let expected_hit_rate = self.calculate_expected_hit_rate(cluster);
485
486            // Only recommend caching if expected hit rate meets threshold
487            if expected_hit_rate < self.config.min_expected_hit_rate_pct {
488                continue;
489            }
490
491            // Calculate cost and latency savings
492            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            // Determine optimal cache configuration
496            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            // Calculate confidence based on data quality
501            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        // Sort by expected cost savings
516        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    /// Calculate expected cache hit rate for a cluster
526    fn calculate_expected_hit_rate(&self, cluster: &RequestCluster) -> f64 {
527        // Base hit rate on cluster characteristics
528        let mut base_rate = 0.0;
529
530        // Higher similarity -> higher hit rate
531        base_rate += cluster.avg_similarity * 50.0;
532
533        // Higher frequency -> higher hit rate potential
534        let freq_factor = (cluster.frequency / 100.0).min(1.0);
535        base_rate += freq_factor * 30.0;
536
537        // Cluster size factor
538        let size_factor = (cluster.members.len() as f64 / 10.0).min(1.0);
539        base_rate += size_factor * 20.0;
540
541        // Apply learning adjustment
542        base_rate *= self.learning_data.hit_rate_adjustment_factor;
543
544        // Cap at 95% (never assume perfect hit rate)
545        base_rate.min(95.0)
546    }
547
548    /// Calculate expected cost savings
549    fn calculate_cost_savings(&self, cluster: &RequestCluster, hit_rate_pct: f64) -> f64 {
550        // Estimate daily requests
551        let daily_requests = cluster.frequency * 60.0 * 24.0;
552
553        // Estimate cost per request (assuming average of $0.001 per request)
554        let cost_per_request = 0.001;
555
556        // Calculate savings
557        let savings = daily_requests * (hit_rate_pct / 100.0) * cost_per_request;
558
559        // Apply learning adjustment
560        savings * self.learning_data.cost_adjustment_factor
561    }
562
563    /// Calculate expected latency savings
564    fn calculate_latency_savings(&self, cluster: &RequestCluster, hit_rate_pct: f64) -> f64 {
565        // Assume cache hit saves ~200ms on average
566        let latency_saved_per_hit = 200.0;
567
568        // Average latency saved per request
569        (hit_rate_pct / 100.0) * latency_saved_per_hit
570    }
571
572    /// Recommend optimal cache size
573    fn recommend_cache_size(&self, cluster: &RequestCluster) -> usize {
574        // Base size on cluster characteristics
575        let base_size = cluster.members.len() * 10;
576
577        // Cap at configured maximum
578        base_size.min(self.config.max_cache_size)
579    }
580
581    /// Recommend cache TTL
582    fn recommend_ttl(&self, cluster: &RequestCluster) -> Duration {
583        // Base TTL on request frequency and pattern stability
584        let base_ttl = if cluster.frequency > 50.0 {
585            // High frequency: shorter TTL
586            Duration::from_secs(1800) // 30 minutes
587        } else if cluster.frequency > 20.0 {
588            // Medium frequency: standard TTL
589            Duration::from_secs(3600) // 1 hour
590        } else {
591            // Lower frequency: longer TTL
592            Duration::from_secs(7200) // 2 hours
593        };
594
595        // Don't exceed configured TTL
596        if base_ttl > self.config.cache_ttl {
597            self.config.cache_ttl
598        } else {
599            base_ttl
600        }
601    }
602
603    /// Recommend similarity threshold
604    fn recommend_similarity_threshold(&self, cluster: &RequestCluster) -> f64 {
605        // Base threshold on cluster similarity
606        let base_threshold: f64 = if cluster.avg_similarity > 0.9 {
607            // Very similar: can use lower threshold
608            0.80
609        } else if cluster.avg_similarity > 0.8 {
610            // Similar: standard threshold
611            0.85
612        } else {
613            // Less similar: higher threshold
614            0.90
615        };
616
617        // Don't go below configured minimum
618        base_threshold.max(self.config.min_similarity_threshold)
619    }
620
621    /// Calculate confidence in the opportunity
622    fn calculate_opportunity_confidence(&self, cluster: &RequestCluster, hit_rate: f64) -> f64 {
623        let mut confidence = 0.5; // Base confidence
624
625        // More cluster members -> higher confidence
626        let size_factor = (cluster.members.len() as f64 / 50.0).min(1.0);
627        confidence += size_factor * 0.2;
628
629        // Higher similarity -> higher confidence
630        confidence += cluster.avg_similarity * 0.15;
631
632        // Higher frequency -> higher confidence
633        let freq_factor = (cluster.frequency / 100.0).min(1.0);
634        confidence += freq_factor * 0.1;
635
636        // Historical accuracy improves confidence
637        confidence += self.stats.prediction_accuracy * 0.05;
638
639        confidence.min(1.0)
640    }
641
642    /// Create a decision from a caching opportunity
643    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        // Build configuration changes
652        let mut config_changes = Vec::new();
653
654        // Enable caching
655        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        // Set cache size
664        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        // Set TTL
676        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        // Set similarity threshold
691        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        // Safety checks
706        let mut safety_checks = Vec::new();
707
708        // Check storage limits
709        let storage_check = self.check_storage_limits(opportunity)?;
710        safety_checks.push(storage_check);
711
712        // Check latency impact
713        let latency_check = self.check_latency_impact(opportunity, criteria)?;
714        safety_checks.push(latency_check);
715
716        // Check cost impact
717        let cost_check = self.check_cost_impact(opportunity, criteria)?;
718        safety_checks.push(cost_check);
719
720        // Verify all safety checks passed
721        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        // Create expected impact
728        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), // 5 minutes to see impact
735            confidence: opportunity.confidence,
736        };
737
738        // Create rollback plan
739        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), // 1 hour
766        };
767
768        // Build justification
769        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    /// Check storage limits safety
823    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    /// Check latency impact safety
840    fn check_latency_impact(
841        &self,
842        opportunity: &CacheOpportunity,
843        criteria: &DecisionCriteria,
844    ) -> DecisionResult<SafetyCheck> {
845        // Cache lookup adds ~5-10ms overhead
846        let cache_overhead_ms = 7.5;
847
848        // Net latency change (overhead - savings)
849        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 // Should improve latency
855        };
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    /// Check cost impact safety
869    fn check_cost_impact(
870        &self,
871        opportunity: &CacheOpportunity,
872        criteria: &DecisionCriteria,
873    ) -> DecisionResult<SafetyCheck> {
874        // Cache has storage cost (~$0.10/GB/month, estimate 1MB per 1000 entries)
875        let storage_cost_per_day =
876            (opportunity.recommended_cache_size as f64 / 1000.0) * 0.001 * 0.1 / 30.0;
877
878        // Net cost change (savings - storage cost)
879        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 // Should save money
885        };
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    /// Update learning model from decision outcome
899    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            // Extract predicted values from decision metadata
904            if let Some(predicted_hit_rate) = decision.metadata.get("expected_hit_rate") {
905                if let Some(predicted_rate) = predicted_hit_rate.as_f64() {
906                    // Get actual hit rate from metrics after
907                    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                        // Update adjustment factor
923                        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                            // Adjust factor to compensate for systematic bias
933                            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            // Learn from cost predictions
950            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                // Update cost adjustment factor
963                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            // Update prediction accuracy
977            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        // Limit history size
1005        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        // Strategy is applicable if:
1028        // 1. We have analysis reports with insights
1029        // 2. We have request pattern data
1030        // 3. Strategy is enabled
1031
1032        if !self.config.enabled {
1033            return false;
1034        }
1035
1036        if input.analysis_reports.is_empty() {
1037            return false;
1038        }
1039
1040        // Check if any reports contain request pattern insights
1041        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        // Create a mutable copy for pattern analysis
1056        let mut strategy = self.clone();
1057        strategy.analyze_request_patterns(input)?;
1058
1059        // Cluster similar requests
1060        let clusters = strategy.cluster_requests();
1061
1062        if clusters.is_empty() {
1063            return Ok(decisions);
1064        }
1065
1066        // Identify caching opportunities
1067        let opportunities = strategy.identify_opportunities(&clusters);
1068
1069        // Create decisions for top opportunities
1070        for opportunity in opportunities.iter().take(3) {
1071            // Limit to top 3
1072            // Check confidence threshold
1073            if opportunity.confidence < self.config.min_confidence {
1074                continue;
1075            }
1076
1077            // Create decision
1078            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        // Validate decision type
1091        if decision.decision_type != DecisionType::EnableCaching {
1092            return Err(DecisionError::ValidationFailed(
1093                "Invalid decision type for caching strategy".to_string(),
1094            ));
1095        }
1096
1097        // Validate confidence
1098        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        // Validate expected impact
1106        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        // Validate safety checks
1118        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        // Only learn from caching decisions
1129        if decision.decision_type != DecisionType::EnableCaching {
1130            return Ok(());
1131        }
1132
1133        // Update learning model
1134        self.update_learning_model(decision, outcome);
1135
1136        // Update statistics
1137        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
1172// Implement Clone for CachingStrategy
1173impl 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        // May or may not generate decisions depending on patterns
1490        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}