Industrial Knowledge Intelligence — a unified asset & operations brain
Turns the documents scattered across a process plant — work orders, P&IDs, SOPs, OEM manuals, emails, nameplate photos, trend charts — into one knowledge graph, then answers operational questions with citations, warns before failures repeat, and proves compliance.
Built for ET AI Hackathon 2026, PS-8 (Unified Asset & Operations Brain).
| Capability | How |
|---|---|
| Ask anything, cited | 3-mode retrieval: vector (single facts), local (asset-centric), PathRAG (causal/multi-entity). Every claim carries [doc:id p3], and a citation opens the original document. |
| Reasoning you can see | PathRAG walks the plant's real topology and shows the chain it followed. |
| Warns you first | Agents watch every new failure, find sibling patterns, and investigate with LLM tool-calling before anyone asks. |
| Turns warnings into work | Each investigation drafts a corrective work order — facts harvested from the graph, prose from the model, approval from a human. |
| Proves compliance | Statutory obligations read from the graph, with evidence documents and one-click scheduling into the approval queue. |
| Captures what is retiring | A voice interview reads the graph first, asks about the assets that engineer actually worked on, and feeds the transcript back through ingestion. |
| Trust by design | Document facts / agent inferences / human corrections are separate tiers. Agent output is grounding-checked; ungrounded claims are flagged, never trusted. |
| Answers before you ask | The investigation that produces an alert pre-fills the answer cache — the follow-up question is an instant cache hit at zero extra cost. |
| Works outside the app | An MCP server exposes nine tools, so any AI assistant can query the plant graph with the same citations and grounding. |
┌──────────── UI (React) ────────────┐
│ Ask · Graph · Alerts · Work Orders │
│ Compliance · Permits · Interview │
└───────────────┬────────────────────┘
│ HTTP + SSE
┌────────▼────────┐ ┌─────────────┐
│ gateway :8000 │◄───────┤ MCP server │
└────────┬────────┘ │ (stdio) │
┌───────────────────────┼──────────┐ └─────────────┘
│ │ │
┌───────▼──────┐ ┌───────▼───────┐ │ ┌──────────────┐
│ retrieval │ │ Redis │ └─►│ MinIO │
│ :8001 │ │ broker · bus │ │ raw documents│
│ PathRAG │ │ cache · locks │ └──────────────┘
└───────┬──────┘ └───────┬───────┘
│ │ celery queues
│ ┌──────────────┴───────────────────────┐
│ ingestion → extraction (6 lanes) → resolution → graphd
│ │ │
└────────┴──────────► Neo4j (the graph) ◄───────────┘
▲
agents (delta stream →
detect → investigate → alert
→ draft work order)
Hard rules
- Writes to Neo4j go through
graphdonly — single writer, batchedUNWIND, one graph version per batch. - Extractors emit
CandidateSubgraph— never DB writes. - Every edge carries provenance
(doc_id, page/span, extractor_version, confidence, source). - All async messaging over Redis; the pipeline topology is declared once in
plantmind_core.queues.Flow. - LLM access only via
plantmind_core.llm— tiered cheap/mid/vision, retries, structured outputs.
- Docker Desktop (running)
- Python 3.11+
- Node 18+
- An OpenRouter API key (open-weight models: Qwen / DeepSeek)
cd plantmind
cp .env.example .envEdit .env — the only value you must set:
OPENROUTER_API_KEY=sk-or-...Every other default works as-is. Docker Compose overrides the infra URLs to
service names automatically, so the same .env serves both run modes.
python -m venv .venv
.venv\Scripts\pip install -e libs\core[celery,dev]
.venv\Scripts\pip install minio openpyxl fakeredis pypdfium2 pillow ^
fastapi "uvicorn[standard]" python-multipart neo4j
# put services/ on sys.path so `python -m graphd.tasks` works from anywhere
echo <abs-path-to-repo>\services > .venv\Lib\site-packages\plantmind_services.pthcd ui
npm install
cp .env.example .env # VITE_GATEWAY_URL=http://localhost:8000docker compose up --buildBrings up infra, applies the Neo4j schema (graph-init runs once and exits —
that's correct), and starts all services. Gateway on :8000.
Infra in Docker, services in your venv — no rebuild on code changes:
python -m tools.serveOne command: infra → schema → 6 celery workers → agents → retrieval:8001 →
gateway:8000. Ctrl+C stops everything. Logs in logs/.
cd ui && npm run dev # http://localhost:5173| Service | URL |
|---|---|
| UI | http://localhost:5173 |
| Gateway (API) | http://localhost:8000 |
| Retrieval | http://localhost:8001 |
| Neo4j browser | http://localhost:7474 (neo4j / your NEO4J_PASSWORD) |
| MinIO console | http://localhost:9001 |
Ingest the sample corpus (42 documents across 3 plant units):
python -m tools.build_kg data/samplesPrints the pipeline topology, submits every file, watches the queues drain and reports what landed. Re-runs are safe — the content-hash gate drops duplicates.
Cheap first spin (no LLM calls — tables parse deterministically):
python -m tools.build_kg data/samples/work_orders.csv data/samples/inspection_records.csvOr drop a file into data/inbox/ and the folder connector syncs it on schedule.
Re-ingesting after a wipe? The content-hash gate lives in Redis. If Neo4j was cleared but Redis was not, every document is dropped as a duplicate and the graph stays empty. Clear the gate first:
docker compose exec -T redis redis-cli FLUSHALL
python -m eval.run_eval # 28 golden questions, as deployed
python -m eval.run_eval --limit 3 # quick spinReports answer accuracy (LLM judge), citation hit rate, mode-routing accuracy
and mean time-to-answer. Results land in eval/results/.
Retrieval ablation — the same questions forced through each strategy, so retrieval is the only variable:
python -m eval.run_ablation # vector vs local vs path
python -m eval.run_ablation --modes vector,path
python -m eval.run_ablation --limit 5The answer cache is disabled per arm (a cache hit would make every arm look identical) and questions that link no entity fall back to vector retrieval and are counted, so no arm is credited for questions it did not handle. Accuracy is also broken down by the question's intended mode — path retrieval should beat the baseline on multi-hop causal questions and roughly tie on single-fact lookups, which is what confirms the router is not paying traversal cost for nothing.
python -m pytest -q # 405 tests, all offline (no API keys, no infra)Every external dependency is faked — Redis via fakeredis, Neo4j/LLM/MinIO via
injected test doubles.
libs/core/ shared package (plantmind_core)
├── schemas/ pydantic contracts — the only shapes crossing services
├── queues/ Route + Flow: the pipeline topology, declared once
├── bus/ RedisBus: queues, locks, streams, cursors
├── cache/ AnswerCache: semantic answer cache
├── llm/ tiered client, structured outputs, ToolAgent
└── storage/ ObjectStore (MinIO)
services/<name>/ service.py (pure logic) + tasks.py (celery adapter)
ingestion/ hash gate → classify → route
extraction/ 6 lanes: table, text, pnid, manual, mail, imaging
resolution/ canonical ids
graphd/ SOLE Neo4j writer + denoise
retrieval/ linker → router → pathfinder → pruner → assembler
agents/ watchers → investigator → alerts → work-order drafts
gateway/ the edge API
connectors/ scheduled data-source sync
interview/ knowledge capture (voice + graph-aware questioning)
mcp_server/ MCP tools over stdio
infra/ containers.py, celery_workers.py, autoscaler, docker/, neo4j/
tools/ serve.py, build_kg.py, autoscale.py
eval/ golden QA set, runner, retrieval ablation
docs/ architecture reports, demo script
data/samples/ the demo corpus
ui/ React + Vite + Tailwind + Supabase
| Symptom | Cause |
|---|---|
| Everything hangs on startup | Neo4j takes ~30s; services wait on its healthcheck. |
graph-init "stopped" |
Expected — it's a one-shot migration. Check it exited 0. |
| Ingest reports every file as duplicate | Redis still holds the content-hash gate from a previous run. FLUSHALL and re-ingest. |
| Vector search errors on dimensions | EMBEDDING_DIM must match the model and the Neo4j vector index. |
SignatureDoesNotMatch opening a document |
MINIO_PUBLIC_ENDPOINT must be the host the browser uses. Presigned URLs are signed with it, never rewritten afterwards — SigV4 covers the Host header. |
| Everything 403s for an engineer | SUPABASE_JWT_SECRET is set but the app_role claim is missing — register the custom_jwt_claims hook, or unset the secret for an open local demo. |
| UI empty, API returns data | Check VITE_GATEWAY_URL and the browser console. |
| Document stuck, never in graph | Check logs/extraction.log; a failed lane releases its hash claim so a resubmit heals it. |
| Inspect a queue | docker compose exec redis redis-cli LLEN q_extract_text |