An evidence-grounded computational drug-discovery workbench.
TheraGraph takes a disease, retrieves the public evidence linking it to therapeutic targets, finds the compounds with measured activity against those targets, predicts their molecular properties, and ranks them for follow-up — while keeping a permanent, auditable line between what was retrieved from a public source and what a model predicted.
TheraGraph does not discover drugs and cannot validate one. It aggregates public evidence, computes molecular properties, predicts a small set of physicochemical and toxicity endpoints, and produces transparent prioritisation scores for human follow-up. Nothing it outputs is evidence that a compound treats a disease.
Disease → Target → Evidence → Compound → Properties → Ranking → Report
MONDO_… BACE1 PMID + CHEMBL… predicted weighted cited,
ENSG… OT score pChEMBL ± uncertainty objectives reproducible
└──────── retrieved, with provenance ─────────┘ └── predicted, with uncertainty ──┘
| Document | What is in it |
|---|---|
| Architecture | System diagram, request lifecycles, module boundaries, technology choices |
| Data & ingestion | The five public sources, normalisation, quality rules, provenance, how to run and re-record ingestion |
| Machine learning | Features, scaffold splits, metrics, calibration, uncertainty, error analysis, measured results |
| The research agent | Plan → execute → validate → synthesise, the 15 tools, reproducible runs, the evaluation suite |
| Example workflows | Three worked examples with every API call |
| Operations | Configuration, security, deployment, monitoring, runbook, CI |
| Design decisions & limitations | What was traded away, and what this system cannot tell you |
| API reference | OpenAPI 3.1; also served live at /docs |
Needs Python 3.11, uv and Node 20+. No API keys, no network access, and no PostgreSQL required to get the whole thing running.
make setup # uv venv + backend (editable) + frontend deps
# No PostgreSQL or Redis running? Write a .env and every command below picks it up.
# (Settings reads the repo-root .env, so this applies to make, the CLI and the API.)
printf 'DATABASE_URL=sqlite+aiosqlite:///./tg.sqlite3\nREDIS_URL=memory://\n' > .env
make migrate # create the schema
make seed-smoke # build the corpus from committed fixtures (no network)
make train # train and register the models
make api # API on :8000
make frontend-dev # UI on :5173 (in a second terminal)Or with Docker (PostgreSQL, Redis, MinIO, API, two workers, the UI):
cp .env.example .env && make docker-upThen open http://localhost:5173, or walk the demo from the terminal with docs/workflows.md.
Everything above is offline. Every upstream API response used to build the shipped
corpus is committed under data/seed/fixtures/, the four MoleculeNet benchmark CSVs are
committed under data/raw/moleculenet/, and with no ANTHROPIC_API_KEY the research
agent runs a deterministic planner over the same tools with the same validation and the
same reports. A demo that needs the internet is a demo that breaks during the demo.
Progressive-tier search (exact → prefix → contains → synonym) over diseases, targets and compounds, so an exact match never loses to a substring. Targets come back ordered by Open Targets' own association score with its per-datatype breakdown, tractability buckets and known safety liabilities.
Evidence from Open Targets' 25 datasources, mapped onto a fixed vocabulary
(genetic_association, known_drug, literature, animal_model, expression,
pathway, functional_genomics, somatic_mutation, biomarker), each record carrying
its source, upstream identifier, resolvable URL, the source's own confidence score, its
retrieval date and the content-hashed dataset version it belongs to. Literature evidence
is linked to Europe PMC publication metadata.
RDKit throughout: standardisation (largest fragment, neutralised), canonical SMILES, InChI, InChIKey as the cross-source identity key, Bemis–Murcko scaffolds. Morgan / MACCS / pattern fingerprints stored as packed bytes. Tanimoto similarity search with the popcount bound pushed into SQL as an index range scan, and SMARTS substructure search with fingerprint pre-screening. 16 curated descriptors — MW, LogP, TPSA, HBD, HBA, rotatable bonds, QED and more — each with a label, a unit and a one-line explanation.
A fingerprint+descriptor Random Forest / XGBoost baseline and a GINE-style graph neural network in pure PyTorch, trained on the identical split and compared with bootstrap confidence intervals. Scaffold splits are the headline; random splits are reported only as the labelled optimism reference, with the gap computed. Calibration (Platt vs isotonic, auto-selected, refused when it does not help), MC-dropout and ensemble uncertainty, applicability-domain flags, and error analysis by property bucket, uncertainty decile and scaffold familiarity.
Measured, offline, on the committed MoleculeNet snapshots:
| Endpoint | Dataset | Model | Primary metric | Scaffold split | 95% CI | Secondary | Random split | Optimism |
|---|---|---|---|---|---|---|---|---|
solubility |
ESOL | random_forest | RMSE (log mol/L) | 0.889 | 0.751–1.026 | R2 0.887 | 0.593 | +33.27% |
solubility |
ESOL | gin | RMSE (log mol/L) | 1.059 | 0.876–1.245 | R2 0.840 | 0.668 | +36.88% |
bbb_permeability |
BBBP | random_forest | ROC-AUC | 0.802 | 0.686–0.898 | PR-AUC 0.956 | 0.923 | +15.1% |
bbb_permeability |
BBBP | gin | ROC-AUC | 0.640 | 0.528–0.745 | PR-AUC 0.929 | 0.900 | +40.53% |
lipophilicity |
Lipophilicity | random_forest | RMSE (logD) | 0.893 | 0.832–0.959 | R2 0.464 | 0.743 | +16.81% |
lipophilicity |
Lipophilicity | gin | RMSE (logD) | 0.731 | 0.673–0.786 | R2 0.641 | 0.597 | +18.25% |
clinical_toxicity |
ClinTox | random_forest | ROC-AUC | 0.842 | 0.698–0.949 | PR-AUC 0.226 | 0.531 | none |
clinical_toxicity |
ClinTox | gin | ROC-AUC | 0.749 | 0.574–0.896 | PR-AUC 0.243 | 0.496 | none |
solubility— random_forest leads on rmse, but its confidence interval overlaps gin — treat the difference as unproven. Test fold 112 molecules, 66 scaffolds, 0 shared with train.bbb_permeability— random_forest leads on roc_auc, but its confidence interval overlaps gin — treat the difference as unproven. Test fold 197 molecules, 197 scaffolds, 0 shared with train.lipophilicity— gin leads on rmse with non-overlapping confidence intervals. Test fold 420 molecules, 420 scaffolds, 0 shared with train.clinical_toxicity— random_forest leads on roc_auc, but its confidence interval overlaps gin — treat the difference as unproven. Test fold 143 molecules, 143 scaffolds, 0 shared with train.
The graph model wins where there is data to win with — GIN beats the baseline on Lipophilicity (4,200 molecules) with non-overlapping intervals, ties on ESOL (1,117) and loses on BBBP — which is why both families are trained and compared rather than one being assumed better. "Unproven" appears three times out of four because on a 112-molecule test fold the bootstrap intervals genuinely overlap. And ClinTox's ROC-AUC of 0.842 is flattering: at a 7.5% positive-class base rate the PR-AUC is 0.226, and at the default threshold the model predicts "not toxic" for everything — it ranks usefully and decides nothing. docs/ml.md reads all of this out, including the finding that the GNN's MC-dropout uncertainty is over-confident at this data scale while the forest's tree variance is conservative.
Nine objectives across measured potency, retrieved evidence support, predicted solubility and toxicity, drug-likeness, molecular weight, lipophilicity, structural novelty and model confidence. Every candidate's score decomposes into each factor's raw value, desirability, weight and contribution — and whether it was discounted for low confidence or an out-of-domain prediction. Three presets ship, weights are user-adjustable, weights renormalise when data is missing, and hard filters exclude with a stated reason.
An explicit plan → execute → adapt → validate → synthesise → report workflow over 15 typed tools. The model decides what to do; tools do every retrieval and computation, and they are the same functions the REST API calls. Then the validator checks each claim's citations against the records the run actually retrieved and deletes the unsupported ones, recording the deletion.
Latest evaluation run — 6 cases, gated in CI:
| Metric | Threshold | Result |
|---|---|---|
| Pass rate | ≥ 0.80 | 1.00 |
| Tool-selection recall | ≥ 0.80 | 1.00 |
| Citation correctness | = 1.00 | 1.00 |
| Structured-output validity | = 1.00 | 1.00 |
| Honesty on unanswerable questions | = 1.00 | 1.00 |
| Hallucination rate | = 0.00 | 0.00 |
Two of the six cases exist only to check that a question the corpus cannot answer gets "that is not in the corpus" rather than an invention.
Every run gets an id like RUN-2026-00183 and stores the query, the dataset versions
read, the model versions used, the prompt template hashes, every tool call with its input
and output, the plan and any adaptations, the predictions, the ranking configuration, the
report, the timings, the token usage and the cost — plus a reproducibility hash over the
scientific inputs only. GET /runs/{id}/replay-plan reports what has drifted since
(corpus growth, retrained models, changed prompts) before you re-run, so a different
answer is explained rather than mysterious.
React 18 + TypeScript across seven views: Research Dashboard, Target Explorer, Compound Explorer, Candidate Comparison, AI Research Assistant, Research Run, Model Evaluation. The evidence graph is interactive and every edge names its source, evidence type, upstream identifier, score and retrieval date. Measured and predicted values are visually distinct everywhere, including in a colourblind-safe chart palette validated against adjacent-pair CVD separation. The assistant shows its plan, its tool calls, its retrieved evidence and a written reasoning summary — never raw chain-of-thought.
backend/
src/theragraph/
core/ config, logging, metrics, tracing, cache, storage, security, errors
db/ 28 models, repositories, Alembic migrations
chem/ RDKit: standardisation, descriptors, fingerprints, similarity
ingestion/ one connector per source + normalisation + quality rules
ml/ features, splits, baseline, GNN, evaluation, calibration, registry
ranking/ desirability curves, weights, filters, explanations
services/ the use cases: discovery, targets, compounds, graph, ranking, runs
agent/ planner, tools, bindings, validator, orchestrator, evals
api/ 13 routers, middleware, error mapping
jobs/ durable queue + worker
mcp_server/ the same tools over MCP
tests_support/ the in-process demo corpus (shipped: CI and the CLI use it)
tests/ unit · integration · api · ml · agent · e2e (467 tests)
alembic/ migrations, dialect-agnostic
frontend/
src/{pages,components,lib,types} (43 tests)
data/
seed/fixtures/ recorded upstream responses — the offline corpus
raw/moleculenet/ the four benchmark CSVs
docs/ architecture · data · ml · agent · workflows · operations · decisions
deploy/ nginx configuration for the SPA + API proxy
.github/workflows/ CI: 6 jobs, all offline
make help # every target, described
make check # lint + typecheck + tests, everything CI runs
make test # 467 backend + 43 frontend tests
make coverage # backend coverage report
make evals # the agent evaluation suite, with its CI thresholds
make demo # bootstrap → seed → train → evals, from scratch, offline
make openapi # regenerate docs/openapi.json
make docker-up # the full stack| Backend | Python 3.11, FastAPI, SQLAlchemy 2.0 async, PostgreSQL 16, Redis, Alembic, PyTorch, scikit-learn, RDKit, S3/MinIO |
| Frontend | React 18, TypeScript, Vite, Tailwind, TanStack Query, Recharts, smiles-drawer |
| Tests | 467 backend (unit, integration, API, database, CLI, ML pipeline, agent, end-to-end) at 79% coverage, 30 modules at 100%; 43 frontend |
| Quality gates | ruff lint + format, mypy (strict, zero errors across 116 modules), eslint (zero warnings), tsc --noEmit, alembic check on both dialects, and the agent eval thresholds |
| CI | 6 offline jobs: backend, backend-on-PostgreSQL, migrations round-trip, frontend, agent evals, container images |
| Deploy | Multi-stage Docker images running as non-root, Compose for local and a fail-closed production overlay, health checks, structured logs, Prometheus metrics, optional OpenTelemetry |
Architecture: a well-structured modular monolith. One FastAPI application, one database, typed calls across explicit module boundaries. Two processes are split out because there is a real reason — the worker (a slow training run must not hold an HTTP connection) and the MCP server (a different protocol for a different client) — and both run the same image. The reasoning is in docs/architecture.md.
| Source | Version | Licence |
|---|---|---|
| Open Targets Platform | release 26.06 | CC0 1.0 (evidence inherits its datasource licence) |
| ChEMBL | ChEMBL_37 | CC BY-SA 3.0 |
| PubChem | rolling | Public domain |
| Europe PMC | rolling | Metadata free to use |
| MoleculeNet | content-hashed snapshot | MIT (distribution) |
Full citations live in the data_source table and are returned by
GET /api/v1/ingestion/sources, so the UI can show a licence next to the data it draws.
Read docs/decisions.md before drawing any scientific conclusion. The short version: the property models are trained on public benchmarks of a few thousand molecules, their scaffold-split confidence intervals are wide and stated, out-of-domain predictions are extrapolations and are flagged as such, evidence scores are Open Targets' judgement rather than TheraGraph's, the corpus covers three disease areas, and nothing here is a claim about clinical efficacy.
Contributions are welcome — CONTRIBUTING.md has the setup, the ground rules every change follows, and the CI gates a pull request must pass. Participation is governed by the Code of Conduct.
Found a vulnerability? Please report it privately as described in SECURITY.md, not as an issue. The deployment security model is in docs/operations.md.
Apache-2.0. The public datasets carry their own licences, listed above and in NOTICE.