1use 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
23pub struct PromptOptimizationStrategy {
25 config: PromptOptimizationConfig,
26 state: Arc<RwLock<StrategyState>>,
27}
28
29#[derive(Debug, Clone)]
31struct StrategyState {
32 active_tests: HashMap<String, ABTest>,
34 completed_tests: Vec<ABTest>,
36 stats: StrategyStats,
38 learning_history: Vec<LearningRecord>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44struct ABTest {
45 id: String,
47 original_prompt_id: String,
49 optimized_prompt_id: String,
51 start_time: DateTime<Utc>,
53 end_time: DateTime<Utc>,
55 traffic_split: f64,
57 expected_token_reduction_pct: f64,
59 expected_quality_impact: f64,
61 observations: TestObservations,
63 status: TestStatus,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69enum TestStatus {
70 Running,
72 Completed,
74 Stopped,
76 RolledOut,
78 RolledBack,
80}
81
82#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84struct TestObservations {
85 control: VariantObservations,
87 treatment: VariantObservations,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93struct VariantObservations {
94 sample_count: u64,
96 total_tokens: u64,
98 avg_tokens: f64,
100 quality_ratings: Vec<f64>,
102 avg_quality: f64,
104 success_rate: f64,
106 successful_requests: u64,
108 failed_requests: u64,
110}
111
112#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114struct StrategyStats {
115 total_evaluations: u64,
117 total_tests_created: u64,
119 successful_rollouts: u64,
121 rollbacks_quality: u64,
123 rollbacks_savings: u64,
125 total_token_savings: u64,
127 total_cost_savings_usd: f64,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133struct LearningRecord {
134 timestamp: DateTime<Utc>,
136 decision_id: String,
138 lesson: String,
140 confidence_adjustment: f64,
142 metadata: HashMap<String, serde_json::Value>,
144}
145
146#[derive(Debug, Clone)]
148struct PromptAnalysis {
149 opportunities: Vec<OptimizationOpportunity>,
151 estimated_token_reduction: u64,
153 estimated_token_reduction_pct: f64,
155 current_avg_tokens: f64,
157 confidence: f64,
159}
160
161#[derive(Debug, Clone)]
163enum OptimizationOpportunity {
164 RemoveRedundancy { phrase: String, occurrences: usize },
166 SimplifyInstructions { section: String, savings: u64 },
168 RemoveExamples { count: usize, savings: u64 },
170 ConsolidateSections { sections: Vec<String>, savings: u64 },
172 ConciseLanguage { before: String, after: String, savings: u64 },
174}
175
176impl PromptOptimizationStrategy {
177 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 async fn analyze_prompt(&self, input: &DecisionInput) -> DecisionResult<PromptAnalysis> {
192 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 for rec in prompt_recommendations {
203 if let Some(redundancy) = self.detect_redundancy(rec) {
205 estimated_savings += redundancy.1;
206 opportunities.push(redundancy.0);
207 }
208
209 if let Some(verbose) = self.detect_verbose_instructions(rec) {
211 estimated_savings += verbose.1;
212 opportunities.push(verbose.0);
213 }
214
215 if let Some(examples) = self.detect_unnecessary_examples(rec) {
217 estimated_savings += examples.1;
218 opportunities.push(examples.0);
219 }
220
221 if let Some(repetitive) = self.detect_repetitive_sections(rec) {
223 estimated_savings += repetitive.1;
224 opportunities.push(repetitive.0);
225 }
226 }
227
228 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 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 fn detect_redundancy(
254 &self,
255 _rec: &crate::analyzer::types::Recommendation,
256 ) -> Option<(OptimizationOpportunity, u64)> {
257 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 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 fn detect_verbose_instructions(
282 &self,
283 _rec: &crate::analyzer::types::Recommendation,
284 ) -> Option<(OptimizationOpportunity, u64)> {
285 Some((
287 OptimizationOpportunity::SimplifyInstructions {
288 section: "main_instructions".to_string(),
289 savings: 15,
290 },
291 15,
292 ))
293 }
294
295 fn detect_unnecessary_examples(
297 &self,
298 _rec: &crate::analyzer::types::Recommendation,
299 ) -> Option<(OptimizationOpportunity, u64)> {
300 Some((
302 OptimizationOpportunity::RemoveExamples {
303 count: 2,
304 savings: 50,
305 },
306 50,
307 ))
308 }
309
310 fn detect_repetitive_sections(
312 &self,
313 _rec: &crate::analyzer::types::Recommendation,
314 ) -> Option<(OptimizationOpportunity, u64)> {
315 Some((
317 OptimizationOpportunity::ConsolidateSections {
318 sections: vec!["intro".to_string(), "outro".to_string()],
319 savings: 30,
320 },
321 30,
322 ))
323 }
324
325 fn calculate_analysis_confidence(
327 &self,
328 opportunities: &[OptimizationOpportunity],
329 input: &DecisionInput,
330 ) -> f64 {
331 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 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 (data_confidence + opportunity_confidence) / 2.0
351 }
352
353 fn estimate_quality_impact(&self, analysis: &PromptAnalysis) -> f64 {
355 let base_impact = -0.05; let mut adjustment = 0.0;
360 for opp in &analysis.opportunities {
361 match opp {
362 OptimizationOpportunity::RemoveRedundancy { .. } => {
363 adjustment += 0.01; }
365 OptimizationOpportunity::SimplifyInstructions { .. } => {
366 adjustment -= 0.02; }
368 OptimizationOpportunity::RemoveExamples { .. } => {
369 adjustment -= 0.03; }
371 OptimizationOpportunity::ConsolidateSections { .. } => {
372 adjustment += 0.005; }
374 OptimizationOpportunity::ConciseLanguage { .. } => {
375 adjustment += 0.01; }
377 }
378 }
379
380 base_impact + adjustment
381 }
382
383 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 let expected_quality_impact = self.estimate_quality_impact(&analysis);
394
395 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), 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), confidence: analysis.confidence,
408 };
409
410 let mut safety_checks = Vec::new();
412
413 let min_quality = criteria.min_quality_score.unwrap_or(3.5);
415 let projected_quality = 4.0 + expected_quality_impact; 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 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 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 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 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 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 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, safety_checks,
581 rollback_plan: Some(rollback_plan),
582 metadata,
583 })
584 }
585
586 fn analyze_test_results(&self, test: &ABTest) -> TestAnalysis {
588 let control = &test.observations.control;
589 let treatment = &test.observations.treatment;
590
591 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 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 let is_statistically_significant =
621 token_significance > 0.95 || quality_significance > 0.95;
622
623 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 fn calculate_significance(&self, n1: u64, n2: u64, mean1: f64, mean2: f64) -> f64 {
650 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
683enum TestRecommendation {
684 ContinueTesting,
686 Rollout,
688 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 if !self.config.enabled {
705 return false;
706 }
707
708 let has_prompt_recommendations = input
710 .recommendations
711 .iter()
712 .any(|r| r.tags.contains(&"prompt".to_string()));
713
714 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 {
729 let mut state = self.state.write().await;
730 state.stats.total_evaluations += 1;
731 }
732
733 let analysis = self.analyze_prompt(input).await?;
735
736 if analysis.estimated_token_reduction_pct < self.config.min_token_reduction_pct {
738 return Ok(decisions); }
740
741 if analysis.confidence < criteria.min_confidence {
742 return Ok(decisions); }
744
745 let decision = self.create_ab_test_decision(analysis, criteria)?;
747
748 {
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 if decision.decision_type != DecisionType::OptimizePrompt {
765 return Err(DecisionError::ValidationFailed(
766 "Decision type mismatch".to_string(),
767 ));
768 }
769
770 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 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 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 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 let (should_update_stats, update_info) = {
815 if let Some(test) = state.active_tests.get_mut(test_id) {
816 if outcome.success {
818 if let Some(actual_impact) = &outcome.actual_impact {
820 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 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 let analysis = self.analyze_test_results(test);
845
846 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 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 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 }; if should_update_stats {
909 if let Some((update_type, is_complete, completed_test)) = update_info {
910 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 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; 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; 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, 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; 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 let sig1 = strategy.calculate_significance(1000, 1000, 100.0, 80.0);
1198 assert!(sig1 > 0.5);
1199
1200 let sig2 = strategy.calculate_significance(10, 10, 100.0, 80.0);
1202 assert!(sig2 < sig1);
1203
1204 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, quality_ratings: vec![4.15; 100],
1239 avg_quality: 4.15, 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 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}