Skip to main content

llmground

Your schema validated. The value was still made up.

Structured output libraries guarantee the shape of extracted data. None of them check whether the values are actually in the document. llmground runs after extraction and tells you which fields are real.

from llmground import verify

report = verify(extracted, source_document)

if report.has_hallucinations:
    route_to_human_review(report.ungrounded)

No API calls. No model. No cost. Zero dependencies.

The bug this catches

extracted = {
    "vendor": "ACME Manufacturing Ltd.",
    "total": 2846.25,          # the invoice says 2,486.25
}

Right types. Passes any JSON schema. Passes Pydantic. The number is a transposition the model invented, and it goes straight into your accounting system. Shape validation cannot see this, because there is nothing wrong with the shape.

GROUNDED    vendor = 'ACME Manufacturing Ltd.'
            found: ACME Manufacturing Ltd. 14 Sundar Industrial Estate...
UNGROUNDED  total = 2846.25
            nearest number in source is '$2,486.25'

6/8 fields grounded (75%), 0 weak, 2 ungrounded

Install

pip install llmground

It works with what you already use

llmground doesn't replace Instructor, Outlines, or PydanticAI. It runs after them. Pydantic models, dataclasses, and plain dicts all work, and Pydantic is optional rather than required.

import instructor
from llmground import verify

invoice = client.chat.completions.create(response_model=Invoice, ...)

report = verify(invoice, source_pdf_text)
if report.grounding_rate < 0.9:
    escalate(report)

Why matching values is harder than it looks

The naive version of this check is str(value) in source, and it produces so many false alarms that people switch it off within a day. Real documents don't write values the way models return them.

Model returns Document says Naive check llmground
4500.0 $4,500.00 miss matched
"2026-01-05" 5 January 2026 miss matched
"O'Brien Ltd" O'Brien Ltd (smart quote) miss matched
1234.56 1.234,56 (European) miss matched
-1234.00 (1,234.00) (accounting) miss matched
"ACME Inc" ACME, Inc. miss matched

Every one of these is handled deterministically, with no model in the loop.

Verdicts

Verdict Meaning
grounded Found in the source
weak Found only by fuzzy match. A human should look.
ungrounded Not in the source. Likely fabricated.
skipped Not verifiable: booleans, nulls, values like "1" or "yes"

Fuzzy matches are weak, not grounded, by default. A 90%-similar string is a different string, and on a financial or medical field that difference is the entire problem. Opt in with accept_fuzzy=True when you're handling OCR noise and know what you're trading away.

Skipped fields are excluded from the grounding rate. Counting booleans as grounded would inflate the score and hide real problems.

Spans, for human review

Every match reports character offsets into the source, so you can highlight the evidence rather than just flagging a field.

for r in report.grounded:
    span = r.match.spans[0]
    highlight(source, span.start, span.end)

That's the difference between telling a reviewer "this field is suspicious" and showing them why.

CLI

llmground extracted.json --source invoice.txt --fail-under 0.9

Exits non-zero below the threshold, so it drops into CI as a quality gate.

Options

verify(
    extracted,
    source,
    accept_fuzzy=False,        # count fuzzy matches as grounded
    fuzzy_threshold=0.85,      # similarity floor for reporting a fuzzy match
    number_tolerance=0.01,     # allowed numeric difference
    day_first=True,            # how to read 03/04/2026
    ignore=["metadata.run_id"],  # skip computed fields
)

Limitations

  • Extractive only. It verifies values that should appear in the source. A field that's legitimately inferred rather than copied (a category label, a computed total) will read as ungrounded. Use ignore for those.
  • Ambiguous dates are not guessed. 03/04/2026 returns no match unless you pass day_first, because a silently wrong date is worse than an unverified one.
  • Grounded is not correct. It confirms a value appears in the document, not that the model pulled it from the right place. A total that matches a different line item's amount will pass.
  • Text sources only. PDFs and images need to be through OCR first.

Development

pip install -e ".[dev]"
pytest

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llmground-0.1.0.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llmground-0.1.0-py3-none-any.whl (18.0 kB view details)

Uploaded Python 3

File details

Details for the file llmground-0.1.0.tar.gz.

File metadata

  • Download URL: llmground-0.1.0.tar.gz
  • Upload date:
  • Size: 21.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.2

File hashes

Hashes for llmground-0.1.0.tar.gz
Algorithm Hash digest
SHA256 17149cae253ede3f0c3dbc8e09532beeea7bab804ef999202072a250f971d8e8
MD5 f0c665245e6a823d4292fb9caa713bb7
BLAKE2b-256 5a089609cf24ea02d8e5fc51c6e357fc011520d471df305a39b929fcc27147f5

See more details on using hashes here.

File details

Details for the file llmground-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: llmground-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.2

File hashes

Hashes for llmground-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cd1f335a00e8a3de5eee6e1452cac54660cbc2792f98a7d94337e594050f1646
MD5 24a9c9aa055db0ef01694c241a550cb0
BLAKE2b-256 8313a2d3a96eb5a1f2df671ff1aad3f53432fd5dd756869573a00ff2165ffecb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page