1use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15use super::metrics::FullAgentMetrics;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum PerformanceRating {
21 Excellent,
23 Good,
25 Fair,
27 Poor,
29}
30
31impl PerformanceRating {
32 pub fn from_score(score: f32) -> Self {
34 if score >= 80.0 {
35 Self::Excellent
36 } else if score >= 60.0 {
37 Self::Good
38 } else if score >= 40.0 {
39 Self::Fair
40 } else {
41 Self::Poor
42 }
43 }
44}
45
46impl std::fmt::Display for PerformanceRating {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Excellent => write!(f, "excellent"),
50 Self::Good => write!(f, "good"),
51 Self::Fair => write!(f, "fair"),
52 Self::Poor => write!(f, "poor"),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum BottleneckCategory {
61 HighLatency,
63 SlowTools,
65 HighErrorRate,
67 HighCost,
69 LowThroughput,
71 TimeoutRisk,
73}
74
75impl std::fmt::Display for BottleneckCategory {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::HighLatency => write!(f, "high_latency"),
79 Self::SlowTools => write!(f, "slow_tools"),
80 Self::HighErrorRate => write!(f, "high_error_rate"),
81 Self::HighCost => write!(f, "high_cost"),
82 Self::LowThroughput => write!(f, "low_throughput"),
83 Self::TimeoutRisk => write!(f, "timeout_risk"),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct Bottleneck {
92 pub category: BottleneckCategory,
94 pub severity: f32,
96 pub description: String,
98 pub affected_component: Option<String>,
100 pub current_value: Option<String>,
102 pub threshold: Option<String>,
104}
105
106impl Bottleneck {
107 pub fn new(
109 category: BottleneckCategory,
110 severity: f32,
111 description: impl Into<String>,
112 ) -> Self {
113 Self {
114 category,
115 severity: severity.clamp(0.0, 100.0),
116 description: description.into(),
117 affected_component: None,
118 current_value: None,
119 threshold: None,
120 }
121 }
122
123 pub fn with_component(mut self, component: impl Into<String>) -> Self {
125 self.affected_component = Some(component.into());
126 self
127 }
128
129 pub fn with_current_value(mut self, value: impl Into<String>) -> Self {
131 self.current_value = Some(value.into());
132 self
133 }
134
135 pub fn with_threshold(mut self, threshold: impl Into<String>) -> Self {
137 self.threshold = Some(threshold.into());
138 self
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
144#[serde(rename_all = "lowercase")]
145pub enum SuggestionPriority {
146 Low,
148 Medium,
150 High,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct Suggestion {
158 pub priority: SuggestionPriority,
160 pub title: String,
162 pub description: String,
164 pub expected_improvement: Option<String>,
166 pub related_to: Option<BottleneckCategory>,
168}
169
170impl Suggestion {
171 pub fn new(
173 priority: SuggestionPriority,
174 title: impl Into<String>,
175 description: impl Into<String>,
176 ) -> Self {
177 Self {
178 priority,
179 title: title.into(),
180 description: description.into(),
181 expected_improvement: None,
182 related_to: None,
183 }
184 }
185
186 pub fn with_improvement(mut self, improvement: impl Into<String>) -> Self {
188 self.expected_improvement = Some(improvement.into());
189 self
190 }
191
192 pub fn with_related_to(mut self, category: BottleneckCategory) -> Self {
194 self.related_to = Some(category);
195 self
196 }
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct PerformanceScores {
203 pub latency_score: f32,
205 pub throughput_score: f32,
207 pub error_rate_score: f32,
209 pub cost_efficiency_score: f32,
211 pub tool_efficiency_score: f32,
213}
214
215impl PerformanceScores {
216 pub fn overall(&self) -> f32 {
218 let weights = [0.25, 0.20, 0.25, 0.15, 0.15];
219 let scores = [
220 self.latency_score,
221 self.throughput_score,
222 self.error_rate_score,
223 self.cost_efficiency_score,
224 self.tool_efficiency_score,
225 ];
226
227 let weighted_sum: f32 = scores.iter().zip(weights.iter()).map(|(s, w)| s * w).sum();
228 weighted_sum.clamp(0.0, 100.0)
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct PerformanceReport {
236 pub agent_id: String,
238 pub overall_score: f32,
240 pub rating: PerformanceRating,
242 pub scores: PerformanceScores,
244 pub bottlenecks: Vec<Bottleneck>,
246 pub suggestions: Vec<Suggestion>,
248 pub timestamp: DateTime<Utc>,
250}
251
252impl PerformanceReport {
253 pub fn new(agent_id: impl Into<String>, scores: PerformanceScores) -> Self {
255 let overall_score = scores.overall();
256 Self {
257 agent_id: agent_id.into(),
258 overall_score,
259 rating: PerformanceRating::from_score(overall_score),
260 scores,
261 bottlenecks: Vec::new(),
262 suggestions: Vec::new(),
263 timestamp: Utc::now(),
264 }
265 }
266
267 pub fn add_bottleneck(&mut self, bottleneck: Bottleneck) {
269 self.bottlenecks.push(bottleneck);
270 }
271
272 pub fn add_suggestion(&mut self, suggestion: Suggestion) {
274 self.suggestions.push(suggestion);
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct AnalysisThresholds {
281 pub good_latency_ms: u64,
283 pub poor_latency_ms: u64,
285 pub good_tool_duration_ms: u64,
287 pub poor_tool_duration_ms: u64,
289 pub good_error_rate: f32,
291 pub poor_error_rate: f32,
293 pub good_tokens_per_second: f64,
295 pub poor_tokens_per_second: f64,
297 pub good_cost_per_1k_tokens: f64,
299 pub poor_cost_per_1k_tokens: f64,
301}
302
303impl Default for AnalysisThresholds {
304 fn default() -> Self {
305 Self {
306 good_latency_ms: 500,
307 poor_latency_ms: 2000,
308 good_tool_duration_ms: 1000,
309 poor_tool_duration_ms: 5000,
310 good_error_rate: 0.05,
311 poor_error_rate: 0.20,
312 good_tokens_per_second: 50.0,
313 poor_tokens_per_second: 10.0,
314 good_cost_per_1k_tokens: 0.01,
315 poor_cost_per_1k_tokens: 0.05,
316 }
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct PerformanceAnalyzer {
323 thresholds: AnalysisThresholds,
325}
326
327impl Default for PerformanceAnalyzer {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333impl PerformanceAnalyzer {
334 pub fn new() -> Self {
336 Self {
337 thresholds: AnalysisThresholds::default(),
338 }
339 }
340
341 pub fn with_thresholds(thresholds: AnalysisThresholds) -> Self {
343 Self { thresholds }
344 }
345
346 pub fn analyze(&self, metrics: &[FullAgentMetrics]) -> Vec<PerformanceReport> {
348 metrics.iter().map(|m| self.analyze_agent(m)).collect()
349 }
350
351 pub fn analyze_agent(&self, metrics: &FullAgentMetrics) -> PerformanceReport {
353 let scores = self.calculate_scores(metrics);
354 let mut report = PerformanceReport::new(&metrics.agent_id, scores);
355
356 let bottlenecks = self.identify_bottlenecks(metrics);
358 for bottleneck in bottlenecks {
359 report.add_bottleneck(bottleneck);
360 }
361
362 let suggestions = self.suggest_optimizations(metrics);
364 for suggestion in suggestions {
365 report.add_suggestion(suggestion);
366 }
367
368 report
369 }
370
371 fn calculate_scores(&self, metrics: &FullAgentMetrics) -> PerformanceScores {
373 PerformanceScores {
374 latency_score: self.calculate_latency_score(metrics),
375 throughput_score: self.calculate_throughput_score(metrics),
376 error_rate_score: self.calculate_error_rate_score(metrics),
377 cost_efficiency_score: self.calculate_cost_efficiency_score(metrics),
378 tool_efficiency_score: self.calculate_tool_efficiency_score(metrics),
379 }
380 }
381
382 fn calculate_latency_score(&self, metrics: &FullAgentMetrics) -> f32 {
384 let avg_latency_ms = metrics
385 .performance
386 .avg_api_latency
387 .map(|d| d.as_millis() as u64)
388 .unwrap_or(0);
389
390 if avg_latency_ms == 0 {
391 return 100.0; }
393
394 self.score_from_range(
395 avg_latency_ms as f64,
396 self.thresholds.good_latency_ms as f64,
397 self.thresholds.poor_latency_ms as f64,
398 true, )
400 }
401
402 fn calculate_throughput_score(&self, metrics: &FullAgentMetrics) -> f32 {
404 let tokens_per_second = metrics.performance.tokens_per_second.unwrap_or(0.0);
405
406 if tokens_per_second == 0.0 {
407 return 50.0; }
409
410 self.score_from_range(
411 tokens_per_second,
412 self.thresholds.poor_tokens_per_second,
413 self.thresholds.good_tokens_per_second,
414 false, )
416 }
417
418 fn calculate_error_rate_score(&self, metrics: &FullAgentMetrics) -> f32 {
420 let error_rate = metrics.error_rate();
421
422 self.score_from_range(
423 error_rate as f64,
424 self.thresholds.good_error_rate as f64,
425 self.thresholds.poor_error_rate as f64,
426 true, )
428 }
429
430 fn calculate_cost_efficiency_score(&self, metrics: &FullAgentMetrics) -> f32 {
432 let total_tokens = metrics.tokens_used.total;
433 if total_tokens == 0 {
434 return 100.0; }
436
437 let cost_per_1k = (metrics.cost / total_tokens as f64) * 1000.0;
438
439 self.score_from_range(
440 cost_per_1k,
441 self.thresholds.good_cost_per_1k_tokens,
442 self.thresholds.poor_cost_per_1k_tokens,
443 true, )
445 }
446
447 fn calculate_tool_efficiency_score(&self, metrics: &FullAgentMetrics) -> f32 {
449 let avg_tool_duration_ms = metrics
450 .performance
451 .avg_tool_duration
452 .map(|d| d.as_millis() as u64)
453 .unwrap_or(0);
454
455 if avg_tool_duration_ms == 0 {
456 return 100.0; }
458
459 self.score_from_range(
460 avg_tool_duration_ms as f64,
461 self.thresholds.good_tool_duration_ms as f64,
462 self.thresholds.poor_tool_duration_ms as f64,
463 true, )
465 }
466
467 fn score_from_range(&self, value: f64, good: f64, poor: f64, lower_is_better: bool) -> f32 {
471 if lower_is_better {
472 if value <= good {
473 100.0
474 } else if value >= poor {
475 0.0
476 } else {
477 let range = poor - good;
478 let position = value - good;
479 (100.0 * (1.0 - position / range)) as f32
480 }
481 } else if value >= good {
482 100.0
483 } else if value <= poor {
484 0.0
485 } else {
486 let range = good - poor;
487 let position = value - poor;
488 (100.0 * (position / range)) as f32
489 }
490 }
491
492 pub fn identify_bottlenecks(&self, metrics: &FullAgentMetrics) -> Vec<Bottleneck> {
494 let mut bottlenecks = Vec::new();
495
496 if let Some(avg_latency) = metrics.performance.avg_api_latency {
498 let latency_ms = avg_latency.as_millis() as u64;
499 if latency_ms > self.thresholds.poor_latency_ms {
500 let severity = ((latency_ms as f32 / self.thresholds.poor_latency_ms as f32)
501 * 50.0)
502 .min(100.0);
503 bottlenecks.push(
504 Bottleneck::new(
505 BottleneckCategory::HighLatency,
506 severity,
507 format!("API latency is {}ms, exceeding threshold", latency_ms),
508 )
509 .with_current_value(format!("{}ms", latency_ms))
510 .with_threshold(format!("{}ms", self.thresholds.poor_latency_ms)),
511 );
512 }
513 }
514
515 if let Some(avg_tool_duration) = metrics.performance.avg_tool_duration {
517 let duration_ms = avg_tool_duration.as_millis() as u64;
518 if duration_ms > self.thresholds.poor_tool_duration_ms {
519 let severity =
520 ((duration_ms as f32 / self.thresholds.poor_tool_duration_ms as f32) * 50.0)
521 .min(100.0);
522
523 let slowest_tool = metrics
525 .tool_calls
526 .iter()
527 .filter_map(|t| t.duration.map(|d| (t.tool_name.clone(), d)))
528 .max_by_key(|(_, d)| d.as_millis());
529
530 let mut bottleneck = Bottleneck::new(
531 BottleneckCategory::SlowTools,
532 severity,
533 format!(
534 "Average tool duration is {}ms, exceeding threshold",
535 duration_ms
536 ),
537 )
538 .with_current_value(format!("{}ms", duration_ms))
539 .with_threshold(format!("{}ms", self.thresholds.poor_tool_duration_ms));
540
541 if let Some((tool_name, _)) = slowest_tool {
542 bottleneck = bottleneck.with_component(tool_name);
543 }
544
545 bottlenecks.push(bottleneck);
546 }
547 }
548
549 let error_rate = metrics.error_rate();
551 if error_rate > self.thresholds.poor_error_rate {
552 let severity = ((error_rate / self.thresholds.poor_error_rate) * 50.0).min(100.0);
553 bottlenecks.push(
554 Bottleneck::new(
555 BottleneckCategory::HighErrorRate,
556 severity,
557 format!(
558 "Error rate is {:.1}%, exceeding threshold",
559 error_rate * 100.0
560 ),
561 )
562 .with_current_value(format!("{:.1}%", error_rate * 100.0))
563 .with_threshold(format!("{:.1}%", self.thresholds.poor_error_rate * 100.0)),
564 );
565 }
566
567 let total_tokens = metrics.tokens_used.total;
569 if total_tokens > 0 {
570 let cost_per_1k = (metrics.cost / total_tokens as f64) * 1000.0;
571 if cost_per_1k > self.thresholds.poor_cost_per_1k_tokens {
572 let severity = ((cost_per_1k / self.thresholds.poor_cost_per_1k_tokens) * 50.0)
573 .min(100.0) as f32;
574 bottlenecks.push(
575 Bottleneck::new(
576 BottleneckCategory::HighCost,
577 severity,
578 format!(
579 "Cost per 1K tokens is ${:.4}, exceeding threshold",
580 cost_per_1k
581 ),
582 )
583 .with_current_value(format!("${:.4}", cost_per_1k))
584 .with_threshold(format!("${:.4}", self.thresholds.poor_cost_per_1k_tokens)),
585 );
586 }
587 }
588
589 if let Some(tokens_per_second) = metrics.performance.tokens_per_second {
591 if tokens_per_second < self.thresholds.poor_tokens_per_second && tokens_per_second > 0.0
592 {
593 let severity = ((self.thresholds.poor_tokens_per_second / tokens_per_second) * 25.0)
594 .min(100.0) as f32;
595 bottlenecks.push(
596 Bottleneck::new(
597 BottleneckCategory::LowThroughput,
598 severity,
599 format!(
600 "Throughput is {:.1} tokens/sec, below threshold",
601 tokens_per_second
602 ),
603 )
604 .with_current_value(format!("{:.1} tokens/sec", tokens_per_second))
605 .with_threshold(format!(
606 "{:.1} tokens/sec",
607 self.thresholds.poor_tokens_per_second
608 )),
609 );
610 }
611 }
612
613 if let (Some(timeout), Some(duration)) = (metrics.timeout, metrics.duration) {
615 let usage_ratio = duration.as_millis() as f64 / timeout.as_millis() as f64;
616 if usage_ratio > 0.8 {
617 let severity = ((usage_ratio - 0.8) * 500.0).min(100.0) as f32;
618 bottlenecks.push(
619 Bottleneck::new(
620 BottleneckCategory::TimeoutRisk,
621 severity,
622 format!(
623 "Execution used {:.0}% of timeout budget",
624 usage_ratio * 100.0
625 ),
626 )
627 .with_current_value(format!("{:.0}%", usage_ratio * 100.0))
628 .with_threshold("80%".to_string()),
629 );
630 }
631 }
632
633 bottlenecks.sort_by(|a, b| {
635 b.severity
636 .partial_cmp(&a.severity)
637 .unwrap_or(std::cmp::Ordering::Equal)
638 });
639
640 bottlenecks
641 }
642
643 pub fn suggest_optimizations(&self, metrics: &FullAgentMetrics) -> Vec<Suggestion> {
645 let mut suggestions = Vec::new();
646
647 if let Some(avg_latency) = metrics.performance.avg_api_latency {
649 let latency_ms = avg_latency.as_millis() as u64;
650 if latency_ms > self.thresholds.good_latency_ms {
651 let priority = if latency_ms > self.thresholds.poor_latency_ms {
652 SuggestionPriority::High
653 } else {
654 SuggestionPriority::Medium
655 };
656 suggestions.push(
657 Suggestion::new(
658 priority,
659 "Reduce API latency",
660 "Consider batching API calls or using a faster model for simple tasks",
661 )
662 .with_improvement("Could reduce latency by 30-50%")
663 .with_related_to(BottleneckCategory::HighLatency),
664 );
665 }
666 }
667
668 if let Some(avg_tool_duration) = metrics.performance.avg_tool_duration {
670 let duration_ms = avg_tool_duration.as_millis() as u64;
671 if duration_ms > self.thresholds.good_tool_duration_ms {
672 let priority = if duration_ms > self.thresholds.poor_tool_duration_ms {
673 SuggestionPriority::High
674 } else {
675 SuggestionPriority::Medium
676 };
677
678 let slow_tools: Vec<_> = metrics
680 .tool_calls
681 .iter()
682 .filter(|t| {
683 t.duration
684 .map(|d| d.as_millis() as u64 > self.thresholds.good_tool_duration_ms)
685 .unwrap_or(false)
686 })
687 .map(|t| t.tool_name.clone())
688 .collect();
689
690 let description = if slow_tools.is_empty() {
691 "Optimize tool execution by caching results or parallelizing calls".to_string()
692 } else {
693 format!(
694 "Optimize slow tools: {}. Consider caching or parallelization",
695 slow_tools.join(", ")
696 )
697 };
698
699 suggestions.push(
700 Suggestion::new(priority, "Optimize tool execution", description)
701 .with_improvement("Could reduce tool execution time by 20-40%")
702 .with_related_to(BottleneckCategory::SlowTools),
703 );
704 }
705 }
706
707 let error_rate = metrics.error_rate();
709 if error_rate > self.thresholds.good_error_rate {
710 let priority = if error_rate > self.thresholds.poor_error_rate {
711 SuggestionPriority::High
712 } else {
713 SuggestionPriority::Medium
714 };
715 suggestions.push(
716 Suggestion::new(
717 priority,
718 "Reduce error rate",
719 "Implement retry logic with exponential backoff, or improve input validation",
720 )
721 .with_improvement("Could reduce errors by 50-70%")
722 .with_related_to(BottleneckCategory::HighErrorRate),
723 );
724 }
725
726 let total_tokens = metrics.tokens_used.total;
728 if total_tokens > 0 {
729 let cost_per_1k = (metrics.cost / total_tokens as f64) * 1000.0;
730 if cost_per_1k > self.thresholds.good_cost_per_1k_tokens {
731 let priority = if cost_per_1k > self.thresholds.poor_cost_per_1k_tokens {
732 SuggestionPriority::High
733 } else {
734 SuggestionPriority::Medium
735 };
736 suggestions.push(
737 Suggestion::new(
738 priority,
739 "Reduce costs",
740 "Consider using a smaller model for simple tasks, or implement prompt caching",
741 )
742 .with_improvement("Could reduce costs by 30-60%")
743 .with_related_to(BottleneckCategory::HighCost),
744 );
745 }
746 }
747
748 if let Some(tokens_per_second) = metrics.performance.tokens_per_second {
750 if tokens_per_second < self.thresholds.good_tokens_per_second && tokens_per_second > 0.0
751 {
752 let priority = if tokens_per_second < self.thresholds.poor_tokens_per_second {
753 SuggestionPriority::High
754 } else {
755 SuggestionPriority::Medium
756 };
757 suggestions.push(
758 Suggestion::new(
759 priority,
760 "Improve throughput",
761 "Consider streaming responses or parallel processing for independent tasks",
762 )
763 .with_improvement("Could improve throughput by 2-3x")
764 .with_related_to(BottleneckCategory::LowThroughput),
765 );
766 }
767 }
768
769 if let (Some(timeout), Some(duration)) = (metrics.timeout, metrics.duration) {
771 let usage_ratio = duration.as_millis() as f64 / timeout.as_millis() as f64;
772 if usage_ratio > 0.8 {
773 suggestions.push(
774 Suggestion::new(
775 SuggestionPriority::High,
776 "Address timeout risk",
777 "Increase timeout or optimize execution to reduce duration",
778 )
779 .with_improvement("Prevent potential timeout failures")
780 .with_related_to(BottleneckCategory::TimeoutRisk),
781 );
782 }
783 }
784
785 if metrics.tool_calls.len() > 10 {
787 let failed_tools = metrics.tool_calls.iter().filter(|t| !t.success).count();
788 if failed_tools > 2 {
789 suggestions.push(
790 Suggestion::new(
791 SuggestionPriority::Medium,
792 "Review tool call patterns",
793 format!(
794 "{} out of {} tool calls failed. Review tool usage patterns",
795 failed_tools,
796 metrics.tool_calls.len()
797 ),
798 )
799 .with_improvement("Could improve reliability"),
800 );
801 }
802 }
803
804 suggestions.sort_by(|a, b| b.priority.cmp(&a.priority));
806
807 suggestions
808 }
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 #[allow(unused_imports)]
815 use crate::agents::monitor::alerts::AgentExecutionStatus;
816
817 fn create_test_metrics(agent_id: &str) -> FullAgentMetrics {
818 FullAgentMetrics::new(agent_id, "test")
819 }
820
821 #[test]
822 fn test_performance_rating_from_score() {
823 assert_eq!(
824 PerformanceRating::from_score(100.0),
825 PerformanceRating::Excellent
826 );
827 assert_eq!(
828 PerformanceRating::from_score(80.0),
829 PerformanceRating::Excellent
830 );
831 assert_eq!(PerformanceRating::from_score(79.9), PerformanceRating::Good);
832 assert_eq!(PerformanceRating::from_score(60.0), PerformanceRating::Good);
833 assert_eq!(PerformanceRating::from_score(59.9), PerformanceRating::Fair);
834 assert_eq!(PerformanceRating::from_score(40.0), PerformanceRating::Fair);
835 assert_eq!(PerformanceRating::from_score(39.9), PerformanceRating::Poor);
836 assert_eq!(PerformanceRating::from_score(0.0), PerformanceRating::Poor);
837 }
838
839 #[test]
840 fn test_bottleneck_creation() {
841 let bottleneck = Bottleneck::new(
842 BottleneckCategory::HighLatency,
843 75.0,
844 "High latency detected",
845 )
846 .with_component("api_call")
847 .with_current_value("2500ms")
848 .with_threshold("2000ms");
849
850 assert_eq!(bottleneck.category, BottleneckCategory::HighLatency);
851 assert_eq!(bottleneck.severity, 75.0);
852 assert_eq!(bottleneck.affected_component, Some("api_call".to_string()));
853 assert_eq!(bottleneck.current_value, Some("2500ms".to_string()));
854 assert_eq!(bottleneck.threshold, Some("2000ms".to_string()));
855 }
856
857 #[test]
858 fn test_suggestion_creation() {
859 let suggestion = Suggestion::new(SuggestionPriority::High, "Reduce latency", "Use caching")
860 .with_improvement("30% improvement")
861 .with_related_to(BottleneckCategory::HighLatency);
862
863 assert_eq!(suggestion.priority, SuggestionPriority::High);
864 assert_eq!(suggestion.title, "Reduce latency");
865 assert_eq!(
866 suggestion.expected_improvement,
867 Some("30% improvement".to_string())
868 );
869 assert_eq!(suggestion.related_to, Some(BottleneckCategory::HighLatency));
870 }
871
872 #[test]
873 fn test_performance_scores_overall() {
874 let scores = PerformanceScores {
875 latency_score: 80.0,
876 throughput_score: 60.0,
877 error_rate_score: 100.0,
878 cost_efficiency_score: 70.0,
879 tool_efficiency_score: 50.0,
880 };
881
882 let overall = scores.overall();
885 assert!((overall - 75.0).abs() < 0.1);
886 }
887
888 #[test]
889 fn test_analyzer_creation() {
890 let analyzer = PerformanceAnalyzer::new();
891 assert_eq!(analyzer.thresholds.good_latency_ms, 500);
892 assert_eq!(analyzer.thresholds.poor_latency_ms, 2000);
893 }
894
895 #[test]
896 fn test_analyze_agent_basic() {
897 let analyzer = PerformanceAnalyzer::new();
898 let metrics = create_test_metrics("agent-1");
899
900 let report = analyzer.analyze_agent(&metrics);
901
902 assert_eq!(report.agent_id, "agent-1");
903 assert!(report.overall_score >= 0.0 && report.overall_score <= 100.0);
904 }
905
906 #[test]
907 fn test_analyze_multiple_agents() {
908 let analyzer = PerformanceAnalyzer::new();
909 let metrics = vec![
910 create_test_metrics("agent-1"),
911 create_test_metrics("agent-2"),
912 create_test_metrics("agent-3"),
913 ];
914
915 let reports = analyzer.analyze(&metrics);
916
917 assert_eq!(reports.len(), 3);
918 assert_eq!(reports[0].agent_id, "agent-1");
919 assert_eq!(reports[1].agent_id, "agent-2");
920 assert_eq!(reports[2].agent_id, "agent-3");
921 }
922
923 #[test]
924 fn test_identify_bottlenecks_high_error_rate() {
925 let analyzer = PerformanceAnalyzer::new();
926 let mut metrics = create_test_metrics("agent-1");
927 metrics.api_calls = 10;
928 metrics.api_calls_successful = 5; let bottlenecks = analyzer.identify_bottlenecks(&metrics);
931
932 assert!(!bottlenecks.is_empty());
933 assert!(bottlenecks
934 .iter()
935 .any(|b| b.category == BottleneckCategory::HighErrorRate));
936 }
937
938 #[test]
939 fn test_identify_bottlenecks_high_cost() {
940 let analyzer = PerformanceAnalyzer::new();
941 let mut metrics = create_test_metrics("agent-1");
942 metrics.tokens_used.total = 1000;
943 metrics.cost = 1.0; let bottlenecks = analyzer.identify_bottlenecks(&metrics);
946
947 assert!(!bottlenecks.is_empty());
948 assert!(bottlenecks
949 .iter()
950 .any(|b| b.category == BottleneckCategory::HighCost));
951 }
952
953 #[test]
954 fn test_suggest_optimizations_high_error_rate() {
955 let analyzer = PerformanceAnalyzer::new();
956 let mut metrics = create_test_metrics("agent-1");
957 metrics.api_calls = 10;
958 metrics.api_calls_successful = 5;
959
960 let suggestions = analyzer.suggest_optimizations(&metrics);
961
962 assert!(!suggestions.is_empty());
963 assert!(suggestions.iter().any(|s| s.title.contains("error")));
964 }
965
966 #[test]
967 fn test_score_from_range_lower_is_better() {
968 let analyzer = PerformanceAnalyzer::new();
969
970 assert_eq!(analyzer.score_from_range(500.0, 500.0, 2000.0, true), 100.0);
972
973 assert_eq!(analyzer.score_from_range(2000.0, 500.0, 2000.0, true), 0.0);
975
976 assert_eq!(analyzer.score_from_range(100.0, 500.0, 2000.0, true), 100.0);
978
979 assert_eq!(analyzer.score_from_range(3000.0, 500.0, 2000.0, true), 0.0);
981
982 let mid_score = analyzer.score_from_range(1250.0, 500.0, 2000.0, true);
984 assert!((mid_score - 50.0).abs() < 1.0);
985 }
986
987 #[test]
988 fn test_score_from_range_higher_is_better() {
989 let analyzer = PerformanceAnalyzer::new();
990
991 assert_eq!(analyzer.score_from_range(50.0, 50.0, 10.0, false), 100.0);
994
995 assert_eq!(analyzer.score_from_range(10.0, 50.0, 10.0, false), 0.0);
997
998 assert_eq!(analyzer.score_from_range(100.0, 50.0, 10.0, false), 100.0);
1000
1001 assert_eq!(analyzer.score_from_range(5.0, 50.0, 10.0, false), 0.0);
1003
1004 let mid_score = analyzer.score_from_range(30.0, 50.0, 10.0, false);
1006 assert!((mid_score - 50.0).abs() < 1.0);
1007 }
1008
1009 #[test]
1010 fn test_performance_report_creation() {
1011 let scores = PerformanceScores {
1012 latency_score: 80.0,
1013 throughput_score: 80.0,
1014 error_rate_score: 80.0,
1015 cost_efficiency_score: 80.0,
1016 tool_efficiency_score: 80.0,
1017 };
1018
1019 let report = PerformanceReport::new("agent-1", scores);
1020
1021 assert_eq!(report.agent_id, "agent-1");
1022 assert_eq!(report.overall_score, 80.0);
1023 assert_eq!(report.rating, PerformanceRating::Excellent);
1024 }
1025
1026 #[test]
1027 fn test_bottleneck_severity_clamping() {
1028 let bottleneck = Bottleneck::new(BottleneckCategory::HighLatency, 150.0, "Test");
1029 assert_eq!(bottleneck.severity, 100.0);
1030
1031 let bottleneck = Bottleneck::new(BottleneckCategory::HighLatency, -10.0, "Test");
1032 assert_eq!(bottleneck.severity, 0.0);
1033 }
1034}