Skip to content

ETEC Case Study — Full CLI Runbook

How we built a 1,000-paper knowledge graph for the post-weaning ETEC discovery program, fused it with an Atlas reasoning run, harmonized it, and queried it for feed-additive candidates — entirely from the graffold-ingest CLI.

Total spend: ~$8 (Llama 3.3 70B on Bedrock). Total wall time: ~1 hour.


Prerequisites

# Install with all extras (LLM, graph, resolvers, storage)
uv sync --extra all

# LLM backend — one of:
export AWS_REGION=us-east-1          # Bedrock (used here: Llama 3.3 70B)
# or run Ollama locally for embeddings (harmonization uses nomic-embed-text)
ollama serve &
ollama pull nomic-embed-text

# Optional: faster PubMed
export NCBI_API_KEY=<your-key>

Model note: --service bedrock-llama uses us.meta.llama3-3-70b-instruct-v1:0 (~$0.72/M tokens, ~5× cheaper than Claude Haiku on output). Also supports bedrock, anthropic, openai, openrouter, ollama.


Step 1 — Seed the institutional backbone

The customer intake form (modality ranks, known dead-ends, constraints, internal programs, untapped hypotheses) becomes the graph's backbone. This is the one program-specific artifact — encoded as a small script since it is customer data, not a reusable feature.

python benchmarks/etec_seed.py
# → 46 entities: 7 KILLED dead-ends, 3 internal programs,
#   5 constraints, 2 hypotheses, assays, targets

