Skip to content

Models

The library's core data model. Every component honors this hierarchy.

Answer

Answer dataclass

Answer(query: str, sentences: list[CitedSentence], faithfulness_score: float, faithfulness_components: FaithfulnessComponents, unsupported_claims: list[str], retrieved_chunks: list[RetrievedChunk], verification_results: list[VerificationResult], strictness: Strictness = 'balanced', was_refused: bool = False, refusal_reason: str | None = None)

The complete output of a pipeline.ask() call.

supported_sentences property

supported_sentences: list['CitedSentence']

Sentences whose verification said is_supported (or had no verifier).

A sentence with no matching VerificationResult counts as supported — verification didn't run, so we don't penalize it. Use answer.unsupported_sentences for the strict complement.

unsupported_sentences property

unsupported_sentences: list['CitedSentence']

Sentences explicitly flagged is_supported=False by the verifier.

cited_sentence_ids property

cited_sentence_ids: frozenset[str]

Union of every source sentence_id cited across all sentences.

Useful for "which sentences from the corpus did this answer pull from?" — without re-walking answer.sentences.

nli_scores property

nli_scores: list[float]

Per-sentence NLI scores in verification_results order.

min_nli_score property

min_nli_score: float

The worst-case sentence-level NLI score.

Returns 1.0 when no verifier ran — there's no evidence of unfaithfulness in the absence of a check.

verification_for

verification_for(sentence_idx: int) -> 'VerificationResult | None'

Return the :class:VerificationResult for sentence_idx, or None.

Lets callers map a CitedSentence index back to its NLI check without manually walking verification_results::

for i, sent in enumerate(answer.sentences):
    vr = answer.verification_for(i)
    if vr and not vr.is_supported:
        log.warning(f"unsupported: {sent.text!r}")
Source code in src/verifiable_rag/models/answer.py
def verification_for(self, sentence_idx: int) -> "VerificationResult | None":
    """Return the :class:`VerificationResult` for *sentence_idx*, or None.

    Lets callers map a CitedSentence index back to its NLI check
    without manually walking ``verification_results``::

        for i, sent in enumerate(answer.sentences):
            vr = answer.verification_for(i)
            if vr and not vr.is_supported:
                log.warning(f"unsupported: {sent.text!r}")
    """
    for vr in self.verification_results:
        if vr.cited_sentence_index == sentence_idx:
            return vr
    return None

audit_trail

audit_trail() -> dict

Structured audit trail as a JSON-serializable dict.

Drop-in for observability stacks — emit this on every answer to track faithfulness over time, alert on unsupported claims, or slice metrics by upstream model. All fields are primitives.

Source code in src/verifiable_rag/models/answer.py
def audit_trail(self) -> dict:
    """Structured audit trail as a JSON-serializable dict.

    Drop-in for observability stacks — emit this on every answer to
    track faithfulness over time, alert on unsupported claims, or
    slice metrics by upstream model. All fields are primitives.
    """
    fc = self.faithfulness_components
    nli = self.nli_scores
    return {
        "query": self.query,
        "strictness": self.strictness,
        "was_refused": self.was_refused,
        "refusal_reason": self.refusal_reason,
        # When verification_ran is False, faithfulness_score defaults
        # to 1.0 — there's no evidence of unfaithfulness, but no
        # evidence of faithfulness either. Distinct from "verifier
        # was configured on the Pipeline" — the Pipeline can have a
        # verifier attached yet skip verification (e.g. when the
        # generator produced no sentences to verify).
        "verification_ran": bool(self.verification_results),
        "faithfulness_score": self.faithfulness_score,
        "faithfulness_components": {
            "retrieval_score": fc.retrieval_score,
            "nli_score": fc.nli_score,
            "generation_logprob": fc.generation_logprob,
        },
        "n_sentences": len(self.sentences),
        "n_supported": len(self.supported_sentences),
        "n_unsupported": len(self.unsupported_sentences),
        "n_verified": len(self.verification_results),
        "min_nli_score": self.min_nli_score,
        "mean_nli_score": sum(nli) / len(nli) if nli else None,
        "unsupported_claims": list(self.unsupported_claims),
        "cited_sentence_ids": sorted(self.cited_sentence_ids),
        "n_retrieved_chunks": len(self.retrieved_chunks),
    }

to_html

to_html(title: str = 'verifiable-rag report') -> str

Render the full audit-trail HTML report for this Answer.

Returns a self-contained HTML document string — inline CSS, no JavaScript, no external dependencies. Write it to a file and open in any browser::

Path("report.html").write_text(answer.to_html())

Shows the query, the answer with per-sentence verification color coding, the faithfulness components, the per-sentence NLI scores, and every reranked passage the generator saw. See :func:verifiable_rag.report.to_html for details.

Source code in src/verifiable_rag/models/answer.py
def to_html(self, title: str = "verifiable-rag report") -> str:
    """Render the full audit-trail HTML report for this Answer.

    Returns a self-contained HTML document string — inline CSS, no
    JavaScript, no external dependencies. Write it to a file and open
    in any browser::

        Path("report.html").write_text(answer.to_html())

    Shows the query, the answer with per-sentence verification color
    coding, the faithfulness components, the per-sentence NLI scores,
    and every reranked passage the generator saw. See
    :func:`verifiable_rag.report.to_html` for details.
    """
    from verifiable_rag.report import to_html

    return to_html(self, title=title)

CitedSentence

CitedSentence dataclass

CitedSentence(text: str, supporting_sentence_ids: tuple[str, ...], confidence: float)

One sentence of generated output, grounded in source sentence IDs.

supporting_sentence_ids references Sentence.id values in the source Document. An empty tuple means no citations — the verifier treats this as unsupported; the abstention layer decides whether to flag or refuse.

