1use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31pub type EntityAnnotation = (String, usize, usize);
33
34pub type AnnotatedText = (String, Vec<EntityAnnotation>);
36
37#[derive(Debug, Clone)]
43pub struct SupportExample {
44 pub text: String,
46 pub entity_text: String,
48 pub start: usize,
50 pub end: usize,
52}
53
54impl SupportExample {
55 pub fn new(
57 text: impl Into<String>,
58 entity_text: impl Into<String>,
59 start: usize,
60 end: usize,
61 ) -> Self {
62 Self {
63 text: text.into(),
64 entity_text: entity_text.into(),
65 start,
66 end,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct FewShotTask {
74 pub entity_type: String,
76 pub support: Vec<SupportExample>,
78 pub query_texts: Vec<String>,
80}
81
82#[derive(Debug, Clone)]
84pub struct FewShotGold {
85 pub text: String,
87 pub entities: Vec<(String, usize, usize)>, }
90
91#[derive(Debug, Clone)]
93pub struct FewShotPrediction {
94 pub text: String,
96 pub predicted: Vec<(String, usize, usize, f64)>, }
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct FewShotTaskResults {
103 pub entity_type: String,
105 pub k: usize,
107 pub precision: f64,
109 pub recall: f64,
111 pub f1: f64,
113 pub num_gold: usize,
115 pub num_predicted: usize,
117 pub num_correct: usize,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct FewShotResults {
124 pub per_type: HashMap<String, FewShotTaskResults>,
126 pub macro_f1: f64,
128 pub micro_f1: f64,
130 pub k_values: Vec<usize>,
132 pub performance_by_k: Vec<(usize, f64)>,
134 pub failed_types: Vec<String>,
136 pub insights: Vec<String>,
138}
139
140#[derive(Debug, Clone)]
146pub struct FewShotEvaluator {
147 pub k_values: Vec<usize>,
149 pub success_threshold: f64,
151}
152
153impl Default for FewShotEvaluator {
154 fn default() -> Self {
155 Self {
156 k_values: vec![1, 2, 5, 10],
157 success_threshold: 0.5,
158 }
159 }
160}
161
162impl FewShotEvaluator {
163 pub fn new(k_values: Vec<usize>) -> Self {
165 Self {
166 k_values,
167 success_threshold: 0.5,
168 }
169 }
170
171 pub fn evaluate(
173 &self,
174 entity_type: &str,
175 k: usize,
176 predictions: &[FewShotPrediction],
177 gold: &[FewShotGold],
178 ) -> FewShotTaskResults {
179 assert_eq!(
180 predictions.len(),
181 gold.len(),
182 "Predictions and gold must have same length"
183 );
184
185 let mut total_correct = 0;
186 let mut total_predicted = 0;
187 let mut total_gold = 0;
188
189 for (pred, g) in predictions.iter().zip(gold.iter()) {
190 total_gold += g.entities.len();
191 total_predicted += pred.predicted.len();
192
193 for (g_text, g_start, g_end) in &g.entities {
195 for (p_text, p_start, p_end, _conf) in &pred.predicted {
196 if g_start == p_start && g_end == p_end {
197 total_correct += 1;
198 break;
199 }
200 if g_text.to_lowercase() == p_text.to_lowercase() {
202 total_correct += 1;
203 break;
204 }
205 }
206 }
207 }
208
209 let precision = if total_predicted == 0 {
211 0.0
212 } else {
213 total_correct as f64 / total_predicted as f64
214 };
215
216 let recall = if total_gold == 0 {
218 0.0
219 } else {
220 total_correct as f64 / total_gold as f64
221 };
222
223 let f1 = if precision + recall == 0.0 {
224 0.0
225 } else {
226 2.0 * precision * recall / (precision + recall)
227 };
228
229 FewShotTaskResults {
230 entity_type: entity_type.to_string(),
231 k,
232 precision,
233 recall,
234 f1,
235 num_gold: total_gold,
236 num_predicted: total_predicted,
237 num_correct: total_correct,
238 }
239 }
240
241 pub fn aggregate(&self, results: Vec<FewShotTaskResults>) -> FewShotResults {
243 let mut per_type: HashMap<String, FewShotTaskResults> = HashMap::new();
244 let mut by_k: HashMap<usize, Vec<f64>> = HashMap::new();
245
246 for r in &results {
247 per_type.insert(r.entity_type.clone(), r.clone());
248 by_k.entry(r.k).or_default().push(r.f1);
249 }
250
251 let macro_f1 = if results.is_empty() {
253 0.0
254 } else {
255 results.iter().map(|r| r.f1).sum::<f64>() / results.len() as f64
256 };
257
258 let total_correct: usize = results.iter().map(|r| r.num_correct).sum();
260 let total_predicted: usize = results.iter().map(|r| r.num_predicted).sum();
261 let total_gold: usize = results.iter().map(|r| r.num_gold).sum();
262
263 let micro_precision = if total_predicted == 0 {
265 0.0
266 } else {
267 total_correct as f64 / total_predicted as f64
268 };
269 let micro_recall = if total_gold == 0 {
271 0.0
272 } else {
273 total_correct as f64 / total_gold as f64
274 };
275 let micro_f1 = if micro_precision + micro_recall == 0.0 {
276 0.0
277 } else {
278 2.0 * micro_precision * micro_recall / (micro_precision + micro_recall)
279 };
280
281 let mut performance_by_k: Vec<_> = by_k
283 .iter()
284 .map(|(k, scores)| (*k, scores.iter().sum::<f64>() / scores.len() as f64))
285 .collect();
286 performance_by_k.sort_by_key(|(k, _)| *k);
287
288 let failed_types: Vec<_> = results
290 .iter()
291 .filter(|r| r.f1 < self.success_threshold)
292 .map(|r| r.entity_type.clone())
293 .collect();
294
295 let mut insights = Vec::new();
297
298 if !performance_by_k.is_empty() {
299 let min_k_f1 = performance_by_k.first().map(|(_, f1)| *f1).unwrap_or(0.0);
300 let max_k_f1 = performance_by_k.last().map(|(_, f1)| *f1).unwrap_or(0.0);
301 let improvement = max_k_f1 - min_k_f1;
302
303 if improvement > 0.2 {
304 insights.push(format!(
305 "Strong learning: +{:.0}% F1 from K=1 to K={}",
306 improvement * 100.0,
307 performance_by_k.last().map(|(k, _)| *k).unwrap_or(10)
308 ));
309 } else if improvement < 0.05 {
310 insights.push(
311 "Minimal improvement with more examples - may need different approach".into(),
312 );
313 }
314 }
315
316 if !failed_types.is_empty() {
317 insights.push(format!(
318 "Struggling with {} entity types: {:?}",
319 failed_types.len(),
320 &failed_types[..failed_types.len().min(3)]
321 ));
322 }
323
324 if macro_f1 < 0.3 {
325 insights.push(
326 "Low overall few-shot performance - consider pre-training on related data".into(),
327 );
328 }
329
330 FewShotResults {
331 per_type,
332 macro_f1,
333 micro_f1,
334 k_values: self.k_values.clone(),
335 performance_by_k,
336 failed_types,
337 insights,
338 }
339 }
340}
341
342pub fn simulate_few_shot_task(
350 entity_type: &str,
351 all_examples: &[AnnotatedText],
352 k: usize,
353 max_queries: usize,
354) -> Option<(FewShotTask, Vec<FewShotGold>)> {
355 let mut matching: Vec<_> = all_examples
357 .iter()
358 .filter(|(_, entities)| !entities.is_empty())
359 .cloned()
360 .collect();
361
362 if matching.len() < k + 1 {
363 return None; }
365
366 let support: Vec<_> = matching
368 .drain(..k)
369 .filter_map(|(text, entities)| {
370 let (entity_text, start, end) = entities.first()?;
371 Some(SupportExample::new(text, entity_text.clone(), *start, *end))
372 })
373 .collect();
374
375 let query_count = matching.len().min(max_queries);
376 let queries: Vec<_> = matching[..query_count].to_vec();
377
378 let task = FewShotTask {
379 entity_type: entity_type.to_string(),
380 support,
381 query_texts: queries.iter().map(|(t, _)| t.clone()).collect(),
382 };
383
384 let gold: Vec<_> = queries
385 .iter()
386 .map(|(text, entities)| FewShotGold {
387 text: text.clone(),
388 entities: entities.clone(),
389 })
390 .collect();
391
392 Some((task, gold))
393}
394
395#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_perfect_predictions() {
405 let evaluator = FewShotEvaluator::default();
406
407 let predictions = vec![FewShotPrediction {
408 text: "Has diabetes".into(),
409 predicted: vec![("diabetes".into(), 4, 12, 0.95)],
410 }];
411
412 let gold = vec![FewShotGold {
413 text: "Has diabetes".into(),
414 entities: vec![("diabetes".into(), 4, 12)],
415 }];
416
417 let results = evaluator.evaluate("DISEASE", 2, &predictions, &gold);
418 assert!((results.f1 - 1.0).abs() < 0.01);
419 assert_eq!(results.num_correct, 1);
420 }
421
422 #[test]
423 fn test_no_predictions() {
424 let evaluator = FewShotEvaluator::default();
425
426 let predictions = vec![FewShotPrediction {
427 text: "Has diabetes".into(),
428 predicted: vec![],
429 }];
430
431 let gold = vec![FewShotGold {
432 text: "Has diabetes".into(),
433 entities: vec![("diabetes".into(), 4, 12)],
434 }];
435
436 let results = evaluator.evaluate("DISEASE", 2, &predictions, &gold);
437 assert!((results.recall).abs() < 0.01);
438 assert_eq!(results.num_correct, 0);
439 }
440
441 #[test]
442 fn test_aggregate_results() {
443 let evaluator = FewShotEvaluator::default();
444
445 let results = vec![
446 FewShotTaskResults {
447 entity_type: "PER".into(),
448 k: 2,
449 precision: 0.8,
450 recall: 0.7,
451 f1: 0.75,
452 num_gold: 10,
453 num_predicted: 8,
454 num_correct: 7,
455 },
456 FewShotTaskResults {
457 entity_type: "ORG".into(),
458 k: 2,
459 precision: 0.6,
460 recall: 0.5,
461 f1: 0.55,
462 num_gold: 10,
463 num_predicted: 9,
464 num_correct: 5,
465 },
466 ];
467
468 let aggregated = evaluator.aggregate(results);
469 assert!((aggregated.macro_f1 - 0.65).abs() < 0.01);
470 assert_eq!(aggregated.per_type.len(), 2);
471 }
472
473 #[test]
474 fn test_failed_types_detection() {
475 let evaluator = FewShotEvaluator::default();
476
477 let results = vec![
478 FewShotTaskResults {
479 entity_type: "EASY".into(),
480 k: 5,
481 precision: 0.9,
482 recall: 0.85,
483 f1: 0.87,
484 num_gold: 10,
485 num_predicted: 10,
486 num_correct: 9,
487 },
488 FewShotTaskResults {
489 entity_type: "HARD".into(),
490 k: 5,
491 precision: 0.2,
492 recall: 0.1,
493 f1: 0.13,
494 num_gold: 10,
495 num_predicted: 5,
496 num_correct: 1,
497 },
498 ];
499
500 let aggregated = evaluator.aggregate(results);
501 assert!(aggregated.failed_types.contains(&"HARD".to_string()));
502 assert!(!aggregated.failed_types.contains(&"EASY".to_string()));
503 }
504}