hallucination-check
Check an answer against the sources it claims to use, and get back the exact sentences the sources do not support.
Install
pip install hallucination-check
Only dependency: numpy. Nothing is downloaded at import or at run time.
Quickstart
import hallucination_check as hc
answer = "The Eiffel Tower is in Paris. It was completed in 1889. It cost 42 million francs."
sources = ["The Eiffel Tower stands in Paris, France, and was completed in 1889."]
report = hc.check(answer, sources)
print(report.summary()) # 66.7 / 100 grounded, 1 claim flagged
print([c.text for c in report.unsupported]) # ['It cost 42 million francs.']
sources can be one string, a list of strings, or a list of dicts with "text" and an
optional "id", so retrieved passages go straight in:
hc.check(answer, [{"id": "wiki:eiffel#3", "text": "..."}, {"id": "wiki:paris#1", "text": "..."}])
What it checks
- The answer is split into claims (one sentence each by default;
granularity="clause"or"paragraph"also work). Every claim is checked on its own. - For each claim the sources are searched for the best-matching span of one to three consecutive sentences. That span, its source id and the score are on the claim, so every verdict can be traced back to the text it came from.
- The support score blends three lexical signals: character 4-gram cosine, word
unigram/bigram cosine, and how much of the claim's content vocabulary the span covers
(light plural and tense stripping, so "site" matches "sites"). A claim scoring at or above
threshold(default 0.55) counts as supported. - A claim carrying a number, date, name or quantity that appears nowhere in the sources is
flagged whatever its similarity, because those are what people actually get wrong.
12%,$4.3 billion,1,200,twenty-three,12 March 2024,November 2022andNASAare all recognized; numbers written in words and in digits compare equal. - A fact has to appear in the same role, not merely somewhere in the pile of passages. A measurement is tied to the unit or head noun after it and a name to the words around it, so "reduced symptoms by 1,200 percent" is not backed up by "enrolled 1,200 participants" in another passage, "11 metres" is not backed up by the 11 in "Apollo 11", and "developed at Pfizer" is not backed up by a passage that only mentions Pfizer's earnings. A possessive in the source counts as the bare name, so "France's capital" grounds a claim about France.
- When the best span states a different number or date for otherwise the same sentence,
the claim also lands in
report.contradictionswith a reason likethe sources say 1,200 where the answer says 4,200. - A capitalized word at the start of a sentence is not treated as a name unless it is an acronym or the start of a multi-word name, so "Cats sleep a lot." is not flagged for "Cats".
- Everything is deterministic: the same answer and sources always give the same report. Nothing is written, nothing is fetched, and the library never prints.
Lexical grounding is a signal, not proof. A claim can score well while quietly reversing
the source's meaning, and a correct paraphrase that shares no vocabulary will score badly.
The embed hook is how you strengthen it: pass any function that turns a list of strings
into vectors, and semantic similarity is blended into the score at equal weight (this
package stays free of model dependencies, so the model stays yours).
from sentence_transformers import SentenceTransformer # your choice, not a dependency
model = SentenceTransformer("all-MiniLM-L6-v2")
report = hc.check(answer, sources, embed=lambda texts: model.encode(texts))
With embed supplied, every source sentence is embedded (up to embed_max_sentences), so
the hook also finds spans that share no words at all with the claim.
API
hc.check(answer, sources, *, threshold=0.55, granularity="sentence", embed=None) -> GroundingReport
hc.check_batch(pairs, **kw) -> list[GroundingReport]
pairs is an iterable of (answer, sources) tuples, or of dicts with "answer" and
"sources" keys. **kw goes to GroundingChecker.
GroundingReport
.score- 0-100, the share of the answer's claims the sources support.claims-list[Claim]in answer order.unsupported- the claims the sources do not back up.contradictions- unsupported claims where a source states a different number or date.citations-dict[claim index -> source id]for the supported claims.n_claims,.n_supported,.threshold,.granularity,.source_ids,.warnings.summary()- human-readable text;.to_dict()- JSON-safe dict
Claim(text, supported, confidence, best_source, best_span, reason)
.text- the claim as it appears in the answer.supported- the verdict.confidence- 0-1 support score for this claim.best_source- id of the source the best span came from ("s1","s2", ... unless you gave ids), orNonewhen nothing matched.best_span- the source text that matched best.reason- one plain sentence saying why, e.g.these appear nowhere in the sources: 42 million.to_dict()
report = hc.check(answer, sources)
report.score # 66.7
report.claims[1].best_span # 'The Eiffel Tower stands in Paris, France, and was completed in 1889.'
report.citations # {0: 's1', 1: 's1'}
report.to_dict()["unsupported"] # [2]
GroundingChecker(threshold=0.55, granularity="sentence", embed=None, embed_weight=0.5, flag_missing_facts=True, char_n=4, window=3, top_k=25, embed_top=5, embed_max_sentences=2000) is the class underneath, with .check(answer, sources) and
.check_batch(pairs). Set flag_missing_facts=False to score on similarity alone.
Edge cases are settled, not accidental: an empty answer scores 100 with no claims; empty sources score 0 with every claim unsupported; an answer identical to a source scores 100; the score is always between 0 and 100.
CLI
hallucination-check ANSWER [-s SOURCE ...] [--threshold 0.55]
[--granularity sentence|clause|paragraph] [--no-fact-flags]
[--json] [--output PATH] [--fail-under SCORE]
ANSWER and each SOURCE may be a file path or the text itself; - reads stdin. A value
that can only have meant a file -- one bare word ending in .txt, .md, .json, .jsonl
or .ndjson -- is an error when no such file exists, rather than being checked as literal
text, so a mistyped path never turns into a confident wrong report.
hallucination-check answer.txt -s passage1.txt -s passage2.txtprints the summary.hallucination-check "Paris is the capital of France." -s "France's capital is Paris."takes text directly.cat answer.txt | hallucination-check - -s retrieved.jsonl --jsonreads the answer from stdin and the passages from a JSON-lines file (one object per line withtextand an optionalid), and printsto_dict()as JSON.--output PATHwrites that JSON to a file;--fail-under 80exits with status 2 when the score is below 80, which is the useful thing to put in CI.
License
MIT
Metadata
Release files for hallucination-check 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hallucination_check-0.1.0.tar.gz | 37.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hallucination_check-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 69.3 kB
Release files / hallucination_check-0.1.0.tar.gz
| Download URL | hallucination_check-0.1.0.tar.gz |
|---|---|
| Size | 37.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af3627b0197450a4a2e11e89f575750e0a994c5287e065d4a7bb1aac32333870
|
|
BLAKE2b-256 checksum How to use checksums |
08953c5ca01aaa5738c2acfe89d87c5ea4469ab9ccf2b83cb15eb5d33e6e019b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / hallucination_check-0.1.0-py3-none-any.whl
| Download URL | hallucination_check-0.1.0-py3-none-any.whl |
|---|---|
| Size | 32.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1fbabdd674c53dfa20e2b3cdd95556e1d85da88151e87219dcc7f06ee88d91ef
|
|
BLAKE2b-256 checksum How to use checksums |
b14ab118cf0693f341bfb402282e2cd1395b5ccc3ca50ee28bab844b26758822
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|