SDRF Wizard AI Assistant

August 14, 2026 · View on GitHub

Backend for the AI assistant in the SDRF Editor's Create New SDRF wizard. The browser app stays a static SPA; this service is the only component that holds API keys and the only one that can reach services a browser cannot (MinerU, PDF downloads, embedding models).

It orchestrates the whole assistant turn: it calls the LLM, dispatches tools, retrieves specification passages, and returns both a natural-language answer and a list of wizard actions the frontend can apply after the user approves them.

What it does

CapabilityHow
Chat orchestration with tool callingapp/llm/agent.py, OpenAI-compatible streaming client in app/llm/client.py
SDRF specification Q&A (RAG)app/rag/ — chunked specification + embeddings, hybrid retrieval
PXD dataset metadata + raw file listapp/tools/pride.py (PRIDE Archive v3)
Paper retrievalapp/tools/literature.py (Europe PMC search + JATS full text)
Paywalled papersapp/parsing/ — MinerU, plus POST /api/uploads/pdf for user-supplied PDFs
Verified ontology termsapp/tools/ontology.py (EBI OLS4)
Template layers and columnsapp/tools/templates.py (bigbio/sdrf-templates)

Setup

cd backend
python3 -m venv .venv && source .venv/bin/activate    # or: uv venv .venv
pip install -r requirements.txt                        # or: uv pip install -r requirements.txt

cp .env.example .env      # then fill in LLM_API_KEY etc.

Build the specification knowledge base (a copy of the specification ships in data/spec/, so this works offline):

python -m app.rag.build_index          # use the bundled copy
python -m app.rag.build_index --fetch  # re-download sdrf.quantms.org/specification.html

Run it:

uvicorn app.main:app --reload --port 8000

Check what is configured:

curl http://localhost:8000/api/health
{
  "status": "ok",
  "llmConfigured": true,
  "embeddingsConfigured": true,
  "mineruConfigured": true,
  "specIndexReady": true,
  "specChunkCount": 98,
  "retrieval": "hybrid"
}

The frontend reads this endpoint and only shows the assistant panel when the backend is reachable and llmConfigured is true. Point the frontend at the backend with assistantBaseUrl in src/environments/environment.ts (default http://localhost:8000).

The same local process also works with the official editor at https://sdrf.quantms.org/sdrf-editor.html: the browser calls localhost:8000 from that HTTPS page. CORS_ORIGINS must include https://sdrf.quantms.org (already the default in .env.example). Add any other hosted origin you use.

Configuration

All settings come from backend/.env (see .env.example). Secrets never leave the server; the browser never sees a key.

LLM

Any OpenAI-compatible /chat/completions endpoint with tool calling:

LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=sk-...
LLM_MODEL=gpt-4o-mini

Works with OpenAI, DeepSeek, Qwen (compatible mode), OpenRouter, vLLM, and Ollama (LLM_BASE_URL=http://localhost:11434/v1). Tool calling is required — a model without it cannot gather evidence.

Embeddings

EMBEDDING_BASE_URL=https://api.openai.com/v1
EMBEDDING_API_KEY=sk-...
EMBEDDING_MODEL=text-embedding-3-small

With embeddings configured, retrieval is hybrid (cosine similarity blended with a BM25-style lexical score). Without them the index still builds and retrieval degrades to lexical only, which answers most rule lookups but is weaker on paraphrased questions. Re-run build_index after adding an embedding key.

The vector store is a numpy matrix plus a JSON sidecar in data/spec_index/ (~100 chunks, so an exhaustive scan is instant). app/rag/store.py is the only file a swap to FAISS or Chroma would touch.

MinerU (PDF parsing)

Used for papers that are not open access. Two contracts:

# Hosted API
MINERU_MODE=api
MINERU_FLAVOR=official
MINERU_BASE_URL=https://mineru.net/api/v4
MINERU_API_KEY=...
# Self-hosted MinerU exposing POST /file_parse
MINERU_MODE=api
MINERU_FLAVOR=simple
MINERU_BASE_URL=http://localhost:8888
# Local `mineru` CLI on this machine (needs the models, possibly a GPU)
MINERU_MODE=local

Parsing is behind app/parsing/base.py:PdfParser, so adding a backend means one new class and one line in app/parsing/factory.py.

If MinerU is unavailable the assistant degrades gracefully: it asks the user to paste the relevant methods text instead (POST /api/uploads/text).

API

EndpointPurpose
POST /api/chatAssistant turn as SSE. Events: status, token, actions, citations, error, done.
POST /api/chat/syncSame turn, buffered into one JSON response. Useful for smoke tests.
POST /api/uploads/pdfMultipart sessionId + file. Parses with MinerU and registers the document.
POST /api/uploads/textMultipart sessionId + text. Registers pasted manuscript text.
GET /api/healthCapability report.

Wizard actions

The assistant never mutates the wizard directly. It calls an internal propose_wizard_actions tool, and the backend validates each proposal against a whitelist in app/schemas.py:ALLOWED_OPS before returning it:

{
  "step": "protocol",
  "op": "setCleavageAgent",
  "args": [{ "name": "Trypsin", "msAccession": "MS:1001251" }],
  "label": "Cleavage agent: Trypsin",
  "reasoning": "Methods section states in-solution trypsin digestion.",
  "confidence": "high"
}

op names mirror the public setters of WizardStateService (src/app/core/services/wizard-state.service.ts), so the frontend bridge dispatches them without a translation table. Anything outside the whitelist is dropped server-side, and the user still has to approve each action in the panel.

Annotation methodology

The system prompt in app/llm/prompts.py follows the methodology of bigbio/sdrf-skills: gather evidence from PRIDE and the paper before proposing anything, resolve every controlled value through OLS rather than recalling it, and check the specification for the rule behind a column before suggesting a value for it.

Smoke tests

python -m pytest tests -q          # unit tests, no network or API keys needed
python scripts/smoke_tools.py      # exercises PRIDE / OLS / Europe PMC / templates (network)