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