What it encodes (from the intake): - KILLED: organic acids, carvacrol, EO blends, Bacillus blends, chelated zinc, insoluble fiber, generic gut-health — never re-proposed - Constraints: feed-additive-only, pellet-stable-71°C, no vaccine, no small molecule - Hypotheses to test: LTB+FedF combo, feed-borne enterotoxin binder - Internal programs (differentiate, don't duplicate): GM Bacillus-FedF, soluble mannans, MCFA blends


Step 2 — Ingest the literature corpus (1,000 papers)

One command runs fetch → extract → fuzzy-resolve → publish over 49 queries, deduplicating papers across queries, then harmonizes the result.

graffold-ingest ingest-corpus \
  --queries benchmarks/etec-queries.txt \
  --source pubmed \
  --service bedrock-llama \
  --output ~/.graffold/parquet/etec-pigs \
  --per-query 40 \
  --paper-cap 1000 \
  --relevance "ETEC|F18|F4|enterotoxin|piglet|swine|porcine|fimbria|E\. ?coli" \
  --harmonize
  • 49 queries span ETEC core + adjacent biology (adhesins, toxins, feed additives, host models, comparative pathogens)
  • --relevance gate drops off-topic papers before extraction
  • Checkpointed: re-run resumes, skipping processed papers
  • --harmonize runs the canonicalization pass at the end

Result: 1,000 papers (207 full-text via Europe PMC) → 15,388 raw entities → 9,050 after fuzzy dedup. ~44 min, ~$5.

For full-text-primary (richer, costlier), swap --source europepmc --full-text.


Step 3 — Ingest the Atlas reasoning run

Layer a real Atlas discovery run's decisions (candidates, kills, board verdicts) on top of the literature. The agteria connector reads Atlas phase-*.md files.

graffold-ingest ingest \
  ~/Developer/agteria/atlas/programs/phibro-etec-piglet-v2/v1 \
  --llm --service bedrock-llama

Or auto-watch a programs directory as Atlas runs finish:

graffold-ingest watch ~/Developer/agteria/atlas/programs --poll 60

Result: +1,197 entities, +2,690 relationships — 209 decision nodes fuse with the literature (Atlas's "FedF"/"F18"/"IL-6" merge into the same nodes).


Step 4 — Harmonize (collapse fragmented entities)

Literature fragments one entity across dozens of nodes ("F18 ETEC", "F18 fimbriae", "E. coli F18"). Harmonization collapses them into canonical nodes.

graffold-ingest harmonize ~/.graffold/parquet/etec-pigs
# → writes ~/.graffold/parquet/etec-pigs-harmonized

Layered for scientific safety: 1. Alias rules (deterministic) with type guard — a Target rule never swallows an InternalProgram 2. Protected types — Program/Hypothesis/Killed/Constraint never merged 3. Embedding merge (Ollama, 0.90 cosine) with a differing-code guard so F17 ≠ F18, IL-6 ≠ IL-10, STa ≠ STb

Result: 9,947 → 8,752 entities. F18-fimbriae 32→3, heat-labile 21→6 (remaining are real distinct subtypes).


Step 5 — Query for candidates

Ask the graph for feed-additive candidates in the customer's ranked modalities:

graffold-ingest ask \
  "What feed-additive candidates target F18 or F4 adhesion or neutralize ETEC enterotoxins?" \
  --graph ~/.graffold/parquet/etec-pigs-harmonized

Generate a prior-knowledge document for the next Atlas run (the feedback loop):

graffold-ingest context "post-weaning ETEC" \
  --graph ~/.graffold/parquet/etec-pigs-harmonized \
  -o prior-knowledge.md
# Atlas run N+1 reads this at startup → won't re-propose carvacrol

Trace a specific target across all sources:

graffold-ingest trajectory "FedF" --graph ~/.graffold/parquet/etec-pigs-harmonized

Step 6 — Inspect / export

# Graph stats
graffold-ingest status

# Export for Neo4j / Neptune / analysis
graffold-ingest export --format parquet -o etec-export.parquet
graffold-ingest export --format jsonl -o etec-graph.jsonl

The Parquet store is the source of truth. Point any backend at it:

GRAPH_BACKEND=neptune NEPTUNE_ENDPOINT=... graffold-ingest serve
GRAPH_BACKEND=spanner SPANNER_INSTANCE=... graffold-ingest serve


Final graph

Metric Value
Papers 1,000 (207 full-text)
Entities (harmonized) 8,752
Relationships 20,150
Evidence citations 2,228
Atlas decisions 209
Feed-additive candidates 150
Total LLM spend ~$8
Wall time ~1 hour

Candidates surfaced (ranked by graph connectivity): chicken egg yolk IgY, bivalent F4/LT VHH construct, recombinant LTB, STb-binding molecule / MBP-STb2 fusion, L. plantarum, Bacillus safensis M01, stapled/cyclised peptides, berberine.

The graph independently surfaced the customer's own untapped hypotheses (STb-binding, recombinant LTB) plus new candidates (egg yolk IgY, bivalent VHH) — while the KILLED nodes block re-proposing the known dead-ends.


Everything above is package code

Every step is a graffold-ingest CLI command backed by src/graffold_ingest/ package modules — not one-off scripts. The only script is etec_seed.py, which encodes this customer's intake (program data, not a reusable feature).

Step Command Module
Seed etec_seed.py (program data)
Literature ingest-corpus connectors/pubmed.py, pipeline/extract.py, resolvers/local.py
Atlas ingest / watch connectors/agteria.py
Harmonize harmonize pipeline/harmonize.py
Query ask / context / trajectory pipeline/query_agent.py, query.py
Export export pipeline/export.py

Relationship to Microsoft GraphRAG

Technical note for diligence. Graffold and GraphRAG share graph-RAG primitives (LLM extraction, Leiden communities, local/global/DRIFT retrieval, Parquet) — as do LlamaIndex and LangChain. These primitives are commoditized and reproducible. Graffold was developed independently; the biomedical discovery-memory thesis was the starting point, not a layer added later.

The primitives are table stakes. The comparison that matters is what a generic graph-RAG tool cannot retrofit cheaply — domain resolution, per-program memory, and reasoning fusion:

Generic graph-RAG (GraphRAG et al.) Graffold
Extraction / Leiden / DRIFT / Parquet Yes Yes (parity — commoditized)
Entity resolution name-match only* canonical: HGNC/UniProt/MONDO/PubChem
Semantic harmonization No Yes (alias + embedding + code-guard)
Literature connectors (PubMed/Europe PMC) No Yes built-in
Graph backends Azure-centric Neo4j, Neptune, Spanner, DuckDB
Cross-run / institutional memory No Yes (kills, decisions, trajectories)
Reasoning fusion (pipeline decisions) No Yes
Fact verification No Yes

*GraphRAG deduplicates "by name (case-folded, whitespace-normalized)… no proper entity resolution step beyond this" (Microsoft team + independent analysis) — the fragmentation Graffold's harmonizer solves (F18: 32 nodes → 3).

Why "just add a bio layer to GraphRAG" misreads the moat: the defensibility isn't the biology code — it's (1) curated domain data + judgment (80K HGNC aliases, ontology grounding, a code-aware harmonizer), (2) per-customer accumulated memory that compounds and can't be replicated by a new entrant, and (3) fusion of a discovery pipeline's actual kill/advance decisions with evidence. The graph is a means; the discovery-memory system is the product.