You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The backend (backend/app.py) is a thin FastAPI layer over the UI-agnostic spatialscribe.analysis capability registry. A session holds one spatial sample (a SpatialSample / AnnData) plus an append-only action log entirely server-side; the browser (or your agent) never touches the AnnData - it asks for viewport points, capability results, verdicts, and copilot answers. Every endpoint runs a capability via cap.run (or the copilot's Claude tool-use loop) and returns JSON. Data stays on the server: you drive the analysis by passing a session id around and pulling back small payloads.
Base URL and prefix
Base URL: http://localhost:8000 (single origin - the built SPA is served at /, and /api/* routes take precedence).
All API routes are under the /api prefix.
Run it with: uvicorn backend.app:app --host 0.0.0.0 --port 8000.
No auth. The copilot endpoints additionally need an LLM configured in the server's environment (an Anthropic API key or an OpenAI-compatible base URL); check llm.available in any state response.
Sessions are in-memory. A server restart or timeout drops them, and later calls return 404 "unknown session" - just reload the section.
The session id
Every session-creating endpoint (POST /api/load_demo, POST /api/load_synthetic, POST /api/load_section) returns a JSON object whose session id lives under the key session_id. Everything else is addressed as /api/{sid}/..., where sid is that value. region_filter and rename_celltype also echo session_id back (they replace the session's AnnData in place). Session-create responses spread the full state object (see below) alongside session_id.
End-to-end curl walkthrough
BASE=http://localhost:8000
# 1. Create a session from the bundled demo. The response carries session_id + initial state.
SID=$(curl -s -X POST "$BASE/api/load_demo?name=breast"| jq -r .session_id)echo"session: $SID"# 2. Read the per-step summary (only what has been computed so far).
curl -s "$BASE/api/$SID/summary"| jq
# 3. Run a rail-step capability (panel | qc | cluster | annotate | spatial).
curl -s -X POST "$BASE/api/$SID/run/qc" \
-H 'Content-Type: application/json' -d '{}'| jq '{ok, error}'# ...or run ANY registered capability by name, getting its value + artifacts back.
curl -s -X POST "$BASE/api/$SID/run_cap/qc_funnel" \
-H 'Content-Type: application/json' -d '{"params": {}}'| jq '.value'# 4. Ask the copilot (needs an LLM configured on the server).
curl -s -X POST "$BASE/api/$SID/copilot" \
-H 'Content-Type: application/json' \
-d '{"prompt": "colour the map by cell type and tell me what you see"}' \
| jq '{reply, map_view, mutated}'# 5. Export. Each is a file download; -OJ keeps the server-supplied filename.
curl -s -OJ "$BASE/api/$SID/export/h5ad"# annotated.h5ad
curl -s -OJ "$BASE/api/$SID/export/script"# analysis.py (re-runnable)
curl -s -OJ "$BASE/api/$SID/export/report"# spatialscribe_report.html
The state object
Returned by GET /api/{sid}/state, spread into every session-create response, and nested as state in run/copilot/pipeline responses. Key fields:
Register a reference by uploading a .h5ad (multipart)
form field file; query label_key? -> same as /reference
POST
/{sid}/reference/auto
Free-text tissue -> auto-choose + load the best pre-computed reference (or CELLxGENE fetch)
body {tissue?, allow_fetch?} -> {ok, auto, match, recommended_mode, route, ...state}; ok:false + recommended_mode:"cluster" when none fits
Analysis / Capabilities
Method
Path
Purpose
Key params / response
POST
/{sid}/run/{step}
Run a rail step: panel, qc, cluster, annotate, spatial
body {params?} -> {ok, error, state}
POST
/{sid}/run_cap/{name}
Run ANY registered capability by name (e.g. qc_funnel, immune_exclusion, neighborhood_enrichment, state_by_celltype, malignant_score, discover_programs, subcluster, rejection_reasons)
body {params?} -> {ok, error, value, artifacts, state}
POST
/{sid}/ran/{step}
Mark a rail step ran (greens the rail after a panel's auto-compute); idempotent
-> state
GET
/{sid}/progress
Coarse progress of the step running now (pollable mid-run)
-> {running, step, frac, label}
POST
/{sid}/run_pipeline
Start the FULL analysis spine as a background job (202)
body {tumour?, rctd?, split?, resolution?} -> {started: true}
token -> {type, text} (the answer, accumulated word by word)
done -> {type, state, loaded, mutated} (final state; loaded = a new section was swapped in, mutated = labels changed in place)
Export
Method
Path
Purpose
Response
GET
/{sid}/export/script
Re-runnable Python script of the session's action log
text/plain attachment analysis.py
GET
/{sid}/export/h5ad
Annotated AnnData
.h5ad file annotated.h5ad
GET
/{sid}/export/report
Self-contained HTML report
text/html attachment spatialscribe_report.html
Misc
Method
Path
Purpose
Response
GET
/health
Liveness + live session count
{ok, sessions}
POST
/llm/provider
Switch the copilot LLM backend at runtime (process-wide)
body {provider} -> {ok, llm}
Drive it from an agent
A bundled skill teaches an agent to operate this API end to end (create a session, run the pipeline, inspect verdicts, ask the copilot, export): see .claude/skills/spatialscribe/ in this repo (SKILL.md plus scripts/ and references/). The pattern is always the same: POST a load endpoint, grab session_id, then address every later call as /api/{session_id}/... and poll progress / pipeline_status for long-running steps.