1#![allow(missing_docs)] use anno::EntityType;
15use anno::{Error, Result};
16use serde::{Deserialize, Serialize};
17use std::path::Path;
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct GoldEntity {
38 pub text: String,
40 pub entity_type: EntityType,
42 pub original_label: String,
44 pub start: usize,
46 pub end: usize,
48}
49
50impl GoldEntity {
51 #[must_use]
55 pub fn new(text: impl Into<String>, entity_type: EntityType, start: usize) -> Self {
56 let text = text.into();
57 let end = start + text.chars().count();
59 Self {
60 text,
61 entity_type,
62 original_label: String::new(),
63 start,
64 end,
65 }
66 }
67
68 pub fn with_span(
70 text: impl Into<String>,
71 entity_type: EntityType,
72 start: usize,
73 end: usize,
74 ) -> Self {
75 Self {
76 text: text.into(),
77 entity_type,
78 original_label: String::new(),
79 start,
80 end,
81 }
82 }
83
84 pub fn with_label(
88 text: impl Into<String>,
89 entity_type: EntityType,
90 original_label: impl Into<String>,
91 start: usize,
92 ) -> Self {
93 let text = text.into();
94 let end = start + text.chars().count();
95 Self {
96 text,
97 entity_type,
98 original_label: original_label.into(),
99 start,
100 end,
101 }
102 }
103
104 pub fn full(
106 text: impl Into<String>,
107 entity_type: EntityType,
108 original_label: impl Into<String>,
109 start: usize,
110 end: usize,
111 ) -> Self {
112 Self {
113 text: text.into(),
114 entity_type,
115 original_label: original_label.into(),
116 start,
117 end,
118 }
119 }
120
121 pub fn overlaps(&self, other: &Self) -> bool {
123 self.start < other.end && other.start < self.end
124 }
125
126 #[must_use]
137 pub fn extract_text(&self, source_text: &str) -> String {
138 let char_count = source_text.chars().count();
139 if self.start >= char_count || self.end > char_count || self.start >= self.end {
140 return String::new();
141 }
142 source_text
143 .chars()
144 .skip(self.start)
145 .take(self.end - self.start)
146 .collect()
147 }
148
149 pub fn span_matches(&self, other: &Self) -> bool {
151 self.start == other.start && self.end == other.end
152 }
153
154 pub fn exact_matches(&self, other: &Self) -> bool {
156 self.span_matches(other) && self.entity_type == other.entity_type
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct JSONNERExample {
174 pub text: String,
175 pub entities: Vec<JSONEntity>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct JSONEntity {
180 pub text: String,
181 pub label: String,
182 pub start: usize,
183 pub end: usize,
184 #[serde(default)]
185 pub confidence: Option<f64>,
186}
187
188pub type JSONLNERExample = JSONNERExample;
192
193pub fn load_json_ner_dataset<P: AsRef<Path>>(path: P) -> Result<Vec<(String, Vec<GoldEntity>)>> {
199 let content = std::fs::read_to_string(path.as_ref()).map_err(Error::Io)?;
200
201 let mut test_cases = Vec::new();
202
203 let is_jsonl = content.lines().count() > 1
205 && content
206 .lines()
207 .all(|line| line.trim().starts_with('{') && line.trim().ends_with('}'));
208
209 if is_jsonl {
210 for (line_num, line) in content.lines().enumerate() {
212 let line = line.trim();
213 if line.is_empty() {
214 continue;
215 }
216
217 let example: JSONLNERExample = serde_json::from_str(line).map_err(|e| {
218 Error::Parse(format!(
219 "Failed to parse JSONL line {}: {}",
220 line_num + 1,
221 e
222 ))
223 })?;
224
225 let entities: Vec<GoldEntity> = example
226 .entities
227 .into_iter()
228 .map(|e| {
229 let entity_type = map_label_to_entity_type(&e.label);
230 GoldEntity::full(e.text, entity_type, &e.label, e.start, e.end)
231 })
232 .collect();
233
234 let validation = crate::eval::validation::validate_ground_truth_entities(
236 &example.text,
237 &entities,
238 false, );
240 if !validation.is_valid {
241 return Err(Error::InvalidInput(format!(
242 "Invalid entities in dataset: {}",
243 validation.errors.join("; ")
244 )));
245 }
246
247 test_cases.push((example.text, entities));
248 }
249 } else {
250 let examples: Vec<JSONNERExample> = serde_json::from_str(&content)
252 .map_err(|e| Error::Parse(format!("Failed to parse JSON: {}", e)))?;
253
254 for example in examples {
255 let entities: Vec<GoldEntity> = example
256 .entities
257 .into_iter()
258 .map(|e| {
259 let entity_type = map_label_to_entity_type(&e.label);
260 GoldEntity::full(e.text, entity_type, &e.label, e.start, e.end)
261 })
262 .collect();
263
264 let validation = crate::eval::validation::validate_ground_truth_entities(
266 &example.text,
267 &entities,
268 false, );
270 if !validation.is_valid {
271 return Err(Error::InvalidInput(format!(
272 "Invalid entities in dataset: {}",
273 validation.errors.join("; ")
274 )));
275 }
276
277 test_cases.push((example.text, entities));
278 }
279 }
280
281 Ok(test_cases)
282}
283
284pub fn load_hf_ner_dataset<P: AsRef<Path>>(path: P) -> Result<Vec<(String, Vec<GoldEntity>)>> {
289 load_json_ner_dataset(path)
292}
293
294fn map_label_to_entity_type(label: &str) -> EntityType {
305 anno::schema::map_to_canonical(label, None)
307}
308
309pub fn load_ner_dataset<P: AsRef<Path>>(path: P) -> Result<Vec<(String, Vec<GoldEntity>)>> {
316 let path = path.as_ref();
317 let extension = path
318 .extension()
319 .and_then(|ext| ext.to_str())
320 .unwrap_or("")
321 .to_lowercase();
322
323 match extension.as_str() {
324 "conll" | "conll2003" | "txt" => {
325 load_conll_2003_dataset_internal(path).or_else(|_| {
327 load_json_ner_dataset(path)
329 })
330 }
331 "json" | "jsonl" => load_json_ner_dataset(path),
332 _ => {
333 load_conll_2003_dataset_internal(path).or_else(|_| load_json_ner_dataset(path))
335 }
336 }
337}
338
339fn load_conll_2003_dataset_internal<P: AsRef<Path>>(
341 path: P,
342) -> Result<Vec<(String, Vec<GoldEntity>)>> {
343 crate::eval::load_conll2003(path)
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct DatasetMetadata {
350 pub name: String,
351 pub format: String,
352 pub language: Option<String>,
353 pub entity_types: Vec<String>,
354 pub num_examples: usize,
355 pub source: Option<String>,
356 pub year: Option<u32>,
357}
358
359pub fn extract_dataset_metadata(
361 examples: &[(String, Vec<GoldEntity>)],
362 name: &str,
363) -> DatasetMetadata {
364 let mut entity_types = std::collections::HashSet::new();
365 for (_, entities) in examples {
366 for entity in entities {
367 let type_str = crate::eval::entity_type_to_string(&entity.entity_type);
368 entity_types.insert(type_str);
369 }
370 }
371
372 DatasetMetadata {
373 name: name.to_string(),
374 format: "auto-detected".to_string(),
375 language: None,
376 entity_types: entity_types.into_iter().collect(),
377 num_examples: examples.len(),
378 source: None,
379 year: None,
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use std::fs::File;
387 use std::io::Write;
388
389 #[test]
390 fn test_load_json_ner_dataset() {
391 let json_content = r#"[
392 {
393 "text": "John Smith works at Acme Corp.",
394 "entities": [
395 {"text": "John Smith", "label": "PER", "start": 0, "end": 10},
396 {"text": "Acme Corp", "label": "ORG", "start": 20, "end": 29}
397 ]
398 }
399 ]"#;
400
401 let temp_dir = std::env::temp_dir();
402 let file_path = temp_dir.join("test_ner.json");
403 let mut file = File::create(&file_path).expect("should create test file");
404 file.write_all(json_content.as_bytes())
405 .expect("should write test file");
406 file.flush().expect("should flush test file");
407
408 let result = load_json_ner_dataset(&file_path).expect("should load test dataset");
409 assert_eq!(result.len(), 1);
410 assert_eq!(result[0].0, "John Smith works at Acme Corp.");
411 assert_eq!(result[0].1.len(), 2);
412
413 std::fs::remove_file(&file_path).ok();
414 }
415
416 #[test]
417 fn test_load_jsonl_ner_dataset() {
418 let jsonl_content = r#"{"text": "John Smith works.", "entities": [{"text": "John Smith", "label": "PER", "start": 0, "end": 10}]}
419{"text": "Acme Corp is hiring.", "entities": [{"text": "Acme Corp", "label": "ORG", "start": 0, "end": 9}]}
420"#;
421
422 let temp_dir = std::env::temp_dir();
423 let file_path = temp_dir.join("test_ner.jsonl");
424 let mut file = File::create(&file_path).unwrap();
425 file.write_all(jsonl_content.as_bytes()).unwrap();
426 file.flush().unwrap();
427
428 let result = load_json_ner_dataset(&file_path).expect("should load test dataset");
429 assert_eq!(result.len(), 2);
430
431 std::fs::remove_file(&file_path).ok();
432 }
433
434 #[test]
435 fn test_map_label_to_entity_type() {
436 assert!(matches!(
438 map_label_to_entity_type("PER"),
439 EntityType::Person
440 ));
441 assert!(matches!(
442 map_label_to_entity_type("ORG"),
443 EntityType::Organization
444 ));
445 assert!(matches!(
446 map_label_to_entity_type("LOC"),
447 EntityType::Location
448 ));
449
450 assert!(matches!(
452 map_label_to_entity_type("MISC"),
453 EntityType::Custom { .. }
454 ));
455
456 assert!(matches!(
458 map_label_to_entity_type("ANIM"),
459 EntityType::Custom { .. }
460 ));
461 }
462
463 #[test]
464 fn test_load_ner_dataset_auto_detect() {
465 let json_content = r#"[{"text": "Test", "entities": []}]"#;
467 let temp_dir = std::env::temp_dir();
468 let file_path = temp_dir.join("test_auto.json");
469 let mut file = File::create(&file_path).expect("should create test file");
470 file.write_all(json_content.as_bytes())
471 .expect("should write test file");
472 file.flush().expect("should flush test file");
473
474 let result = load_ner_dataset(&file_path);
475 assert!(result.is_ok());
476
477 std::fs::remove_file(&file_path).ok();
478 }
479}