Publication-grade clinical-trial annotation with a local multi-agent LLM pipeline.
Agent Annotate takes a list of ClinicalTrials.gov NCT IDs and produces structured, evidence-backed annotations for each trial — running entirely on your own machine against models served by Ollama. No data leaves your host except read-only queries to public research APIs.
This is the standalone repackaging: a single self-contained FastAPI service with no external auth, no sibling services, and a one-command installer. It runs on any machine that can run the models (macOS, Linux, or Windows).
For each trial it answers six fields, each backed by cited evidence:
| Field | Question |
|---|---|
classification |
Is the intervention an antimicrobial peptide (AMP) or not? |
peptide |
Is the drug a peptide, and what is its sequence/identity? |
sequence |
The amino-acid sequence, when identifiable. |
delivery_mode |
How is it administered (IV, topical, oral, …)? |
outcome |
Did the trial succeed, fail, or is the outcome unknown? |
reason_for_failure |
If it failed, why? |
The pipeline works in three stages:
- Research — ~25 parallel agents query public databases (ClinicalTrials.gov, PubMed/PMC, FDA, UniProt, ChEMBL, RCSB PDB, DRAMP, DBAASP, SEC EDGAR, NIH RePORTER, and more) and collect evidence.
- Annotation — a primary LLM proposes each field from the evidence.
- Blind multi-model verification — three different model families independently verify each annotation from distinct adversarial personas (conservative / evidence-strict / adversarial), and a reconciler resolves disagreements. This cognitive diversity is what makes the output publication-grade.
Deep technical detail lives in
docs/METHODOLOGY.mdand the end-user walkthrough indocs/USER_GUIDE.md.
- OS: macOS, Linux, or Windows 10/11 (PowerShell).
- Python: 3.10 or newer (3.11 / 3.12 recommended for the widest wheel support).
- Ollama: installed automatically by the installer.
- RAM: the default
mac_minimodel profile fits comfortably in 16–24 GB. The optionalserverprofile needs 240 GB+. - Disk: ~25–30 GB for the default models.
- Network: outbound HTTPS to public research APIs (see Network egress). The API boots fine offline, but annotation jobs need internet.
git clone <this-repo-url> agent-annotate
cd agent-annotate
git checkout birollabs-migration
./install.sh # installs Ollama + models + Python deps (use --server for big models)
./run.sh # starts the API; auto-selects a free port and prints the URLgit clone <this-repo-url> agent-annotate
cd agent-annotate
git checkout birollabs-migration
.\install.ps1 # installs Ollama + models + Python deps (-Server for big models)
.\run.ps1 # starts the API; auto-selects a free port and prints the URLrun.sh / run.ps1 pick the first free port starting at 9005 and print the
exact URL (e.g. http://127.0.0.1:9005). Pin one with PORT=8080 ./run.sh. The
examples below use 9005 — substitute whatever port it printed.
Verify it's up (use the printed port):
curl http://127.0.0.1:9005/api/health
# {"status":"ok","version":"0.1.0"}Open the same URL in a browser for the bundled web UI.
install.sh / install.ps1 is idempotent — safe to re-run. It runs a full
preflight and warns (without aborting) on anything that could bite you:
- Preflight checks — verifies Python 3.10+,
venv/pip,git,curl, and network reachability; measures RAM and free disk and warns if either is below what the chosen model profile needs, so you find out before a multi-GB pull fails. - Installs Ollama if absent, starts its server, and waits for it.
- Pulls the LLM models for the chosen profile (cached models verify instantly).
- Creates a
.venvvirtualenv and installsrequirements.txt. - Creates the runtime directories (
results/…,logs/) and copies.env.example→.env.
It prints a summary with a warning count at the end. Flags: --server (large
profile) and --skip-models (set everything up but skip the pulls). Re-pull a
single model anytime with ollama pull <model>.
The pipeline's model selection lives in
config/default_config.yaml under
orchestrator.hardware_profile. Two profiles ship:
| Role | Model |
|---|---|
| Primary annotator + reconciler (all 6 fields) | qwen3:14b |
| Annotator / verifier (adversarial) | llama3.1:8b |
| Verifier (conservative) | gemma3:12b |
| Verifier (evidence-strict) | qwen3:8b |
./install.sh pulls exactly these four.
Premium reasoning model (kimi-k2-thinking) for the hardest fields plus larger
verifiers (gemma2:27b, qwen2.5:32b, phi4:14b). Install with
./install.sh --server and set hardware_profile: "server" in the config.
Missing models are also auto-pulled by the app on first use, but a cold pull mid-job can be slow — pre-pulling via the installer avoids first-job timeouts.
The API is open (no authentication). By default it accepts any valid
NCT ID (NCT followed by 8 digits). Jobs are queued and processed one at a time.
curl -s -X POST http://127.0.0.1:9005/api/jobs \
-H 'Content-Type: application/json' \
-d '{"nct_ids": ["NCT12345678", "NCT87654321"]}' # replace with real NCT IDs{ "job_id": "a1b2c3d4e5f6", "status": "queued", "total_trials": 2, "queue_position": 0 }curl -s http://127.0.0.1:9005/api/jobs/a1b2c3d4e5f6
# full job record incl. status: queued → running → completed (per-trial progress)
# live pipeline view (which agent is running on which trial):
curl -s http://127.0.0.1:9005/api/status/pipeline/a1b2c3d4e5f6curl -s http://127.0.0.1:9005/api/results/a1b2c3d4e5f6 # full JSON
curl -s http://127.0.0.1:9005/api/results/a1b2c3d4e5f6/summary # summary
curl -sOJ http://127.0.0.1:9005/api/results/a1b2c3d4e5f6/csv # download CSV| Method & path | Purpose |
|---|---|
GET /api/health |
Liveness probe. |
POST /api/jobs |
Create/queue a job ({"nct_ids": [...]}). |
GET /api/jobs · GET /api/jobs/{id} |
List jobs · job detail. |
POST /api/jobs/{id}/cancel · /resume |
Cancel · resume a job. |
GET /api/status/pipeline/{id} |
Live per-trial pipeline status. |
GET /api/results/{id} · /csv · /summary |
Results JSON · CSV · summary. |
GET /api/status/models |
Models available in Ollama. |
GET/PUT /api/settings · POST /api/settings/reload |
Runtime config. |
GET /api/review · POST /api/review/{job}/{nct}/{field} |
Human review queue / corrections. |
Interactive API docs are always available at
http://127.0.0.1:9005/docs (Swagger UI).
Copy .env.example to .env. Everything is optional:
| Variable | Default | Purpose |
|---|---|---|
AGENT_ANNOTATE_PORT |
9005 |
App's self-reported port (diagnostics). |
OLLAMA_HOST / OLLAMA_PORT |
localhost / 11434 |
Where Ollama is served. |
OLLAMA_TIMEOUT |
600 |
Max seconds per LLM call. |
PUBMED_API_KEY |
— | NCBI key → 10 req/s (vs 3). |
OPENALEX_EMAIL / CROSSREF_EMAIL |
— | "Polite pool" contact emails. |
CORS_ORIGINS |
localhost:5173,9005 |
Browser origins allowed to call the API. |
Models, the research/annotation agent graph, evidence thresholds, verifier
personas, per-model timeouts, and hardware_profile. Edit and call
POST /api/settings/reload (or restart) to apply.
HOST=0.0.0.0 PORT=8080 ./run.sh # listen on all interfaces, port 8080Agent Annotate is annotation-only: it accepts any valid NCT ID and runs entirely on its own. There is no concordance / inter-rater-agreement (AC1, Cohen's κ) analysis in the product — it just annotates.
If you have your own human annotations and want to score the agent against them,
drop a ground-truth CSV at docs/human_ground_truth_train_df.csv (the column
layout expected by scripts/score_full_corpus.py). That enables the optional
dev scoring scripts under scripts/ (e.g. compare_jobs.py,
score_full_corpus.py) and lets the EDAM memory learn from your labels. It is
never required — the annotator works fully without it, and job submission is
not restricted by it.
.
├── app/ # FastAPI service: routers, services, models, prebuilt SPA
│ ├── main.py # ASGI entrypoint (app.main:app)
│ ├── config.py # env + path configuration
│ ├── routers/ # /api/* endpoints
│ └── services/ # orchestrator, ollama client, persistence, memory (EDAM)
├── agents/ # research + annotation + verification agents
├── config/ # default_config.yaml (the pipeline definition)
├── frontend/ # React/Vite source for the web UI (prebuilt copy in app/static/spa)
├── scripts/ # dev/eval tooling (scoring, slice-builders, regression tests)
├── docs/ # methodology, paper, strategy, user guide, business docs
├── install.sh / .ps1 # dependency installers
├── run.sh / .ps1 # start the API
├── requirements.txt
└── .env.example
Generated at runtime (git-ignored): results/, logs/, .venv/.
Everything the app writes lives under the repo root and is git-ignored. Nothing is uploaded anywhere — only read-only queries go out to the public research APIs (see Network egress); annotation results never leave your machine.
| Path | What's stored |
|---|---|
results/json/<job_id>.json |
Final annotation result for a job — every field + its supporting evidence. |
results/csv/<job_id>_*.csv |
Exported CSV of a job's annotations (one row per trial). |
results/jobs/<job_id>.json |
Job/queue state record — used to restore queued jobs after a restart. |
results/research/<job_id>/ |
Per-trial raw research evidence + _meta.json. Written incrementally so a crashed job can resume. |
results/annotations/<job_id>/ |
Per-trial annotation output as it's produced (mid-run progress). |
results/atomic_pub_cache/ |
Cached per-publication assessments (re-used across trials). |
results/edam.db |
SQLite "EDAM" memory store (self-learning corrections). -wal/-shm are transient. |
results/review_queue.json |
Human review-queue corrections submitted via the review API. |
logs/agent_annotate.log |
Rotating application log (10 MB × 10 backups). |
.env |
Your local config (copied from .env.example). |
Outside the repo:
| Path | What's stored |
|---|---|
~/.ollama/models/ |
Downloaded LLM weights — the bulk of the disk footprint (~30 GB for the default profile). Relocate by setting OLLAMA_MODELS. |
.venv/ |
The Python virtualenv (lives in-repo but is git-ignored). |
How a job flows through storage: submit → the job record lands in
results/jobs/ and the queue → research agents write evidence into
results/research/<job_id>/ → annotation + verification write into
results/annotations/<job_id>/ → on completion the merged result is written to
results/json/<job_id>.json (and CSV on export). Because each stage persists to
disk, an interrupted job resumes from where it stopped via
POST /api/jobs/{id}/resume.
Cleaning up: delete results/<*>/<job_id>* to remove a single job, or wipe
the whole results/ tree to reset all output — the app recreates the directory
structure on the next start. Model weights are managed separately with
ollama rm <model>.
- Run the test suite:
bash scripts/run_full_regression.sh(source-level trip-wires + versioned regression tests + live API integration tests). - Score a job vs ground truth:
python scripts/score_full_corpus.py/python scripts/compare_jobs.py(require the GT CSV indocs/). - Build NCT slices: the
scripts/pick_*.pyutilities. - The living development docs are
CONTINUATION_PLAN.md,LEARNING_RUN_PLAN.md, anddocs/AGENT_STRATEGY_ROADMAP.md.
| Symptom | Fix |
|---|---|
503 Ollama is unreachable on job submit |
Start Ollama: ollama serve. Confirm curl localhost:11434/api/tags. |
| Job stuck at "running" / very slow | First call cold-loads the model. Big models (qwen3:14b) take time; raise OLLAMA_TIMEOUT. |
ollama pull fails / times out |
Re-run ollama pull <model>; check disk space and network. |
| Annotation fields all "Unknown" | Likely no internet egress to the research APIs — see below. |
Address already in use |
Another process holds the port — PORT=9006 ./run.sh. |
| pip install fails on Python 3.13/3.14 | Use Python 3.11 or 3.12: PYTHON_BIN=$(which python3.12) ./install.sh. |
Web UI 404s but /api/health works |
The prebuilt SPA at app/static/spa/ is missing; the API still works headless. |
Real annotation queries these public hosts (read-only):
clinicaltrials.gov, api.fda.gov, accessdata.fda.gov,
eutils.ncbi.nlm.nih.gov / pubmed.ncbi.nlm.nih.gov, europepmc.org,
api.openalex.org, api.semanticscholar.org, api.crossref.org, doi.org,
rest.uniprot.org, dramp.cpu-bioinfor.org, dbaasp.org, www.ebi.ac.uk,
*.rcsb.org, aps.unmc.edu, guidetopharmacology.org, trialsearch.who.int,
api.reporter.nih.gov, efts.sec.gov / www.sec.gov, lite.duckduckgo.com.
The app boots and health-checks fine without them; only annotation degrades.