Build cited answers¶
Retrieval gives you chunks that know where they came from. This page turns that into (1) an LLM answer with citations and (2) a viewer that highlights the source on the page.
1. A cited LLM answer¶
result.context is the prompt block — numbered chunks, each tagged with
its citation:
from ingestlib.services import retrieve
result = retrieve("how were participants recruited?", top_k=5)
print(result.context)
[1] (doc 7b6b95d79149 · p.4 · methods)
Participants were recruited through community centers in Cairo…
[2] (doc 7b6b95d79149 · p.7 · results)
Of the 214 participants enrolled…
Hand it to any LLM with an instruction to cite:
from ingestlib.foundations.llm import chat
answer = chat(
f"Answer using ONLY the context. Cite sources as [n].\n\n"
f"Context:\n{result.context}\n\nQuestion: {result.question}"
)
# "Participants were recruited through community centers [1], with 214 enrolled [2]."
Your application resolves [n] back to result.hits[n-1] — and from
there to a page.
2. From a hit to a page highlight¶
Each hit carries pages and region_ids — the exact parse regions the
chunk covers. Resolve them against the stored parse:
from ingestlib.storage import artifacts
hit = result.hits[0]
doc_id = hit.chunk.document_id
parse = artifacts.load_parse(doc_id) # structure only — fast
for page_num, region_ids in hit.chunk.region_ids.items():
page = next(p for p in parse.pages if p.page_num == page_num)
boxes = [r.bbox for r in page.regions if r.region_id in region_ids]
print(f"page {page_num}: {[b.as_tuple() for b in boxes]}")
# page 4: [(72.0, 160.4, 523.1, 380.9), (72.0, 390.0, 523.1, 512.6)]
3. Serve the page image¶
The rendered page PNGs live in the artifact store:
key = artifacts.page_image_key(doc_id, page_num)
png_bytes = artifacts.read_blob(key) # works on s3 AND local
read_blob is backend-agnostic; on S3 you can instead presign the key
(get_s3_client().generate_presigned_url) and let the browser fetch it
directly.
Draw the boxes over the PNG — bbox.as_tuple() is (x0, y0, x1, y1) in
page-render pixel coordinates — and the answer literally points at its
source.
Why this works reliably¶
Two invariants protect the chain (details in Provenance & citations):
- chunk boundaries never cut through a region, so
region_idsis exact - the payload rides on the vector, so no join against a second database can drift out of sync
Next: Run fully local — the same pipeline with nothing leaving your machine.