feat(auto): zero-config evaluation of vanilla pydantic models#176
feat(auto): zero-config evaluation of vanilla pydantic models#176sromoam wants to merge 6 commits into
Conversation
Add stickler.evaluate(gt, pred) / stickler.eval_for(cls): turn any plain
pydantic BaseModel (e.g. a Strands agent response_model) into a scored
stickler evaluation with no StructuredModel subclass, JSON schema, or
x-aws-stickler-* annotation.
New src/stickler/auto/ package:
- inference.py: per-field comparator/threshold/weight inference from the
live python type + field-name tokens (Optional-unwrap, enum/Literal/date
handling, availability gate). Weights default to 1.0; name-token weight
bumps are opt-in via weight_hints. Semantic/BERT/LLM never auto-selected.
- builder.py: walks cls.model_fields (never the lossy/crashing
model_json_schema path), recurses nested BaseModel / List[BaseModel] with
cycle detection, delegates to ModelFactory.create_model_from_fields, caches
shadow classes in a WeakKeyDictionary.
- facade.py: evaluate / eval_for / EvalSpec / EvalResult. Normalizes instances
via model_dump(mode="json") and reads metrics from a single compare_with
call (field_scores + derived precision/recall/f1/accuracy). .explain()
surfaces per-field provenance.
Overrides ({field: ComparableField}) are honored verbatim at highest
precedence. Exported from top-level stickler. 23 new tests; full suite green.
…king)
The overrides={} param only matched top-level field names and silently
dropped nested / list-element keys (e.g. "lines.price"), which is a footgun.
Rather than ship a partial API, remove it entirely: evaluate/eval_for now
infer every field, with weight_hints as the only tuning knob.
A proper nested override API (dotted paths, model-list weight-only guard,
loud errors on unknown keys) can be added later; the InferredSpec config
dict remains the single contract a future producer would target.
Removes overrides from evaluate/eval_for/EvalSpec/structured_model_for,
the override provenance branch, and the override test. 22 auto tests pass;
full suite green.
For the "I don't want to configure anything or read docs" user: show that stickler.evaluate(gt, pred) scores a vanilla Pydantic model with zero setup, and that .explain() makes every inferred decision defensible. - docs: Getting-Started/ultra-quick-start.md (+ nav) — the one-line pitch, a full worked Invoice example (enum/date/optional/nested list), the inference cheat-sheet, and how to defend/override. - docs: Guides/Use-Cases/evaluate-strands-agent.md (+ nav) — the whole integration for a Strands agent's structured_output, single-pair and dataset loops, and the [llm] extra note. - examples/notebooks/Ultra_Quick_Start.ipynb — clean (output-free, per repo convention); every code cell verified to run end-to-end. - examples/scripts/strands_agent_eval_demo.py — runs the real agent when [llm] + creds are present, else falls back to a fabricated prediction so it's always runnable. - examples/README.md — surface both new artifacts.
Drop the "I'm too lazy..." quote framing (reads as passive-aggressive to a reader who didn't say it) in favor of a plain "evaluate as-is, no config required" opener, in both the doc page and the notebook. Remove all em dashes from the auto docs, notebook, examples, and package docstrings/README per user preference; replace with commas, colons, parentheses, or separate sentences.
The prior "remove em dashes" pass missed the notebook: JSON serializes non-ascii as backslash-u escapes, so a literal-character grep reported it clean when 13 em dashes were still present. Rewrote the affected markdown cells with plain punctuation (also dropping the leftover "defend"/"stay lazy" tone that was already softened in the doc page), and fixed the cell-4 import ordering ruff flagged. Notebook re-verified: valid, lint-clean, executes end-to-end.
Add a short section after Installation so the vanilla-Pydantic evaluation path is discoverable from the front door, linking to the Ultra Quick Start.
Security Scan ResultsASH Security Scan Report
Scan Metadata
SummaryScanner ResultsThe table below shows findings by scanner, with status based on severity thresholds and dependencies:
Top 7 HotspotsFiles with the highest number of security findings:
Detailed FindingsShow 19 actionable findingsFinding 1: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 2: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 3: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 4: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 5: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 6: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 7: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 8: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 9: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 10: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 11: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 12: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 13: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 14: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 15: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 16: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 17: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 18: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag
Description: Code Snippet: Finding 19: package_managers.uv.uv-missing-dependency-cooldown.uv-missing-dependency-cooldown
Description: Code Snippet: Report generated by Automated Security Helper (ASH) at 2026-07-23T16:26:19+00:00 |
What
Add a zero-config evaluation path so a vanilla Pydantic model (for example, a Strands agent
response_model) can be scored with a single call, noStructuredModelsubclass, comparators, thresholds, or schema required:Why
Configuring an evaluation is the main adoption barrier: users want the benefits of a tuned eval without making (and later defending) per-field decisions. The two pre-existing entry points also fail on real models:
from_json_schema(Model.model_json_schema())crashes onOptional/Unionand degrades enums/dates, and unannotated fields fall back to string edit-distance even forfloat/bool/date. This infers a sensible comparator per field from the live Python type plus field-name hints, so "unconfigured" already means "reasonably configured," and.explain()makes every choice auditable.How
New additive
src/stickler/auto/package (nothing existing changed except top-level exports):inference.py: per-field comparator/threshold/weight from the type (bool/Enum/Literal->Exact,int->Numeric exact,float->Numeric tolerant,date->Date,str->Levenshtein) plus name-token refinement (*_id->Exact,*amount->Numeric,notes->Fuzzy).Optionalunwrapping, an availability gate (Semantic/BERT/LLM are never auto-selected; missing optional deps degrade and are recorded).builder.py: walks livecls.model_fields(never the lossy/crashingmodel_json_schemapath), recurses nestedBaseModel/List[BaseModel]with cycle detection, delegates toModelFactory.create_model_from_fields, caches shadow classes in aWeakKeyDictionary.facade.py:evaluate/eval_for/EvalSpec/EvalResult. Normalizes instances viamodel_dump(mode="json")and reads bothfield_scoresand derived precision/recall/f1/accuracy from a singlecompare_withcall.Weights default to uniform 1.0 (business-criticality is not encoded in a plain model);
weight_hints=Trueopts into name-based weighting. Per-field overrides were prototyped but removed for now: the top-level-only version silently dropped nested keys, so it is deferred rather than shipped half-working. TheInferredSpecconfig dict remains the single contract a future override or agentic-config layer would target.Docs and examples
ultra-quick-start.md(in nav).evaluate-strands-agent.md(in nav).examples/notebooks/Ultra_Quick_Start.ipynb(clean, verified to run end-to-end).examples/scripts/strands_agent_eval_demo.py(runs the real agent with[llm]+ creds, else falls back to a fabricated prediction so it is always runnable).README.mdblurb linking to the Ultra Quick Start.Testing
tests/auto/; full suite green (1450 passed, 2 skipped).ruff checkclean on all new files (including the notebook).mkdocs buildrenders both new pages; internal links resolve.__all__-consistency test.