Skip to content

Citations

An answer assembled from three retrieved passages and returned as prose cannot be audited. When it turns out to be wrong, nobody can say whether the corpus was wrong, retrieval was wrong, or the model invented it — and the support agent holding it cannot show the customer where the policy statement came from.

Footnote-shaped strings inside the answer text are not a fix. An answer that cites nothing and an answer whose footnotes were invented both parse the same way: as text.

The shape

from tesserix_adk.rag import check_grounding, cite, excerpt

retrieved = cite(await retriever.retrieve(question, scope=HANDBOOK))
answer = await model_answers(question, retrieved)   # a CitedAnswer

check_grounding(answer, retrieved)                  # raises rather than returns

answer.text                                         # the claims, as prose
answer.sources(answer.claims[0])                    # what that claim rests on
excerpt(retrieved[0], document)                     # the exact characters cited

cite turns a RetrievalResult into citations. check_grounding runs before the answer is returned to anyone. excerpt resolves a citation back to the span of the document version it was made from.

What a citation pins

Field Why it is there
document_id, document_version The document may be updated between retrieval and answer. Resolving against whatever it says now shows a reader text the answer was never built from.
chunk_id, span A citation to a document is a citation to forty pages.
tenant Carried on the citation, so a result crossing a call boundary cannot lose the only thing that says who may read it.
score, branches Whether the passage was an exact match or the vector's opinion — and the reranker's score where one ran.
retrieved_at, locator When it was read, and where a reader goes to look.

cite builds these from the chunk metadata the store carries: version, start and end are required, and uri, page and section are used where present. A chunk missing the version or the span raises ConfigurationError rather than producing a citation that resolves to the wrong place — the ingest that wrote the chunk is where that is fixed.

The answer is structured

CitedAnswer is claims and citations. Each Claim carries the citation_ids it rests on: several citations may support one claim, and one citation may support several claims. Two citations may not share an id, because a claim naming it would be ambiguous.

Grounding fails closed

check_grounding raises, and never repairs:

  • a claim resting on no citation at all → UncitedClaimError. Where the corpus returned nothing, the answer is a refusal, not an answer with the citations left off.
  • a claim naming an id the answer does not carry, or a citation naming a document or a version this run did not retrieve → UngroundedCitationError, listing what is missing and what was available. Stripping the offending citation would leave the claim standing with nothing behind it, which is the exact failure this surface exists to catch.
  • a citation into another tenant → TenantCrossingError.

Provenance travels

answer.provenance() is every citation id some claim rests on. MemoryRecord.citations holds them, so a summary written back to memory from retrieved content can still be asked where it came from. A fact whose sources are gone is a claim about the corpus that the corpus cannot answer for.

citation_attributes(citations) gives span attributes — counts, document ids and versions, under adk.citation.*. Never document text: a tracing backend is not the corpus, and it outlives every redaction rule the corpus has.

When the source is gone

CitationResolver resolves a citation against the live corpus. A chunk erased under a right-to-erasure request resolves to a tombstone — erased=True and no text — rather than disappearing, so an audit can still see that the answer was built on something that has since been removed.

Known limitations

Rendering citations in a product interface is out of scope, as is corpus-level freshness and document lifecycle management.