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
32#[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 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; token_infos.push(TokenInfo {
53 text: token,
54 confidence,
55 });
56 }
57
58 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
84pub struct AnalysisMetrics {
86 pub total_tokens: usize,
88 pub avg_confidence: f64,
90 pub min_confidence: f64,
92 pub max_confidence: f64,
94 pub low_confidence_tokens: usize,
96 pub flagged_tokens: usize,
98}
99
100impl AnalysisMetrics {
101 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
129pub fn detect_issues(analysis: &TokenAnalysis) -> Vec<String> {
131 let mut issues = Vec::new();
132
133 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 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 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); 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}