llm_token_visualizer/
utils.rs1use crate::data::{TokenAnalysis, TokenFlag, TokenInfo};
2
3pub 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
32pub 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 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; token_infos.push(TokenInfo {
49 text: token,
50 confidence,
51 });
52 }
53
54 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
80pub 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
118pub fn detect_issues(analysis: &TokenAnalysis) -> Vec<String> {
120 let mut issues = Vec::new();
121
122 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 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 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); 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}