VerificationResult

VerificationResult dataclass

VerificationResult(cited_sentence_index: int, claim_text: str, is_supported: bool, nli_score: float, supporting_span: Span | None = None)

NLI-based faithfulness check for one CitedSentence.

FaithfulnessComponents

FaithfulnessComponents dataclass

FaithfulnessComponents(retrieval_score: float, nli_score: float, generation_logprob: float | None = None)

Decomposed faithfulness signal — exposed for auditability.

Document

Document dataclass

Document(doc_id: str, source_path: Path, sections: list[Section], page_breaks: list[int] = list(), full_text: str | None = None, metadata: dict[str, Any] = dict(), parser_name: str | None = None)

A parsed source document.

Spans throughout the pipeline use char offsets into Document.full_text (when supplied) so they can cross page boundaries. page_breaks maps those offsets back to page numbers via Document.page_for_offset().

page_breaks[i] is the char offset where page i begins. It must start at 0.

page_for_offset

page_for_offset(offset: int) -> int

Return the page index (0-indexed) containing offset.

Source code in src/verifiable_rag/models/document.py
def page_for_offset(self, offset: int) -> int:
    """Return the page index (0-indexed) containing *offset*."""
    if not self.page_breaks:
        raise ValueError(
            f"Document {self.doc_id!r} has no page_breaks; cannot map offset to page"
        )
    if offset < 0:
        raise ValueError(f"offset must be >= 0, got {offset}")
    # bisect_right finds insertion point AFTER any equal entries; subtract 1
    return max(0, bisect.bisect_right(self.page_breaks, offset) - 1)

pages_for_span

pages_for_span(span: Span) -> tuple[int, int]

Return (first_page, last_page) inclusive — the page range a span touches.

Source code in src/verifiable_rag/models/document.py
def pages_for_span(self, span: Span) -> tuple[int, int]:
    """Return (first_page, last_page) inclusive — the page range a span touches."""
    if span.doc_id != self.doc_id:
        raise ValueError(
            f"Span doc_id {span.doc_id!r} does not match document {self.doc_id!r}"
        )
    first = self.page_for_offset(span.char_start)
    # char_end is exclusive; subtract 1 so a span ending exactly on a page break
    # does not get attributed to the next page
    last_offset = max(span.char_start, span.char_end - 1)
    last = self.page_for_offset(last_offset)
    return first, last

Section

Section dataclass

Section(id: str, title: str | None, paragraphs: list[Paragraph], span: Span)

Paragraph

Paragraph dataclass

Paragraph(id: str, sentences: list[Sentence], span: Span)

Sentence

Sentence dataclass

Sentence(id: str, text: str, span: Span)

Atomic citable unit. Every sentence has a globally unique id and a Span.

Sentences may cross page boundaries — Span uses document-level char offsets. The page(s) a sentence touches are looked up via Document.pages_for_span().

Chunk

Chunk dataclass

Chunk(chunk_id: str, text: str, doc_id: str, sentence_ids: tuple[str, ...], span: Span, metadata: dict[str, Any] = dict())

Retrieval unit.

Chunks carry the sentence_ids of every source sentence they contain so the citation layer can map retrieved chunks back to exact spans. This is the mechanism that decouples chunking granularity from citation granularity.

RetrievedChunk

RetrievedChunk dataclass

RetrievedChunk(chunk: Chunk, score: float, retrieval_method: str)

A chunk returned from the index with its retrieval score.

Span

Span dataclass

Span(doc_id: str, char_start: int, char_end: int, bboxes: tuple[PageBBox, ...] = ())

Exact source location of a piece of text.

Offsets are character positions into the full document text, so spans naturally support text that crosses page boundaries. Use Document.page_for_offset()/pages_for_span() to recover page numbers when bboxes are not populated.

Invariant: every object in the pipeline that wraps text from a source document must carry a Span. Losing a Span is a bug.

pages property

pages: tuple[int, ...]

Page numbers this span touches, derived from bboxes. Empty if unknown.

merge classmethod

merge(spans: list[Span]) -> Span

Return a bounding Span covering all spans in the list.

bboxes from input spans are union-ed and re-sorted by (page, y0, x0). Duplicate (page, bbox) pairs are dropped.

Source code in src/verifiable_rag/models/span.py
@classmethod
def merge(cls, spans: list[Span]) -> Span:
    """Return a bounding Span covering all spans in the list.

    bboxes from input spans are union-ed and re-sorted by (page, y0, x0).
    Duplicate (page, bbox) pairs are dropped.
    """
    if not spans:
        raise ValueError("Cannot merge empty span list")
    doc_ids = {s.doc_id for s in spans}
    if len(doc_ids) > 1:
        raise ValueError(f"Cannot merge spans from different documents: {doc_ids}")

    seen: set[tuple[int, BBox]] = set()
    all_bboxes: list[PageBBox] = []
    for s in spans:
        for pb in s.bboxes:
            key = (pb.page, pb.bbox)
            if key not in seen:
                seen.add(key)
                all_bboxes.append(pb)
    all_bboxes.sort(key=lambda pb: (pb.page, pb.bbox.y0, pb.bbox.x0))

    return cls(
        doc_id=spans[0].doc_id,
        char_start=min(s.char_start for s in spans),
        char_end=max(s.char_end for s in spans),
        bboxes=tuple(all_bboxes),
    )

BBox

BBox dataclass

BBox(x0: float, y0: float, x1: float, y1: float)

Page-coordinate bounding box (PDF user-space units).

PageBBox

PageBBox dataclass

PageBBox(page: int, bbox: BBox)

A bounding box on a specific page.

Spans crossing page boundaries carry one PageBBox per page they touch (e.g. a sentence whose tail wraps onto the next page has two).