Skip to main content

A lightweight Python library for hallucination detection and explainability in LLM responses, with claim-level faithfulness verification.

Project description

cavaquinho

Faithfulness hallucination detection for LLM responses

License PyPI version CI HuDEx paper


Most hallucination detectors return a single score. Cavaquinho tells you which claim contradicts which sentence — and runs fully local, with no external API required.

Cavaquinho decomposes a response into atomic claims, verifies each one against a provided context using Natural Language Inference, and returns a structured result with per-claim labels, scores, and evidence.

Inspired by HuDEx: Integrating Hallucination Detection and Explainability.

Installation

# Default — no torch required (bring your own classifier)
pip install cavaquinho

# With local DeBERTa NLI classifier
pip install "cavaquinho[nli]"

Quickstart

import cavaquinho as caco

validator = caco.caco()

result = validator.validate(
    response="The LGPD was created in 2015 during Dilma Rousseff's government.",
    context="The LGPD was enacted on August 14, 2018, by President Michel Temer."
)

print(result.score)             # 1.0
print(result.is_hallucination)  # True
print(result.summary)
# 1 of 1 claim(s) contradict the provided context.
# Main conflict: 'The LGPD was created in 2015...'
# contradicting evidence: 'The LGPD was enacted on August 14, 2018...'. Score: 1.00.

Claim-level inspection

Each claim in the response is verified independently. The result exposes the full trace:

for claim in result.claims:
    print(claim.text)      # "The LGPD was created in 2015..."
    print(claim.label)     # Labels.VALUE_CONTRADICTION
    print(claim.score)     # 1.0
    print(claim.evidence)  # "The LGPD was enacted on August 14, 2018..."
    print(claim.reason)    # "The LGPD was enacted on August 14, 2018..."

Using the result

result = validator.validate(response=response, context=context)

# threshold-based decision
if result.is_hallucination:
    response = retry_generation()

# per-claim decision
for claim in result.claims:
    if claim.label == caco.Labels.VALUE_CONTRADICTION and claim.score > 0.8:
        logger.warning(f"Conflicting claim: {claim.text}")
        logger.warning(f"Context evidence: {claim.evidence}")

# score-based soft warning
if result.score > 0.3:
    ui.show_disclaimer("This response may contain inaccurate information.")

RAG integration

The context parameter accepts the documents your retriever already returned. No additional steps are required.

LangChain

import cavaquinho as caco

source_docs = retriever.invoke(query)
context = "\n".join([doc.page_content for doc in source_docs])
response = llm.invoke(query)

validator = caco.caco()
result = validator.validate(response=response, context=context)

LlamaIndex

import cavaquinho as caco

response = query_engine.query("What is the LGPD?")
context = "\n".join([node.text for node in response.source_nodes])

validator = caco.caco()
result = validator.validate(response=str(response), context=context)

Direct LLM call

import cavaquinho as caco
from openai import OpenAI

client = OpenAI()
docs = retriever.search(query)
context = "\n".join(docs)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": f"Context: {context}"},
        {"role": "user", "content": query}
    ]
).choices[0].message.content

validator = caco.caco()
result = validator.validate(response=response, context=context)

Configuration

All components have sensible defaults. Each can be replaced independently.

from cavaquinho import caco
from cavaquinho.extractor import LLMExtractor
from cavaquinho.classifier import DeBERTaClassifier
from cavaquinho.aggregator import Aggregator
from openai import OpenAI

validator = caco(
    extractor=LLMExtractor(
        llm_fn=lambda p: OpenAI().chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": p}]
        ).choices[0].message.content
    ),
    classifier=DeBERTaClassifier(
        model_name="cross-encoder/nli-deberta-v3-base"
    ),
    aggregator=Aggregator(
        threshold=0.6,
        language="english"
    ),
    language="english"
)

Portuguese

validator = caco.caco(language="portuguese")

result = validator.validate(
    response="A LGPD foi criada em 2015.",
    context="A LGPD foi sancionada em 14 de agosto de 2018."
)

How it works

Three components run in sequence:

1. Claim extraction. The response is split into atomic sentences using NLTK. Each sentence is treated as an independent, verifiable assertion. An LLMExtractor is available for higher-precision decomposition when a language model client is available.

2. NLI classification. For each claim, the context is split into sentences and the claim is compared against each one using a fine-tuned DeBERTa NLI model (cross-encoder/nli-deberta-v3-base). The sentence with the highest contradiction score is used as the evidence field. Classification runs concurrently across all claims via ThreadPoolExecutor.

3. Weighted aggregation. Scores are combined using label-weighted averaging. Contradiction labels carry full weight (1.0), neutral labels carry partial weight (0.5), and entailment labels carry no weight (0.0). The aggregated score is compared against a configurable threshold to produce the final is_hallucination decision.

response
    │
    ▼
