ReportFast builds static HTML reports of pathology slides. A report is one file of cards; every card links into the xOpat v3 viewer with the whole session carried in the URL fragment. Nothing is served, no JavaScript runs in the page, and the file can be mailed as it is.
The point is to remove the failure nobody notices. A link pointing at data the tile server cannot open still loads the viewer, still renders a card, and shows it black with no error anywhere. The library exists to make that pair of mistakes — a wrong reference, a wrong index — impossible to make quietly.
One command installs the library; the skill comes with it:
uv add "report-fast[mlflow] @ git+https://github.com/RationAI/reporting.git"The skill — the procedure an agent follows — ships inside the package, and
reportfast skill show prints it. Nothing installs anything. A project that wants
Claude Code to load the skill as its own commits a skills/ directory, or installs
it through Claude Code's own mechanism; either way the library's job ends at
carrying the file.
Then describe the report: which cases, which slides, which overlays, what colours.
The agent writes a Python script, the library builds the sessions and renders the
page. There is no build command — a build would be a second, worse way to write the
same loop over files. The script is the report's record: it names the folders, the
colours, the order and the layout, and unlike a spec file it can be re-run.
Everything below is for the cases where the agent is not doing it.
Nothing here requires an agent, or one vendor over another. The package is plain
Python with one dependency, and the thing it asks for is a script. The skill is a
markdown file with a name and a description at the top — readable by a human, by
another agent, or by any tool that takes instructions from a file — and no code path
in the library calls an agent or a model. What an agent buys is that the procedure in
the skill gets applied consistently to a folder of slides; read the same file and
write the script yourself, and the result is the same report.
- The session object.
XopatSessionholds the document the viewer boots from and turns it into a URL. Build it from a slide and overlays, or read back a session someone else authored — a dict, JSON text, a file, a pasted viewer link. Fields the library has never heard of survive untouched, so its vocabulary is never the viewer's ceiling. - Sources. Slides and overlays from a mounted folder or from an MLflow run's
artifacts, matched to each other by file stem.
case_matrixreports what paired and what dropped, which is the difference between a finding and a wrong folder. - Two page components.
SlideCardandSlideGrid, plusSectionfor grouping andReport's title, subtitle and preamble as the page's whole prose budget. Custom blocks areBaseComponentsubclasses. - MLflow integration. Read a run's artifacts as DataIDs without downloading them,
and publish a report back to a run — which happens only when
publish()is called. - Reproducible output. The same report renders byte-identical HTML: no timestamps, ids derived from position rather than chance.
What it deliberately does not have: a schema, a manifest format, a CLI that composes a report, a probe that runs at build time, a copy of the viewer's feature list. Each was built, and each was deleted; DESIGN.md has the reasons.
Not on PyPI.
uv add "report-fast @ git+https://github.com/RationAI/reporting.git"
uv add "report-fast[mlflow] @ git+https://github.com/RationAI/reporting.git" # + MLflow
uv add --editable /path/to/reporting # a checkoutOne optional extra, mlflow (>=2.8,<4). The deployment runs two MLflow tracking
servers (a 2.16 one and an s3-backed 3.16 one); both client major versions read both,
and the only measured break is a 3.x client publishing to the 2.16 server — a loud
404, not a silent one. Which server a report is built against is a per-call choice
(Mlflow(tracking_uri=…) / $MLFLOW_TRACKING_URI); the artifact prefix travels with
the server. Everything else needs only python-fasthtml, and import report_fast
works without the extra at all. Python 3.10+.
The library needs nothing else. The agent needs to know the skill exists — apply the one subsection that matches your agent; the steps are one-time.
Claude loads skills from folders it scans, so install the library and copy the bundled skill where Claude looks — once per machine, then every project picks it up:
site_packages=$(dirname "$(python -c 'import report_fast; print(report_fast.__file__)')")
cp -r "$site_packages/skill" ~/.claude/skills/reportfast(From a working checkout of this repo rather than an install, import report_fast resolves to the checkout, which holds no bundled copy — take the
source directly instead: cp -r skills/reportfast ~/.claude/skills/reportfast.)
A new session then sees the skill and loads it automatically when a task matches its description. One caveat: that folder is a copy, so after a package update re-run the two lines — the wheel carries the corrections, and a stale copy is a stale procedure. To run Claude Code on the institution's models rather than Anthropic's subscription, Cerit documents the VS Code integration here.
Other agents read a startup-instructions file instead of scanning skill folders —
AGENTS.md, QWEN.md, or whatever yours loads at session start. Commit a pointer
to it in the project where reports get built:
echo 'Before writing any report script, run `uvx report-fast skill show` and follow it, including the references it names.' >> AGENTS.mdThe sentence is a pointer, not a copy: the agent runs the command and reads the
skill fresh from the package, so there is nothing to re-copy when the package
updates. uvx fetches the package on the spot, so this works even before the
library is installed in the project's environment. Commit it once and every
teammate's sessions pick it up.
No setup exists, because none is needed: reportfast skill show prints the
procedure, and a person who follows it and writes the script produces the same
report. Every code path in the library is ordinary Python with no model call.
from pathlib import Path
from report_fast import Report, SlideGrid, XopatSession
sessions = [
XopatSession.from_slide(path, name=path.stem)
for path in sorted(Path("/mnt/slides").glob("*.tif"))
]
Report(title="Cohort QC", subtitle=f"{len(sessions)} slides").add(
SlideGrid(sessions=sessions)
).write("report.html")Masks over slides, matched by stem — case_001.svs and case_001.tiff are one case:
from report_fast import Drive, Mask, MlflowRun, Report, SlideGrid, case_matrix
matrix = case_matrix(
Drive("/mnt/data/colon/dysplasia"),
[
Mask("Tissue", Drive("/mnt/data/tissue_masks"), color="#ffff00", opacity=0.5),
Mask("Grades", MlflowRun("41d5e1d7d43641ea8f645f9b7945e9f7", "annot_masks"),
classes=3, palette=["#ffffff", "#ff0000", "#00ff00"]),
],
only=["1094_18_HE_0", "8625_13_HE_A"], # the cohort, in report order
min_layers=2,
)
Report(title="Dysplasia QC", blocks=[SlideGrid(sessions=matrix.sessions)]).write("report.html")
print(matrix.coverage, matrix.dropped) # what paired; what got filteredNothing is written unless a path is named: write() returns the path, to_html()
returns the string and touches no disk. Other entry points are
XopatSession.from_url / from_file / from_config for sessions that already exist,
sessions_from_folder, Mlflow for runs, and XopatEndpoint to aim one call at
another deployment.
Mlflow.publish() is the only name in the library that uploads, and it is a separate
line in a script rather than a flag that could be set. Nothing implies it — not a
config key, not a CI job, not an obvious-looking request: the workflow file has no
publish step and a test asserts that it never gains one. An artifact logged to
somebody's run is in the record, and there is no clean way to take it back out.
Credentials go through the environment, never into a script or a report.
On reproducibility: the record of a report is its script, not a copy of its
inputs, so publish() stores no config by default — the script names the folders,
colours, order and layout and can be re-run, which a frozen config cannot. Where a
run should carry the inputs anyway, publish(extra_dir=…) logs a directory beside
the page under report/conf; SKILL.md, What a report is recorded by, writes a
provenance manifest into one.
XOPAT_BASE_URL (viewer root), XOPAT_WSI_BASE_URL (tile server — its own mount, not
under the viewer's), XOPAT_IMAGE_PROTOCOL, XOPAT_MOUNT_ROOT (the prefix stripped to
form a DataID), MLFLOW_TRACKING_URI, MLFLOW_WEB_URL, and
REPORTFAST_MLFLOW_ARTIFACT_PREFIX (the DataID namespace a deployment's tile server
registers MLflow artifacts under — wrong value, no error, no overlays; see the skill).
Each can also be passed per call. references/deployment.md lists every one with its
default and where it is read.
Two facts that cost time when unknown:
- The path selects the viewer version — this host serves v2 at
/xopat/and v3 at/v3/, and a v3 session opened by v2 looks like it loaded. - Slide paths are shared state. A session names slides by path, never by bytes, so the machine building a report and the tile server must see the same file under the same root. Nothing checks this; it surfaces when someone opens the page.
reportfast reads: it prints the procedure an agent follows, and writes nothing.
skill show |
print SKILL.md; --reference references/deployment.md prints a bundled file, including the examples/*.json session fixtures |
Exit codes: 0 fine, 1 the file asked for is not in the bundle, 4 used wrongly.
There used to be skill install and skill where; see Reversals in DESIGN.md.
| Looking for | Read |
|---|---|
| The procedure an agent follows | reportfast skill show, or skills/reportfast/SKILL.md |
Session shape, params, the black-card checklist |
reportfast skill show --reference references/xopat-v3.md |
| Deployment coordinates, DataID construction, environment | reportfast skill show --reference references/deployment.md |
| MLflow: artifacts, addresses, publishing | reportfast skill show --reference references/mlflow.md |
| A real session to imitate | reportfast skill show --reference examples/dysplasia_case.json |
| Why it is built this way, and what was ruled out | DESIGN.md |
report_fast/ splits into the wire format (xopat.py, session.py, layer.py),
rendering (core.py, components/), and the edges (masks.py,
mlflow.py, skill.py, __main__.py). Nothing here copies the viewer's vocabulary:
layer.py is the one file naming a shader type, and everything else about the viewer
comes from its own source — see test_there_is_no_copy_of_the_viewer_vocabulary.
uv run pytest -q # no server, no mount, no slide opened, no network
uv run ruff check .Each test file also runs standalone (uv run python tests/test_cli.py). CI runs both
on Python 3.10 and 3.12 and has no publish step. Three properties are asserted rather
than assumed: output is byte-identical for the same inputs, no workflow publishes, and
every API name the skill's prose offers actually exists.