structured-eval 0.1.0__tar.gz → 0.2.0__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.
- structured_eval-0.2.0/PKG-INFO +426 -0
- structured_eval-0.2.0/README.md +383 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/pyproject.toml +3 -3
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/__init__.py +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_f1.py +5 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_precision.py +5 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_prf1.py +5 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_recall.py +5 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/base.py +19 -2
- structured_eval-0.2.0/structured_eval/metrics/character_f1.py +81 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/composite_score.py +2 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/date_distance_score.py +8 -2
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/exponential_numeric_score.py +7 -2
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/fuzzy.py +7 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/levenshtein.py +7 -2
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/numeric.py +8 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/numeric_closeness.py +5 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_accuracy.py +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_f1.py +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_precision.py +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_prf1.py +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_recall.py +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_type_validity.py +2 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/regex_match.py +7 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/metric.py +2 -1
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/schema_validity/metric.py +4 -1
- structured_eval-0.2.0/structured_eval/metrics/token_f1.py +83 -0
- structured_eval-0.2.0/structured_eval/metrics/url_match.py +89 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/__init__.py +2 -0
- structured_eval-0.2.0/structured_eval/metrics/utils/null.py +20 -0
- structured_eval-0.2.0/structured_eval.egg-info/PKG-INFO +426 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/SOURCES.txt +2 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/requires.txt +1 -1
- structured_eval-0.1.0/PKG-INFO +0 -322
- structured_eval-0.1.0/README.md +0 -279
- structured_eval-0.1.0/structured_eval/metrics/character_f1.py +0 -50
- structured_eval-0.1.0/structured_eval/metrics/token_f1.py +0 -44
- structured_eval-0.1.0/structured_eval.egg-info/PKG-INFO +0 -322
- {structured_eval-0.1.0 → structured_eval-0.2.0}/LICENSE +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/setup.cfg +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/base.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/by_index.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/by_key.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/factory.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/hungarian.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/api.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/aggregator.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/evaluator.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/metric_runner.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/parser.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/report_builder.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/tree_builder.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/base.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/json_parser.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/yaml_parser.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/_adapter.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/deepeval.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/langsmith.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_accuracy.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_cardinality.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_exact_match.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_jaccard_similarity.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/coverage_leaf_score.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/exact.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/field_faithfulness.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/invoker.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/mean_score.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_exact_match.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/overall_leaf_score.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/presence.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/dsl.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/engine.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/schema_validity/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/schema_validity/validator.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/structural_similarity.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/type_match.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/array.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/calculate.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/number.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/object_utils.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/config.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/context.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/metric_result.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/array_node.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/base.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/object_node.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/scalar.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/result.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/sample.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/py.typed +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/reporting/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/reporting/console.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/__init__.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/flatten.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/paths.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/structured_diff.py +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/dependency_links.txt +0 -0
- {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: structured-eval
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The LLM Structured Output Evaluation Framework
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/kirillpechurin/structured-eval
|
|
7
|
+
Project-URL: Repository, https://github.com/kirillpechurin/structured-eval
|
|
8
|
+
Project-URL: Issues, https://github.com/kirillpechurin/structured-eval/issues
|
|
9
|
+
Keywords: structured llm evaluation,structured eval,json eval,llm,structured output,pydantic
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pydantic>=2.5.0
|
|
24
|
+
Provides-Extra: yaml
|
|
25
|
+
Requires-Dist: pyyaml>=6.0.1; extra == "yaml"
|
|
26
|
+
Provides-Extra: fuzzy
|
|
27
|
+
Requires-Dist: rapidfuzz>=3.0.0; extra == "fuzzy"
|
|
28
|
+
Provides-Extra: jsonschema
|
|
29
|
+
Requires-Dist: jsonschema>=4.20.0; extra == "jsonschema"
|
|
30
|
+
Provides-Extra: rules
|
|
31
|
+
Requires-Dist: jsonpath-ng>=1.6.0; extra == "rules"
|
|
32
|
+
Provides-Extra: diff
|
|
33
|
+
Requires-Dist: deepdiff>=7.0.0; extra == "diff"
|
|
34
|
+
Provides-Extra: align
|
|
35
|
+
Requires-Dist: scipy>=1.13.0; extra == "align"
|
|
36
|
+
Provides-Extra: deepeval
|
|
37
|
+
Requires-Dist: deepeval>=3.0.0; extra == "deepeval"
|
|
38
|
+
Provides-Extra: langsmith
|
|
39
|
+
Requires-Dist: langsmith>=0.8.0; extra == "langsmith"
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Requires-Dist: structured-eval[align,diff,fuzzy,jsonschema,rules,yaml]; extra == "all"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# structured-eval
|
|
45
|
+
|
|
46
|
+
[](https://github.com/kirillpechurin/structured-eval/actions/workflows/ci.yml)
|
|
47
|
+
[](https://codecov.io/gh/kirillpechurin/structured-eval)
|
|
48
|
+
[](https://pypi.org/project/structured-eval/)
|
|
49
|
+
[](https://pypi.org/project/structured-eval/)
|
|
50
|
+
[](https://github.com/kirillpechurin/structured-eval/blob/main/LICENSE)
|
|
51
|
+
|
|
52
|
+
**The LLM Structured Output Evaluation Framework**
|
|
53
|
+
|
|
54
|
+
When interacting with an LLM, returning structured data is a common task.
|
|
55
|
+
And in any such integration, the quality of the returned data deserves attention —
|
|
56
|
+
it should be evaluated with the appropriate tools.
|
|
57
|
+
However, quality evaluation of structured data is often limited to the following:
|
|
58
|
+
|
|
59
|
+
- Parses correctly
|
|
60
|
+
- Data types are correct
|
|
61
|
+
- Required fields are present and there are no extra fields
|
|
62
|
+
|
|
63
|
+
That is not enough to be fully confident the data is correct, because these
|
|
64
|
+
mechanisms target structural validation, not the values themselves.
|
|
65
|
+
|
|
66
|
+
**structured-eval** focuses on evaluating the quality of an LLM's structured output,
|
|
67
|
+
such as JSON or YAML. The library makes it possible to compare
|
|
68
|
+
values, objects, and arrays at every level of nesting, which lets you check
|
|
69
|
+
|
|
70
|
+
- Which fields matched or are close to the expected ones, and which are not
|
|
71
|
+
- Whether values are grounded in a source
|
|
72
|
+
- Whether fields are logically consistent with one another
|
|
73
|
+
|
|
74
|
+
This kind of field-level evaluation gives you a systematic way to target improvements.
|
|
75
|
+
A few examples of what you can learn using structured-eval:
|
|
76
|
+
|
|
77
|
+
- How close an output value is to the expected one
|
|
78
|
+
- Which scalars, objects, and arrays have problems most often
|
|
79
|
+
- At dataset scale — how stable the LLM is
|
|
80
|
+
|
|
81
|
+
## What it checks
|
|
82
|
+
|
|
83
|
+
Correctness is a set of checks, where each check builds on the ones below it:
|
|
84
|
+
|
|
85
|
+
| Level | Check | How you can check it |
|
|
86
|
+
|-------|-------------------------------------------|------------------------------|
|
|
87
|
+
| L0 | Parses correctly | structured-eval / jsonschema |
|
|
88
|
+
| L1 | Data types are correct | structured-eval / jsonschema |
|
|
89
|
+
| L2 | Required fields are present | structured-eval / jsonschema |
|
|
90
|
+
| L3 | No extra fields | structured-eval / jsonschema |
|
|
91
|
+
| L4 | Values match or are close to the expected | structured-eval |
|
|
92
|
+
| L5 | Values are grounded in a source | structured-eval |
|
|
93
|
+
| L6 | Fields are logically consistent | structured-eval |
|
|
94
|
+
|
|
95
|
+
**The value of structured-eval is to provide a complete quality evaluation of
|
|
96
|
+
structured data.**
|
|
97
|
+
|
|
98
|
+
## Install
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install structured-eval # core depends only on Pydantic
|
|
102
|
+
pip install "structured-eval[all]" # + YAML, fuzzy, schema, rules, scipy alignment…
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
By default only `pydantic` is required, but to extend functionality there are
|
|
106
|
+
[extras](docs/getting-started.md#install) — install only what you need.
|
|
107
|
+
|
|
108
|
+
## Quick start
|
|
109
|
+
|
|
110
|
+
The example uses a course record. The expected and the actual are structurally
|
|
111
|
+
identical, but several values differ. A small evaluation config says *how* to
|
|
112
|
+
judge each field, and the report shows where the data diverges:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from structured_eval import evaluate
|
|
116
|
+
from structured_eval.models import EvalConfig, FieldConfig
|
|
117
|
+
from structured_eval.metrics import Numeric, TokenF1
|
|
118
|
+
|
|
119
|
+
expected = {
|
|
120
|
+
"course_id": "COURSE-101",
|
|
121
|
+
"title": "Introduction to Python",
|
|
122
|
+
"published": True,
|
|
123
|
+
"duration_hours": 12,
|
|
124
|
+
"rating": 4.8,
|
|
125
|
+
"modules": [
|
|
126
|
+
{"name": "Basics", "lessons": 5},
|
|
127
|
+
{"name": "Functions", "lessons": 4},
|
|
128
|
+
{"name": "Classes", "lessons": 3},
|
|
129
|
+
],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
actual = {
|
|
133
|
+
"course_id": "COURSE-101",
|
|
134
|
+
"title": "Intro to Python", # paraphrased
|
|
135
|
+
"published": True,
|
|
136
|
+
"duration_hours": 10, # off by 2
|
|
137
|
+
"rating": 4.5, # off by 0.3
|
|
138
|
+
"modules": [
|
|
139
|
+
{"name": "Basics", "lessons": 5},
|
|
140
|
+
{"name": "Functions", "lessons": 4},
|
|
141
|
+
], # the "Classes" module is missing
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
config = EvalConfig(fields={
|
|
145
|
+
"title": FieldConfig(metrics=[TokenF1()]), # reward paraphrases
|
|
146
|
+
"duration_hours": FieldConfig(metrics=[Numeric(tolerance=2)]),
|
|
147
|
+
"rating": FieldConfig(metrics=[Numeric(tolerance=0.5)]), # close enough is fine
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
report = evaluate(actual, expected, config)
|
|
151
|
+
|
|
152
|
+
report.score # 0.8889 — close, with the gaps pinpointed
|
|
153
|
+
report.field_scores["title"].score # 0.6667 — a paraphrase gets partial credit
|
|
154
|
+
report.field_scores["duration_hours"].score # 1.0 — within tolerance
|
|
155
|
+
report.field_scores["modules"].score # 0.6667 — 2 of 3 modules recovered
|
|
156
|
+
report.field_scores["modules[0]"].score # 1.0 — the first module is spot-on
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
You can assign several metrics to a single field and make the field's
|
|
160
|
+
representative (`key_metric`) — the score that lands in the total — a separate
|
|
161
|
+
aggregating metric over them. For example, `CompositeScore` blends metrics with
|
|
162
|
+
given weights, while `MeanScore` takes their plain mean; metrics not included in
|
|
163
|
+
the representative are still computed alongside, for detail:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from structured_eval import evaluate
|
|
167
|
+
from structured_eval.models import EvalConfig, FieldConfig
|
|
168
|
+
from structured_eval.metrics import (
|
|
169
|
+
CompositeScore, ExactMatch, Fuzzy, MeanScore, Numeric, NumericCloseness, TokenF1,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
config = EvalConfig(fields={
|
|
173
|
+
# the field's representative — a weighted blend of token_f1 and fuzzy (exact_match is for detail only)
|
|
174
|
+
"title": FieldConfig(
|
|
175
|
+
metrics=[ExactMatch(), TokenF1(), Fuzzy()],
|
|
176
|
+
key_metric=CompositeScore(weights={"exact_match": 0.1, "token_f1": 0.6, "fuzzy": 0.3}),
|
|
177
|
+
),
|
|
178
|
+
# the field's representative — the plain mean of two numeric metrics
|
|
179
|
+
"rating": FieldConfig(
|
|
180
|
+
metrics=[Numeric(tolerance=0.5), NumericCloseness()],
|
|
181
|
+
key_metric=MeanScore(),
|
|
182
|
+
),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
report = evaluate(
|
|
186
|
+
{"title": "Intro to Python", "rating": 4.5},
|
|
187
|
+
{"title": "Introduction to Python", "rating": 4.8},
|
|
188
|
+
config,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
report.field_scores["title"].score # 0.6432 — CompositeScore: 0.1·exact_match + 0.6·token_f1 + 0.3·fuzzy
|
|
192
|
+
report.field_scores["title"].metrics["exact_match"] # 0.0 — computed alongside, for detail
|
|
193
|
+
report.field_scores["rating"].score # 0.96875 — MeanScore: mean of numeric and numeric_closeness
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This is the key idea: *comparison is a metric*, not a separate "matcher" with a
|
|
197
|
+
pre-computed similarity. Learn more —
|
|
198
|
+
[comparison is a metric](docs/core-concepts/comparison-is-a-metric.md).
|
|
199
|
+
|
|
200
|
+
Every field is scored — nested objects and array elements of any depth included —
|
|
201
|
+
so you see not a single pass/fail, but exactly which fields match and which
|
|
202
|
+
don't.
|
|
203
|
+
|
|
204
|
+
### Sensible default metrics
|
|
205
|
+
|
|
206
|
+
**structured-eval** ships a default metric for every node type, so you only
|
|
207
|
+
configure the fields where the default isn't what you want. With no config at
|
|
208
|
+
all, the same data is scored by these default metrics:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
report = evaluate(actual, expected) # no config
|
|
212
|
+
|
|
213
|
+
report.score # 0.4444 — scored entirely by the defaults
|
|
214
|
+
report.field_scores["title"].score # 0.0 — exact match: "Intro to Python" ≠ "Introduction to Python"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Each node type gets a structural default, and every node's headline score (its
|
|
218
|
+
*representative*) defaults to the mean of its own metrics:
|
|
219
|
+
|
|
220
|
+
| Node | Default metric | What it does |
|
|
221
|
+
|---------------------|------------------|-------------------------------------------------|
|
|
222
|
+
| scalar (leaf) | `ExactMatch` | the value must match exactly |
|
|
223
|
+
| object | `ObjectAccuracy` | mean correctness of its fields |
|
|
224
|
+
| array | `ArrayAccuracy` | mean correctness of its aligned elements |
|
|
225
|
+
| any node (headline) | `MeanScore` | the node's representative = mean of its metrics |
|
|
226
|
+
|
|
227
|
+
Exact match is a strict baseline. Tuning metrics per field, as in the first
|
|
228
|
+
example, is how you tell the evaluator what "close enough" means for *your* data.
|
|
229
|
+
The defaults and the representative score are covered in
|
|
230
|
+
[the evaluation model](docs/core-concepts/evaluation-model.md) and the
|
|
231
|
+
[metric catalog](docs/metrics/index.md).
|
|
232
|
+
|
|
233
|
+
## Explore — every level of correctness
|
|
234
|
+
|
|
235
|
+
structured-eval covers the whole ladder, L0 through L6. Behind each level there
|
|
236
|
+
is a tool and a concept page:
|
|
237
|
+
|
|
238
|
+
### Structural checks
|
|
239
|
+
|
|
240
|
+
Levels L0–L3 let you check successful parsing, data types, required fields, and
|
|
241
|
+
the absence of extra fields.
|
|
242
|
+
|
|
243
|
+
Validate against a Pydantic model or JSON Schema, with no ground-truth answer:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from pydantic import BaseModel
|
|
247
|
+
from structured_eval import evaluate
|
|
248
|
+
from structured_eval.models import EvalConfig
|
|
249
|
+
from structured_eval.metrics import SchemaValidity
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Course(BaseModel):
|
|
253
|
+
title: str
|
|
254
|
+
duration_hours: int
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
report = evaluate(
|
|
258
|
+
actual={"title": "ML", "duration_hours": "twelve"},
|
|
259
|
+
expected=None,
|
|
260
|
+
config=EvalConfig(key_metric=SchemaValidity(Course))
|
|
261
|
+
)
|
|
262
|
+
report.score # 0.0
|
|
263
|
+
report.metrics["schema_validity"].root().extra["schema_errors"]
|
|
264
|
+
# {'type_errors': ['duration_hours'], 'missing_required': [], 'extra_fields': []}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Value correctness
|
|
268
|
+
|
|
269
|
+
Level L4 lets you choose how flexibly to judge values,
|
|
270
|
+
on your own criteria — for leaves as well as objects and arrays.
|
|
271
|
+
|
|
272
|
+
**structured-eval** provides a large set of metrics, see more in the
|
|
273
|
+
[metric catalog](docs/metrics/index.md).
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from structured_eval import evaluate
|
|
277
|
+
from structured_eval.models import EvalConfig, FieldConfig
|
|
278
|
+
from structured_eval.metrics import Numeric, TokenF1
|
|
279
|
+
|
|
280
|
+
report = evaluate(
|
|
281
|
+
actual={"title": "Intro to Python", "duration_hours": 11},
|
|
282
|
+
expected={"title": "Introduction to Python", "duration_hours": 12},
|
|
283
|
+
config=EvalConfig(fields={
|
|
284
|
+
"title": FieldConfig(metrics=[TokenF1()]), # token overlap
|
|
285
|
+
"duration_hours": FieldConfig(metrics=[Numeric(tolerance=2)]),
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
report.field_scores["title"].score # 0.6667 — partial credit for a paraphrase
|
|
289
|
+
report.field_scores["duration_hours"].score # 1.0 — within tolerance
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Leaf scores can be rolled up into a metric on the object or array. The base
|
|
293
|
+
metrics for such structures are precision, recall, F1 — see more in the full
|
|
294
|
+
[metric catalog](docs/metrics/index.md).
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from structured_eval import evaluate
|
|
298
|
+
from structured_eval.models import EvalConfig
|
|
299
|
+
from structured_eval.metrics import ObjectF1
|
|
300
|
+
|
|
301
|
+
report = evaluate(
|
|
302
|
+
actual={"a": 1, "b": 9},
|
|
303
|
+
expected={"a": 1, "b": 2, "c": 3},
|
|
304
|
+
config=EvalConfig(metrics=[ObjectF1()])
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
report.metrics["object_f1"].root() # 0.4
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
For an array of objects the elements are first *aligned* (which actual element
|
|
311
|
+
corresponds to which expected one), and then each pair is evaluated field by
|
|
312
|
+
field. You set the strategy on `ArrayFieldConfig` — for example, `by_key` matches
|
|
313
|
+
elements by a key, and array order stops mattering:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from structured_eval import evaluate
|
|
317
|
+
from structured_eval.models import ArrayFieldConfig, ArrayStrategy, EvalConfig
|
|
318
|
+
from structured_eval.metrics import ArrayF1
|
|
319
|
+
|
|
320
|
+
config = EvalConfig(fields={"items": ArrayFieldConfig(
|
|
321
|
+
strategy=ArrayStrategy.BY_KEY, # match elements by the sku key
|
|
322
|
+
params={"key": "sku"},
|
|
323
|
+
metrics=[ArrayF1()],
|
|
324
|
+
)})
|
|
325
|
+
|
|
326
|
+
report = evaluate(
|
|
327
|
+
{"items": [{"sku": "B", "qty": 5}, {"sku": "A", "qty": 2}]},
|
|
328
|
+
{"items": [{"sku": "A", "qty": 2}, {"sku": "B", "qty": 3}]},
|
|
329
|
+
config,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
report.array_matches["items"].matched # [(0, 1), (1, 0)] — A↔A, B↔B despite the order
|
|
333
|
+
report.field_scores["items"].score # 0.5 — A matched (qty 2), B didn't (qty 5 ≠ 3)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Learn more about strategies in
|
|
337
|
+
[array alignment](docs/core-concepts/array-alignment.md).
|
|
338
|
+
|
|
339
|
+
### Source grounding
|
|
340
|
+
|
|
341
|
+
Level L5 lets you catch hallucinations by checking each value against a source.
|
|
342
|
+
Note that `expected` is not required for the computation.
|
|
343
|
+
|
|
344
|
+
Learn more — [field faithfulness](docs/metrics/catalog/field_faithfulness.md).
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from structured_eval import evaluate
|
|
348
|
+
from structured_eval.models import EvalConfig
|
|
349
|
+
from structured_eval.metrics import FieldFaithfulness
|
|
350
|
+
|
|
351
|
+
report = evaluate(
|
|
352
|
+
actual={"title": "Introduction to Python", "duration_hours": 40},
|
|
353
|
+
expected=None,
|
|
354
|
+
config=EvalConfig(metrics=[FieldFaithfulness()]),
|
|
355
|
+
source="Course: Introduction to Python. Duration: 12 hours.",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
report.metrics["field_faithfulness"].by_path # {'title': 1.0, 'duration_hours': 0.0 ← 40 ≠ 12}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Logical consistency of values
|
|
362
|
+
|
|
363
|
+
Level L6 offers an interface for describing cross-field business rules with a
|
|
364
|
+
small DSL.
|
|
365
|
+
|
|
366
|
+
Learn more — [rule pass rate](docs/metrics/catalog/rule-pass-rate.md).
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from structured_eval import evaluate
|
|
370
|
+
from structured_eval.models import EvalConfig
|
|
371
|
+
from structured_eval.metrics import Rule, RulePassRate
|
|
372
|
+
|
|
373
|
+
report = evaluate(
|
|
374
|
+
actual={"subtotal": 100, "tax": 20, "total": 130},
|
|
375
|
+
expected=None,
|
|
376
|
+
config=EvalConfig(key_metric=RulePassRate([Rule("$.total").eq("$.subtotal + $.tax")]))
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
report.score # 0.0 — 130 ≠ 120
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Scale it
|
|
383
|
+
|
|
384
|
+
**structured-eval** offers ways to evaluate
|
|
385
|
+
|
|
386
|
+
- A whole dataset
|
|
387
|
+
- Prompt stability across repeated runs
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from structured_eval import evaluate_batch, evaluate_consistency
|
|
391
|
+
from structured_eval.models import Sample
|
|
392
|
+
|
|
393
|
+
# one report per sample + dataset aggregates
|
|
394
|
+
batch = evaluate_batch([
|
|
395
|
+
Sample(actual={"x": 1}, expected={"x": 1}),
|
|
396
|
+
Sample(actual={"x": 1}, expected={"x": 2}),
|
|
397
|
+
])
|
|
398
|
+
batch.score # 0.5
|
|
399
|
+
batch.perfect_response_rate # 0.5
|
|
400
|
+
|
|
401
|
+
# repeated runs of the same prompt → which fields drift?
|
|
402
|
+
runs = [
|
|
403
|
+
Sample(actual={"sentiment": "positive", "score": 0.9}, expected={"sentiment": "positive", "score": 0.9}),
|
|
404
|
+
Sample(actual={"sentiment": "positive", "score": 0.9}, expected={"sentiment": "positive", "score": 0.9}),
|
|
405
|
+
Sample(actual={"sentiment": "neutral", "score": 0.9}, expected={"sentiment": "positive", "score": 0.9}),
|
|
406
|
+
]
|
|
407
|
+
report = evaluate_consistency(runs, variance_threshold=0.05)
|
|
408
|
+
report.stable_fields # ['score']
|
|
409
|
+
report.unstable_fields # ['sentiment'] — flipped on one run
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Documentation
|
|
413
|
+
|
|
414
|
+
- **[Introduction](docs/introduction.md)** — the L0–L6 ladder and why values matter.
|
|
415
|
+
- **[Getting started](docs/getting-started.md)** — install → first evaluation →
|
|
416
|
+
reading and tuning the report.
|
|
417
|
+
- **Core concepts** — [the evaluation model](docs/core-concepts/evaluation-model.md) ·
|
|
418
|
+
[comparison is a metric](docs/core-concepts/comparison-is-a-metric.md) ·
|
|
419
|
+
[array alignment](docs/core-concepts/array-alignment.md)
|
|
420
|
+
- **[Evaluation functions](docs/evaluation/index.md)** — `evaluate`,
|
|
421
|
+
`evaluate_batch`, `evaluate_consistency`.
|
|
422
|
+
- **[Metric catalog](docs/metrics/index.md)** — every metric, plus how to write your own.
|
|
423
|
+
|
|
424
|
+
## License
|
|
425
|
+
|
|
426
|
+
Apache-2.0 — see [LICENSE](LICENSE).
|