1use async_trait::async_trait;
58use chrono::{DateTime, Duration as ChronoDuration, Utc};
59use serde::{Deserialize, Serialize};
60use std::collections::HashMap;
61use std::sync::Arc;
62use std::time::Instant;
63use tokio::sync::RwLock;
64use tracing::{debug, info, warn};
65use uuid::Uuid;
66
67use super::stats::{
68 calculate_percentiles, mean, percentage_change, std_dev, CircularBuffer,
69 ExponentialMovingAverage,
70};
71use super::traits::{Analyzer, AnalyzerConfig, AnalyzerError, AnalyzerResult, AnalyzerState};
72use super::types::{
73 Action, ActionType, Alert, AlertStatus, AnalysisReport, AnalyzerEvent, AnalyzerStats,
74 Confidence, Evidence, EvidenceType, FeedbackType, Impact, ImpactMetric, Insight, InsightCategory, Priority,
75 Recommendation, ReportSummary, RiskLevel, Severity,
76};
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct QualityAnalyzerConfig {
81 #[serde(flatten)]
83 pub base: AnalyzerConfig,
84
85 pub sla_p95_ms: f64,
87
88 pub sla_p99_ms: f64,
90
91 pub min_success_rate_pct: f64,
93
94 pub min_avg_rating: f64,
96
97 pub error_rate_warning_pct: f64,
99
100 pub error_rate_critical_pct: f64,
102
103 pub min_samples_for_quality: usize,
105
106 pub quality_degradation_threshold_pct: f64,
108
109 pub feedback_window_size: usize,
111}
112
113impl Default for QualityAnalyzerConfig {
114 fn default() -> Self {
115 Self {
116 base: AnalyzerConfig {
117 id: "quality-analyzer".to_string(),
118 enabled: true,
119 event_filter: vec![
120 "response".to_string(),
121 "feedback".to_string(),
122 "request".to_string(),
123 ],
124 window_duration: std::time::Duration::from_secs(300), slide_interval: std::time::Duration::from_secs(60), sample_rate: 1.0,
127 max_buffer_size: 10000,
128 max_memory_bytes: 100 * 1024 * 1024, analysis_interval: std::time::Duration::from_secs(60),
130 enable_tracing: true,
131 },
132 sla_p95_ms: 1000.0,
133 sla_p99_ms: 2000.0,
134 min_success_rate_pct: 99.5,
135 min_avg_rating: 4.0,
136 error_rate_warning_pct: 1.0,
137 error_rate_critical_pct: 5.0,
138 min_samples_for_quality: 10,
139 quality_degradation_threshold_pct: 10.0,
140 feedback_window_size: 100,
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
147struct QualityMetrics {
148 latencies: CircularBuffer<f64>,
150
151 total_requests: u64,
153 successful_requests: u64,
154 failed_requests: u64,
155
156 error_counts: HashMap<String, u64>,
158
159 ratings: CircularBuffer<f64>,
161
162 positive_feedback: u64,
164 negative_feedback: u64,
165 neutral_feedback: u64,
166
167 completeness_scores: CircularBuffer<f64>,
169
170 success_rate_ema: ExponentialMovingAverage,
172 rating_ema: ExponentialMovingAverage,
173
174 baseline_success_rate: Option<f64>,
176 baseline_avg_rating: Option<f64>,
177 baseline_p95_latency: Option<f64>,
178
179 sla_violations_p95: u64,
181 sla_violations_p99: u64,
182 sla_checks: u64,
183
184 model_metrics: HashMap<String, ModelQualityMetrics>,
186
187 last_quality_check: DateTime<Utc>,
189 first_event_time: Option<DateTime<Utc>>,
190}
191
192#[derive(Debug, Clone)]
193struct ModelQualityMetrics {
194 total_requests: u64,
195 successful_requests: u64,
196 avg_rating: f64,
197 rating_count: u64,
198}
199
200impl ModelQualityMetrics {
201 fn new() -> Self {
202 Self {
203 total_requests: 0,
204 successful_requests: 0,
205 avg_rating: 0.0,
206 rating_count: 0,
207 }
208 }
209
210 fn success_rate(&self) -> f64 {
211 if self.total_requests == 0 {
212 return 100.0;
213 }
214 (self.successful_requests as f64 / self.total_requests as f64) * 100.0
215 }
216}
217
218impl QualityMetrics {
219 fn new(config: &QualityAnalyzerConfig) -> Self {
220 Self {
221 latencies: CircularBuffer::new(1000),
222 total_requests: 0,
223 successful_requests: 0,
224 failed_requests: 0,
225 error_counts: HashMap::new(),
226 ratings: CircularBuffer::new(config.feedback_window_size),
227 positive_feedback: 0,
228 negative_feedback: 0,
229 neutral_feedback: 0,
230 completeness_scores: CircularBuffer::new(1000),
231 success_rate_ema: ExponentialMovingAverage::new(0.1),
232 rating_ema: ExponentialMovingAverage::new(0.1),
233 baseline_success_rate: None,
234 baseline_avg_rating: None,
235 baseline_p95_latency: None,
236 sla_violations_p95: 0,
237 sla_violations_p99: 0,
238 sla_checks: 0,
239 model_metrics: HashMap::new(),
240 last_quality_check: Utc::now(),
241 first_event_time: None,
242 }
243 }
244
245 fn current_success_rate(&self) -> f64 {
246 if self.total_requests == 0 {
247 return 100.0;
248 }
249 (self.successful_requests as f64 / self.total_requests as f64) * 100.0
250 }
251
252 fn current_error_rate(&self) -> f64 {
253 100.0 - self.current_success_rate()
254 }
255
256 fn average_rating(&self) -> Option<f64> {
257 let values = self.ratings.to_vec();
258 if values.is_empty() {
259 return None;
260 }
261 Some(mean(&values))
262 }
263
264 fn average_completeness(&self) -> Option<f64> {
265 let values = self.completeness_scores.to_vec();
266 if values.is_empty() {
267 return None;
268 }
269 Some(mean(&values))
270 }
271
272 fn sla_compliance_rate(&self) -> f64 {
273 if self.sla_checks == 0 {
274 return 100.0;
275 }
276 let violations = self.sla_violations_p95 + self.sla_violations_p99;
277 ((self.sla_checks - violations) as f64 / self.sla_checks as f64) * 100.0
278 }
279}
280
281pub struct QualityAnalyzer {
283 config: QualityAnalyzerConfig,
284 state: AnalyzerState,
285 metrics: Arc<RwLock<QualityMetrics>>,
286 stats: Arc<RwLock<AnalyzerStats>>,
287 start_time: Instant,
288 last_report_time: Arc<RwLock<Instant>>,
289}
290
291impl QualityAnalyzer {
292 pub fn new(config: QualityAnalyzerConfig) -> Self {
294 let metrics = QualityMetrics::new(&config);
295
296 Self {
297 config,
298 state: AnalyzerState::Initialized,
299 metrics: Arc::new(RwLock::new(metrics)),
300 stats: Arc::new(RwLock::new(AnalyzerStats::default())),
301 start_time: Instant::now(),
302 last_report_time: Arc::new(RwLock::new(Instant::now())),
303 }
304 }
305
306 pub fn with_defaults() -> Self {
308 Self::new(QualityAnalyzerConfig::default())
309 }
310
311 async fn process_response_event(
313 &mut self,
314 timestamp: DateTime<Utc>,
315 model: String,
316 latency_ms: u64,
317 success: bool,
318 error: Option<String>,
319 ) -> AnalyzerResult<()> {
320 let mut metrics = self.metrics.write().await;
321
322 if metrics.first_event_time.is_none() {
324 metrics.first_event_time = Some(timestamp);
325 }
326
327 metrics.total_requests += 1;
329 if success {
330 metrics.successful_requests += 1;
331 } else {
332 metrics.failed_requests += 1;
333 }
334
335 if let Some(err) = error {
337 *metrics.error_counts.entry(err).or_insert(0) += 1;
338 }
339
340 let latency_f64 = latency_ms as f64;
342 metrics.latencies.push(latency_f64);
343
344 let success_val = if success { 1.0 } else { 0.0 };
346 metrics.success_rate_ema.update(success_val);
347
348 let model_metric = metrics
350 .model_metrics
351 .entry(model)
352 .or_insert_with(ModelQualityMetrics::new);
353 model_metric.total_requests += 1;
354 if success {
355 model_metric.successful_requests += 1;
356 }
357
358 if metrics.latencies.len() >= 10 {
360 let latencies_vec = metrics.latencies.to_vec();
361 let percentiles = calculate_percentiles(latencies_vec.clone(), &[0.95, 0.99]);
362
363 metrics.sla_checks += 1;
364
365 if let Some((_, p95)) = percentiles.first() {
366 if *p95 > self.config.sla_p95_ms {
367 metrics.sla_violations_p95 += 1;
368 }
369 }
370
371 if let Some((_, p99)) = percentiles.get(1) {
372 if *p99 > self.config.sla_p99_ms {
373 metrics.sla_violations_p99 += 1;
374 }
375 }
376 }
377
378 if metrics.total_requests == 100 && metrics.baseline_success_rate.is_none() {
380 metrics.baseline_success_rate = Some(metrics.current_success_rate());
381
382 if metrics.latencies.len() >= 10 {
383 let latencies_vec = metrics.latencies.to_vec();
384 let percentiles = calculate_percentiles(latencies_vec, &[0.95]);
385 if let Some((_, p95)) = percentiles.first() {
386 metrics.baseline_p95_latency = Some(*p95);
387 }
388 }
389 }
390
391 Ok(())
392 }
393
394 async fn process_feedback_event(
396 &mut self,
397 timestamp: DateTime<Utc>,
398 model: String,
399 rating: Option<f64>,
400 feedback_type: Option<String>,
401 ) -> AnalyzerResult<()> {
402 let mut metrics = self.metrics.write().await;
403
404 if let Some(r) = rating {
406 let normalized_rating = r.max(0.0).min(5.0);
408 metrics.ratings.push(normalized_rating);
409 metrics.rating_ema.update(normalized_rating);
410
411 if let Some(model_metric) = metrics.model_metrics.get_mut(&model) {
413 let count = model_metric.rating_count as f64;
414 model_metric.avg_rating =
415 (model_metric.avg_rating * count + normalized_rating) / (count + 1.0);
416 model_metric.rating_count += 1;
417 }
418
419 if normalized_rating >= 4.0 {
421 metrics.positive_feedback += 1;
422 } else if normalized_rating >= 3.0 {
423 metrics.neutral_feedback += 1;
424 } else {
425 metrics.negative_feedback += 1;
426 }
427
428 if metrics.ratings.len() == self.config.feedback_window_size
430 && metrics.baseline_avg_rating.is_none()
431 {
432 metrics.baseline_avg_rating = metrics.average_rating();
433 }
434 }
435
436 Ok(())
437 }
438
439 async fn generate_insights(&self) -> Vec<Insight> {
441 let metrics = self.metrics.read().await;
442 let mut insights = Vec::new();
443
444 if metrics.total_requests < self.config.min_samples_for_quality as u64 {
446 return insights;
447 }
448
449 let error_rate = metrics.current_error_rate();
451 if error_rate >= self.config.error_rate_critical_pct {
452 insights.push(self.create_error_rate_insight(
453 error_rate,
454 Severity::Critical,
455 &metrics,
456 ));
457 } else if error_rate >= self.config.error_rate_warning_pct {
458 insights.push(self.create_error_rate_insight(
459 error_rate,
460 Severity::Warning,
461 &metrics,
462 ));
463 }
464
465 let success_rate = metrics.current_success_rate();
467 if success_rate < self.config.min_success_rate_pct {
468 insights.push(self.create_low_success_rate_insight(success_rate, &metrics));
469 }
470
471 let sla_compliance = metrics.sla_compliance_rate();
473 if sla_compliance < 95.0 {
474 insights.push(self.create_sla_violation_insight(sla_compliance, &metrics));
475 }
476
477 if let Some(avg_rating) = metrics.average_rating() {
479 if avg_rating < self.config.min_avg_rating {
480 insights.push(self.create_low_rating_insight(avg_rating, &metrics));
481 }
482 }
483
484 if let Some(baseline_sr) = metrics.baseline_success_rate {
486 let degradation = percentage_change(baseline_sr, success_rate);
487 if degradation.abs() >= self.config.quality_degradation_threshold_pct {
488 insights.push(self.create_degradation_insight(degradation, &metrics));
489 }
490 }
491
492 insights
493 }
494
495 async fn generate_recommendations(&self) -> Vec<Recommendation> {
497 let metrics = self.metrics.read().await;
498 let mut recommendations = Vec::new();
499
500 if metrics.total_requests < self.config.min_samples_for_quality as u64 {
502 return recommendations;
503 }
504
505 let error_rate = metrics.current_error_rate();
506 let success_rate = metrics.current_success_rate();
507
508 if error_rate >= self.config.error_rate_warning_pct {
510 recommendations.push(Recommendation {
511 id: Uuid::new_v4().to_string(),
512 analyzer: "quality-analyzer".to_string(),
513 timestamp: Utc::now(),
514 priority: if error_rate >= self.config.error_rate_critical_pct {
515 Priority::Urgent
516 } else {
517 Priority::High
518 },
519 title: "Investigate and fix error sources".to_string(),
520 description: format!(
521 "Error rate is {:.2}%. Review error logs and implement fixes for top error types: {}",
522 error_rate,
523 self.format_top_errors(&metrics)
524 ),
525 action: Action {
526 action_type: ActionType::Investigate,
527 instructions: "Review error logs and implement error handling improvements"
528 .to_string(),
529 parameters: Default::default(),
530 estimated_effort: Some(4.0),
531 risk_level: RiskLevel::Low,
532 },
533 expected_impact: Impact {
534 quality: Some(ImpactMetric {
535 name: "error_rate".to_string(),
536 current: error_rate,
537 expected: self.config.error_rate_warning_pct / 2.0,
538 unit: "%".to_string(),
539 improvement_pct: ((error_rate - self.config.error_rate_warning_pct / 2.0) / error_rate) * 100.0,
540 }),
541 performance: None,
542 cost: None,
543 description: format!("Reduce error rate from {:.2}% to {:.2}%", error_rate, self.config.error_rate_warning_pct / 2.0),
544 },
545 confidence: Confidence::new(0.8),
546 related_insights: vec![],
547 tags: vec!["quality".to_string(), "errors".to_string()],
548 });
549 }
550
551 if success_rate < self.config.min_success_rate_pct {
553 recommendations.push(Recommendation {
554 id: Uuid::new_v4().to_string(),
555 analyzer: "quality-analyzer".to_string(),
556 timestamp: Utc::now(),
557 priority: Priority::High,
558 title: "Improve request success rate".to_string(),
559 description: format!(
560 "Success rate is {:.2}%, below target of {:.2}%. Implement retry logic and better error handling.",
561 success_rate, self.config.min_success_rate_pct
562 ),
563 action: Action {
564 action_type: ActionType::ConfigChange,
565 instructions: "Enable exponential backoff retry logic".to_string(),
566 parameters: Default::default(),
567 estimated_effort: Some(2.0),
568 risk_level: RiskLevel::Low,
569 },
570 expected_impact: Impact {
571 quality: Some(ImpactMetric {
572 name: "success_rate".to_string(),
573 current: success_rate,
574 expected: self.config.min_success_rate_pct,
575 unit: "%".to_string(),
576 improvement_pct: ((self.config.min_success_rate_pct - success_rate) / success_rate) * 100.0,
577 }),
578 performance: None,
579 cost: None,
580 description: format!("Improve success rate from {:.2}% to {:.2}%", success_rate, self.config.min_success_rate_pct),
581 },
582 confidence: Confidence::new(0.8),
583 related_insights: vec![],
584 tags: vec!["quality".to_string(), "reliability".to_string()],
585 });
586 }
587
588 if metrics.sla_compliance_rate() < 95.0 {
590 recommendations.push(Recommendation {
591 id: Uuid::new_v4().to_string(),
592 analyzer: "quality-analyzer".to_string(),
593 timestamp: Utc::now(),
594 priority: Priority::High,
595 title: "Improve SLA compliance".to_string(),
596 description: format!(
597 "SLA compliance is {:.2}%. Optimize performance to meet P95 ({:.0}ms) and P99 ({:.0}ms) targets.",
598 metrics.sla_compliance_rate(),
599 self.config.sla_p95_ms,
600 self.config.sla_p99_ms
601 ),
602 action: Action {
603 action_type: ActionType::Optimize,
604 instructions: "Enable caching and optimize slow queries".to_string(),
605 parameters: Default::default(),
606 estimated_effort: Some(8.0),
607 risk_level: RiskLevel::Medium,
608 },
609 expected_impact: Impact {
610 performance: Some(ImpactMetric {
611 name: "p95_latency".to_string(),
612 current: metrics.sla_violations_p95 as f64,
613 expected: 0.0,
614 unit: "violations".to_string(),
615 improvement_pct: 100.0,
616 }),
617 cost: None,
618 quality: None,
619 description: "Reduce P95 latency SLA violations to zero".to_string(),
620 },
621 confidence: Confidence::new(0.6),
622 related_insights: vec![],
623 tags: vec!["quality".to_string(), "sla".to_string()],
624 });
625 }
626
627 if let Some(avg_rating) = metrics.average_rating() {
629 if avg_rating < self.config.min_avg_rating {
630 recommendations.push(Recommendation {
631 id: Uuid::new_v4().to_string(),
632 analyzer: "quality-analyzer".to_string(),
633 timestamp: Utc::now(),
634 priority: Priority::Medium,
635 title: "Improve response quality based on user feedback".to_string(),
636 description: format!(
637 "Average rating is {:.2}/5.0, below target of {:.2}. Review negative feedback and improve response quality.",
638 avg_rating, self.config.min_avg_rating
639 ),
640 action: Action {
641 action_type: ActionType::Optimize,
642 instructions: "Analyze negative feedback patterns and adjust prompts"
643 .to_string(),
644 parameters: Default::default(),
645 estimated_effort: Some(6.0),
646 risk_level: RiskLevel::Medium,
647 },
648 expected_impact: Impact {
649 quality: Some(ImpactMetric {
650 name: "quality_score".to_string(),
651 current: avg_rating,
652 expected: self.config.min_avg_rating,
653 unit: "rating".to_string(),
654 improvement_pct: ((self.config.min_avg_rating - avg_rating) / avg_rating) * 100.0,
655 }),
656 performance: None,
657 cost: None,
658 description: format!("Improve quality score from {:.2} to {:.2}", avg_rating, self.config.min_avg_rating),
659 },
660 confidence: Confidence::new(0.6),
661 related_insights: vec![],
662 tags: vec!["quality".to_string(), "feedback".to_string()],
663 });
664 }
665 }
666
667 recommendations
668 }
669
670 fn create_error_rate_insight(
671 &self,
672 error_rate: f64,
673 severity: Severity,
674 metrics: &QualityMetrics,
675 ) -> Insight {
676 Insight {
677 id: Uuid::new_v4().to_string(),
678 analyzer: "quality-analyzer".to_string(),
679 timestamp: Utc::now(),
680 severity,
681 confidence: Confidence::new(0.8),
682 title: format!("High error rate: {:.2}%", error_rate),
683 description: format!(
684 "Error rate is {:.2}%, which exceeds the {} threshold. Total failures: {}. Top errors: {}",
685 error_rate,
686 if severity == Severity::Critical { "critical" } else { "warning" },
687 metrics.failed_requests,
688 self.format_top_errors(metrics)
689 ),
690 category: InsightCategory::Quality,
691 evidence: vec![
692 Evidence {
693 evidence_type: EvidenceType::Statistical,
694 description: format!("Error rate: {:.2}%", error_rate),
695 data: serde_json::json!({"value": error_rate}),
696 timestamp: Utc::now(),
697 },
698 Evidence {
699 evidence_type: EvidenceType::Statistical,
700 description: format!("Failed requests: {}", metrics.failed_requests),
701 data: serde_json::json!({"value": metrics.failed_requests as f64}),
702 timestamp: Utc::now(),
703 },
704 ],
705 metrics: {
706 let mut m = HashMap::new();
707 m.insert("error_rate_pct".to_string(), error_rate);
708 m.insert("failed_requests".to_string(), metrics.failed_requests as f64);
709 m
710 },
711 tags: vec![
712 "quality".to_string(),
713 "errors".to_string(),
714 "reliability".to_string(),
715 ],
716 }
717 }
718
719 fn create_low_success_rate_insight(
720 &self,
721 success_rate: f64,
722 metrics: &QualityMetrics,
723 ) -> Insight {
724 Insight {
725 id: Uuid::new_v4().to_string(),
726 analyzer: "quality-analyzer".to_string(),
727 timestamp: Utc::now(),
728 severity: Severity::Warning,
729 confidence: Confidence::new(0.8),
730 title: format!(
731 "Success rate {:.2}% below target {:.2}%",
732 success_rate, self.config.min_success_rate_pct
733 ),
734 description: format!(
735 "Current success rate is {:.2}%, which is below the target of {:.2}%. {} successful out of {} total requests.",
736 success_rate,
737 self.config.min_success_rate_pct,
738 metrics.successful_requests,
739 metrics.total_requests
740 ),
741 category: InsightCategory::Quality,
742 evidence: vec![Evidence {
743 evidence_type: EvidenceType::Statistical,
744 description: format!("Success rate: {:.2}%", success_rate),
745 data: serde_json::json!({"value": success_rate}),
746 timestamp: Utc::now(),
747 }],
748 metrics: {
749 let mut m = HashMap::new();
750 m.insert("success_rate_pct".to_string(), success_rate);
751 m.insert(
752 "successful_requests".to_string(),
753 metrics.successful_requests as f64,
754 );
755 m.insert("total_requests".to_string(), metrics.total_requests as f64);
756 m
757 },
758 tags: vec!["quality".to_string(), "success-rate".to_string()],
759 }
760 }
761
762 fn create_sla_violation_insight(
763 &self,
764 sla_compliance: f64,
765 metrics: &QualityMetrics,
766 ) -> Insight {
767 Insight {
768 id: Uuid::new_v4().to_string(),
769 analyzer: "quality-analyzer".to_string(),
770 timestamp: Utc::now(),
771 severity: if sla_compliance < 90.0 {
772 Severity::Critical
773 } else {
774 Severity::Warning
775 },
776 confidence: Confidence::new(0.8),
777 title: format!("SLA compliance at {:.1}%", sla_compliance),
778 description: format!(
779 "SLA compliance is {:.1}%, with {} P95 violations and {} P99 violations. Targets: P95 < {:.0}ms, P99 < {:.0}ms",
780 sla_compliance,
781 metrics.sla_violations_p95,
782 metrics.sla_violations_p99,
783 self.config.sla_p95_ms,
784 self.config.sla_p99_ms
785 ),
786 category: InsightCategory::Quality,
787 evidence: vec![
788 Evidence {
789 evidence_type: EvidenceType::Statistical,
790 description: format!("SLA compliance: {:.1}%", sla_compliance),
791 data: serde_json::json!({"value": sla_compliance}),
792 timestamp: Utc::now(),
793 },
794 Evidence {
795 evidence_type: EvidenceType::Statistical,
796 description: format!("P95 violations: {}", metrics.sla_violations_p95),
797 data: serde_json::json!({"value": metrics.sla_violations_p95 as f64}),
798 timestamp: Utc::now(),
799 },
800 ],
801 metrics: {
802 let mut m = HashMap::new();
803 m.insert("sla_compliance_pct".to_string(), sla_compliance);
804 m.insert(
805 "p95_violations".to_string(),
806 metrics.sla_violations_p95 as f64,
807 );
808 m.insert(
809 "p99_violations".to_string(),
810 metrics.sla_violations_p99 as f64,
811 );
812 m
813 },
814 tags: vec!["quality".to_string(), "sla".to_string()],
815 }
816 }
817
818 fn create_low_rating_insight(&self, avg_rating: f64, metrics: &QualityMetrics) -> Insight {
819 let total_feedback =
820 metrics.positive_feedback + metrics.negative_feedback + metrics.neutral_feedback;
821
822 Insight {
823 id: Uuid::new_v4().to_string(),
824 analyzer: "quality-analyzer".to_string(),
825 timestamp: Utc::now(),
826 severity: if avg_rating < 3.0 {
827 Severity::Error
828 } else {
829 Severity::Warning
830 },
831 confidence: Confidence::new(0.6),
832 title: format!(
833 "Average rating {:.2}/5.0 below target {:.2}/5.0",
834 avg_rating, self.config.min_avg_rating
835 ),
836 description: format!(
837 "Average user rating is {:.2}/5.0, below target of {:.2}/5.0. {} positive, {} neutral, {} negative (total: {})",
838 avg_rating,
839 self.config.min_avg_rating,
840 metrics.positive_feedback,
841 metrics.neutral_feedback,
842 metrics.negative_feedback,
843 total_feedback
844 ),
845 category: InsightCategory::Quality,
846 evidence: vec![Evidence {
847 evidence_type: EvidenceType::Statistical,
848 description: format!("Average rating: {:.2}/5.0", avg_rating),
849 data: serde_json::json!({"value": avg_rating}),
850 timestamp: Utc::now(),
851 }],
852 metrics: {
853 let mut m = HashMap::new();
854 m.insert("avg_rating".to_string(), avg_rating);
855 m.insert("positive_feedback".to_string(), metrics.positive_feedback as f64);
856 m.insert("negative_feedback".to_string(), metrics.negative_feedback as f64);
857 m.insert("neutral_feedback".to_string(), metrics.neutral_feedback as f64);
858 m
859 },
860 tags: vec!["quality".to_string(), "feedback".to_string()],
861 }
862 }
863
864 fn create_degradation_insight(&self, degradation: f64, metrics: &QualityMetrics) -> Insight {
865 Insight {
866 id: Uuid::new_v4().to_string(),
867 analyzer: "quality-analyzer".to_string(),
868 timestamp: Utc::now(),
869 severity: Severity::Warning,
870 confidence: Confidence::new(0.6),
871 title: format!("Quality degradation detected: {:.1}%", degradation.abs()),
872 description: format!(
873 "Success rate has degraded by {:.1}% from baseline. Current: {:.2}%, Baseline: {:.2}%",
874 degradation.abs(),
875 metrics.current_success_rate(),
876 metrics.baseline_success_rate.unwrap_or(0.0)
877 ),
878 category: InsightCategory::Quality,
879 evidence: vec![Evidence {
880 evidence_type: EvidenceType::TimeSeries,
881 description: format!("Quality degradation: {:.1}%", degradation.abs()),
882 data: serde_json::json!({"value": degradation}),
883 timestamp: Utc::now(),
884 }],
885 metrics: {
886 let mut m = HashMap::new();
887 m.insert("degradation_pct".to_string(), degradation);
888 m.insert(
889 "current_success_rate".to_string(),
890 metrics.current_success_rate(),
891 );
892 m.insert(
893 "baseline_success_rate".to_string(),
894 metrics.baseline_success_rate.unwrap_or(0.0),
895 );
896 m
897 },
898 tags: vec!["quality".to_string(), "degradation".to_string()],
899 }
900 }
901
902 fn format_top_errors(&self, metrics: &QualityMetrics) -> String {
903 let mut errors: Vec<_> = metrics.error_counts.iter().collect();
904 errors.sort_by(|a, b| b.1.cmp(a.1));
905
906 errors
907 .iter()
908 .take(3)
909 .map(|(err, count)| format!("{} ({})", err, count))
910 .collect::<Vec<_>>()
911 .join(", ")
912 }
913}
914
915#[async_trait]
916impl Analyzer for QualityAnalyzer {
917 fn name(&self) -> &str {
918 "quality-analyzer"
919 }
920
921 fn config(&self) -> &AnalyzerConfig {
922 &self.config.base
923 }
924
925 fn state(&self) -> AnalyzerState {
926 self.state.clone()
927 }
928
929 async fn start(&mut self) -> AnalyzerResult<()> {
930 if self.state == AnalyzerState::Running {
931 return Err(AnalyzerError::InvalidStateTransition {
932 from: self.state.as_str().to_string(),
933 to: AnalyzerState::Starting.as_str().to_string(),
934 });
935 }
936
937 info!("Starting Quality Analyzer");
938 self.state = AnalyzerState::Starting;
939
940 self.state = AnalyzerState::Running;
942 info!("Quality Analyzer started successfully");
943
944 Ok(())
945 }
946
947 async fn stop(&mut self) -> AnalyzerResult<()> {
948 if self.state != AnalyzerState::Running {
949 return Err(AnalyzerError::InvalidStateTransition {
950 from: self.state.as_str().to_string(),
951 to: AnalyzerState::Draining.as_str().to_string(),
952 });
953 }
954
955 info!("Stopping Quality Analyzer");
956 self.state = AnalyzerState::Draining;
957
958 self.state = AnalyzerState::Stopped;
960 info!("Quality Analyzer stopped");
961
962 Ok(())
963 }
964
965 async fn process_event(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
966 if self.state != AnalyzerState::Running {
967 return Err(AnalyzerError::NotRunning);
968 }
969
970 let mut stats = self.stats.write().await;
972 stats.events_processed += 1;
973 drop(stats);
975
976 match event {
978 AnalyzerEvent::Response {
979 timestamp,
980 model,
981 latency_ms,
982 success,
983 error,
984 ..
985 } => {
986 self.process_response_event(timestamp, model, latency_ms, success, error)
987 .await?;
988 }
989 AnalyzerEvent::Feedback {
990 timestamp,
991 request_id,
992 rating,
993 feedback_type,
994 metadata,
995 ..
996 } => {
997 let model = metadata
999 .get("model")
1000 .unwrap_or(&request_id)
1001 .to_string();
1002
1003 let feedback_str = match feedback_type {
1004 FeedbackType::Positive => "positive",
1005 FeedbackType::Negative => "negative",
1006 FeedbackType::Rating => "rating",
1007 FeedbackType::Quality => "quality",
1008 FeedbackType::Custom => "custom",
1009 };
1010 self.process_feedback_event(timestamp, model, Some(rating), Some(feedback_str.to_string()))
1011 .await?;
1012 }
1013 _ => {
1014 debug!("Ignoring event type not relevant to quality analysis");
1015 }
1016 }
1017
1018 Ok(())
1019 }
1020
1021 async fn generate_report(&self) -> AnalyzerResult<AnalysisReport> {
1022 let metrics = self.metrics.read().await;
1023 let stats = self.stats.read().await;
1024
1025 let now = Utc::now();
1026 let period_start = metrics
1027 .first_event_time
1028 .unwrap_or_else(|| now - ChronoDuration::minutes(5));
1029
1030 let insights = self.generate_insights().await;
1031 let recommendations = self.generate_recommendations().await;
1032
1033 let mut report_metrics = HashMap::new();
1035 report_metrics.insert(
1036 "success_rate_pct".to_string(),
1037 metrics.current_success_rate(),
1038 );
1039 report_metrics.insert("error_rate_pct".to_string(), metrics.current_error_rate());
1040 report_metrics.insert("total_requests".to_string(), metrics.total_requests as f64);
1041 report_metrics.insert(
1042 "successful_requests".to_string(),
1043 metrics.successful_requests as f64,
1044 );
1045 report_metrics.insert(
1046 "failed_requests".to_string(),
1047 metrics.failed_requests as f64,
1048 );
1049
1050 if let Some(avg_rating) = metrics.average_rating() {
1051 report_metrics.insert("avg_rating".to_string(), avg_rating);
1052 }
1053
1054 report_metrics.insert(
1055 "sla_compliance_pct".to_string(),
1056 metrics.sla_compliance_rate(),
1057 );
1058 report_metrics.insert(
1059 "positive_feedback".to_string(),
1060 metrics.positive_feedback as f64,
1061 );
1062 report_metrics.insert(
1063 "negative_feedback".to_string(),
1064 metrics.negative_feedback as f64,
1065 );
1066
1067 let critical_issues_count = insights
1068 .iter()
1069 .filter(|i| i.severity == Severity::Critical)
1070 .count();
1071 let warnings_count = insights
1072 .iter()
1073 .filter(|i| i.severity == Severity::Warning)
1074 .count();
1075
1076 let summary = ReportSummary {
1077 events_processed: stats.events_processed,
1078 events_per_second: 0.0, insights_count: insights.len() as u64,
1080 recommendations_count: recommendations.len() as u64,
1081 alerts_count: 0,
1082 analysis_duration_ms: 0, highlights: vec![
1084 format!("Critical issues: {}", critical_issues_count),
1085 format!("Warnings: {}", warnings_count),
1086 ],
1087 };
1088
1089 Ok(AnalysisReport {
1090 analyzer: self.name().to_string(),
1091 timestamp: now,
1092 period_start,
1093 period_end: now,
1094 summary,
1095 insights,
1096 recommendations,
1097 alerts: vec![],
1098 metrics: report_metrics,
1099 })
1100 }
1101
1102 fn get_stats(&self) -> AnalyzerStats {
1103 AnalyzerStats {
1106 analyzer: "QualityAnalyzer".to_string(),
1107 events_processed: 0,
1108 events_dropped: 0,
1109 insights_generated: 0,
1110 recommendations_generated: 0,
1111 alerts_triggered: 0, analysis_time_ms: 0,
1113 avg_analysis_time_us: 0.0, memory_usage_bytes: 0,
1115 error_count: 0, last_error: None,
1117 uptime_seconds: 0,
1118 }
1119 }
1120
1121 async fn reset(&mut self) -> AnalyzerResult<()> {
1122 info!("Resetting Quality Analyzer");
1123
1124 let mut metrics = self.metrics.write().await;
1125 *metrics = QualityMetrics::new(&self.config);
1126
1127 let mut stats = self.stats.write().await;
1128 *stats = AnalyzerStats::default();
1129
1130 Ok(())
1131 }
1132
1133 async fn health_check(&self) -> AnalyzerResult<()> {
1134 if self.state != AnalyzerState::Running {
1135 return Err(AnalyzerError::NotRunning);
1136 }
1137
1138 let metrics = self.metrics.read().await;
1139
1140 if let Some(last_event) = metrics.first_event_time {
1142 let elapsed = Utc::now() - last_event;
1143 if elapsed > ChronoDuration::minutes(10) {
1144 warn!("No events received in the last 10 minutes");
1145 }
1146 }
1147
1148 Ok(())
1149 }
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154 use super::*;
1155 use std::collections::HashMap;
1156
1157 #[tokio::test]
1158 async fn test_quality_analyzer_lifecycle() {
1159 let mut analyzer = QualityAnalyzer::with_defaults();
1160
1161 assert_eq!(analyzer.state(), AnalyzerState::Initialized);
1162
1163 analyzer.start().await.unwrap();
1164 assert_eq!(analyzer.state(), AnalyzerState::Running);
1165
1166 analyzer.stop().await.unwrap();
1167 assert_eq!(analyzer.state(), AnalyzerState::Stopped);
1168 }
1169
1170 #[tokio::test]
1171 async fn test_response_event_processing() {
1172 let mut analyzer = QualityAnalyzer::with_defaults();
1173 analyzer.start().await.unwrap();
1174
1175 let event = AnalyzerEvent::Response {
1176 timestamp: Utc::now(),
1177 request_id: "req-1".to_string(),
1178 model: "gpt-4".to_string(),
1179 completion_tokens: 100,
1180 total_tokens: 200,
1181 latency_ms: 500,
1182 success: true,
1183 error: None,
1184 metadata: HashMap::new(),
1185 };
1186
1187 analyzer.process_event(event).await.unwrap();
1188
1189 let metrics = analyzer.metrics.read().await;
1190 assert_eq!(metrics.total_requests, 1);
1191 assert_eq!(metrics.successful_requests, 1);
1192 assert_eq!(metrics.failed_requests, 0);
1193 }
1194
1195 #[tokio::test]
1196 async fn test_error_tracking() {
1197 let mut analyzer = QualityAnalyzer::with_defaults();
1198 analyzer.start().await.unwrap();
1199
1200 for i in 0..7 {
1202 let success = i < 5;
1203 let error = if success {
1204 None
1205 } else {
1206 Some("timeout".to_string())
1207 };
1208
1209 let event = AnalyzerEvent::Response {
1210 timestamp: Utc::now(),
1211 request_id: format!("req-{}", i),
1212 model: "gpt-4".to_string(),
1213 completion_tokens: 100,
1214 total_tokens: 200,
1215 latency_ms: 500,
1216 success,
1217 error,
1218 metadata: HashMap::new(),
1219 };
1220
1221 analyzer.process_event(event).await.unwrap();
1222 }
1223
1224 let metrics = analyzer.metrics.read().await;
1225 assert_eq!(metrics.total_requests, 7);
1226 assert_eq!(metrics.successful_requests, 5);
1227 assert_eq!(metrics.failed_requests, 2);
1228 assert!((metrics.current_success_rate() - 71.43).abs() < 0.1);
1229 }
1230
1231 #[tokio::test]
1232 async fn test_feedback_processing() {
1233 let mut analyzer = QualityAnalyzer::with_defaults();
1234 analyzer.start().await.unwrap();
1235
1236 for rating in vec![5.0, 4.5, 4.0, 3.5, 3.0] {
1238 let event = AnalyzerEvent::Feedback {
1239 timestamp: Utc::now(),
1240 request_id: "req-1".to_string(),
1241 rating: Some(rating),
1242 feedback_type: Some("positive".to_string()),
1243 comment: None,
1244 metadata: {
1245 let mut m = HashMap::new();
1246 m.insert("model".to_string(), serde_json::json!("gpt-4"));
1247 m
1248 },
1249 };
1250
1251 analyzer.process_event(event).await.unwrap();
1252 }
1253
1254 let metrics = analyzer.metrics.read().await;
1255 let avg_rating = metrics.average_rating().unwrap();
1256 assert!((avg_rating - 4.0).abs() < 0.1);
1257 }
1258
1259 #[tokio::test]
1260 async fn test_high_error_rate_insight() {
1261 let config = QualityAnalyzerConfig {
1262 error_rate_warning_pct: 5.0,
1263 min_samples_for_quality: 10,
1264 ..Default::default()
1265 };
1266
1267 let mut analyzer = QualityAnalyzer::new(config);
1268 analyzer.start().await.unwrap();
1269
1270 for i in 0..10 {
1272 let success = i < 8; let event = AnalyzerEvent::Response {
1275 timestamp: Utc::now(),
1276 request_id: format!("req-{}", i),
1277 model: "gpt-4".to_string(),
1278 completion_tokens: 100,
1279 total_tokens: 200,
1280 latency_ms: 500,
1281 success,
1282 error: if success {
1283 None
1284 } else {
1285 Some("error".to_string())
1286 },
1287 metadata: HashMap::new(),
1288 };
1289
1290 analyzer.process_event(event).await.unwrap();
1291 }
1292
1293 let report = analyzer.generate_report().await.unwrap();
1294 assert!(!report.insights.is_empty());
1295 assert!(report
1296 .insights
1297 .iter()
1298 .any(|i| i.title.contains("error rate")));
1299 }
1300
1301 #[tokio::test]
1302 async fn test_low_rating_insight() {
1303 let config = QualityAnalyzerConfig {
1304 min_avg_rating: 4.0,
1305 min_samples_for_quality: 5,
1306 ..Default::default()
1307 };
1308
1309 let mut analyzer = QualityAnalyzer::new(config);
1310 analyzer.start().await.unwrap();
1311
1312 for i in 0..10 {
1314 let event = AnalyzerEvent::Response {
1315 timestamp: Utc::now(),
1316 request_id: format!("req-{}", i),
1317 model: "gpt-4".to_string(),
1318 completion_tokens: 100,
1319 total_tokens: 200,
1320 latency_ms: 500,
1321 success: true,
1322 error: None,
1323 metadata: HashMap::new(),
1324 };
1325 analyzer.process_event(event).await.unwrap();
1326 }
1327
1328 for rating in vec![2.0, 2.5, 3.0, 2.5, 3.0] {
1330 let event = AnalyzerEvent::Feedback {
1331 timestamp: Utc::now(),
1332 request_id: "req-1".to_string(),
1333 rating: Some(rating),
1334 feedback_type: Some("negative".to_string()),
1335 comment: None,
1336 metadata: {
1337 let mut m = HashMap::new();
1338 m.insert("model".to_string(), serde_json::json!("gpt-4"));
1339 m
1340 },
1341 };
1342
1343 analyzer.process_event(event).await.unwrap();
1344 }
1345
1346 let report = analyzer.generate_report().await.unwrap();
1347 assert!(report.insights.iter().any(|i| i.title.contains("rating")));
1348 }
1349
1350 #[tokio::test]
1351 async fn test_sla_compliance_tracking() {
1352 let config = QualityAnalyzerConfig {
1353 sla_p95_ms: 500.0,
1354 sla_p99_ms: 1000.0,
1355 min_samples_for_quality: 10,
1356 ..Default::default()
1357 };
1358
1359 let mut analyzer = QualityAnalyzer::new(config);
1360 analyzer.start().await.unwrap();
1361
1362 for latency in vec![100, 200, 300, 400, 600, 800, 1200, 300, 400, 500, 600, 700] {
1364 let event = AnalyzerEvent::Response {
1365 timestamp: Utc::now(),
1366 request_id: "req-1".to_string(),
1367 model: "gpt-4".to_string(),
1368 completion_tokens: 100,
1369 total_tokens: 200,
1370 latency_ms: latency,
1371 success: true,
1372 error: None,
1373 metadata: HashMap::new(),
1374 };
1375
1376 analyzer.process_event(event).await.unwrap();
1377 }
1378
1379 let metrics = analyzer.metrics.read().await;
1380 assert!(metrics.sla_checks > 0);
1381 }
1382
1383 #[tokio::test]
1384 async fn test_recommendation_generation() {
1385 let config = QualityAnalyzerConfig {
1386 error_rate_warning_pct: 5.0,
1387 min_samples_for_quality: 10,
1388 ..Default::default()
1389 };
1390
1391 let mut analyzer = QualityAnalyzer::new(config);
1392 analyzer.start().await.unwrap();
1393
1394 for i in 0..10 {
1396 let success = i < 7; let event = AnalyzerEvent::Response {
1399 timestamp: Utc::now(),
1400 request_id: format!("req-{}", i),
1401 model: "gpt-4".to_string(),
1402 completion_tokens: 100,
1403 total_tokens: 200,
1404 latency_ms: 500,
1405 success,
1406 error: if success {
1407 None
1408 } else {
1409 Some("error".to_string())
1410 },
1411 metadata: HashMap::new(),
1412 };
1413
1414 analyzer.process_event(event).await.unwrap();
1415 }
1416
1417 let report = analyzer.generate_report().await.unwrap();
1418 assert!(!report.recommendations.is_empty());
1419 assert!(report
1420 .recommendations
1421 .iter()
1422 .any(|r| r.title.contains("error") || r.title.contains("success")));
1423 }
1424
1425 #[tokio::test]
1426 async fn test_reset() {
1427 let mut analyzer = QualityAnalyzer::with_defaults();
1428 analyzer.start().await.unwrap();
1429
1430 for _ in 0..5 {
1432 let event = AnalyzerEvent::Response {
1433 timestamp: Utc::now(),
1434 request_id: "req-1".to_string(),
1435 model: "gpt-4".to_string(),
1436 completion_tokens: 100,
1437 total_tokens: 200,
1438 latency_ms: 500,
1439 success: true,
1440 error: None,
1441 metadata: HashMap::new(),
1442 };
1443
1444 analyzer.process_event(event).await.unwrap();
1445 }
1446
1447 let metrics_before = analyzer.metrics.read().await;
1448 assert_eq!(metrics_before.total_requests, 5);
1449 drop(metrics_before);
1450
1451 analyzer.reset().await.unwrap();
1453
1454 let metrics_after = analyzer.metrics.read().await;
1455 assert_eq!(metrics_after.total_requests, 0);
1456 }
1457
1458 #[tokio::test]
1459 async fn test_health_check() {
1460 let mut analyzer = QualityAnalyzer::with_defaults();
1461
1462 assert!(analyzer.health_check().await.is_err());
1464
1465 analyzer.start().await.unwrap();
1467 assert!(analyzer.health_check().await.is_ok());
1468 }
1469}