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.
Files changed (106) hide show
  1. structured_eval-0.2.0/PKG-INFO +426 -0
  2. structured_eval-0.2.0/README.md +383 -0
  3. {structured_eval-0.1.0 → structured_eval-0.2.0}/pyproject.toml +3 -3
  4. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/__init__.py +2 -0
  5. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_f1.py +5 -1
  6. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_precision.py +5 -1
  7. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_prf1.py +5 -1
  8. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_recall.py +5 -1
  9. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/base.py +19 -2
  10. structured_eval-0.2.0/structured_eval/metrics/character_f1.py +81 -0
  11. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/composite_score.py +2 -1
  12. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/date_distance_score.py +8 -2
  13. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/exponential_numeric_score.py +7 -2
  14. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/fuzzy.py +7 -1
  15. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/levenshtein.py +7 -2
  16. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/numeric.py +8 -0
  17. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/numeric_closeness.py +5 -1
  18. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_accuracy.py +2 -0
  19. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_f1.py +2 -0
  20. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_precision.py +2 -0
  21. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_prf1.py +2 -0
  22. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_recall.py +2 -0
  23. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_type_validity.py +2 -1
  24. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/regex_match.py +7 -1
  25. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/metric.py +2 -1
  26. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/schema_validity/metric.py +4 -1
  27. structured_eval-0.2.0/structured_eval/metrics/token_f1.py +83 -0
  28. structured_eval-0.2.0/structured_eval/metrics/url_match.py +89 -0
  29. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/__init__.py +2 -0
  30. structured_eval-0.2.0/structured_eval/metrics/utils/null.py +20 -0
  31. structured_eval-0.2.0/structured_eval.egg-info/PKG-INFO +426 -0
  32. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/SOURCES.txt +2 -0
  33. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/requires.txt +1 -1
  34. structured_eval-0.1.0/PKG-INFO +0 -322
  35. structured_eval-0.1.0/README.md +0 -279
  36. structured_eval-0.1.0/structured_eval/metrics/character_f1.py +0 -50
  37. structured_eval-0.1.0/structured_eval/metrics/token_f1.py +0 -44
  38. structured_eval-0.1.0/structured_eval.egg-info/PKG-INFO +0 -322
  39. {structured_eval-0.1.0 → structured_eval-0.2.0}/LICENSE +0 -0
  40. {structured_eval-0.1.0 → structured_eval-0.2.0}/setup.cfg +0 -0
  41. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/__init__.py +0 -0
  42. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/__init__.py +0 -0
  43. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/base.py +0 -0
  44. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/by_index.py +0 -0
  45. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/by_key.py +0 -0
  46. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/factory.py +0 -0
  47. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/alignment/hungarian.py +0 -0
  48. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/api.py +0 -0
  49. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/__init__.py +0 -0
  50. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/aggregator.py +0 -0
  51. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/evaluator.py +0 -0
  52. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/metric_runner.py +0 -0
  53. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/parser.py +0 -0
  54. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/report_builder.py +0 -0
  55. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/engine/tree_builder.py +0 -0
  56. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/__init__.py +0 -0
  57. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/base.py +0 -0
  58. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/json_parser.py +0 -0
  59. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/formats/yaml_parser.py +0 -0
  60. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/__init__.py +0 -0
  61. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/_adapter.py +0 -0
  62. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/deepeval.py +0 -0
  63. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/integrations/langsmith.py +0 -0
  64. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_accuracy.py +0 -0
  65. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_cardinality.py +0 -0
  66. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_exact_match.py +0 -0
  67. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/array_jaccard_similarity.py +0 -0
  68. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/coverage_leaf_score.py +0 -0
  69. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/exact.py +0 -0
  70. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/field_faithfulness.py +0 -0
  71. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/invoker.py +0 -0
  72. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/mean_score.py +0 -0
  73. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/object_exact_match.py +0 -0
  74. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/overall_leaf_score.py +0 -0
  75. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/presence.py +0 -0
  76. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/__init__.py +0 -0
  77. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/dsl.py +0 -0
  78. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/rule_pass_rate/engine.py +0 -0
  79. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/schema_validity/__init__.py +0 -0
  80. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/schema_validity/validator.py +0 -0
  81. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/structural_similarity.py +0 -0
  82. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/type_match.py +0 -0
  83. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/array.py +0 -0
  84. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/calculate.py +0 -0
  85. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/number.py +0 -0
  86. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/metrics/utils/object_utils.py +0 -0
  87. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/__init__.py +0 -0
  88. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/config.py +0 -0
  89. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/context.py +0 -0
  90. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/metric_result.py +0 -0
  91. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/__init__.py +0 -0
  92. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/array_node.py +0 -0
  93. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/base.py +0 -0
  94. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/object_node.py +0 -0
  95. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/nodes/scalar.py +0 -0
  96. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/result.py +0 -0
  97. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/models/sample.py +0 -0
  98. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/py.typed +0 -0
  99. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/reporting/__init__.py +0 -0
  100. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/reporting/console.py +0 -0
  101. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/__init__.py +0 -0
  102. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/flatten.py +0 -0
  103. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/paths.py +0 -0
  104. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval/utils/structured_diff.py +0 -0
  105. {structured_eval-0.1.0 → structured_eval-0.2.0}/structured_eval.egg-info/dependency_links.txt +0 -0
  106. {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
+ [![CI](https://github.com/kirillpechurin/structured-eval/actions/workflows/ci.yml/badge.svg)](https://github.com/kirillpechurin/structured-eval/actions/workflows/ci.yml)
47
+ [![codecov](https://codecov.io/gh/kirillpechurin/structured-eval/graph/badge.svg)](https://codecov.io/gh/kirillpechurin/structured-eval)
48
+ [![PyPI](https://img.shields.io/pypi/v/structured-eval.svg)](https://pypi.org/project/structured-eval/)
49
+ [![Python versions](https://img.shields.io/pypi/pyversions/structured-eval.svg)](https://pypi.org/project/structured-eval/)
50
+ [![License](https://img.shields.io/pypi/l/structured-eval.svg)](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).