Loading Eval Sets
Everything on this page returns or accepts an EvalSet — see that
page first for what EvalItem, GoldAnchor, and GoldSpan actually hold.
The native JSONL format
write_jsonl writes context-grid’s own round-trip format: one header line, then one
question per line.
import contextgrid as cg
anchor = cg.GoldAnchor(source_id="handbook.pdf", quote="Employees accrue 1.5 days of leave per month.")item = cg.EvalItem( id="q1", question="How much leave do employees accrue per month?", anchors=(anchor,), qtype=cg.QuestionType.FACTOID,)evalset = cg.EvalSet(id="handbook-quiz", items=(item,))path = cg.write_jsonl(evalset, "quiz.jsonl")print(open(path).read()){"_evalset": {"id": "handbook-quiz", "version": 1, "source": "manual", "meta": {}}}{"id": "q1", "question": "How much leave do employees accrue per month?", "gold": [], "anchors": [{"source_id": "handbook.pdf", "quote": "Employees accrue 1.5 days of leave per month.", "grade": 2, "page_hint": null, "occurrence": 0}], "qtype": "factoid", "answer": null, "meta": {}}write_jsonl(evalset, path) creates any missing parent directories for you.
write_jsonl(evalset: EvalSet, path: str | Path) -> Pathread_jsonl
read_jsonl(path: str | Path) -> EvalSetReads that same shape back:
es = cg.read_jsonl("quiz.jsonl")print(len(es), es.id, es.version, es.source)1 handbook-quiz 1 manualThe header line is optional. read_jsonl also accepts a bare file of item lines with no
_evalset header — a minimal file you can hand-write is just this:
{"id": "q1", "question": "What is the notice period for resignation?", "anchors": [{"source_id": "handbook.pdf", "quote": "Employees must give 30 days written notice."}]}Only id and question are required on an item; anchors, gold, qtype, answer, and
meta all have defaults. Write that line to disk and read it back:
from pathlib import Path
Path("minimal.jsonl").write_text( '{"id": "q1", "question": "What is the notice period for resignation?", ' '"anchors": [{"source_id": "handbook.pdf", "quote": "Employees must give 30 days written notice."}]}\n')
es = cg.read_jsonl("minimal.jsonl")print(len(es), es.id, es.version, es.source, es.meta)1 minimal 1 import {}The CSV format
CSV is meant for editing in a spreadsheet, not as the primary storage format — it can only
round-trip one anchor per item (see below). Everything else on that anchor survives, including
occurrence, and so does the item’s meta.
A meta cell that isn’t JSON — a note somebody typed while the file was open in a spreadsheet
— is read as {} rather than raising, so one stray cell can’t take down a file of good
questions.
write_csv
write_csv(evalset: EvalSet, path: str | Path) -> PathColumns: id, question, source_id, quote, grade, page, occurrence, qtype, answer, meta.
path = cg.write_csv(evalset, "quiz.csv")print(open(path).read())id,question,source_id,quote,grade,page,occurrence,qtype,answer,metaq1,How much leave do employees accrue per month?,handbook.pdf,Employees accrue 1.5 days of leave per month.,2,,0,factoid,,meta is written as JSON in a single cell — ugly in a spreadsheet, but it round-trips, and
meta.reviewed is what assess() counts for its ”% reviewed” figure. (JSON has no tuples:
a ("a", 1) inside meta comes back as ["a", 1].)
read_csv
read_csv(path: str | Path, *, evalset_id: str | None = None) -> EvalSetColumn names are matched case-insensitively, and several spellings work for each field:
| Field | Accepted column names |
|---|---|
| item id | id, question_id, qid |
| question | question, query, q |
| source document | source_id, document, doc, doc_id, file, filename |
| quote | quote, evidence, answer_span, context, passage |
| answer | answer, expected_answer, gold_answer |
| question type | qtype, type, question_type, category |
| page | page, page_hint, page_number |
| grade | grade, relevance, rel |
| occurrence | occurrence, occurrence_index, nth |
| meta | meta, metadata |
A minimal file needs only a question column and, if you want evidence attached, both a
quote column and a source column. Write this to minimal2.csv and read it back:
question,source_id,quoteWhat is the notice period for resignation?,handbook.pdf,Employees must give 30 days written notice.How many sick days per year?,handbook.pdf,Employees receive 10 paid sick days per year.from pathlib import Path
Path("minimal2.csv").write_text( "question,source_id,quote\n" "What is the notice period for resignation?,handbook.pdf,Employees must give 30 days written notice.\n" "How many sick days per year?,handbook.pdf,Employees receive 10 paid sick days per year.\n")
es = cg.read_csv("minimal2.csv")for it in es: print(it.id, it.question)q1 What is the notice period for resignation?q2 How many sick days per year?With no id column, ids are assigned q1, q2, … in row order — which means ids are
not stable if you reorder the rows later.
A missing question column is the one thing read_csv refuses outright:
Path("bad.csv").write_text("foo,bar\n1,2\n")
try: cg.read_csv("bad.csv") # columns: foo, barexcept cg.EvalSetError as e: print("EvalSetError:", e)EvalSetError: bad.csv has no question column. Expected one of: question, query, q. Found: foo, barA row that’s just missing an optional column (answer, qtype, …) is not an error — that
field is left None or the row gets no anchor, silently.
Importing benchmark formats
read_beir
read_beir(queries_path: str | Path, qrels_path: str | Path, *, evalset_id: str = "beir") -> EvalSetReads a BEIR queries.jsonl (one {"_id", "text"} per line) plus a TSV of judgements with
columns query-id, corpus-id/document id, score. Only judgements with score > 0 are
kept; a query with no positive judgement is dropped entirely, not kept as unanswerable.
{"_id": "1", "text": "What is the notice period for resignation?"}query-id corpus-id score1 handbook.pdf 1Write those two files, then read them together:
from pathlib import Path
Path("beir_queries.jsonl").write_text( '{"_id": "1", "text": "What is the notice period for resignation?"}\n')Path("beir_qrels.tsv").write_text("query-id\tcorpus-id\tscore\n1\thandbook.pdf\t1\n")
es = cg.read_beir("beir_queries.jsonl", "beir_qrels.tsv")print(len(es), es.id, es.source, es.meta)for it in es: print(it.id, it.question, it.meta)1 beir beir {'granularity': 'document', 'note': 'BEIR gold is document-level. It compares retrievers fairly and cannot compare chunkers fairly, because every chunk of a gold document counts as relevant regardless of whether it holds the evidence.'}1 What is the notice period for resignation? {'gold_documents': [('handbook.pdf', 1)]}read_legalbench_rag
read_legalbench_rag(path: str | Path, *, evalset_id: str = "legalbench-rag") -> EvalSetReads {"tests": [...]} or a bare [...] array. Each test needs a query and a snippets
list of {"file_path", "span": [start, end]}. This is the one importer that produces
resolved gold spans directly, because LegalBench-RAG ships character offsets, not quotes.
import jsonpayload = { "tests": [{ "query": "What is the notice period for resignation?", "answer": "30 days", "snippets": [ {"file_path": "handbook.txt", "span": [120, 167]}, {"file_path": "handbook.txt"}, # missing span - dropped, not an error ], }]}json.dump(payload, open("lbrag.json", "w"))
es = cg.read_legalbench_rag("lbrag.json")print(len(es), es.id, es.source, es.meta)for it in es: print(it.id, it.question, it.answer, it.gold)1 legalbench-rag legalbench-rag {'granularity': 'span', 'source_file': 'lbrag.json', 'tests_in_file': 1, 'tests_skipped': 0, 'snippets_skipped': {'had no `span`': 1}}lb0 What is the notice period for resignation? 30 days (GoldSpan(span=Span('handbook.txt', 120, 167), grade=2),)Two kinds of bad snippet are treated differently:
- Missing
file_pathorspan, or aspanshorter than two elements — dropped quietly, counted inevalset.meta["snippets_skipped"](by reason) andevalset.meta["tests_skipped"](tests with noqueryat all). - Wrong type —
spanas a string,testsas an object, a snippet that isn’t an object — raisesEvalSetErrornaming the file and what it expected. A silently-wrong import (treating"117,202"as characters 1 to 1) is worse than a hard failure here.
Call describe_skipped(evalset) after loading to turn the skip counts into one readable line:
from contextgrid.evalset.io import describe_skippedprint(describe_skipped(es))1 snippet skipped: 1 had no `span`.describe_skipped reads straight from evalset.meta, so it can’t say something different
from what actually happened during the import.
Picking the reader by file extension
from contextgrid.evalset.io import read_evalset
es = read_evalset("quiz.jsonl") # -> read_jsonles = read_evalset("quiz.csv") # -> read_csvread_evalset(path: str | Path) -> EvalSetread_evalset dispatches purely on the file extension (.csv goes to read_csv,
everything else goes to read_jsonl) — it does not look at the file’s actual content. A
.csv file that’s really JSON, or the reverse, gets read with the wrong parser and fails
with that parser’s error rather than being auto-detected.
Use cg.read_jsonl, cg.read_csv, cg.read_beir, and cg.read_legalbench_rag directly
when you know your format; reach for read_evalset only when the extension is a reliable
signal in your pipeline.