[ RuleExtractor / LLMExtractor ]   →   ["claim 1", "claim 2", ...]
    │
    ▼ (concurrent)
[ DeBERTaClassifier            ]   →   [ClaimResult, ClaimResult, ...]
    │
    ▼
[ Aggregator                   ]   →   ValidationResult

Custom components

Every component implements an abstract contract. Custom implementations are supported without modifying any other part of the pipeline:

from cavaquinho.extractor.base import ExtractorContract
from cavaquinho.classifier.base import ClassifierContract
from cavaquinho.models import ClaimResult

class MyExtractor(ExtractorContract):
    def extract(self, response: str, context: str, prompt: str | None = None) -> list[str]:
        ...

class MyClassifier(ClassifierContract):
    def classify(self, claim: str, context: str) -> ClaimResult:
        ...

validator = caco.caco(extractor=MyExtractor(), classifier=MyClassifier())

Output schema

@dataclass(frozen=True)
class ClaimResult:
    text: str            # extracted claim
    evidence: str        # context sentence used in comparison
    label: Labels        # VALUE_ENTAILMENT | VALUE_NEUTRAL | VALUE_CONTRADICTION
    score: float         # model confidence 0.0–1.0
    reason: str | None   # contradicting evidence sentence when label is VALUE_CONTRADICTION

@dataclass(frozen=True)
class ValidationResult:
    score: float                    # weighted aggregate score
    is_hallucination: bool          # score > threshold
    claims: tuple[ClaimResult, ...] # per-claim breakdown (immutable)
    summary: str                    # natural language description of the result

Benchmark — ASSIN2 Portuguese NLI

Evaluated on the ASSIN2 Brazilian Portuguese validation split (500 sentence pairs, balanced). Binary task: entailment detection vs. non-entailment.

Model Accuracy F1-entailment F1-none ms/sample
Majority baseline 0.500 0.667 0.000
cross-encoder/nli-deberta-v3-base (default) 0.882 0.885 0.879 29.5
MoritzLaurer/mDeBERTa-v3-base-mnli-xnli 0.876 0.884 0.866 28.9

Reproduce with: python -m benchmarks.nli_benchmark --n 500

Limitations

  • Faithfulness scope. Cavaquinho verifies whether the response contradicts the provided context. It does not verify factual accuracy against external knowledge — that requires a separate retrieval or knowledge-base step.
  • Context dependency. Detection quality is proportional to context quality. Incomplete or irrelevant retrieved documents will reduce accuracy.
  • False positives on ambiguous contexts. When the context contains multiple facts about overlapping subjects, the NLI model may classify semantically consistent claims as contradictions.
  • Python 3.10+. Tested on Python 3.11 and 3.12. Python 3.14 is functional but produces deprecation warnings from PyTorch.

Roadmap

v0.2 — Foundation fixes ✅

See CHANGELOG.md for the full list of changes.

v0.3 — Confidence

  • Self-consistency detection via response sampling
  • Consistency scoring across multiple sampled outputs

v0.4 — Factual

  • Optional external search integration (Tavily, Wikipedia API)
  • Factual verification without a pre-supplied context

Contributing

Contributions are welcome. Please open an issue before submitting a pull request for significant changes.

Named after Caco, the cat

Caco, chief hallucination auditor
Caco — chief hallucination auditor

References

License

MIT

Project details


Download files

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

Source Distribution

cavaquinho-0.2.1.tar.gz (6.7 kB view details)

Uploaded Source

Built Distribution

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

cavaquinho-0.2.1-py3-none-any.whl (6.2 kB view details)

Uploaded Python 3

File details

Details for the file cavaquinho-0.2.1.tar.gz.

File metadata

  • Download URL: cavaquinho-0.2.1.tar.gz
  • Upload date:
  • Size: 6.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cavaquinho-0.2.1.tar.gz
Algorithm Hash digest
SHA256 141ac687c3d87a7c7916b39afc285972954d0b273c45866a263fe2dd6cd46e19
MD5 5dbf4827eb4d79855b1a5e6c6d0dfec5
BLAKE2b-256 adbb4c4597de2806fb99dd5b3c68c787e4adc19b3e48bca723537cbd36f48abe

See more details on using hashes here.

Provenance

The following attestation bundles were made for cavaquinho-0.2.1.tar.gz:

Publisher: publish.yml on felipetp-ctrl/cavaquinho

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cavaquinho-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: cavaquinho-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 6.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cavaquinho-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4fe93774af4424d601b86bc85b358dcf7824297241584e5bf5736fc21c64a01f
MD5 2862b2fb3e5303d798929b6a70ee4660
BLAKE2b-256 67a01b74190581d418971eee71368065fbb7eaea5b93748111d9b61fb217e905

See more details on using hashes here.

Provenance

The following attestation bundles were made for cavaquinho-0.2.1-py3-none-any.whl:

Publisher: publish.yml on felipetp-ctrl/cavaquinho

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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