Skip to main content

llm_optimizer_processor/decision/
prompt_optimization.rs

1//! Prompt Optimization Strategy
2//!
3//! This strategy analyzes prompts for optimization opportunities, estimates token savings,
4//! generates A/B test decisions for prompt variants, tracks quality metrics during tests,
5//! and makes final rollout decisions based on results.
6
7use async_trait::async_trait;
8use chrono::{DateTime, Duration as ChronoDuration, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use uuid::Uuid;
14
15use super::config::PromptOptimizationConfig;
16use super::error::{DecisionError, DecisionResult};
17use super::traits::OptimizationStrategy;
18use super::types::{
19    ComparisonOperator, ConfigChange, ConfigType, Decision, DecisionCriteria, DecisionInput,
20    DecisionOutcome, DecisionType, ExpectedImpact, RollbackCondition, RollbackPlan, SafetyCheck,
21};
22
23/// Prompt optimization strategy that reduces token usage while maintaining quality
24pub struct PromptOptimizationStrategy {
25    config: PromptOptimizationConfig,
26    state: Arc<RwLock<StrategyState>>,
27}
28
29/// Internal state for the strategy
30#[derive(Debug, Clone)]
31struct StrategyState {
32    /// Active A/B tests
33    active_tests: HashMap<String, ABTest>,
34    /// Completed tests
35    completed_tests: Vec<ABTest>,
36    /// Statistics
37    stats: StrategyStats,
38    /// Learning history
39    learning_history: Vec<LearningRecord>,
40}
41
42/// An A/B test for prompt optimization
43#[derive(Debug, Clone, Serialize, Deserialize)]
44struct ABTest {
45    /// Test ID
46    id: String,
47    /// Original prompt identifier
48    original_prompt_id: String,
49    /// Optimized prompt identifier
50    optimized_prompt_id: String,
51    /// Start time
52    start_time: DateTime<Utc>,
53    /// End time (planned)
54    end_time: DateTime<Utc>,
55    /// Traffic split (0-1, proportion going to optimized variant)
56    traffic_split: f64,
57    /// Expected token reduction percentage
58    expected_token_reduction_pct: f64,
59    /// Expected quality impact
60    expected_quality_impact: f64,
61    /// Observations collected
62    observations: TestObservations,
63    /// Test status
64    status: TestStatus,
65}
66
67/// Status of an A/B test
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69enum TestStatus {
70    /// Test is running
71    Running,
72    /// Test completed successfully
73    Completed,
74    /// Test was stopped early (e.g., quality degradation)
75    Stopped,
76    /// Test rolled out (optimized variant won)
77    RolledOut,
78    /// Test rolled back (original variant better)
79    RolledBack,
80}
81
82/// Observations collected during an A/B test
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84struct TestObservations {
85    /// Control group (original prompt) observations
86    control: VariantObservations,
87    /// Treatment group (optimized prompt) observations
88    treatment: VariantObservations,
89}
90
91/// Observations for a single variant
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93struct VariantObservations {
94    /// Sample count
95    sample_count: u64,
96    /// Total tokens used
97    total_tokens: u64,
98    /// Average tokens per request
99    avg_tokens: f64,
100    /// Quality ratings (if available)
101    quality_ratings: Vec<f64>,
102    /// Average quality
103    avg_quality: f64,
104    /// Success rate
105    success_rate: f64,
106    /// Successful requests
107    successful_requests: u64,
108    /// Failed requests
109    failed_requests: u64,
110}
111
112/// Strategy statistics
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114struct StrategyStats {
115    /// Total optimizations evaluated
116    total_evaluations: u64,
117    /// Total A/B tests created
118    total_tests_created: u64,
119    /// Successful rollouts
120    successful_rollouts: u64,
121    /// Rollbacks due to quality degradation
122    rollbacks_quality: u64,
123    /// Rollbacks due to insufficient savings
124    rollbacks_savings: u64,
125    /// Total token savings achieved (cumulative)
126    total_token_savings: u64,
127    /// Total cost savings (USD)
128    total_cost_savings_usd: f64,
129}
130
131/// Record of learning from a decision outcome
132#[derive(Debug, Clone, Serialize, Deserialize)]
133struct LearningRecord {
134    /// Timestamp
135    timestamp: DateTime<Utc>,
136    /// Decision ID
137    decision_id: String,
138    /// What was learned
139    lesson: String,
140    /// Confidence adjustment
141    confidence_adjustment: f64,
142    /// Metadata
143    metadata: HashMap<String, serde_json::Value>,
144}
145
146/// Analysis result for a prompt
147#[derive(Debug, Clone)]
148struct PromptAnalysis {
149    /// Identified optimization opportunities
150    opportunities: Vec<OptimizationOpportunity>,
151    /// Estimated token reduction
152    estimated_token_reduction: u64,
153    /// Estimated token reduction percentage
154    estimated_token_reduction_pct: f64,
155    /// Current average token count
156    current_avg_tokens: f64,
157    /// Confidence in the analysis
158    confidence: f64,
159}
160
161/// An optimization opportunity in a prompt
162#[derive(Debug, Clone)]
163enum OptimizationOpportunity {
164    /// Remove redundant phrases
165    RemoveRedundancy { phrase: String, occurrences: usize },
166    /// Simplify verbose instructions
167    SimplifyInstructions { section: String, savings: u64 },
168    /// Remove unnecessary examples
169    RemoveExamples { count: usize, savings: u64 },
170    /// Consolidate repetitive sections
171    ConsolidateSections { sections: Vec<String>, savings: u64 },
172    /// Use more concise language
173    ConciseLanguage { before: String, after: String, savings: u64 },
174}
175
176impl PromptOptimizationStrategy {
177    /// Create a new prompt optimization strategy
178    pub fn new(config: PromptOptimizationConfig) -> Self {
179        Self {
180            config,
181            state: Arc::new(RwLock::new(StrategyState {
182                active_tests: HashMap::new(),
183                completed_tests: Vec::new(),
184                stats: StrategyStats::default(),
185                learning_history: Vec::new(),
186            })),
187        }
188    }
189
190    /// Analyze a prompt for optimization opportunities
191    async fn analyze_prompt(&self, input: &DecisionInput) -> DecisionResult<PromptAnalysis> {
192        // Extract prompt information from recommendations
193        let prompt_recommendations = input
194            .recommendations
195            .iter()
196            .filter(|r| r.tags.contains(&"prompt".to_string()));
197
198        let mut opportunities = Vec::new();
199        let mut estimated_savings = 0u64;
200
201        // Analyze for various optimization opportunities
202        for rec in prompt_recommendations {
203            // Check for redundancy patterns
204            if let Some(redundancy) = self.detect_redundancy(rec) {
205                estimated_savings += redundancy.1;
206                opportunities.push(redundancy.0);
207            }
208
209            // Check for verbose instructions
210            if let Some(verbose) = self.detect_verbose_instructions(rec) {
211                estimated_savings += verbose.1;
212                opportunities.push(verbose.0);
213            }
214
215            // Check for unnecessary examples
216            if let Some(examples) = self.detect_unnecessary_examples(rec) {
217                estimated_savings += examples.1;
218                opportunities.push(examples.0);
219            }
220
221            // Check for repetitive sections
222            if let Some(repetitive) = self.detect_repetitive_sections(rec) {
223                estimated_savings += repetitive.1;
224                opportunities.push(repetitive.0);
225            }
226        }
227
228        // Get current token metrics
229        let current_avg_tokens = input
230            .current_metrics
231            .request_count
232            .max(1) as f64;
233
234        let estimated_token_reduction_pct = if current_avg_tokens > 0.0 {
235            (estimated_savings as f64 / current_avg_tokens) * 100.0
236        } else {
237            0.0
238        };
239
240        // Calculate confidence based on amount of data and clarity of opportunities
241        let confidence = self.calculate_analysis_confidence(&opportunities, input);
242
243        Ok(PromptAnalysis {
244            opportunities,
245            estimated_token_reduction: estimated_savings,
246            estimated_token_reduction_pct,
247            current_avg_tokens,
248            confidence,
249        })
250    }
251
252    /// Detect redundant phrases in prompts
253    fn detect_redundancy(
254        &self,
255        _rec: &crate::analyzer::types::Recommendation,
256    ) -> Option<(OptimizationOpportunity, u64)> {
257        // Simplified detection - in production, this would use NLP analysis
258        // Looking for repetitive phrases in the recommendation description
259        let phrases = vec![
260            ("please make sure to", 5),
261            ("it is important that", 5),
262            ("you should always", 4),
263        ];
264
265        for (phrase, token_cost) in phrases {
266            // In a real implementation, we'd parse the actual prompt text
267            // For now, return a sample opportunity
268            return Some((
269                OptimizationOpportunity::RemoveRedundancy {
270                    phrase: phrase.to_string(),
271                    occurrences: 1,
272                },
273                token_cost,
274            ));
275        }
276
277        None
278    }
279
280    /// Detect verbose instructions
281    fn detect_verbose_instructions(
282        &self,
283        _rec: &crate::analyzer::types::Recommendation,
284    ) -> Option<(OptimizationOpportunity, u64)> {
285        // Simplified detection
286        Some((
287            OptimizationOpportunity::SimplifyInstructions {
288                section: "main_instructions".to_string(),
289                savings: 15,
290            },
291            15,
292        ))
293    }
294
295    /// Detect unnecessary examples
296    fn detect_unnecessary_examples(
297        &self,
298        _rec: &crate::analyzer::types::Recommendation,
299    ) -> Option<(OptimizationOpportunity, u64)> {
300        // In production, would analyze if examples are actually helping quality
301        Some((
302            OptimizationOpportunity::RemoveExamples {
303                count: 2,
304                savings: 50,
305            },
306            50,
307        ))
308    }
309
310    /// Detect repetitive sections
311    fn detect_repetitive_sections(
312        &self,
313        _rec: &crate::analyzer::types::Recommendation,
314    ) -> Option<(OptimizationOpportunity, u64)> {
315        // Would use structural analysis to find repeated content
316        Some((
317            OptimizationOpportunity::ConsolidateSections {
318                sections: vec!["intro".to_string(), "outro".to_string()],
319                savings: 30,
320            },
321            30,
322        ))
323    }
324
325    /// Calculate confidence in the analysis
326    fn calculate_analysis_confidence(
327        &self,
328        opportunities: &[OptimizationOpportunity],
329        input: &DecisionInput,
330    ) -> f64 {
331        // Base confidence from data volume
332        let data_confidence = if input.current_metrics.request_count > 1000 {
333            0.9
334        } else if input.current_metrics.request_count > 100 {
335            0.75
336        } else {
337            0.6
338        };
339
340        // Opportunity clarity confidence
341        let opportunity_confidence = if opportunities.is_empty() {
342            0.5
343        } else if opportunities.len() >= 3 {
344            0.9
345        } else {
346            0.7
347        };
348
349        // Combined confidence
350        (data_confidence + opportunity_confidence) / 2.0
351    }
352
353    /// Estimate quality impact of optimization
354    fn estimate_quality_impact(&self, analysis: &PromptAnalysis) -> f64 {
355        // Conservative estimate - assumes some quality degradation
356        let base_impact = -0.05; // -5% quality by default
357
358        // Adjust based on type of optimizations
359        let mut adjustment = 0.0;
360        for opp in &analysis.opportunities {
361            match opp {
362                OptimizationOpportunity::RemoveRedundancy { .. } => {
363                    adjustment += 0.01; // Removing redundancy might improve clarity
364                }
365                OptimizationOpportunity::SimplifyInstructions { .. } => {
366                    adjustment -= 0.02; // Simplifying might lose nuance
367                }
368                OptimizationOpportunity::RemoveExamples { .. } => {
369                    adjustment -= 0.03; // Removing examples likely reduces quality
370                }
371                OptimizationOpportunity::ConsolidateSections { .. } => {
372                    adjustment += 0.005; // Consolidation might improve coherence
373                }
374                OptimizationOpportunity::ConciseLanguage { .. } => {
375                    adjustment += 0.01; // Conciseness might help
376                }
377            }
378        }
379
380        base_impact + adjustment
381    }
382
383    /// Create an A/B test decision
384    fn create_ab_test_decision(
385        &self,
386        analysis: PromptAnalysis,
387        criteria: &DecisionCriteria,
388    ) -> DecisionResult<Decision> {
389        let test_id = Uuid::new_v4().to_string();
390        let now = Utc::now();
391
392        // Calculate expected impact
393        let expected_quality_impact = self.estimate_quality_impact(&analysis);
394
395        // Estimate cost savings based on token reduction
396        // Assuming $0.002 per 1K tokens (rough average)
397        let tokens_saved_per_day = analysis.estimated_token_reduction as f64 * 1000.0;
398        let cost_savings_per_day = (tokens_saved_per_day / 1000.0) * 0.002;
399
400        let expected_impact = ExpectedImpact {
401            cost_change_usd: Some(-cost_savings_per_day),
402            latency_change_ms: Some(0.0), // Prompt optimization shouldn't affect latency much
403            quality_change: Some(expected_quality_impact),
404            throughput_change_rps: None,
405            success_rate_change_pct: None,
406            time_to_impact: std::time::Duration::from_secs(3600), // 1 hour
407            confidence: analysis.confidence,
408        };
409
410        // Safety checks
411        let mut safety_checks = Vec::new();
412
413        // Quality check
414        let min_quality = criteria.min_quality_score.unwrap_or(3.5);
415        let projected_quality = 4.0 + expected_quality_impact; // Assuming baseline of 4.0
416        let quality_check_passed = projected_quality >= min_quality
417            && expected_quality_impact.abs() <= (self.config.max_quality_degradation_pct / 100.0);
418
419        safety_checks.push(SafetyCheck {
420            name: "quality_check".to_string(),
421            passed: quality_check_passed,
422            details: format!(
423                "Projected quality: {:.2}, Min required: {:.2}, Max degradation: {:.1}%",
424                projected_quality, min_quality, self.config.max_quality_degradation_pct
425            ),
426            timestamp: now,
427        });
428
429        // Token reduction check
430        let token_reduction_check_passed =
431            analysis.estimated_token_reduction_pct >= self.config.min_token_reduction_pct;
432
433        safety_checks.push(SafetyCheck {
434            name: "token_reduction_check".to_string(),
435            passed: token_reduction_check_passed,
436            details: format!(
437                "Estimated reduction: {:.1}%, Min required: {:.1}%",
438                analysis.estimated_token_reduction_pct, self.config.min_token_reduction_pct
439            ),
440            timestamp: now,
441        });
442
443        // Confidence check
444        let confidence_check_passed = analysis.confidence >= criteria.min_confidence;
445
446        safety_checks.push(SafetyCheck {
447            name: "confidence_check".to_string(),
448            passed: confidence_check_passed,
449            details: format!(
450                "Analysis confidence: {:.2}, Min required: {:.2}",
451                analysis.confidence, criteria.min_confidence
452            ),
453            timestamp: now,
454        });
455
456        // Overall safety check
457        let all_checks_passed = safety_checks.iter().all(|c| c.passed);
458
459        if !all_checks_passed {
460            return Err(DecisionError::ValidationFailed(
461                "Safety checks failed for prompt optimization".to_string(),
462            ));
463        }
464
465        // Configuration changes
466        let config_changes = vec![
467            ConfigChange {
468                config_type: ConfigType::Prompt,
469                path: "prompt.optimization.enabled".to_string(),
470                old_value: Some(serde_json::json!(false)),
471                new_value: serde_json::json!(true),
472                description: "Enable optimized prompt variant for A/B testing".to_string(),
473            },
474            ConfigChange {
475                config_type: ConfigType::Prompt,
476                path: "prompt.ab_test.traffic_split".to_string(),
477                old_value: Some(serde_json::json!(0.0)),
478                new_value: serde_json::json!(self.config.ab_test_traffic_split),
479                description: format!(
480                    "Route {:.0}% of traffic to optimized variant",
481                    self.config.ab_test_traffic_split * 100.0
482                ),
483            },
484        ];
485
486        // Rollback plan
487        let rollback_plan = RollbackPlan {
488            description: "Rollback to original prompt if quality degrades".to_string(),
489            revert_changes: vec![
490                ConfigChange {
491                    config_type: ConfigType::Prompt,
492                    path: "prompt.optimization.enabled".to_string(),
493                    old_value: Some(serde_json::json!(true)),
494                    new_value: serde_json::json!(false),
495                    description: "Disable optimized prompt variant".to_string(),
496                },
497                ConfigChange {
498                    config_type: ConfigType::Prompt,
499                    path: "prompt.ab_test.traffic_split".to_string(),
500                    old_value: Some(serde_json::json!(self.config.ab_test_traffic_split)),
501                    new_value: serde_json::json!(0.0),
502                    description: "Stop routing traffic to optimized variant".to_string(),
503                },
504            ],
505            trigger_conditions: vec![
506                RollbackCondition {
507                    metric: "quality_score".to_string(),
508                    operator: ComparisonOperator::LessThan,
509                    threshold: min_quality,
510                    description: format!(
511                        "Quality score drops below {:.2}",
512                        min_quality
513                    ),
514                },
515                RollbackCondition {
516                    metric: "quality_degradation_pct".to_string(),
517                    operator: ComparisonOperator::GreaterThan,
518                    threshold: self.config.max_quality_degradation_pct,
519                    description: format!(
520                        "Quality degradation exceeds {:.1}%",
521                        self.config.max_quality_degradation_pct
522                    ),
523                },
524                RollbackCondition {
525                    metric: "success_rate_pct".to_string(),
526                    operator: ComparisonOperator::LessThan,
527                    threshold: 95.0,
528                    description: "Success rate drops below 95%".to_string(),
529                },
530            ],
531            max_duration: std::time::Duration::from_secs(
532                self.config.ab_test_duration.as_secs()
533            ),
534        };
535
536        // Build justification
537        let justification = format!(
538            "Prompt optimization opportunity identified with {:.1}% token reduction potential. \
539             {} optimization opportunities found. Expected cost savings: ${:.2}/day. \
540             Quality impact: {:.1}%. A/B test will run for {} hours with {:.0}% traffic split.",
541            analysis.estimated_token_reduction_pct,
542            analysis.opportunities.len(),
543            cost_savings_per_day,
544            expected_quality_impact * 100.0,
545            self.config.ab_test_duration.as_secs() / 3600,
546            self.config.ab_test_traffic_split * 100.0
547        );
548
549        let mut metadata = HashMap::new();
550        metadata.insert(
551            "test_id".to_string(),
552            serde_json::json!(test_id),
553        );
554        metadata.insert(
555            "opportunity_count".to_string(),
556            serde_json::json!(analysis.opportunities.len()),
557        );
558        metadata.insert(
559            "estimated_token_reduction".to_string(),
560            serde_json::json!(analysis.estimated_token_reduction),
561        );
562        metadata.insert(
563            "estimated_token_reduction_pct".to_string(),
564            serde_json::json!(analysis.estimated_token_reduction_pct),
565        );
566
567        Ok(Decision {
568            id: Uuid::new_v4().to_string(),
569            timestamp: now,
570            strategy: self.name().to_string(),
571            decision_type: DecisionType::OptimizePrompt,
572            confidence: analysis.confidence,
573            expected_impact,
574            config_changes,
575            justification,
576            related_insights: Vec::new(),
577            related_recommendations: Vec::new(),
578            priority: self.priority(),
579            requires_approval: expected_quality_impact.abs() > 0.03, // Require approval for >3% quality change
580            safety_checks,
581            rollback_plan: Some(rollback_plan),
582            metadata,
583        })
584    }
585
586    /// Analyze A/B test results for statistical significance
587    fn analyze_test_results(&self, test: &ABTest) -> TestAnalysis {
588        let control = &test.observations.control;
589        let treatment = &test.observations.treatment;
590
591        // Calculate relative differences
592        let token_reduction_pct = if control.avg_tokens > 0.0 {
593            ((control.avg_tokens - treatment.avg_tokens) / control.avg_tokens) * 100.0
594        } else {
595            0.0
596        };
597
598        let quality_change_pct = if control.avg_quality > 0.0 {
599            ((treatment.avg_quality - control.avg_quality) / control.avg_quality) * 100.0
600        } else {
601            0.0
602        };
603
604        // Simplified statistical significance test (would use proper t-test in production)
605        let token_significance = self.calculate_significance(
606            control.sample_count,
607            treatment.sample_count,
608            control.avg_tokens,
609            treatment.avg_tokens,
610        );
611
612        let quality_significance = self.calculate_significance(
613            control.quality_ratings.len() as u64,
614            treatment.quality_ratings.len() as u64,
615            control.avg_quality,
616            treatment.avg_quality,
617        );
618
619        // Overall significance
620        let is_statistically_significant =
621            token_significance > 0.95 || quality_significance > 0.95;
622
623        // Recommendation
624        let recommendation = if !is_statistically_significant {
625            TestRecommendation::ContinueTesting
626        } else if token_reduction_pct >= self.config.min_token_reduction_pct
627            && quality_change_pct.abs() <= self.config.max_quality_degradation_pct
628        {
629            TestRecommendation::Rollout
630        } else if quality_change_pct < -(self.config.max_quality_degradation_pct) {
631            TestRecommendation::Rollback
632        } else if token_reduction_pct < self.config.min_token_reduction_pct {
633            TestRecommendation::Rollback
634        } else {
635            TestRecommendation::ContinueTesting
636        };
637
638        TestAnalysis {
639            token_reduction_pct,
640            quality_change_pct,
641            token_significance,
642            quality_significance,
643            is_statistically_significant,
644            recommendation,
645        }
646    }
647
648    /// Calculate statistical significance (simplified)
649    fn calculate_significance(&self, n1: u64, n2: u64, mean1: f64, mean2: f64) -> f64 {
650        // Simplified significance calculation
651        // In production, would use proper t-test with variance
652        let min_samples = n1.min(n2);
653        let diff_magnitude = (mean1 - mean2).abs();
654        let avg_magnitude = (mean1.abs() + mean2.abs()) / 2.0;
655
656        let relative_diff = if avg_magnitude > 0.0 {
657            diff_magnitude / avg_magnitude
658        } else {
659            0.0
660        };
661
662        // Simple heuristic: more samples + larger difference = higher significance
663        let sample_factor = (min_samples as f64 / 100.0).min(1.0);
664        let diff_factor = (relative_diff * 10.0).min(1.0);
665
666        (sample_factor * 0.7 + diff_factor * 0.3).clamp(0.0, 1.0)
667    }
668}
669
670/// Analysis of A/B test results
671#[derive(Debug, Clone)]
672struct TestAnalysis {
673    token_reduction_pct: f64,
674    quality_change_pct: f64,
675    token_significance: f64,
676    quality_significance: f64,
677    is_statistically_significant: bool,
678    recommendation: TestRecommendation,
679}
680
681/// Recommendation from test analysis
682#[derive(Debug, Clone, Copy, PartialEq, Eq)]
683enum TestRecommendation {
684    /// Continue testing (need more data)
685    ContinueTesting,
686    /// Roll out optimized variant
687    Rollout,
688    /// Roll back to original
689    Rollback,
690}
691
692#[async_trait]
693impl OptimizationStrategy for PromptOptimizationStrategy {
694    fn name(&self) -> &str {
695        "prompt_optimization"
696    }
697
698    fn priority(&self) -> u32 {
699        self.config.priority
700    }
701
702    fn is_applicable(&self, input: &DecisionInput) -> bool {
703        // Check if strategy is enabled
704        if !self.config.enabled {
705            return false;
706        }
707
708        // Check if there are prompt-related recommendations
709        let has_prompt_recommendations = input
710            .recommendations
711            .iter()
712            .any(|r| r.tags.contains(&"prompt".to_string()));
713
714        // Check if we have sufficient data
715        let has_sufficient_data = input.current_metrics.request_count >= 100;
716
717        has_prompt_recommendations && has_sufficient_data
718    }
719
720    async fn evaluate(
721        &self,
722        input: &DecisionInput,
723        criteria: &DecisionCriteria,
724    ) -> DecisionResult<Vec<Decision>> {
725        let mut decisions = Vec::new();
726
727        // Update statistics
728        {
729            let mut state = self.state.write().await;
730            state.stats.total_evaluations += 1;
731        }
732
733        // Analyze the prompt for optimization opportunities
734        let analysis = self.analyze_prompt(input).await?;
735
736        // Check if optimization meets minimum thresholds
737        if analysis.estimated_token_reduction_pct < self.config.min_token_reduction_pct {
738            return Ok(decisions); // No decision - savings too small
739        }
740
741        if analysis.confidence < criteria.min_confidence {
742            return Ok(decisions); // No decision - confidence too low
743        }
744
745        // Create A/B test decision
746        let decision = self.create_ab_test_decision(analysis, criteria)?;
747
748        // Update statistics
749        {
750            let mut state = self.state.write().await;
751            state.stats.total_tests_created += 1;
752        }
753
754        decisions.push(decision);
755        Ok(decisions)
756    }
757
758    async fn validate(
759        &self,
760        decision: &Decision,
761        criteria: &DecisionCriteria,
762    ) -> DecisionResult<()> {
763        // Verify decision type
764        if decision.decision_type != DecisionType::OptimizePrompt {
765            return Err(DecisionError::ValidationFailed(
766                "Decision type mismatch".to_string(),
767            ));
768        }
769
770        // Verify confidence
771        if decision.confidence < criteria.min_confidence {
772            return Err(DecisionError::ValidationFailed(format!(
773                "Confidence {:.2} below minimum {:.2}",
774                decision.confidence, criteria.min_confidence
775            )));
776        }
777
778        // Verify expected impact
779        if let Some(quality_change) = decision.expected_impact.quality_change {
780            let quality_degradation_pct = -quality_change * 100.0;
781            if quality_degradation_pct > self.config.max_quality_degradation_pct {
782                return Err(DecisionError::ValidationFailed(format!(
783                    "Expected quality degradation {:.1}% exceeds maximum {:.1}%",
784                    quality_degradation_pct, self.config.max_quality_degradation_pct
785                )));
786            }
787        }
788
789        // Verify safety checks passed
790        if !decision.safety_checks.iter().all(|c| c.passed) {
791            return Err(DecisionError::ValidationFailed(
792                "Not all safety checks passed".to_string(),
793            ));
794        }
795
796        Ok(())
797    }
798
799    async fn learn(
800        &mut self,
801        decision: &Decision,
802        outcome: &DecisionOutcome,
803    ) -> DecisionResult<()> {
804        let mut state = self.state.write().await;
805
806        // Extract test ID from decision metadata
807        let test_id = decision
808            .metadata
809            .get("test_id")
810            .and_then(|v| v.as_str())
811            .ok_or_else(|| DecisionError::Internal("Missing test_id in metadata".to_string()))?;
812
813        // Check if we have an active test - use a scope to limit the borrow
814        let (should_update_stats, update_info) = {
815            if let Some(test) = state.active_tests.get_mut(test_id) {
816                // Update test observations based on outcome
817                if outcome.success {
818                    // Analyze the actual impact
819                    if let Some(actual_impact) = &outcome.actual_impact {
820                        // Calculate actual token reduction
821                        let actual_token_reduction_pct = if outcome.metrics_before.request_count > 0 {
822                            -actual_impact.cost_change_usd / outcome.metrics_before.total_cost_usd * 100.0
823                        } else {
824                            0.0
825                        };
826
827                        // Update test observations (simplified - would track per-variant in production)
828                        test.observations.treatment.sample_count += 1;
829                        test.observations.treatment.avg_tokens =
830                            outcome.metrics_before.request_count as f64 * (1.0 - actual_token_reduction_pct / 100.0);
831
832                        if let Some(quality_after) = outcome.metrics_after.as_ref().and_then(|m| m.avg_rating) {
833                            test.observations.treatment.quality_ratings.push(quality_after);
834                            test.observations.treatment.avg_quality = test
835                                .observations
836                                .treatment
837                                .quality_ratings
838                                .iter()
839                                .sum::<f64>()
840                                / test.observations.treatment.quality_ratings.len() as f64;
841                        }
842
843                        // Analyze test results
844                        let analysis = self.analyze_test_results(test);
845
846                        // Prepare update info based on recommendation
847                        let update_type = match analysis.recommendation {
848                            TestRecommendation::Rollout => {
849                                test.status = TestStatus::RolledOut;
850                                let token_savings = (test.expected_token_reduction_pct / 100.0
851                                    * outcome.metrics_before.request_count as f64) as u64;
852                                Some((
853                                    "rollout",
854                                    token_savings,
855                                    -actual_impact.cost_change_usd,
856                                    format!(
857                                        "Successful rollout: {:.1}% token reduction, {:.1}% quality change",
858                                        analysis.token_reduction_pct, analysis.quality_change_pct
859                                    ),
860                                    0.05f64,
861                                ))
862                            }
863                            TestRecommendation::Rollback => {
864                                test.status = TestStatus::RolledBack;
865                                let rollback_type = if analysis.quality_change_pct < -(self.config.max_quality_degradation_pct) {
866                                    "quality"
867                                } else {
868                                    "savings"
869                                };
870                                Some((
871                                    rollback_type,
872                                    0u64,
873                                    0.0f64,
874                                    format!(
875                                        "Rolled back: insufficient savings or quality degradation. \
876                                         Token reduction: {:.1}%, Quality change: {:.1}%",
877                                        analysis.token_reduction_pct, analysis.quality_change_pct
878                                    ),
879                                    -0.05f64,
880                                ))
881                            }
882                            TestRecommendation::ContinueTesting => None,
883                        };
884
885                        // Check if test is complete
886                        let is_complete = test.status != TestStatus::Running;
887                        let completed_test = if is_complete { Some(test.clone()) } else { None };
888
889                        (true, Some((update_type, is_complete, completed_test)))
890                    } else {
891                        (false, None)
892                    }
893                } else {
894                    // Decision failed - rollback test
895                    test.status = TestStatus::Stopped;
896                    let lesson = format!(
897                        "Test failed: {}",
898                        outcome.error.as_ref().unwrap_or(&"Unknown error".to_string())
899                    );
900                    (true, Some((Some(("failed", 0u64, 0.0f64, lesson, -0.1f64)), false, None)))
901                }
902            } else {
903                (false, None)
904            }
905        }; // End of borrow scope
906
907        // Now update state.stats without holding a borrow to active_tests
908        if should_update_stats {
909            if let Some((update_type, is_complete, completed_test)) = update_info {
910                // Update stats based on the recommendation
911                if let Some((action, token_savings, cost_savings, lesson, confidence_adj)) = update_type {
912                    match action {
913                        "rollout" => {
914                            state.stats.successful_rollouts += 1;
915                            state.stats.total_token_savings += token_savings;
916                            state.stats.total_cost_savings_usd += cost_savings;
917                        }
918                        "quality" => {
919                            state.stats.rollbacks_quality += 1;
920                        }
921                        "savings" => {
922                            state.stats.rollbacks_savings += 1;
923                        }
924                        "failed" => {
925                            state.stats.rollbacks_quality += 1;
926                        }
927                        _ => {}
928                    }
929
930                    state.learning_history.push(LearningRecord {
931                        timestamp: Utc::now(),
932                        decision_id: decision.id.clone(),
933                        lesson,
934                        confidence_adjustment: confidence_adj,
935                        metadata: HashMap::new(),
936                    });
937                }
938
939                // Move to completed tests if done
940                if is_complete {
941                    if let Some(test) = completed_test {
942                        state.completed_tests.push(test);
943                    }
944                    state.active_tests.remove(test_id);
945                }
946            }
947        }
948
949        Ok(())
950    }
951
952    fn get_stats(&self) -> serde_json::Value {
953        let state = futures::executor::block_on(self.state.read());
954        serde_json::json!({
955            "total_evaluations": state.stats.total_evaluations,
956            "total_tests_created": state.stats.total_tests_created,
957            "active_tests": state.active_tests.len(),
958            "completed_tests": state.completed_tests.len(),
959            "successful_rollouts": state.stats.successful_rollouts,
960            "rollbacks_quality": state.stats.rollbacks_quality,
961            "rollbacks_savings": state.stats.rollbacks_savings,
962            "total_token_savings": state.stats.total_token_savings,
963            "total_cost_savings_usd": state.stats.total_cost_savings_usd,
964            "learning_records": state.learning_history.len(),
965        })
966    }
967}
968
969#[cfg(test)]
970mod tests {
971    use super::*;
972    use crate::analyzer::types::{
973        Action, ActionType, Confidence as AnalyzerConfidence, Impact, Priority, Recommendation,
974        RiskLevel,
975    };
976    use crate::decision::types::SystemMetrics;
977
978    fn create_test_config() -> PromptOptimizationConfig {
979        PromptOptimizationConfig {
980            enabled: true,
981            priority: 85,
982            min_confidence: 0.7,
983            min_token_reduction_pct: 10.0,
984            ab_test_duration: ChronoDuration::hours(1),
985            ab_test_traffic_split: 0.1,
986            min_quality_score: 4.0,
987            max_quality_degradation_pct: 5.0,
988        }
989    }
990
991    fn create_test_input() -> DecisionInput {
992        let now = Utc::now();
993        DecisionInput {
994            timestamp: now,
995            analysis_reports: Vec::new(),
996            insights: Vec::new(),
997            recommendations: vec![Recommendation {
998                id: "rec1".to_string(),
999                analyzer: "prompt_analyzer".to_string(),
1000                timestamp: now,
1001                priority: Priority::High,
1002                title: "Optimize prompt".to_string(),
1003                description: "Prompt contains redundant instructions".to_string(),
1004                action: Action {
1005                    action_type: ActionType::Optimize,
1006                    instructions: "Remove redundant phrases".to_string(),
1007                    parameters: HashMap::new(),
1008                    estimated_effort: Some(1.0),
1009                    risk_level: RiskLevel::Low,
1010                },
1011                expected_impact: Impact {
1012                    performance: None,
1013                    cost: None,
1014                    quality: None,
1015                    description: "Reduce token usage".to_string(),
1016                },
1017                confidence: AnalyzerConfidence::new(0.8),
1018                related_insights: Vec::new(),
1019                tags: vec!["prompt".to_string()],
1020            }],
1021            current_metrics: SystemMetrics {
1022                avg_latency_ms: 200.0,
1023                p95_latency_ms: 300.0,
1024                p99_latency_ms: 400.0,
1025                success_rate_pct: 98.0,
1026                error_rate_pct: 2.0,
1027                throughput_rps: 10.0,
1028                total_cost_usd: 100.0,
1029                daily_cost_usd: 50.0,
1030                avg_rating: Some(4.2),
1031                cache_hit_rate_pct: Some(30.0),
1032                request_count: 1000,
1033                timestamp: now,
1034            },
1035            context: HashMap::new(),
1036        }
1037    }
1038
1039    #[tokio::test]
1040    async fn test_strategy_creation() {
1041        let config = create_test_config();
1042        let strategy = PromptOptimizationStrategy::new(config);
1043
1044        assert_eq!(strategy.name(), "prompt_optimization");
1045        assert_eq!(strategy.priority(), 85);
1046    }
1047
1048    #[tokio::test]
1049    async fn test_is_applicable_with_prompt_recommendations() {
1050        let config = create_test_config();
1051        let strategy = PromptOptimizationStrategy::new(config);
1052        let input = create_test_input();
1053
1054        assert!(strategy.is_applicable(&input));
1055    }
1056
1057    #[tokio::test]
1058    async fn test_is_applicable_without_prompt_recommendations() {
1059        let config = create_test_config();
1060        let strategy = PromptOptimizationStrategy::new(config);
1061        let mut input = create_test_input();
1062        input.recommendations.clear();
1063
1064        assert!(!strategy.is_applicable(&input));
1065    }
1066
1067    #[tokio::test]
1068    async fn test_is_applicable_insufficient_data() {
1069        let config = create_test_config();
1070        let strategy = PromptOptimizationStrategy::new(config);
1071        let mut input = create_test_input();
1072        input.current_metrics.request_count = 50; // Less than minimum 100
1073
1074        assert!(!strategy.is_applicable(&input));
1075    }
1076
1077    #[tokio::test]
1078    async fn test_analyze_prompt() {
1079        let config = create_test_config();
1080        let strategy = PromptOptimizationStrategy::new(config);
1081        let input = create_test_input();
1082
1083        let analysis = strategy.analyze_prompt(&input).await.unwrap();
1084
1085        assert!(!analysis.opportunities.is_empty());
1086        assert!(analysis.estimated_token_reduction > 0);
1087        assert!(analysis.confidence > 0.0);
1088    }
1089
1090    #[tokio::test]
1091    async fn test_evaluate_generates_decision() {
1092        let config = create_test_config();
1093        let strategy = PromptOptimizationStrategy::new(config);
1094        let input = create_test_input();
1095        let criteria = DecisionCriteria::default();
1096
1097        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1098
1099        assert_eq!(decisions.len(), 1);
1100        let decision = &decisions[0];
1101        assert_eq!(decision.decision_type, DecisionType::OptimizePrompt);
1102        assert!(decision.confidence >= criteria.min_confidence);
1103        assert!(!decision.safety_checks.is_empty());
1104        assert!(decision.rollback_plan.is_some());
1105    }
1106
1107    #[tokio::test]
1108    async fn test_evaluate_low_savings_no_decision() {
1109        let mut config = create_test_config();
1110        config.min_token_reduction_pct = 90.0; // Set very high threshold
1111        let strategy = PromptOptimizationStrategy::new(config);
1112        let input = create_test_input();
1113        let criteria = DecisionCriteria::default();
1114
1115        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1116
1117        assert_eq!(decisions.len(), 0);
1118    }
1119
1120    #[tokio::test]
1121    async fn test_validate_valid_decision() {
1122        let config = create_test_config();
1123        let strategy = PromptOptimizationStrategy::new(config);
1124        let input = create_test_input();
1125        let criteria = DecisionCriteria::default();
1126
1127        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1128        let decision = &decisions[0];
1129
1130        let result = strategy.validate(decision, &criteria).await;
1131        assert!(result.is_ok());
1132    }
1133
1134    #[tokio::test]
1135    async fn test_validate_wrong_decision_type() {
1136        let config = create_test_config();
1137        let strategy = PromptOptimizationStrategy::new(config);
1138        let criteria = DecisionCriteria::default();
1139
1140        let mut decision = Decision {
1141            id: Uuid::new_v4().to_string(),
1142            timestamp: Utc::now(),
1143            strategy: "prompt_optimization".to_string(),
1144            decision_type: DecisionType::ModelSwitch, // Wrong type
1145            confidence: 0.8,
1146            expected_impact: ExpectedImpact {
1147                cost_change_usd: None,
1148                latency_change_ms: None,
1149                quality_change: None,
1150                throughput_change_rps: None,
1151                success_rate_change_pct: None,
1152                time_to_impact: std::time::Duration::from_secs(3600),
1153                confidence: 0.8,
1154            },
1155            config_changes: Vec::new(),
1156            justification: "Test".to_string(),
1157            related_insights: Vec::new(),
1158            related_recommendations: Vec::new(),
1159            priority: 85,
1160            requires_approval: false,
1161            safety_checks: vec![SafetyCheck {
1162                name: "test".to_string(),
1163                passed: true,
1164                details: "Test".to_string(),
1165                timestamp: Utc::now(),
1166            }],
1167            rollback_plan: None,
1168            metadata: HashMap::new(),
1169        };
1170
1171        let result = strategy.validate(&decision, &criteria).await;
1172        assert!(result.is_err());
1173    }
1174
1175    #[tokio::test]
1176    async fn test_validate_low_confidence() {
1177        let config = create_test_config();
1178        let strategy = PromptOptimizationStrategy::new(config);
1179        let mut criteria = DecisionCriteria::default();
1180        criteria.min_confidence = 0.95; // Very high threshold
1181
1182        let input = create_test_input();
1183        let default_criteria = DecisionCriteria::default();
1184        let decisions = strategy.evaluate(&input, &default_criteria).await.unwrap();
1185        let decision = &decisions[0];
1186
1187        let result = strategy.validate(decision, &criteria).await;
1188        assert!(result.is_err());
1189    }
1190
1191    #[tokio::test]
1192    async fn test_calculate_significance() {
1193        let config = create_test_config();
1194        let strategy = PromptOptimizationStrategy::new(config);
1195
1196        // Test with large sample and large difference
1197        let sig1 = strategy.calculate_significance(1000, 1000, 100.0, 80.0);
1198        assert!(sig1 > 0.5);
1199
1200        // Test with small sample
1201        let sig2 = strategy.calculate_significance(10, 10, 100.0, 80.0);
1202        assert!(sig2 < sig1);
1203
1204        // Test with no difference
1205        let sig3 = strategy.calculate_significance(1000, 1000, 100.0, 100.0);
1206        assert!(sig3 < sig1);
1207    }
1208
1209    #[tokio::test]
1210    async fn test_analyze_test_results() {
1211        let config = create_test_config();
1212        let strategy = PromptOptimizationStrategy::new(config);
1213
1214        let test = ABTest {
1215            id: "test1".to_string(),
1216            original_prompt_id: "orig".to_string(),
1217            optimized_prompt_id: "opt".to_string(),
1218            start_time: Utc::now(),
1219            end_time: Utc::now() + ChronoDuration::hours(1),
1220            traffic_split: 0.1,
1221            expected_token_reduction_pct: 15.0,
1222            expected_quality_impact: -0.02,
1223            observations: TestObservations {
1224                control: VariantObservations {
1225                    sample_count: 1000,
1226                    total_tokens: 100000,
1227                    avg_tokens: 100.0,
1228                    quality_ratings: vec![4.2; 100],
1229                    avg_quality: 4.2,
1230                    success_rate: 98.0,
1231                    successful_requests: 980,
1232                    failed_requests: 20,
1233                },
1234                treatment: VariantObservations {
1235                    sample_count: 100,
1236                    total_tokens: 8500,
1237                    avg_tokens: 85.0, // 15% reduction
1238                    quality_ratings: vec![4.15; 100],
1239                    avg_quality: 4.15, // Small quality drop
1240                    success_rate: 97.0,
1241                    successful_requests: 97,
1242                    failed_requests: 3,
1243                },
1244            },
1245            status: TestStatus::Running,
1246        };
1247
1248        let analysis = strategy.analyze_test_results(&test);
1249
1250        assert!(analysis.token_reduction_pct > 10.0);
1251        assert!(analysis.quality_change_pct.abs() < 5.0);
1252    }
1253
1254    #[tokio::test]
1255    async fn test_get_stats() {
1256        let config = create_test_config();
1257        let strategy = PromptOptimizationStrategy::new(config);
1258
1259        let stats = strategy.get_stats();
1260
1261        assert!(stats.is_object());
1262        assert!(stats.get("total_evaluations").is_some());
1263        assert!(stats.get("total_tests_created").is_some());
1264        assert!(stats.get("successful_rollouts").is_some());
1265    }
1266
1267    #[tokio::test]
1268    async fn test_estimate_quality_impact() {
1269        let config = create_test_config();
1270        let strategy = PromptOptimizationStrategy::new(config);
1271
1272        let analysis = PromptAnalysis {
1273            opportunities: vec![
1274                OptimizationOpportunity::RemoveRedundancy {
1275                    phrase: "test".to_string(),
1276                    occurrences: 2,
1277                },
1278            ],
1279            estimated_token_reduction: 50,
1280            estimated_token_reduction_pct: 15.0,
1281            current_avg_tokens: 300.0,
1282            confidence: 0.8,
1283        };
1284
1285        let impact = strategy.estimate_quality_impact(&analysis);
1286
1287        // Should be a small negative or positive number
1288        assert!(impact.abs() < 0.2);
1289    }
1290
1291    #[tokio::test]
1292    async fn test_decision_metadata() {
1293        let config = create_test_config();
1294        let strategy = PromptOptimizationStrategy::new(config);
1295        let input = create_test_input();
1296        let criteria = DecisionCriteria::default();
1297
1298        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1299        let decision = &decisions[0];
1300
1301        assert!(decision.metadata.contains_key("test_id"));
1302        assert!(decision.metadata.contains_key("opportunity_count"));
1303        assert!(decision.metadata.contains_key("estimated_token_reduction"));
1304    }
1305
1306    #[tokio::test]
1307    async fn test_rollback_plan() {
1308        let config = create_test_config();
1309        let strategy = PromptOptimizationStrategy::new(config);
1310        let input = create_test_input();
1311        let criteria = DecisionCriteria::default();
1312
1313        let decisions = strategy.evaluate(&input, &criteria).await.unwrap();
1314        let decision = &decisions[0];
1315
1316        let rollback_plan = decision.rollback_plan.as_ref().unwrap();
1317        assert!(!rollback_plan.trigger_conditions.is_empty());
1318        assert!(!rollback_plan.revert_changes.is_empty());
1319        assert!(rollback_plan.max_duration.as_secs() > 0);
1320    }
1321}