Information extraction pipeline for industrial maintenance work orders. Ingests PDFs, Excel files, and JSON from CMMS systems; extracts seven structured fields per record; and serves results via a REST API with a Next.js UI.
| Field | Type | Example |
|---|---|---|
equipment |
surface form | "hydraulic pump" |
canonical_tag |
resolved identifier | HYDRAULIC_PUMP |
failure_mode |
11-class label | LEAK, BROKEN, WORN … |
activity |
surface form | "replaced" |
component |
surface form | "shaft seal" |
severity |
4-class label | CRITICAL, HIGH, MEDIUM, LOW |
cause |
5-class label | CONTAMINATION, WEAR, ELECTRICAL … |
flowchart LR
PDF[PDF] -->|4-tier segmenter| IG
XLS[Excel / JSON] --> IG
IG[Ingest\nSHA-256 · content IDs] --> SRC[(sources\ntable)]
IG --> NM
IG --> WS
subgraph Training
NM[Normalize\nrule → fuzzy → embed] --- TR[(tag_resolutions)]
WS[Weak Supervision\nSnorkel LFs × 3 fields] -->|weak_labels.jsonl| EX
EX[Extract\nspaCy NER × 6 fields] --- ML[(MLflow\nruns · metrics)]
EX --> MDL[(models/\nner_*_v1.2.0)]
end
MDL -->|load| API
SRC --- API
subgraph Serve
API[FastAPI :8765\nPOST /upload\nGET /pipeline/*\nGET /analytics/*] -->|async| GEM[Gemini 2.0 Flash\ncause fallback]
API --- DDB[(DuckDB\nMTBF · bad-actors\nspike detection)]
end
API -->|REST| UI[Next.js UI :3000\nupload · results · CSV]
spaCy NER, schema v1.2.0, gold set 250 records (span-level greedy 1:1 overlap).
| Field | F1 |
|---|---|
equipment |
0.894 |
activity |
0.871 |
failure_mode |
0.854 |
component |
0.850 |
cause |
— (no gold annotations; 0.6% LF coverage — Gemini fallback active) |
severity |
— (no gold annotations; 8.6% LF coverage) |
Requires Python 3.11+ and uv.
uv sync
uv run python -m spacy download en_core_web_mdOptional — PostgreSQL + MLflow tracking (Docker):
docker compose up -dOptional — Gemini cause extraction:
export GEMINI_API_KEY=<key> # from aistudio.google.com# Ingest MaintIE corpus
DATABASE_URL=sqlite:///maintex.db bash scripts/demo_ingest.sh
# Resolve equipment surface forms
DATABASE_URL=sqlite:///maintex.db bash scripts/demo_normalize.sh
# Generate weak labels (Snorkel)
uv run python -c "from maintex.extract.weak_labels import run; run()"
# Train NER models + evaluate
bash scripts/demo_extract.sh# Backend — port 8765
DATABASE_URL=sqlite:///maintex.db uv run uvicorn maintex.serve.app:app --port 8765
# Frontend — port 3000 (in maintex-ui/)
npm run devSwagger UI: http://localhost:8765/docs
uv run python scripts/analytics.pyContent-addressed IDs — source_id and fact_id are SHA-256 over content. Same input always produces the same ID, regardless of when or where it runs.
source_id = sha256(dataset_name ‖ native_id ‖ raw_text)[:16]
fact_id = sha256(source_id ‖ field ‖ char_start ‖ char_end ‖ schema_version)[:16]
run_id = ULID() ← execution identity only, excluded from all fingerprints
Reproducibility — output_fingerprint is SHA-256 over all content rows (sorted by primary key, timestamps excluded). Two runs over the same corpus must produce identical fingerprints.
Gold set isolation — training runs exclusively on silver_release.json. The 250-record gold set is never seen during training or hyperparameter selection.
Stateless upload — POST /upload runs inference in-memory and returns JSON. No database writes.
uv run pytest tests/531 tests covering unit, integration, and idempotency. No external services required.
| Test file | What it verifies |
|---|---|
tests/test_idempotency.py |
6 idempotency proofs across all pipeline stages |
tests/unit/test_labeling.py |
LF coverage and label correctness |
tests/unit/test_fuzzy.py |
Entity resolver tiers (rule → fuzzy → embedding) |
tests/unit/test_train.py |
Training pipeline contract |
| Store | Written by | Read by |
|---|---|---|
sources table |
Ingest | Normalize, Serve |
facts table |
Extract (inference) | Serve |
tag_resolutions table |
Normalize | Serve, Analytics |
runs table |
All stages | Serve |
weak_labels.jsonl |
Weak supervision | Extract, Analytics |
models/ner_*_v1.2.0 |
Extract (training) | Extract (inference) |
maintex/
├── src/maintex/
│ ├── ingest/ # PDF (4-tier), Excel, JSON loaders
│ ├── normalize/ # Rule → fuzzy → embedding entity resolution
│ ├── extract/ # Weak supervision LFs, NER training, Gemini fallback
│ ├── serve/ # FastAPI app + upload endpoint
│ └── analytics/ # DuckDB MTBF / bad-actor / spike queries
├── conf/
│ ├── equipment_master.yaml # 246 canonical tags + aliases
│ └── label_schema.yaml # schema v1.2.0, all class enums
├── data/gold/ # 250-record held-out evaluation set (read-only)
├── models/ # Trained spaCy NER models
├── docs/ # Architecture, evaluation methodology, provenance design
└── tests/ # 531 tests
| Constant | Value | Location |
|---|---|---|
MIN_RECORD_CHARS |
15 |
ingest/load.py |
| Upload size limit | 50 MB |
serve/upload.py |
| Schema version | 1.2.0 |
conf/label_schema.yaml |
| Canonical tags | 246 |
conf/equipment_master.yaml |
docs/architecture.md— stage-by-stage data flow and weak supervision designdocs/provenance_design.md— full ID scheme and fingerprint contractdocs/evaluation.md— scoring methodology and per-field results