Skip to main content

llm_token_visualizer/
utils.rs

1use crate::data::{TokenAnalysis, TokenFlag, TokenInfo};
2
3/// Simple tokenizer for demo purposes - splits on whitespace and punctuation
4pub fn simple_tokenize(text: &str) -> Vec<String> {
5    let mut tokens = Vec::new();
6    let mut current_token = String::new();
7
8    for ch in text.chars() {
9        if ch.is_whitespace() {
10            if !current_token.is_empty() {
11                tokens.push(current_token.clone());
12                current_token.clear();
13            }
14        } else if ch.is_ascii_punctuation() {
15            if !current_token.is_empty() {
16                tokens.push(current_token.clone());
17                current_token.clear();
18            }
19            tokens.push(ch.to_string());
20        } else {
21            current_token.push(ch);
22        }
23    }
24
25    if !current_token.is_empty() {
26        tokens.push(current_token);
27    }
28
29    tokens
30}
31
32/// Create a basic token analysis from text with random confidence scores
33/// This is useful for testing when you don't have actual LLM confidence data
34pub fn create_mock_analysis(text: &str) -> TokenAnalysis {
35    use std::collections::hash_map::DefaultHasher;
36    use std::hash::{Hash, Hasher};
37
38    let tokens = simple_tokenize(text);
39    let mut token_infos = Vec::new();
40
41    for token in tokens {
42        // Use a simple hash-based "random" confidence for consistent results
43        let mut hasher = DefaultHasher::new();
44        token.hash(&mut hasher);
45        let hash = hasher.finish();
46        let confidence = 0.5 + (hash % 100) as f64 / 200.0; // Range: 0.5 to 1.0
47
48        token_infos.push(TokenInfo {
49            text: token,
50            confidence,
51        });
52    }
53
54    // Add some mock flags
55    let mut flags = Vec::new();
56    if token_infos.len() > 5 {
57        flags.push(TokenFlag {
58            start: 0,
59            end: 2,
60            flag: "fact".to_string(),
61            description: Some("Opening statement".to_string()),
62        });
63
64        if token_infos.len() > 10 {
65            flags.push(TokenFlag {
66                start: token_infos.len() - 3,
67                end: token_infos.len(),
68                flag: "uncertain".to_string(),
69                description: Some("Concluding remarks".to_string()),
70            });
71        }
72    }
73
74    TokenAnalysis {
75        tokens: token_infos,
76        flags,
77    }
78}
79
80/// Calculate various metrics from token analysis
81pub struct AnalysisMetrics {
82    pub total_tokens: usize,
83    pub avg_confidence: f64,
84    pub min_confidence: f64,
85    pub max_confidence: f64,
86    pub low_confidence_tokens: usize,
87    pub flagged_tokens: usize,
88}
89
90impl AnalysisMetrics {
91    pub fn from_analysis(analysis: &TokenAnalysis) -> Self {
92        let confidences: Vec<f64> = analysis.tokens.iter().map(|t| t.confidence).collect();
93
94        let total_tokens = confidences.len();
95        let avg_confidence = confidences.iter().sum::<f64>() / total_tokens as f64;
96        let min_confidence = confidences.iter().fold(f64::INFINITY, |a, &b| a.min(b));
97        let max_confidence = confidences.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
98        let low_confidence_tokens = confidences.iter().filter(|&&c| c < 0.5).count();
99
100        let mut flagged_tokens = std::collections::HashSet::new();
101        for flag in &analysis.flags {
102            for i in flag.start..flag.end {
103                flagged_tokens.insert(i);
104            }
105        }
106
107        Self {
108            total_tokens,
109            avg_confidence,
110            min_confidence,
111            max_confidence,
112            low_confidence_tokens,
113            flagged_tokens: flagged_tokens.len(),
114        }
115    }
116}
117
118/// Detect potential issues in token analysis
119pub fn detect_issues(analysis: &TokenAnalysis) -> Vec<String> {
120    let mut issues = Vec::new();
121
122    // Check for very low confidence tokens
123    let low_conf_tokens: Vec<(usize, &TokenInfo)> = analysis
124        .tokens
125        .iter()
126        .enumerate()
127        .filter(|(_, token)| token.confidence < 0.3)
128        .collect();
129
130    if !low_conf_tokens.is_empty() {
131        issues.push(format!(
132            "Found {} tokens with very low confidence (<0.3)",
133            low_conf_tokens.len()
134        ));
135    }
136
137    // Check for sudden confidence drops
138    for window in analysis.tokens.windows(3) {
139        let confidences: Vec<f64> = window.iter().map(|t| t.confidence).collect();
140        if confidences[1] < confidences[0] - 0.4 && confidences[1] < confidences[2] - 0.4 {
141            issues.push(
142                "Detected sudden confidence drop (potential hallucination point)".to_string(),
143            );
144            break;
145        }
146    }
147
148    // Check for overconfident sequences
149    let mut overconfident_streak = 0;
150    for token in &analysis.tokens {
151        if token.confidence > 0.95 {
152            overconfident_streak += 1;
153        } else {
154            if overconfident_streak > 5 {
155                issues.push(
156                    "Detected long sequence of overconfident tokens (potential hallucination)"
157                        .to_string(),
158                );
159            }
160            overconfident_streak = 0;
161        }
162    }
163
164    issues
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_simple_tokenize() {
173        let text = "Hello, world! This is a test.";
174        let tokens = simple_tokenize(text);
175        assert_eq!(
176            tokens,
177            vec!["Hello", ",", "world", "!", "This", "is", "a", "test", "."]
178        );
179    }
180
181    #[test]
182    fn test_create_mock_analysis() {
183        let text = "This is a test sentence.";
184        let analysis = create_mock_analysis(text);
185
186        assert!(!analysis.tokens.is_empty());
187        assert_eq!(analysis.tokens.len(), 6); // "This", "is", "a", "test", "sentence", "."
188
189        for token in &analysis.tokens {
190            assert!(token.confidence >= 0.5 && token.confidence <= 1.0);
191        }
192    }
193
194    #[test]
195    fn test_analysis_metrics() {
196        let analysis = TokenAnalysis {
197            tokens: vec![
198                TokenInfo {
199                    text: "test1".to_string(),
200                    confidence: 0.8,
201                },
202                TokenInfo {
203                    text: "test2".to_string(),
204                    confidence: 0.3,
205                },
206                TokenInfo {
207                    text: "test3".to_string(),
208                    confidence: 0.9,
209                },
210            ],
211            flags: vec![TokenFlag {
212                start: 0,
213                end: 2,
214                flag: "test".to_string(),
215                description: None,
216            }],
217        };
218
219        let metrics = AnalysisMetrics::from_analysis(&analysis);
220        assert_eq!(metrics.total_tokens, 3);
221        assert_eq!(metrics.low_confidence_tokens, 1);
222        assert_eq!(metrics.flagged_tokens, 2);
223        assert!((metrics.avg_confidence - 0.6667).abs() < 0.001);
224    }
225
226    #[test]
227    fn test_detect_issues() {
228        let analysis = TokenAnalysis {
229            tokens: vec![
230                TokenInfo {
231                    text: "good".to_string(),
232                    confidence: 0.8,
233                },
234                TokenInfo {
235                    text: "bad".to_string(),
236                    confidence: 0.2,
237                },
238                TokenInfo {
239                    text: "good".to_string(),
240                    confidence: 0.9,
241                },
242            ],
243            flags: vec![],
244        };
245
246        let issues = detect_issues(&analysis);
247        assert!(!issues.is_empty());
248        assert!(issues
249            .iter()
250            .any(|issue| issue.contains("very low confidence")));
251    }
252}