fieldbench 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fieldbench
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A cross-domain, field-level benchmark for schema-driven document extraction
5
5
  Author: Frank Thomas
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fieldbench"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "A cross-domain, field-level benchmark for schema-driven document extraction"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -5,7 +5,7 @@ from .corpus import list_documents, score_corpus
5
5
  from .run import LLMRunner, Runner, run_corpus
6
6
  from .scoring import SCORER_VERSION, FieldResult, compare_field
7
7
 
8
- __version__ = "0.2.2"
8
+ __version__ = "0.2.3"
9
9
 
10
10
  __all__ = [
11
11
  "SCORER_VERSION",
@@ -66,29 +66,44 @@ def discover_categories(root: Path) -> list[str]:
66
66
  return cats
67
67
 
68
68
 
69
- def _schema_mappings(root: Path, schema_ref: str | None, cache: dict) -> dict:
70
- """{field_name: mappings} from a schema YAML (`fields.<name>.mappings`).
71
-
72
- Enum-alias folding is what lets `10K/A` match GT `10-K/A`. Best-effort:
73
- a missing/unreadable schema yields no mappings, never an error.
74
- """
69
+ def _load_schema(root: Path, schema_ref: str | None, cache: dict) -> dict:
70
+ """Parsed schema YAML (`{fields: {...}}`), cached. {} on missing/unreadable."""
75
71
  if not schema_ref:
76
72
  return {}
77
73
  if schema_ref in cache:
78
74
  return cache[schema_ref]
79
- out: dict = {}
80
- path = root / schema_ref
81
75
  try:
82
- schema = yaml.safe_load(path.read_text())
83
- for name, spec in (schema.get("fields") or {}).items():
84
- if isinstance(spec, dict) and isinstance(spec.get("mappings"), dict):
85
- out[name] = spec["mappings"]
86
- except (OSError, yaml.YAMLError, AttributeError):
76
+ out = yaml.safe_load((root / schema_ref).read_text()) or {}
77
+ except (OSError, yaml.YAMLError):
87
78
  out = {}
88
79
  cache[schema_ref] = out
89
80
  return out
90
81
 
91
82
 
83
+ def _schema_mappings(root: Path, schema_ref: str | None, cache: dict) -> dict:
84
+ """{field_name: mappings} from a schema YAML (`fields.<name>.mappings`).
85
+
86
+ Enum-alias folding is what lets `10K/A` match GT `10-K/A`. Best-effort:
87
+ a missing/unreadable schema yields no mappings, never an error.
88
+ """
89
+ schema = _load_schema(root, schema_ref, cache)
90
+ return {
91
+ name: spec["mappings"]
92
+ for name, spec in (schema.get("fields") or {}).items()
93
+ if isinstance(spec, dict) and isinstance(spec.get("mappings"), dict)
94
+ }
95
+
96
+
97
+ def _schema_enums(root: Path, schema_ref: str | None, cache: dict) -> dict:
98
+ """{field_name: options} for enum-typed fields, for enum canonicalization."""
99
+ schema = _load_schema(root, schema_ref, cache)
100
+ return {
101
+ name: spec["options"]
102
+ for name, spec in (schema.get("fields") or {}).items()
103
+ if isinstance(spec, dict) and spec.get("type") == "enum" and isinstance(spec.get("options"), list)
104
+ }
105
+
106
+
92
107
  def _iter_docs(root: Path, category: str):
93
108
  cat = root / category
94
109
  for expected_path in sorted((cat / "expected").glob("*.expected.json")):
@@ -155,10 +170,16 @@ def score_corpus(
155
170
  if not (results_dir / f"{stem}.json").exists():
156
171
  missing += 1
157
172
  prediction = _load_prediction(results_dir, stem)
158
- mappings = _schema_mappings(corpus_root, manifest.get("schema"), schema_cache)
173
+ field_maps = _schema_mappings(corpus_root, manifest.get("schema"), schema_cache)
174
+ field_enums = _schema_enums(corpus_root, manifest.get("schema"), schema_cache)
159
175
  fields = [
160
176
  compare_field(
161
- name, exp, prediction.get(name), fuzzy_threshold=fuzzy_threshold, mappings=mappings.get(name)
177
+ name,
178
+ exp,
179
+ prediction.get(name),
180
+ fuzzy_threshold=fuzzy_threshold,
181
+ mappings=field_maps.get(name),
182
+ enum_options=field_enums.get(name),
162
183
  )
163
184
  for name, exp in expected.items()
164
185
  ]
@@ -20,7 +20,7 @@ from typing import Any, Literal
20
20
 
21
21
  Bucket = Literal["correct_absence", "hallucination", "miss", "match", "wrong_value"]
22
22
 
23
- SCORER_VERSION = "0.2.1"
23
+ SCORER_VERSION = "0.2.2"
24
24
 
25
25
 
26
26
  # ── Normalization helpers ─────────────────────────────────────────────
@@ -221,6 +221,29 @@ def resolve_mapping(value: str, mappings: dict) -> str:
221
221
  return value
222
222
 
223
223
 
224
+ def fold_enum(value: str, options: list) -> str:
225
+ """Canonicalize an enum value to the option contained within it.
226
+
227
+ Enum fields carry a fixed option set; a prediction is correct if it names
228
+ the right option, regardless of surrounding wording. E.g. for a
229
+ governing-law enum with option "New York", both "State of New York" and
230
+ "the laws of the State of New York" fold to "New York". Word-boundary
231
+ containment, longest option wins (so "North Dakota" beats "Dakota").
232
+ Returns the value unchanged if no option matches.
233
+ """
234
+ if not options:
235
+ return value
236
+ v = re.sub(r"\s+", " ", str(value).strip().lower())
237
+ best = None
238
+ for opt in options:
239
+ o = str(opt).strip().lower()
240
+ if o and re.search(r"\b" + re.escape(o) + r"\b", v) and (
241
+ best is None or len(o) > len(best[1])
242
+ ):
243
+ best = (opt, o)
244
+ return best[0] if best else value
245
+
246
+
224
247
  # ── Result ────────────────────────────────────────────────────────────
225
248
 
226
249
 
@@ -250,6 +273,7 @@ def compare_field(
250
273
  actual: Any,
251
274
  fuzzy_threshold: float = 0.0,
252
275
  mappings: dict | None = None,
276
+ enum_options: list | None = None,
253
277
  ) -> FieldResult:
254
278
  """Compare one expected value against one extracted value.
255
279
 
@@ -308,6 +332,8 @@ def compare_field(
308
332
  # Scalar / string
309
333
  if isinstance(expected, str) and isinstance(actual, str):
310
334
  e, a = expected, actual
335
+ if enum_options:
336
+ e, a = fold_enum(e, enum_options), fold_enum(a, enum_options)
311
337
  if mappings:
312
338
  e, a = resolve_mapping(e, mappings), resolve_mapping(a, mappings)
313
339
  if e.strip().lower() == a.strip().lower():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fieldbench
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A cross-domain, field-level benchmark for schema-driven document extraction
5
5
  Author: Frank Thomas
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes