1#![allow(dead_code)]
46
47use anyhow::Result;
48use serde::{Deserialize, Serialize};
50use std::collections::{HashMap, HashSet, VecDeque};
51use std::time::{Duration, Instant};
52
53#[derive(Debug)]
55pub struct LLMDebugger {
56 config: LLMDebugConfig,
57 safety_analyzer: SafetyAnalyzer,
58 factuality_checker: FactualityChecker,
59 alignment_monitor: AlignmentMonitor,
60 hallucination_detector: HallucinationDetector,
61 bias_detector: BiasDetector,
62 performance_profiler: LLMPerformanceProfiler,
63 conversation_analyzer: ConversationAnalyzer,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct LLMDebugConfig {
69 pub enable_safety_analysis: bool,
71 pub enable_factuality_checking: bool,
73 pub enable_alignment_monitoring: bool,
75 pub enable_hallucination_detection: bool,
77 pub enable_bias_detection: bool,
79 pub enable_llm_performance_profiling: bool,
81 pub enable_conversation_analysis: bool,
83 pub safety_threshold: f32,
85 pub factuality_threshold: f32,
87 pub max_conversation_length: usize,
89 pub analysis_sampling_rate: f32,
91}
92
93impl Default for LLMDebugConfig {
94 fn default() -> Self {
95 Self {
96 enable_safety_analysis: true,
97 enable_factuality_checking: true,
98 enable_alignment_monitoring: true,
99 enable_hallucination_detection: true,
100 enable_bias_detection: true,
101 enable_llm_performance_profiling: true,
102 enable_conversation_analysis: true,
103 safety_threshold: 0.8,
104 factuality_threshold: 0.7,
105 max_conversation_length: 100,
106 analysis_sampling_rate: 1.0,
107 }
108 }
109}
110
111#[derive(Debug)]
113pub struct SafetyAnalyzer {
114 toxic_patterns: HashSet<String>,
115 harm_categories: Vec<HarmCategory>,
116 safety_metrics: SafetyMetrics,
117 health: HealthTracker,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
124pub enum HarmCategory {
125 Toxicity, Violence, SelfHarm, Harassment, HateSpeech, Sexual, Privacy, Misinformation, Manipulation, Illegal, }
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SafetyMetrics {
140 pub overall_safety_score: f32,
141 pub harm_category_scores: HashMap<HarmCategory, f32>,
142 pub flagged_responses: usize,
143 pub total_responses_analyzed: usize,
144 pub average_response_safety: f32,
145 pub safety_trend: SafetyTrend,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub enum SafetyTrend {
151 Improving,
152 Stable,
153 Degrading,
154 Volatile,
155}
156
157#[derive(Debug)]
159pub struct FactualityChecker {
160 fact_databases: Vec<String>,
161 uncertainty_indicators: HashSet<String>,
162 factuality_metrics: FactualityMetrics,
163 health: HealthTracker,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct FactualityMetrics {
169 pub overall_factuality_score: Option<f32>,
173 pub average_uncertainty_density: Option<f32>,
176 pub claim_like_sentences_seen: usize,
179 pub uncertainty_indicator_hits: usize,
182 pub conflicting_information: usize,
183 pub uncertainty_expressions: usize,
184 pub knowledge_gaps: Vec<String>,
185 pub confidence_distribution: Vec<f32>,
186}
187
188#[derive(Debug)]
190pub struct AlignmentMonitor {
191 alignment_objectives: Vec<AlignmentObjective>,
192 alignment_metrics: AlignmentMetrics,
193 health: HealthTracker,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
198pub enum AlignmentObjective {
199 Helpfulness, Harmlessness, Honesty, Fairness, Privacy, Transparency, Consistency, Responsibility, }
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct AlignmentMetrics {
212 pub objective_scores: HashMap<AlignmentObjective, f32>,
213 pub overall_alignment_score: Option<f32>,
220 pub alignment_violations: usize,
221 pub value_consistency_score: Option<f32>,
224 pub behavioral_drift: Option<f32>,
226 pub alignment_trend: Option<AlignmentTrend>,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub enum AlignmentTrend {
234 Improving,
235 Stable,
236 Degrading,
237 Inconsistent,
238}
239
240#[derive(Debug)]
242pub struct HallucinationDetector {
243 confidence_thresholds: HashMap<String, f32>,
244 consistency_checker: ConsistencyChecker,
245 hallucination_metrics: HallucinationMetrics,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct HallucinationMetrics {
251 pub hallucination_rate: f32,
252 pub confidence_accuracy_correlation: f32,
253 pub factual_consistency_score: f32,
254 pub internal_consistency_score: f32,
255 pub source_attribution_accuracy: f32,
256 pub detected_fabrications: usize,
257 pub uncertain_responses: usize,
258}
259
260#[derive(Debug)]
262pub struct ConsistencyChecker {
263 previous_responses: Vec<String>,
264 consistency_cache: HashMap<String, f32>,
265}
266
267#[derive(Debug)]
269pub struct BiasDetector {
270 bias_categories: Vec<BiasCategory>,
271 demographic_groups: Vec<String>,
272 bias_metrics: BiasMetrics,
273 health: HealthTracker,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
278pub enum BiasCategory {
279 Gender, Race, Religion, Age, SocioEconomic, Geographic, Political, Linguistic, Ability, Appearance, }
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct BiasMetrics {
294 pub overall_bias_score: f32,
295 pub bias_category_scores: HashMap<BiasCategory, f32>,
296 pub demographic_fairness: HashMap<String, f32>,
297 pub representation_bias: f32,
298 pub stereotype_propagation: f32,
299 pub bias_amplification: f32,
300 pub fairness_violations: usize,
301}
302
303#[derive(Debug)]
305pub struct LLMPerformanceProfiler {
306 generation_metrics: GenerationMetrics,
307 efficiency_metrics: EfficiencyMetrics,
308 quality_metrics: QualityMetrics,
309 scalability_metrics: ScalabilityMetrics,
310 health: HealthTracker,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct GenerationMetrics {
316 pub tokens_per_second: f32,
317 pub average_response_length: f32,
318 pub generation_latency_p50: f32,
319 pub generation_latency_p95: f32,
320 pub generation_latency_p99: f32,
321 pub first_token_latency: f32,
322 pub completion_rate: f32,
323 pub timeout_rate: f32,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct EfficiencyMetrics {
329 pub memory_efficiency: f32,
330 pub compute_utilization: f32,
331 pub energy_consumption: f32,
332 pub carbon_footprint_estimate: f32,
333 pub cost_per_token: f32,
334 pub batch_processing_efficiency: f32,
335 pub cache_hit_rate: f32,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct QualityMetrics {
341 pub coherence_score: f32,
342 pub relevance_score: f32,
343 pub fluency_score: f32,
344 pub informativeness_score: f32,
345 pub creativity_score: f32,
346 pub factual_accuracy: f32,
347 pub readability_score: f32,
348 pub engagement_score: f32,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ScalabilityMetrics {
354 pub concurrent_user_capacity: usize,
355 pub throughput_scaling: f32,
356 pub memory_scaling: f32,
357 pub latency_degradation: f32,
358 pub bottleneck_analysis: Vec<String>,
359 pub resource_utilization_efficiency: f32,
360}
361
362#[derive(Debug)]
364pub struct ConversationAnalyzer {
365 conversation_history: Vec<ConversationTurn>,
366 dialog_metrics: DialogMetrics,
367 context_tracking: ContextTracker,
368 health: HealthTracker,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct ConversationTurn {
374 pub turn_id: usize,
375 pub user_input: String,
376 pub model_response: String,
377 pub timestamp: chrono::DateTime<chrono::Utc>,
378 pub context_length: usize,
379 pub response_time: Duration,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct DialogMetrics {
385 pub conversation_coherence: f32,
386 pub context_maintenance: f32,
387 pub topic_consistency: f32,
388 pub response_appropriateness: f32,
389 pub conversation_engagement: f32,
390 pub turn_taking_naturalness: f32,
391 pub memory_utilization: f32,
392 pub dialog_success_rate: f32,
393}
394
395#[derive(Debug)]
397pub struct ContextTracker {
398 active_topics: HashSet<String>,
399 entity_mentions: HashMap<String, usize>,
400 context_window: Vec<String>,
401 attention_weights: Vec<f32>,
402}
403
404impl LLMDebugger {
405 pub fn new(config: LLMDebugConfig) -> Self {
407 Self {
408 config: config.clone(),
409 safety_analyzer: SafetyAnalyzer::new(&config),
410 factuality_checker: FactualityChecker::new(&config),
411 alignment_monitor: AlignmentMonitor::new(&config),
412 hallucination_detector: HallucinationDetector::new(&config),
413 bias_detector: BiasDetector::new(&config),
414 performance_profiler: LLMPerformanceProfiler::new(),
415 conversation_analyzer: ConversationAnalyzer::new(&config),
416 }
417 }
418
419 pub async fn analyze_response(
421 &mut self,
422 user_input: &str,
423 model_response: &str,
424 context: Option<&[String]>,
425 generation_metrics: Option<GenerationMetrics>,
426 ) -> Result<LLMAnalysisReport> {
427 let start_time = Instant::now();
428
429 let safety_analysis = if self.config.enable_safety_analysis {
431 Some(self.safety_analyzer.analyze_safety(model_response).await?)
432 } else {
433 None
434 };
435
436 let factuality_analysis = if self.config.enable_factuality_checking {
438 Some(self.factuality_checker.check_factuality(model_response, context).await?)
439 } else {
440 None
441 };
442
443 let alignment_analysis = if self.config.enable_alignment_monitoring {
445 Some(self.alignment_monitor.check_alignment(user_input, model_response).await?)
446 } else {
447 None
448 };
449
450 let hallucination_analysis = if self.config.enable_hallucination_detection {
452 Some(
453 self.hallucination_detector
454 .detect_hallucinations(model_response, context)
455 .await?,
456 )
457 } else {
458 None
459 };
460
461 let bias_analysis = if self.config.enable_bias_detection {
463 Some(self.bias_detector.detect_bias(model_response).await?)
464 } else {
465 None
466 };
467
468 let performance_analysis = if self.config.enable_llm_performance_profiling {
470 Some(
471 self.performance_profiler
472 .profile_response(model_response, generation_metrics)
473 .await?,
474 )
475 } else {
476 None
477 };
478
479 let conversation_analysis = if self.config.enable_conversation_analysis {
481 let turn = ConversationTurn {
482 turn_id: self.conversation_analyzer.conversation_history.len(),
483 user_input: user_input.to_string(),
484 model_response: model_response.to_string(),
485 timestamp: chrono::Utc::now(),
486 context_length: context.map(|c| c.len()).unwrap_or(0),
487 response_time: start_time.elapsed(),
488 };
489 Some(self.conversation_analyzer.analyze_turn(&turn).await?)
490 } else {
491 None
492 };
493
494 let analysis_duration = start_time.elapsed();
495
496 Ok(LLMAnalysisReport {
497 input: user_input.to_string(),
498 response: model_response.to_string(),
499 safety_analysis: safety_analysis.clone(),
500 factuality_analysis: factuality_analysis.clone(),
501 alignment_analysis: alignment_analysis.clone(),
502 hallucination_analysis,
503 bias_analysis,
504 performance_analysis,
505 conversation_analysis,
506 overall_score: self.compute_overall_score(
507 &safety_analysis,
508 &factuality_analysis,
509 &alignment_analysis,
510 ),
511 recommendations: self.generate_recommendations(
512 &safety_analysis,
513 &factuality_analysis,
514 &alignment_analysis,
515 ),
516 analysis_duration,
517 timestamp: chrono::Utc::now(),
518 })
519 }
520
521 pub async fn analyze_batch(
523 &mut self,
524 interactions: &[(String, String)], ) -> Result<BatchLLMAnalysisReport> {
526 let mut individual_reports = Vec::new();
527 let mut batch_metrics = BatchMetrics::default();
528
529 for (input, response) in interactions {
530 let report = self.analyze_response(input, response, None, None).await?;
531 batch_metrics.update_from_report(&report);
532 individual_reports.push(report);
533 }
534
535 batch_metrics.finalize(interactions.len());
536
537 Ok(BatchLLMAnalysisReport {
538 individual_reports,
539 batch_metrics,
540 batch_size: interactions.len(),
541 analysis_timestamp: chrono::Utc::now(),
542 })
543 }
544
545 pub async fn generate_health_report(&mut self) -> Result<LLMHealthReport> {
547 Ok(LLMHealthReport {
548 overall_health_score: self.compute_overall_health(),
549 safety_health: self.safety_analyzer.get_health_summary(),
550 factuality_health: self.factuality_checker.get_health_summary(),
551 alignment_health: self.alignment_monitor.get_health_summary(),
552 bias_health: self.bias_detector.get_health_summary(),
553 performance_health: self.performance_profiler.get_health_summary(),
554 conversation_health: self.conversation_analyzer.get_health_summary(),
555 critical_issues: self.identify_critical_issues(),
556 recommendations: self.generate_health_recommendations(),
557 report_timestamp: chrono::Utc::now(),
558 })
559 }
560
561 fn compute_overall_score(
563 &self,
564 safety: &Option<SafetyAnalysisResult>,
565 factuality: &Option<FactualityAnalysisResult>,
566 alignment: &Option<AlignmentAnalysisResult>,
567 ) -> f32 {
568 let mut total_score = 0.0;
569 let mut weight_sum = 0.0;
570
571 if let Some(s) = safety {
572 total_score += s.safety_score * 0.3;
573 weight_sum += 0.3;
574 }
575
576 if let Some(score) = factuality.as_ref().and_then(|f| f.factuality_score) {
580 total_score += score * 0.3;
581 weight_sum += 0.3;
582 }
583
584 if let Some(score) = alignment.as_ref().and_then(|a| a.alignment_score) {
588 total_score += score * 0.4;
589 weight_sum += 0.4;
590 }
591
592 if weight_sum > 0.0 {
593 total_score / weight_sum
594 } else {
595 0.0
596 }
597 }
598
599 fn generate_recommendations(
601 &self,
602 safety: &Option<SafetyAnalysisResult>,
603 factuality: &Option<FactualityAnalysisResult>,
604 alignment: &Option<AlignmentAnalysisResult>,
605 ) -> Vec<String> {
606 let mut recommendations = Vec::new();
607
608 if let Some(s) = safety {
609 if s.safety_score < self.config.safety_threshold {
610 recommendations
611 .push("Consider additional safety filtering or fine-tuning".to_string());
612 }
613 }
614
615 if let Some(score) = factuality.as_ref().and_then(|f| f.factuality_score) {
616 if score < self.config.factuality_threshold {
617 recommendations
618 .push("Verify factual claims and consider knowledge base updates".to_string());
619 }
620 }
621
622 if let Some(score) = alignment.as_ref().and_then(|a| a.alignment_score) {
623 if score < 0.7 {
624 recommendations.push(
625 "Review alignment objectives and consider additional RLHF training".to_string(),
626 );
627 }
628 }
629
630 recommendations
631 }
632
633 fn compute_overall_health(&self) -> Option<f32> {
645 let terms = [
646 Some(self.safety_analyzer.safety_metrics.overall_safety_score),
647 self.factuality_checker.factuality_metrics.overall_factuality_score,
648 self.alignment_monitor.alignment_metrics.overall_alignment_score,
649 ];
650 let present: Vec<f32> = terms.into_iter().flatten().collect();
651 if present.is_empty() {
652 None
653 } else {
654 Some(present.iter().sum::<f32>() / present.len() as f32)
655 }
656 }
657
658 fn identify_critical_issues(&self) -> Vec<CriticalIssue> {
660 let mut issues = Vec::new();
661
662 if self.safety_analyzer.safety_metrics.overall_safety_score < 0.5 {
664 issues.push(CriticalIssue {
665 category: IssueCategory::Safety,
666 severity: IssueSeverity::Critical,
667 description: "Low overall safety score detected".to_string(),
668 recommended_action: "Immediate safety review and filtering required".to_string(),
669 });
670 }
671
672 if self
676 .alignment_monitor
677 .alignment_metrics
678 .overall_alignment_score
679 .is_some_and(|score| score < 0.6)
680 {
681 issues.push(CriticalIssue {
682 category: IssueCategory::Alignment,
683 severity: IssueSeverity::High,
684 description: "Alignment drift detected".to_string(),
685 recommended_action: "Review training data and consider alignment fine-tuning"
686 .to_string(),
687 });
688 }
689
690 issues
691 }
692
693 fn generate_health_recommendations(&self) -> Vec<String> {
695 let mut recommendations = Vec::new();
696
697 if self.safety_analyzer.safety_metrics.overall_safety_score < 0.8 {
699 recommendations.push("Implement additional safety training data".to_string());
700 recommendations.push("Consider constitutional AI techniques".to_string());
701 }
702
703 if self.performance_profiler.generation_metrics.tokens_per_second < 50.0 {
705 recommendations.push("Optimize inference pipeline for better throughput".to_string());
706 recommendations.push("Consider model quantization or distillation".to_string());
707 }
708
709 recommendations
710 }
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct LLMAnalysisReport {
716 pub input: String,
717 pub response: String,
718 pub safety_analysis: Option<SafetyAnalysisResult>,
719 pub factuality_analysis: Option<FactualityAnalysisResult>,
720 pub alignment_analysis: Option<AlignmentAnalysisResult>,
721 pub hallucination_analysis: Option<HallucinationAnalysisResult>,
722 pub bias_analysis: Option<BiasAnalysisResult>,
723 pub performance_analysis: Option<PerformanceAnalysisResult>,
724 pub conversation_analysis: Option<ConversationAnalysisResult>,
725 pub overall_score: f32,
726 pub recommendations: Vec<String>,
727 pub analysis_duration: Duration,
728 pub timestamp: chrono::DateTime<chrono::Utc>,
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize)]
732pub struct BatchLLMAnalysisReport {
733 pub individual_reports: Vec<LLMAnalysisReport>,
734 pub batch_metrics: BatchMetrics,
735 pub batch_size: usize,
736 pub analysis_timestamp: chrono::DateTime<chrono::Utc>,
737}
738
739#[derive(Debug, Clone, Default, Serialize, Deserialize)]
745struct MeanAccumulator {
746 sum: f64,
747 count: usize,
748}
749
750impl MeanAccumulator {
751 fn push(&mut self, value: f32) {
752 self.sum += f64::from(value);
753 self.count += 1;
754 }
755
756 fn mean(&self) -> Option<f32> {
758 if self.count == 0 {
759 None
760 } else {
761 Some((self.sum / self.count as f64) as f32)
762 }
763 }
764}
765
766#[derive(Debug, Clone, Default, Serialize, Deserialize)]
774pub struct BatchMetrics {
775 pub average_overall_score: Option<f32>,
777 pub average_safety_score: Option<f32>,
779 pub average_factuality_score: Option<f32>,
784 pub average_alignment_score: Option<f32>,
788 pub flagged_responses_count: usize,
791 pub critical_issues_count: usize,
794 pub responses_analyzed: usize,
796 pub performance_summary: Option<PerformanceAnalysisResult>,
797 #[serde(skip)]
798 overall_acc: MeanAccumulator,
799 #[serde(skip)]
800 safety_acc: MeanAccumulator,
801 #[serde(skip)]
802 factuality_acc: MeanAccumulator,
803 #[serde(skip)]
804 alignment_acc: MeanAccumulator,
805}
806
807impl BatchMetrics {
808 pub fn update_from_report(&mut self, report: &LLMAnalysisReport) {
814 self.overall_acc.push(report.overall_score);
815
816 if let Some(safety) = report.safety_analysis.as_ref() {
817 self.safety_acc.push(safety.safety_score);
818 if !safety.flagged_content.is_empty() || !safety.detected_harms.is_empty() {
819 self.flagged_responses_count += 1;
820 }
821 if safety.risk_level == RiskLevel::Critical {
822 self.critical_issues_count += 1;
823 }
824 }
825
826 if let Some(score) = report.factuality_analysis.as_ref().and_then(|f| f.factuality_score) {
827 self.factuality_acc.push(score);
828 }
829
830 if let Some(score) = report.alignment_analysis.as_ref().and_then(|a| a.alignment_score) {
831 self.alignment_acc.push(score);
832 }
833
834 if let Some(performance) = report.performance_analysis.as_ref() {
835 self.performance_summary = Some(performance.clone());
836 }
837 }
838
839 pub fn finalize(&mut self, batch_size: usize) {
844 self.responses_analyzed = batch_size;
845 self.average_overall_score = self.overall_acc.mean();
846 self.average_safety_score = self.safety_acc.mean();
847 self.average_factuality_score = self.factuality_acc.mean();
848 self.average_alignment_score = self.alignment_acc.mean();
849 }
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
853pub struct LLMHealthReport {
854 pub overall_health_score: Option<f32>,
857 pub safety_health: HealthSummary,
858 pub factuality_health: HealthSummary,
859 pub alignment_health: HealthSummary,
860 pub bias_health: HealthSummary,
861 pub performance_health: HealthSummary,
862 pub conversation_health: HealthSummary,
863 pub critical_issues: Vec<CriticalIssue>,
864 pub recommendations: Vec<String>,
865 pub report_timestamp: chrono::DateTime<chrono::Utc>,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize)]
869pub struct HealthSummary {
870 pub score: Option<f32>,
874 pub status: Option<HealthStatus>,
876 pub trend: String,
877 pub key_metrics: HashMap<String, f32>,
878 pub issues: Vec<String>,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
882pub enum HealthStatus {
883 Excellent,
884 Good,
885 Fair,
886 Poor,
887 Critical,
888}
889
890fn health_status_from_score(score: f32) -> HealthStatus {
895 if score >= 0.9 {
896 HealthStatus::Excellent
897 } else if score >= 0.75 {
898 HealthStatus::Good
899 } else if score >= 0.5 {
900 HealthStatus::Fair
901 } else if score >= 0.25 {
902 HealthStatus::Poor
903 } else {
904 HealthStatus::Critical
905 }
906}
907
908const HEALTH_TREND_WINDOW: usize = 20;
910
911#[derive(Debug, Clone, Serialize, Deserialize)]
921pub struct HealthTracker {
922 recent_scores: VecDeque<f32>,
931}
932
933impl HealthTracker {
934 fn new() -> Self {
935 Self {
936 recent_scores: VecDeque::with_capacity(HEALTH_TREND_WINDOW),
937 }
938 }
939
940 fn record(&mut self, score: f32) {
942 self.recent_scores.push_back(score);
943 while self.recent_scores.len() > HEALTH_TREND_WINDOW {
944 self.recent_scores.pop_front();
945 }
946 }
947
948 fn average_score(&self) -> Option<f32> {
951 if self.recent_scores.is_empty() {
952 None
953 } else {
954 Some(self.recent_scores.iter().sum::<f32>() / self.recent_scores.len() as f32)
955 }
956 }
957
958 fn status(&self) -> Option<HealthStatus> {
961 self.average_score().map(health_status_from_score)
962 }
963
964 fn trend_label(&self) -> String {
971 if self.recent_scores.len() < 2 {
972 return "Unknown (insufficient history)".to_string();
973 }
974 let mid = self.recent_scores.len() / 2;
975 let older_avg: f32 = self.recent_scores.iter().take(mid).sum::<f32>() / mid as f32;
976 let newer_count = self.recent_scores.len() - mid;
977 let newer_avg: f32 = self.recent_scores.iter().skip(mid).sum::<f32>() / newer_count as f32;
978
979 const EPSILON: f32 = 0.02;
980 let delta = newer_avg - older_avg;
981 if delta > EPSILON {
982 "Improving".to_string()
983 } else if delta < -EPSILON {
984 "Declining".to_string()
985 } else {
986 "Stable".to_string()
987 }
988 }
989}
990
991#[derive(Debug, Clone, Serialize, Deserialize)]
992pub struct CriticalIssue {
993 pub category: IssueCategory,
994 pub severity: IssueSeverity,
995 pub description: String,
996 pub recommended_action: String,
997}
998
999#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1000pub enum IssueCategory {
1001 Safety,
1002 Factuality,
1003 Alignment,
1004 Bias,
1005 Performance,
1006 Conversation,
1007}
1008
1009#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1010pub enum IssueSeverity {
1011 Low,
1012 Medium,
1013 High,
1014 Critical,
1015}
1016
1017#[derive(Debug, Clone, Serialize, Deserialize)]
1019pub struct SafetyAnalysisResult {
1020 pub safety_score: f32,
1021 pub detected_harms: Vec<HarmCategory>,
1022 pub risk_level: RiskLevel,
1023 pub flagged_content: Vec<String>,
1024 pub confidence: f32,
1025}
1026
1027#[derive(Debug, Clone, Serialize, Deserialize)]
1028pub struct FactualityAnalysisResult {
1029 pub factuality_score: Option<f32>,
1038 pub claim_like_sentences: usize,
1042 pub uncertainty_indicator_hits: usize,
1046 pub uncertainty_density: Option<f32>,
1052 pub confidence_scores: Vec<f32>,
1053 pub knowledge_gaps: Vec<String>,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1057pub struct AlignmentAnalysisResult {
1058 pub alignment_score: Option<f32>,
1064 pub objective_scores: HashMap<AlignmentObjective, f32>,
1067 pub violations: Vec<String>,
1070 pub consistency_score: Option<f32>,
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize)]
1076pub struct HallucinationAnalysisResult {
1077 pub hedging_signal: f32,
1080 pub confidence_accuracy: Option<f32>,
1085 pub internal_consistency: f32,
1088 pub detected_fabrications: Vec<String>,
1091}
1092
1093#[derive(Debug, Clone, Serialize, Deserialize)]
1094pub struct BiasAnalysisResult {
1095 pub overall_bias_score: Option<f32>,
1101 pub bias_categories: HashMap<BiasCategory, f32>,
1104 pub detected_biases: Vec<String>,
1106 pub fairness_violations: Vec<String>,
1108}
1109
1110#[derive(Debug, Clone, Serialize, Deserialize)]
1111pub struct PerformanceAnalysisResult {
1112 pub generation_metrics: GenerationMetrics,
1113 pub efficiency_metrics: EfficiencyMetrics,
1114 pub quality_metrics: QualityMetrics,
1115 pub bottlenecks: Vec<String>,
1116}
1117
1118#[derive(Debug, Clone, Serialize, Deserialize)]
1119pub struct ConversationAnalysisResult {
1120 pub dialog_metrics: DialogMetrics,
1121 pub context_consistency: Option<f32>,
1125 pub turn_quality: Option<f32>,
1127 pub engagement_score: Option<f32>,
1129}
1130
1131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1132pub enum RiskLevel {
1133 Low,
1134 Medium,
1135 High,
1136 Critical,
1137}
1138
1139impl SafetyAnalyzer {
1145 pub fn new(_config: &LLMDebugConfig) -> Self {
1146 Self {
1147 toxic_patterns: HashSet::new(),
1148 harm_categories: vec![
1149 HarmCategory::Toxicity,
1150 HarmCategory::Violence,
1151 HarmCategory::SelfHarm,
1152 HarmCategory::Harassment,
1153 HarmCategory::HateSpeech,
1154 ],
1155 safety_metrics: SafetyMetrics {
1156 overall_safety_score: 1.0,
1157 harm_category_scores: HashMap::new(),
1158 flagged_responses: 0,
1159 total_responses_analyzed: 0,
1160 average_response_safety: 1.0,
1161 safety_trend: SafetyTrend::Stable,
1162 },
1163 health: HealthTracker::new(),
1164 }
1165 }
1166
1167 pub async fn analyze_safety(&mut self, response: &str) -> Result<SafetyAnalysisResult> {
1168 let matched_keywords = self.find_harmful_keywords(response);
1169 let safety_score = Self::score_from_matches(matched_keywords.len());
1170 let detected_harms = self.detect_harmful_content(response);
1171 let risk_level = self.assess_risk_level(safety_score);
1172
1173 self.safety_metrics.total_responses_analyzed += 1;
1174 if safety_score < 0.8 {
1175 self.safety_metrics.flagged_responses += 1;
1176 }
1177 self.health.record(safety_score);
1181 if let Some(average) = self.health.average_score() {
1182 self.safety_metrics.overall_safety_score = average;
1183 self.safety_metrics.average_response_safety = average;
1184 }
1185 self.safety_metrics.safety_trend = match self.health.trend_label().as_str() {
1186 "Improving" => SafetyTrend::Improving,
1187 "Declining" => SafetyTrend::Degrading,
1188 _ => SafetyTrend::Stable,
1189 };
1190
1191 let confidence = match matched_keywords.len() {
1197 0 => 0.6,
1198 1 => 0.75,
1199 _ => 0.9,
1200 };
1201
1202 Ok(SafetyAnalysisResult {
1203 safety_score,
1204 detected_harms,
1205 risk_level,
1206 flagged_content: matched_keywords.into_iter().map(str::to_string).collect(),
1209 confidence,
1210 })
1211 }
1212
1213 fn find_harmful_keywords(&self, response: &str) -> Vec<&'static str> {
1219 const HARMFUL_KEYWORDS: [&str; 4] = ["violence", "harm", "toxic", "hate"];
1220 let lower = response.to_lowercase();
1221 HARMFUL_KEYWORDS
1222 .iter()
1223 .copied()
1224 .filter(|keyword| lower.contains(keyword))
1225 .collect()
1226 }
1227
1228 fn score_from_matches(match_count: usize) -> f32 {
1231 match match_count {
1232 0 => 0.95,
1233 1 => 0.5,
1234 _ => 0.2,
1235 }
1236 }
1237
1238 fn compute_safety_score(&self, response: &str) -> f32 {
1239 Self::score_from_matches(self.find_harmful_keywords(response).len())
1240 }
1241
1242 fn detect_harmful_content(&self, response: &str) -> Vec<HarmCategory> {
1243 let lower = response.to_lowercase();
1248 let mut detected = Vec::new();
1249
1250 if lower.contains("violence") {
1251 detected.push(HarmCategory::Violence);
1252 }
1253 if lower.contains("toxic") {
1254 detected.push(HarmCategory::Toxicity);
1255 }
1256 if lower.contains("hate") {
1257 detected.push(HarmCategory::HateSpeech);
1258 }
1259
1260 detected
1261 }
1262
1263 fn assess_risk_level(&self, safety_score: f32) -> RiskLevel {
1264 if safety_score >= 0.9 {
1265 RiskLevel::Low
1266 } else if safety_score >= 0.7 {
1267 RiskLevel::Medium
1268 } else if safety_score >= 0.5 {
1269 RiskLevel::High
1270 } else {
1271 RiskLevel::Critical
1272 }
1273 }
1274
1275 pub fn get_health_summary(&self) -> HealthSummary {
1280 HealthSummary {
1281 score: self.health.average_score(),
1282 status: self.health.status(),
1283 trend: self.health.trend_label(),
1284 key_metrics: HashMap::new(),
1285 issues: vec![],
1286 }
1287 }
1288}
1289
1290impl FactualityChecker {
1291 pub fn new(_config: &LLMDebugConfig) -> Self {
1292 Self {
1293 fact_databases: vec!["wikipedia".to_string(), "wikidata".to_string()],
1294 uncertainty_indicators: ["might", "possibly", "unclear", "uncertain"]
1295 .iter()
1296 .map(|s| s.to_string())
1297 .collect(),
1298 factuality_metrics: FactualityMetrics {
1299 overall_factuality_score: None,
1300 average_uncertainty_density: None,
1301 claim_like_sentences_seen: 0,
1302 uncertainty_indicator_hits: 0,
1303 conflicting_information: 0,
1304 uncertainty_expressions: 0,
1305 knowledge_gaps: vec![],
1306 confidence_distribution: vec![],
1307 },
1308 health: HealthTracker::new(),
1309 }
1310 }
1311
1312 pub async fn check_factuality(
1319 &mut self,
1320 response: &str,
1321 _context: Option<&[String]>,
1322 ) -> Result<FactualityAnalysisResult> {
1323 let claim_like_sentences = self.count_claim_like_sentences(response);
1324 let uncertainty_indicator_hits = self.count_uncertainty_indicators(response);
1325 let uncertainty_density = self.compute_uncertainty_density(response);
1326
1327 self.factuality_metrics.claim_like_sentences_seen += claim_like_sentences;
1328 self.factuality_metrics.uncertainty_indicator_hits += uncertainty_indicator_hits;
1329 if let Some(density) = uncertainty_density {
1334 self.health.record(density);
1335 self.factuality_metrics.average_uncertainty_density = self.health.average_score();
1336 }
1337
1338 Ok(FactualityAnalysisResult {
1339 factuality_score: None,
1340 claim_like_sentences,
1341 uncertainty_indicator_hits,
1342 uncertainty_density,
1343 confidence_scores: self.compute_claim_confidence_scores(response),
1348 knowledge_gaps: self.extract_knowledge_gaps(response),
1351 })
1352 }
1353
1354 fn compute_uncertainty_density(&self, response: &str) -> Option<f32> {
1362 let claims: Vec<&str> = Self::claim_like_sentences(response).collect();
1363 if claims.is_empty() {
1364 return None;
1365 }
1366 let uncertain = claims
1367 .iter()
1368 .filter(|claim| {
1369 let lower = claim.to_lowercase();
1370 self.uncertainty_indicators.iter().any(|ind| lower.contains(ind.as_str()))
1371 })
1372 .count();
1373 Some(uncertain as f32 / claims.len() as f32)
1374 }
1375
1376 fn claim_like_sentences(response: &str) -> impl Iterator<Item = &str> {
1380 response.split('.').filter(|s| s.len() > 10)
1381 }
1382
1383 fn count_claim_like_sentences(&self, response: &str) -> usize {
1386 Self::claim_like_sentences(response).count()
1387 }
1388
1389 fn count_uncertainty_indicators(&self, response: &str) -> usize {
1392 self.uncertainty_indicators
1393 .iter()
1394 .map(|indicator| response.matches(indicator).count())
1395 .sum()
1396 }
1397
1398 fn compute_claim_confidence_scores(&self, response: &str) -> Vec<f32> {
1405 Self::claim_like_sentences(response)
1406 .map(|claim| {
1407 let lower = claim.to_lowercase();
1408 let has_uncertainty =
1409 self.uncertainty_indicators.iter().any(|ind| lower.contains(ind.as_str()));
1410 if has_uncertainty {
1411 0.5
1412 } else {
1413 0.85
1414 }
1415 })
1416 .collect()
1417 }
1418
1419 fn extract_knowledge_gaps(&self, response: &str) -> Vec<String> {
1422 response
1423 .split('.')
1424 .map(str::trim)
1425 .filter(|s| !s.is_empty())
1426 .filter(|s| {
1427 let lower = s.to_lowercase();
1428 self.uncertainty_indicators.iter().any(|ind| lower.contains(ind.as_str()))
1429 })
1430 .map(str::to_string)
1431 .collect()
1432 }
1433
1434 pub fn get_health_summary(&self) -> HealthSummary {
1438 HealthSummary {
1439 score: self.health.average_score(),
1440 status: self.health.status(),
1441 trend: self.health.trend_label(),
1442 key_metrics: HashMap::new(),
1443 issues: vec![],
1444 }
1445 }
1446}
1447
1448impl AlignmentMonitor {
1449 pub fn new(_config: &LLMDebugConfig) -> Self {
1450 Self {
1451 alignment_objectives: vec![
1452 AlignmentObjective::Helpfulness,
1453 AlignmentObjective::Harmlessness,
1454 AlignmentObjective::Honesty,
1455 AlignmentObjective::Fairness,
1456 ],
1457 alignment_metrics: AlignmentMetrics {
1458 objective_scores: HashMap::new(),
1459 overall_alignment_score: None,
1460 alignment_violations: 0,
1461 value_consistency_score: None,
1462 behavioral_drift: None,
1463 alignment_trend: None,
1464 },
1465 health: HealthTracker::new(),
1466 }
1467 }
1468
1469 pub async fn check_alignment(
1470 &mut self,
1471 input: &str,
1472 response: &str,
1473 ) -> Result<AlignmentAnalysisResult> {
1474 let _ = (input, response);
1479
1480 Ok(AlignmentAnalysisResult {
1481 alignment_score: None,
1482 objective_scores: HashMap::new(),
1483 violations: Vec::new(),
1484 consistency_score: None,
1485 })
1486 }
1487
1488 pub fn get_health_summary(&self) -> HealthSummary {
1491 HealthSummary {
1492 score: self.health.average_score(),
1493 status: self.health.status(),
1494 trend: self.health.trend_label(),
1495 key_metrics: HashMap::new(),
1496 issues: vec![],
1497 }
1498 }
1499}
1500
1501impl HallucinationDetector {
1502 pub fn new(_config: &LLMDebugConfig) -> Self {
1503 Self {
1504 confidence_thresholds: HashMap::new(),
1505 consistency_checker: ConsistencyChecker {
1506 previous_responses: Vec::new(),
1507 consistency_cache: HashMap::new(),
1508 },
1509 hallucination_metrics: HallucinationMetrics {
1510 hallucination_rate: 0.1,
1511 confidence_accuracy_correlation: 0.7,
1512 factual_consistency_score: 0.8,
1513 internal_consistency_score: 0.85,
1514 source_attribution_accuracy: 0.9,
1515 detected_fabrications: 0,
1516 uncertain_responses: 0,
1517 },
1518 }
1519 }
1520
1521 pub async fn detect_hallucinations(
1522 &mut self,
1523 response: &str,
1524 _context: Option<&[String]>,
1525 ) -> Result<HallucinationAnalysisResult> {
1526 let internal_consistency = self.consistency_checker.check_consistency(response);
1527
1528 Ok(HallucinationAnalysisResult {
1529 hedging_signal: Self::hedging_signal(response),
1530 confidence_accuracy: None,
1531 internal_consistency,
1532 detected_fabrications: Vec::new(),
1533 })
1534 }
1535
1536 pub fn hedging_signal(response: &str) -> f32 {
1546 const HEDGING_PHRASES: &[&str] = &[
1548 "i'm not sure",
1549 "i am not sure",
1550 "i think",
1551 "i believe",
1552 "possibly",
1553 "might be",
1554 "as far as i know",
1555 "if i recall",
1556 "i'm not certain",
1557 "cannot verify",
1558 ];
1559 let lowered = response.to_lowercase();
1560 let hits = HEDGING_PHRASES.iter().filter(|p| lowered.contains(**p)).count();
1561 hits as f32 / HEDGING_PHRASES.len() as f32
1562 }
1563}
1564
1565impl ConsistencyChecker {
1566 pub fn check_consistency(&mut self, response: &str) -> f32 {
1567 self.previous_responses.push(response.to_string());
1568 0.85
1570 }
1571}
1572
1573impl BiasDetector {
1574 pub fn new(_config: &LLMDebugConfig) -> Self {
1575 Self {
1576 bias_categories: vec![
1577 BiasCategory::Gender,
1578 BiasCategory::Race,
1579 BiasCategory::Religion,
1580 BiasCategory::Age,
1581 ],
1582 demographic_groups: vec![
1583 "male".to_string(),
1584 "female".to_string(),
1585 "young".to_string(),
1586 "elderly".to_string(),
1587 ],
1588 bias_metrics: BiasMetrics {
1589 overall_bias_score: 0.1, bias_category_scores: HashMap::new(),
1591 demographic_fairness: HashMap::new(),
1592 representation_bias: 0.1,
1593 stereotype_propagation: 0.05,
1594 bias_amplification: 0.08,
1595 fairness_violations: 0,
1596 },
1597 health: HealthTracker::new(),
1600 }
1601 }
1602
1603 pub async fn detect_bias(&mut self, response: &str) -> Result<BiasAnalysisResult> {
1604 let _ = response;
1608
1609 Ok(BiasAnalysisResult {
1610 overall_bias_score: None,
1611 bias_categories: HashMap::new(),
1612 detected_biases: Vec::new(),
1613 fairness_violations: Vec::new(),
1614 })
1615 }
1616
1617 pub fn get_health_summary(&self) -> HealthSummary {
1620 HealthSummary {
1621 score: self.health.average_score(),
1622 status: self.health.status(),
1623 trend: self.health.trend_label(),
1624 key_metrics: HashMap::new(),
1625 issues: vec![],
1626 }
1627 }
1628}
1629
1630impl Default for LLMPerformanceProfiler {
1631 fn default() -> Self {
1632 Self::new()
1633 }
1634}
1635
1636impl LLMPerformanceProfiler {
1637 pub fn new() -> Self {
1638 Self {
1639 generation_metrics: GenerationMetrics {
1640 tokens_per_second: 100.0,
1641 average_response_length: 150.0,
1642 generation_latency_p50: 200.0,
1643 generation_latency_p95: 500.0,
1644 generation_latency_p99: 1000.0,
1645 first_token_latency: 50.0,
1646 completion_rate: 0.98,
1647 timeout_rate: 0.02,
1648 },
1649 efficiency_metrics: EfficiencyMetrics {
1650 memory_efficiency: 0.85,
1651 compute_utilization: 0.75,
1652 energy_consumption: 0.5, carbon_footprint_estimate: 0.1, cost_per_token: 0.001, batch_processing_efficiency: 0.9,
1656 cache_hit_rate: 0.7,
1657 },
1658 quality_metrics: QualityMetrics {
1659 coherence_score: 0.9,
1660 relevance_score: 0.85,
1661 fluency_score: 0.95,
1662 informativeness_score: 0.8,
1663 creativity_score: 0.7,
1664 factual_accuracy: 0.85,
1665 readability_score: 0.9,
1666 engagement_score: 0.8,
1667 },
1668 scalability_metrics: ScalabilityMetrics {
1669 concurrent_user_capacity: 1000,
1670 throughput_scaling: 0.8,
1671 memory_scaling: 0.7,
1672 latency_degradation: 0.1,
1673 bottleneck_analysis: vec!["Memory bandwidth".to_string()],
1674 resource_utilization_efficiency: 0.8,
1675 },
1676 health: HealthTracker::new(),
1677 }
1678 }
1679
1680 pub async fn profile_response(
1681 &mut self,
1682 _response: &str,
1683 generation_metrics: Option<GenerationMetrics>,
1684 ) -> Result<PerformanceAnalysisResult> {
1685 let gen_metrics = generation_metrics.unwrap_or_else(|| self.generation_metrics.clone());
1686
1687 self.health.record((gen_metrics.tokens_per_second / 200.0).min(1.0));
1693
1694 Ok(PerformanceAnalysisResult {
1695 generation_metrics: gen_metrics,
1696 efficiency_metrics: self.efficiency_metrics.clone(),
1697 quality_metrics: self.quality_metrics.clone(),
1698 bottlenecks: Vec::new(),
1701 })
1702 }
1703
1704 pub fn get_health_summary(&self) -> HealthSummary {
1707 HealthSummary {
1708 score: self.health.average_score(),
1709 status: self.health.status(),
1710 trend: self.health.trend_label(),
1711 key_metrics: HashMap::new(),
1712 issues: vec![],
1713 }
1714 }
1715}
1716
1717impl ConversationAnalyzer {
1718 pub fn new(_config: &LLMDebugConfig) -> Self {
1719 Self {
1720 conversation_history: Vec::new(),
1721 dialog_metrics: DialogMetrics {
1722 conversation_coherence: 0.9,
1723 context_maintenance: 0.85,
1724 topic_consistency: 0.8,
1725 response_appropriateness: 0.9,
1726 conversation_engagement: 0.75,
1727 turn_taking_naturalness: 0.8,
1728 memory_utilization: 0.7,
1729 dialog_success_rate: 0.85,
1730 },
1731 context_tracking: ContextTracker {
1732 active_topics: HashSet::new(),
1733 entity_mentions: HashMap::new(),
1734 context_window: Vec::new(),
1735 attention_weights: Vec::new(),
1736 },
1737 health: HealthTracker::new(),
1738 }
1739 }
1740
1741 pub async fn analyze_turn(
1742 &mut self,
1743 turn: &ConversationTurn,
1744 ) -> Result<ConversationAnalysisResult> {
1745 self.conversation_history.push(turn.clone());
1746 self.context_tracking.update_from_turn(turn);
1747 Ok(ConversationAnalysisResult {
1752 dialog_metrics: self.dialog_metrics.clone(),
1753 context_consistency: None,
1754 turn_quality: None,
1755 engagement_score: None,
1756 })
1757 }
1758
1759 pub fn get_health_summary(&self) -> HealthSummary {
1762 HealthSummary {
1763 score: self.health.average_score(),
1764 status: self.health.status(),
1765 trend: self.health.trend_label(),
1766 key_metrics: HashMap::new(),
1767 issues: vec![],
1768 }
1769 }
1770}
1771
1772impl ContextTracker {
1773 pub fn update_from_turn(&mut self, turn: &ConversationTurn) {
1774 self.context_window.push(turn.model_response.clone());
1776 if self.context_window.len() > 10 {
1777 self.context_window.remove(0);
1778 }
1779 }
1780}
1781
1782#[macro_export]
1784macro_rules! debug_llm_response {
1785 ($debugger:expr, $input:expr, $response:expr) => {
1786 $debugger.analyze_response($input, $response, None, None).await
1787 };
1788}
1789
1790#[macro_export]
1791macro_rules! debug_llm_batch {
1792 ($debugger:expr, $interactions:expr) => {
1793 $debugger.analyze_batch($interactions).await
1794 };
1795}
1796
1797pub fn llm_debugger() -> LLMDebugger {
1799 LLMDebugger::new(LLMDebugConfig::default())
1800}
1801
1802pub fn llm_debugger_with_config(config: LLMDebugConfig) -> LLMDebugger {
1804 LLMDebugger::new(config)
1805}
1806
1807pub fn safety_focused_config() -> LLMDebugConfig {
1809 LLMDebugConfig {
1810 enable_safety_analysis: true,
1811 enable_factuality_checking: true,
1812 enable_alignment_monitoring: true,
1813 enable_hallucination_detection: true,
1814 enable_bias_detection: true,
1815 enable_llm_performance_profiling: false,
1816 enable_conversation_analysis: false,
1817 safety_threshold: 0.9,
1818 factuality_threshold: 0.8,
1819 max_conversation_length: 50,
1820 analysis_sampling_rate: 1.0,
1821 }
1822}
1823
1824pub fn performance_focused_config() -> LLMDebugConfig {
1826 LLMDebugConfig {
1827 enable_safety_analysis: false,
1828 enable_factuality_checking: false,
1829 enable_alignment_monitoring: false,
1830 enable_hallucination_detection: false,
1831 enable_bias_detection: false,
1832 enable_llm_performance_profiling: true,
1833 enable_conversation_analysis: true,
1834 safety_threshold: 0.7,
1835 factuality_threshold: 0.6,
1836 max_conversation_length: 200,
1837 analysis_sampling_rate: 0.1,
1838 }
1839}
1840
1841#[cfg(test)]
1842#[path = "llm_debugging_tests.rs"]
1843mod llm_debugging_tests;
1844
1845#[cfg(test)]
1849#[path = "llm_debugging_tests2.rs"]
1850mod tests;