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.
- {fieldbench-0.2.2/src/fieldbench.egg-info → fieldbench-0.2.3}/PKG-INFO +1 -1
- {fieldbench-0.2.2 → fieldbench-0.2.3}/pyproject.toml +1 -1
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench/__init__.py +1 -1
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench/corpus.py +36 -15
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench/scoring.py +27 -1
- {fieldbench-0.2.2 → fieldbench-0.2.3/src/fieldbench.egg-info}/PKG-INFO +1 -1
- {fieldbench-0.2.2 → fieldbench-0.2.3}/LICENSE +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/README.md +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/setup.cfg +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench/aggregate.py +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench/cli.py +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench/run.py +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench.egg-info/SOURCES.txt +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench.egg-info/dependency_links.txt +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench.egg-info/entry_points.txt +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench.egg-info/requires.txt +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/src/fieldbench.egg-info/top_level.txt +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/tests/test_corpus.py +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/tests/test_run.py +0 -0
- {fieldbench-0.2.2 → fieldbench-0.2.3}/tests/test_scoring.py +0 -0
|
@@ -66,29 +66,44 @@ def discover_categories(root: Path) -> list[str]:
|
|
|
66
66
|
return cats
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|