A toolkit for seeing how language models reason about legal questions, not just whether they land on the right answer.
Ask ten models the same bar-exam-style question twenty times each and you get two
hundred answers. Most benchmarks grade each one against a key. trbench instead embeds
every answer, clusters the embeddings, and shows the distinct lines of reasoning the
models actually took: which of them are wrong, how stable each model is across samples,
and whether a scoring rubric derived from a real court opinion agrees with them.
The same 314 answers (318 collected; four duplicate ids were merged) to one Statute of Frauds question. Left: generic sentence embeddings group by topic. Right: instruction-tuned embeddings ("represent the legal conclusion and reasoning") separate them by conclusion. Colours are an automated keyword verdict, not human labels.
| Part | What it does |
|---|---|
trbench/ (Python package, pip install -e .) |
Collect answers from OpenAI and Replicate-hosted models, embed them with hkunlp/instructor-large, reduce the embeddings with UMAP (a dimensionality-reduction method), cluster them with HDBSCAN (a density-based clustering algorithm), and write a run file with representatives, central and peripheral members, and, for answers written in IRAC form (issue, rule, application, conclusion), GPT-4o doctrine labels. One command, trbench, with a subcommand per task, including an adversarial test that injects nonsense answers and checks they are isolated. |
runs/ |
29 saved clustering runs and the model answers behind them, for the free-form and IRAC pipelines. |
rubric-automation/ |
Recursive Rubric Decomposition (RRD): turns a question plus a gold answer into a weighted, atomic scoring rubric, audits its coverage, and scores sample answers. Standard library only, runs offline against a mock model, tested. |
frontend/ (Next.js 16, React 19) |
A portal to browse every saved run, run judges over clusters, and drive the four-stage source → question → rubric → judged-centroids → expert-review workflow. Reads and writes the JSON under legal-workflow-data/. |
instructions/ |
The prompt canon for the four workflow roles (Frank, Karthic, Dasha, Zak: the persona names given to the source-intake, rubric-building, judging, and expert-review roles). Loaded by the portal at runtime. |
git clone https://github.com/at350/tr-benchmarking && cd tr-benchmarking
python3 -m venv .venv && source .venv/bin/activate
pip install torch --index-url https://download.pytorch.org/whl/cpu # optional: CPU-only PyTorch, much smaller
pip install -e ".[all]" # extras: viz (figures), pdf (demo PDF), dev (pytest); plain `pip install -e .` has none
trbench --helpPython 3.10 or newer. The portal needs Node 22.6 or newer: cd frontend && npm ci.
Commands that call model APIs read OPENAI_API_KEY and REPLICATE_API_TOKEN from the
environment or a .env file (cp .env.example .env).
# Summarise a saved run: which models fall in which cluster, and whether a cluster disagrees with itself
trbench inspect runs/irac/results/run_20260303_163604.json summary
trbench inspect runs/free-form/results/run_20260217_153621.json verdicts
# Re-cluster the saved free-form answers into a scratch folder (downloads the 1.3 GB embedding
# model on first use, a few minutes on a CPU, no API calls); drop --results-dir to add the run to runs/
trbench cluster --results-dir /tmp/trbench-runs
# Build a rubric from a gold answer, offline
cd rubric-automation && python rrd_legal.py --demo --weighting doctrinal --verbose && cd ..
# Browse every run and the workflow demo data in the portal
cd frontend && npm run dev # http://localhost:3000/lsh-runsecho "Your legal question here." > my_question.txt
# See the request plan first: models, count, cost in requests. No API calls.
trbench irac-benchmark --question my_question.txt --per-model 10 --dry-run
# Then run it: 10 answers per model, parsed into IRAC, clustered, doctrines labelled.
trbench irac-benchmark --question my_question.txt --per-model 10
trbench inspect runs/irac/results/run_<timestamp>.json summary
# Add deliberately wrong answers to that dataset and check they are isolated.
trbench poison --input runs/irac/responses/responses_<timestamp>.jsonRestrict the models with --openai-models / --replicate-models (comma-separated), top up a
finished run whose models failed or were skipped with --resume runs/irac/responses/responses_<timestamp>.json
(a run stopped part-way writes nothing), and point --output-dir elsewhere to keep experiments apart.
A full run is about 200 model calls and takes tens of minutes, most of it waiting on the
providers; without any key the benchmark commands print the plan and exit without writing.
The portal lists every run_<timestamp>.json under runs/free-form/results/ and
runs/irac/results/ and refreshes when one appears; runs written to another --output-dir
are not shown. The three built-in poisons were written for the saved farmland question
(runs/irac/questions/question_farmland.txt), so on
your own question they are off-topic rather than doctrinally wrong answers (see
docs/clustering.md).
| Command | What it does | Calls paid APIs |
|---|---|---|
trbench cluster |
Embed and cluster a saved free-form responses file (--method lsh for the locality-sensitive-hashing + Louvain baseline) |
no |
trbench inspect <run> {summary,small,verdicts,excerpts} |
Summarise a run file | no |
trbench generate --provider {openai,replicate} |
Collect free-form answers to a question, appending to a responses file | yes |
trbench robust-benchmark |
Free-form answers from every model for one question, then cluster | yes |
trbench irac-benchmark --question FILE |
IRAC-structured answers from every model, cluster, label doctrines | yes |
trbench poison |
Inject wrong answers into a saved IRAC dataset and re-cluster | labels only |
trbench visualize |
The two embedding maps and the cluster-size chart (needs the viz extra) |
no |
trbench grid-search |
Sweep UMAP/HDBSCAN settings against the verdict heuristic | no |
trbench bridge --input FILE |
JSON-in / JSON-out clustering used by the portal | no |
trbench replicate-check |
One small request to confirm a Replicate token works | yes |
Every command has --help; the benchmark commands have --dry-run. LSH_MOCK_EMBEDDINGS=1
swaps the embedding model for random vectors so the pipeline can be exercised without a download.
- Generate. The same question goes to each model 20 times at temperature 0.7 (1.0 for
gpt-5-nano). In the IRAC variant the system prompt demands a strict{issue, rule, application, conclusion}JSON object; malformed answers are repaired where possible and otherwise dropped and counted. - Embed. Answers are encoded with
hkunlp/instructor-largeunder an instruction that names what should matter ("the legal reasoning components ... of this text"). This is the step that makes clusters track conclusions instead of vocabulary (figure above). - Cluster. UMAP (cosine, 10 dimensions, fixed seed) then HDBSCAN (
min_cluster_size=5,min_samples=2). Points HDBSCAN cannot place are reported as noise rather than forced into a cluster. - Explain. For each cluster: the medoid, the members nearest and farthest from the centre, the per-model breakdown, and in the IRAC pipeline the doctrines the cluster relies on with softmax-normalised similarity scores.
- Stress-test.
trbench poisonadds five copies each of three deliberately wrong answers (alien law, the wrong doctrine, criminal law applied to a contract) and reports which cluster each landed in.
Details, file formats, and reproducibility notes: docs/clustering.md.
Latest saved IRAC run per question. The first line of trbench inspect <run> summary prints these numbers. "Noise" is the count of answers HDBSCAN left unclustered. All six questions are items from the law subset of SuperGPQA; runs/README.md lists the item ids and the licence.
| Question (abridged) | Run | Answers | Models | Clusters | Noise | Largest cluster |
|---|---|---|---|---|---|---|
| Father's oral promise to pay son's loans if he marries (Statute of Frauds, marriage) | irac/results/run_20260303_163604 |
176 | 9 | 13 | 0 | 41 |
| Farmland deed, bounced $10,000 check, parol evidence objection | irac/results/run_20260223_233818 |
180 | 9 | 13 | 0 | 25 |
| Same question with 15 poisoned answers injected | irac/results/run_20260303_155256_poisoned |
195 | 12 | 15 | 0 | 25 |
| Merchant's signed firm offer, later revocation (UCC 2-205) | irac/results/run_20260224_005948 |
180 | 9 | 10 | 0 | 40 |
| Missing dog, posted reward, finder unaware of it | irac/results/run_20260224_153911 |
180 | 9 | 15 | 8 | 20 |
| "If you will mow my lawn..." neighbour promise (consideration) | irac/results/run_20260224_154905 |
179 | 9 | 13 | 1 | 21 |
| Couple shopping, injury in a department store (IIED) | irac/results/run_20260224_003329 |
179 | 9 | 12 | 0 | 40 |
Free-form baseline on the marriage question: free-form/results/run_20260217_153621, 314 answers (318 collected, four duplicate llama-3-70b ids merged at ingest) from 9 models, 13 clusters, none unclustered, largest cluster 83. The two figures at the top of this page come from that run.
The portal's /legal-workflow page runs a four-stage workflow whose state is plain JSON in legal-workflow-data/:
| Stage | Role | Produces |
|---|---|---|
| Intake | Frank | A locked benchmark packet from a real source (the text of three Statute of Frauds opinions is in cases/): routing to a doctrine pack, extraction sheet, gold answer, and a reverse-engineered question every model will be asked. |
| Rubric | Karthic | A modular weighted rubric (Modules 0–4) with scoring anchors and failure labels; optionally an original-vs-variation pair. |
| Judge | Dasha | Model answers are clustered (through trbench bridge), and a judge panel scores each cluster's centroid against the rubric, so hundreds of answers cost a handful of judge calls. |
| Review | Zak | Only when the judges cannot reach a strict majority: a scoped packet for a human expert and a structured decision record. |
legal-workflow-data/ ships with 22 packets, 12 rubric packs, 10 judged runs, and 3 review records so the pages have content on a fresh clone. See frontend/README.md.
- Models queried by the IRAC pipeline:
gpt-4o,gpt-4-turbo,gpt-5-nano,gpt-5.2(OpenAI API);google/gemini-3-flash,google/gemini-3-pro,meta/llama-4-maverick-instruct,deepseek-ai/deepseek-v3.1,anthropic/claude-4.5-sonnet,anthropic/claude-3.5-haiku(Replicate);xai/grok-4ifENABLE_GROK4=true. The free-form runs also sampledgpt-3.5-turbo,gpt-5-mini,claude-3.5-sonnet, andllama-3-70b. The portal's judge and drafting features offer current OpenAI, Anthropic, and Gemini models directly. - Data (
datasets/,cases/,outlines/): the law subset of SuperGPQA (656 multiple-choice questions, Open Data Commons Attribution licence), browsable at/database-viewand the source of the six benchmark questions; the public-domain text of three appellate opinions that the workflow demo is built on; and anoutlines/folder for your own law-school outline PDFs (none are redistributed). Attribution and terms: datasets/README.md, cases/README.md. - Saved runs: 14 free-form and 15 IRAC clustering runs under
runs/, including the poisoned-data runs.
pip install -e ".[dev]" # pytest (already included in .[all])
pytest # parsing, provider client, run-file builder, clustering bridge (mock embeddings), rubric pipeline
cd frontend && npm run lint && npx tsc --noEmit && npm run test:dasha-comparison && npm run buildCI runs both on every push. See CONTRIBUTING.md.
tr-benchmarking/
├── trbench/ Python package: text.py, pipeline.py, density_clustering.py, providers.py, results.py, irac/, cli/
├── runs/ free-form/ and irac/: responses/ (model answers) and results/ (run files)
├── rubric-automation/ RRD package, examples/, tests/
├── frontend/ Next.js portal (src/app pages and API routes, src/lib server logic)
├── instructions/ prompt canon for Frank / Karthic / Dasha / Zak, plus the live-demo script
├── legal-workflow-data/ JSON written by the portal: packets, rubric packs, runs, reviews, uploaded artifacts
├── datasets/ cases/ outlines/ prompt-libraries/ SuperGPQA subset, opinion texts, your own outline PDFs, historical prompts
├── docs/ figures/ and clustering.md
├── experiments/ earlier benchmark scripts kept for provenance
├── tests/ pytest suite for the package
├── scripts/generate_live_demo_pdf.py renders the demo script to PDF (needs the pdf extra)
├── pyproject.toml LICENSE CITATION.cff CONTRIBUTING.md .env.example
└── .github/workflows/ci.yml
This is a research prototype from a university project on technology for the law. Things a reader should know before relying on it:
- Full benchmark runs cost money and time (roughly 200 model calls plus one GPT-4o call per cluster); the saved runs exist so the analysis can be explored without that. Use
--dry-runto see the plan first. - Cluster doctrine labels come from a model, so they can differ between runs on identical input. The clusters themselves use a fixed UMAP seed and are stable for a given set of package versions; exact reproduction of a saved run needs the same umap-learn, numba, and scikit-learn versions. Run files written by this version record those versions under
metadata.versions; the saved runs predate this, and the environment that produced them was not recorded. - Clusters often line up with model family as much as with reasoning (in
run_20260303_163604, one cluster is 20 of 20gpt-5.2answers), so part of what is being clustered is a model's house style. Reading the representatives, not just the counts, is part of the method. - Model identifiers are pinned in the command defaults; as providers retire models, pass your own lists.
- The portal stores state as files on disk and is meant to run locally for one user at a time. Its API routes have no authentication, and several of them spend provider credits or start long model runs, so keep it on localhost and do not point
ALLOWED_DEV_ORIGINSat an untrusted network. Record ids and stored file paths from clients are validated and confined tolegal-workflow-data/; uploads are limited to PDF, text, and Markdown files of at most 25 MB.
Built by a three-person team for a university course on technology for the law (COMP_SCI 397/497) between February and April 2026, with AI coding assistance. From the commit history: the portal, the instruction canon, and the workflow data were developed mainly by DavidL0417; the IRAC benchmark and poison test by Alan Tai; the rubric-automation package by Clark Hanlon; the shared clustering engine was joint work. The September 2026 commits are a cleanup, hardening, and packaging pass over the whole repository.
Code is released under the MIT License. The SuperGPQA subset under datasets/ is
redistributed under the Open Data Commons Attribution License v1.0, with attribution to its authors (datasets/README.md);
the opinions under cases/ are public records (cases/README.md). If you use this work, please cite it with the metadata in
CITATION.cff (GitHub shows a "Cite this repository" button).
- Su et al., One Embedder, Any Task: Instruction-Finetuned Text Embeddings (2022) — instructor embeddings
- McInnes et al., UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction (2018)
- Campello et al., Density-Based Clustering Based on Hierarchical Density Estimates (2013) — HDBSCAN
- Blondel et al., Fast unfolding of communities in large networks (2008) — Louvain baseline
- SuperGPQA

