An open, locally-hosted reference implementation that given one alarm (with its sensor frame and asset metadata) it gathers context, runs specialist checks, and issues a structured evidence package that a policy gate routes to either an automatic work-order or a human technician. The tools and the intelligence are what make it usable β a per-alarm job is fast enough to call from an operator UI.
This implementation keeps that architecture but replaces the closed, vendor-locked stack with open equivalents:
| role in the original pattern | open source used here |
|---|---|
| Agent orchestration / tracing / HTTP | OpenAI Agents SDK (Agent, Runner, as_tool, trace processors, FastAPI) |
| Reasoning + light-triage brain | google/gemma:12b (gemma4:12b-mlx) via Ollama's OpenAI-compatible endpoint |
| Reasoning and fast tier (one family) | one open model, two temperature tiers |
| Document parsing / OCR (scanned playbooks) | Tesseract (+ optional Ollama vision model glm-ocr) |
| Unstructured RAG (procedure manuals) | Ollama embeddings (qwen3-embedding:0.6b) + a numpy vector store |
| Semi-structured search (past remedy tickets) | same vector store, second corpus |
| Specialist signal analysis | SciPy (signal.welch, FFT) + NumPy z-score anomaly detection |
| Text-to-SQL over the warehouse | an LLM-driven / template SQL tool over SQLite |
| Secure sandboxed runtime + declarative policy | a YAML policy engine (policy.yaml) gating every tool call and action |
| Content-safety model | a transparent, auditable heuristic gate (injection / exfiltration / policy) |
| Agent evaluator over traces | a trace-based evaluation script reading the SDK's span log |
| accelerated data handling (reference design runs on GPU) | CPU NumPy/Pandas stand-ins (swap-in ready) |
Nothing leaves the machine except the model's own calls to the local Ollama server. The brain is a 12B open model; the specialists are classical DSP tools.
Design note: the reference design mentions a larger βsuperβ model for complex reasoning and a small βnanoβ model for light tasks. With a single local 12B model we express the same two tiers as the same model at different temperatures / roles rather than two different checkpoints.
Machinery is increasingly connected β compressors, motors, bearings β so technicians are flooded with data yet must still filter it to find the signal that matters. Hundreds of alarms per hour, thousands of sensor readings, and a mix of documentation formats. For each important alarm a technician asks:
- Has this asset/class hit this alarm before? What was done? What fixed it?
- What does the specific playbook say for this error code, plant and location?
- Is the signal real? A βtemperature highβ β anomaly or trend? Maintenance in progress?
Then they must write a recommendation (usually a work-order). The per-alarm analysis agent automates exactly that generic loop: filter, gather, confirm with a specialist, and draft a reusable recommendation β so a human can focus on the hard cases.
The system is a single-call, per-alarm analysis pipeline. Every stage below lives in its own box; the numbered arrows are the order a request flows through them. Nothing crosses the machine boundary except the model's own calls to the local Ollama server.
Each box is one component; β is the direction of data/control.
ββββββββββββββββββββββββββββ POST /analyze ββββββββββββββββββββββββββββββ
β UPSTREAM CONSUMERS β 1 alarm payload β FastAPI SERVER (server.py) β
β β’ SCADA / IIoT stream β βββββββββββββββββββΆ β β’ AlarmPayload (pydantic) β
β β’ operator-UI button β {event, asset, β β’ POST /analyze β
β β’ batch pre-analyzer β alarm_code, value} β β’ GET /policy , /health β
ββββββββββββββββββββββββββββ βββββββββββββββ¬βββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PIPELINE pipeline.py :: analyze_alarm() β
β orchestrate: agent β extract β gate β route β
βββββ¬ββββββββββββββββ¬ββββββββββββββββββββββββ¬ββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β AGENT RUN β β GATE β β ROUTING β
β (see Β§2 + Β§3) ββ β (see Β§4) ββ β (decision β tgt) β
βββββββββββββββββ ββββββββββββββββββββ ββββββββββ¬βββββββββ
β β
βΌ βΌ
βββββββββββββββββββββ ββββββββββββββββββββββββββ
β TRACE PROCESSOR β read by β OUTGOING ROUTES β
β trace.py (1 JSON β ββββββββββββββΆ β β’ work_order_system β
β file / run) β ββββββββββββββ β β’ technician_queue β
βββββββββββββββββββββ β EVALUATOR β ββββββββββββββββββββββββββ
β eval.py β
β trace-based β
ββββββββββββββββ
The whole thing is exposed as one HTTP endpoint (POST /analyze).
Anything upstream β an in-stream filter, an on-click button in the operator UI,
or a batch pre-analyzer β calls it the same way. The pipeline
(pipeline.py:analyze_alarm) is the only thing that server, batch driver, and
tests call; it returns the full response
{alarm, evidence, evidence_package, gate, routing, raw_model_output, traces_dir, wall_time_s}.
Three agents, one open model (gemma4:12b-mlx via Ollama). The root
investigator owns Phase 1 and Phase 3 as its own tool calls; Phase 2 is a
handoff to two specialist subagents exposed to the investigator as
agent-as-tool wrappers (as_tool). Evidence accumulates on the run
AlarmContext, never in the prompt.
βββββββββββββββββββββββββββββββββββββββββββββββββ
β OLLAMA (local, OpenAI-compatible /v1) β
β gemma4:12b-mlx qwen3-embedding:0.6b β
β glm-ocr (opt) β nothing leaves the box β
βββββββββββββββββ²ββββββββββββββββ²ββββββββββββββββ
β reasoner tier β fast tier
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β INVESTIGATOR (root agent) model = reasoner max_turns = 12 β
β alarm_agent/agents.py :: build_agents() β
β β
β PHASE 1 β gather evidence (its direct tool calls) β
β ββ gather_alarm_history SQL over event/time-series warehouse β
β ββ text_to_sql NL β SQL (read-only warehouse) β
β ββ search_playbooks RAG over OCR'd procedure manuals β
β ββ search_remedy_history RAG over past remedy / work-order tix β
β β
β PHASE 2 β specialist analysis (handoff to subagents, as tools) β
β ββ consult_vibration_specialist ββΆ VIBRATION SPECIALIST ββΆ analyze_vibration β
β ββ consult_anomaly_specialist ββΆ ANOMALY SPECIALIST ββΆ analyze_anomaly β
β β
β PHASE 3 β action synthesis (its direct tool call, run ONCE) β
β ββ synthesize_evidence_package ββΆ {observation, root_cause, β
β remedy, recommended_action, β
β action_type, confidence} (JSON) β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββ
β handoff + JSON findings β evidence_package (JSON)
βΌ βΌ
ββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββ
β VIBRATION SPECIALIST (sub) β β ANOMALY SPECIALIST (sub) β
β model = reasoner β β model = fast β
β tool: analyze_vibration β β tool: analyze_anomaly β
β β’ SciPy signal.welch (FFT) β β β’ NumPy z-score vs recent history β
β β’ BPFO / BPFI defect peaks β β β’ outlier(real) vs artefact/trend β
β β’ rotor balance / bearing β β β’ verdict + mean/std/z β
ββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββ
Why agents-as-tools: a 12B model is unreliable with strict output schemas, so
every structured boundary is a tool that returns JSON text instead of an
output schema the model must honor. synthesize_evidence_package also records the
package onto the context, which is the preferred source extract_package reads
(agents.py:extract_package).
Phase 1 β gather evidence (tools.py). The investigator answers
βwhat happened before, and what do the playbooks say?β, pulling from two data
stores. Every call first passes the policy allow-list (_gate).
βββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ
β EVENT / TIME-SERIES β gather_alarm_history β VECTOR STORE (numpy) β
β WAREHOUSE (SQLite) β ββββββββββββββββββββββΆ β data/index β
β alarm_event, asset, β text_to_sql β ββ corpus: playbooks β
β sensor_frame β (NL β SQL, read-only) β β β OCR'd manuals β
βββββββββββββββββββββββββββ β ββ corpus: remedies β
β² β β past work-order tix β
β seed.py ββββββββββββββββ²βββββββββββββββ
β (synthetic plant, embed, OCR) β embed / search
β qwen3-embedding
βββββββββββββ΄βββββββββββ
β EMBEDDER + OCR β
β Ollama / Tesseract β
ββββββββββββββββββββββββ
Phase 2 β specialist analysis (tools.py). A generic alarm
(βTemperature Highβ, βVibration Spikeβ) is confirmed in context by a specialist
before any recommendation:
consult_vibration_specialistβanalyze_vibration: FFT of the accelerometer series, extracts bearing defect frequencies (BPFO/BPFI) β βis the bearing actually failing, or a sensor artefact / rotor-balance issue?β.consult_anomaly_specialistβanalyze_anomaly: z-score of the reported value against its history β βoutlier (real) or within the normal band (artefact/ trend)?β.
Phase 3 β action generation (tools.py::synthesize_evidence_package). The
investigator assembles a structured package β observation, root-cause
hypothesis, remedy, recommended action, action_type, confidence β as JSON, then
that package moves to the gate. action_type is constrained to
[raise_work_order, schedule_maintenance, escalate_to_technician].
The gate (gate.py::evaluate) decides what happens to the package. Order
matters: action policy β content safety β confidence. Each stage is its own
decision box; the first hit wins.
evidence_package
β
βΌ
ββββββββββββββββββββββββββββββββββββ action_type in deny-list?
β β ACTION POLICY β βββ yes βββΆ ββββββββββββββββββββββββββββ
β policy.check_action() β β REFUSE β
β deny: actuate_equip, β βββ no ββββ β blocked_action set, β
β reset_interlock, β β escalate to technician β
β override_alarm β β (with evidence attached) β
βββββββββββββββββ¬βββββββββββββββββββ ββββββββββββββββββββββββββββ
β pass
βΌ
ββββββββββββββββββββββββββββββββββββ injection / exfiltration / offensive?
β β‘ CONTENT SAFETY β βββ yes βββΆ ββββββββββββββββββββββββββββ
β policy.check_content() β β REFUSE β
β (transparent heuristics; no β βββ no ββββ β refuse, escalate to tech β
β proprietary model) β ββββββββββββββββββββββββββββ
βββββββββββββββββ¬βββββββββββββββββββ
β pass
βΌ
ββββββββββββββββββββββββββββββββββββ confidence β₯ 0.70 & not escalate?
β β’ CONFIDENCE β βββ yes βββΆ ββββββββββββββββββββββββββββ
β auto_dispatch_min_confidence β β AUTO-DISPATCH β
β = 0.70 ; escalate below β βββ no ββββ β β work_order_system β
β + escalate_to_technician always β ββββββββββββββββββββββββββββ
β routes to a human β
βββββββββββββββββ¬βββββββββββββββββββ
β confidence < 0.70 / agent asked for human
βΌ
ββββββββββββββββββββββββββββββββββββ
β ESCALATE β βββΆ technician_queue (evidence pre-attached)
β decision = "escalate" β
ββββββββββββββββββββββββββββββββββββ
Everything in Β§2βΒ§4 is enforced by one policy engine (policy.py, backed by
policy.yaml). It is the secure runtime: the model can only ask; the
policy decides, before any tool runs and before any action dispatches.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β POLICY ENGINE policy.py :: Policy (loaded from policy.yaml) β
β β
β β TOOL ALLOW-LIST allowed_tools (7) β anything else denied β
β β‘ DATA-SOURCE SCOPE warehouse / playbooks / remedies β
β read-only + NO outbound network (no exfiltration) β
β β’ ACTION ALLOW/DENY allow: raise_work_order, schedule_maintenance, β
β escalate_to_technician β
β deny: actuate_equip, reset_interlock, β
β override_alarm (physical/destructive) β
β β£ CONTENT SAFETY block: prompt_injection, exfiltration, β
β offensive ; max_output_chars = 4000 β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββ
β check_tool() before every tool β check_action /
β (tools.py::_gate wraps all 7 tools) β check_content after
βΌ βΌ
[ Phase 1 & 2 tools ] [ Phase 3 package β gate ]
Every run writes one JSON trace file to data/traces/
(trace.py), capturing generations, tool calls, and handoffs β the open
substitute for a proprietary trace/eval backend. The evaluator
(eval.py) reads those traces to score the agent.
run_alarm() trace.py eval.py
βββββββββββΆ data/traces/alarm_<id>.json βββββββΆ score traces
(Runner.run_sync, tracing on) (root cause / action / gate)
The learning loop: the βpast remedyβ corpus (remedies) is the agent's
growing memory. Each time a diagnosis proves right it can be added to remedies,
improving future search_remedy_history hits β so the agent improves in
cost/performance as more alarms pass through it.
Running the demo over a 3-event critical stream (MTR-101 vibration spike, CMP-101 temperature high, CMP-102 pressure low):
ANALYZING E1001 A-VIB-SPIKE @ MTR-101
root_cause : Rotor imbalance / coupling misalignment (175.78 Hz dominant)
remedy : Prior 11.2 mm/s cases resolved by rotor re-balancing (rm-003)
action : schedule_maintenance (confidence 0.90)
DECISION : AUTO_DISPATCH -> work_order_system
ANALYZING E1002 A-TEMP-HIGH @ CMP-101
root_cause : reading judged a sensor artefact by the anomaly specialist
remedy : clean heat exchanger / repair lube oil pump
action : schedule_maintenance (confidence 0.85)
DECISION : AUTO_DISPATCH -> work_order_system
ANALYZING E1003 A-PRESS-LOW @ CMP-102
root_cause : seal leak / discharge-valve fault
remedy : per past remedy rm-004, discharge seal leak repaired
action : raise_work_order (confidence 0.85)
DECISION : AUTO_DISPATCH -> work_order_system
All three events reach a gated decision with evidence attached; each trace shows
all three phases plus RAG and specialist checks ran. The deterministic
gate_demo additionally shows escalate (low confidence) and refuse
(denied physical action / exfiltration) so every path is visible.
-
Ollama running, with these models pulled:
ollama pull gemma4:12b-mlx # brain (reasoner + fast tier) ollama pull qwen3-embedding:0.6b # semantic search ollama pull glm-ocr:latest # optional: vision OCR
-
Tesseract on
PATH(brew install tesseracton macOS). -
Python 3.11+ and a network connection to the Ollama HTTP endpoint (
http://11434by default; it exposes an OpenAI-compatible/v1).
./run.shIt creates a venv, installs deps, applies the openai-agents/openai compat patch, seeds the synthetic plant, runs the gate matrix, the end-to-end demo, and the evaluator.
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
./.venv/bin/python scripts/patch_agents_sdk.py # openai>=2.19 compat
./.venv/bin/python -m alarm_agent.seed # build warehouse + indexes + OCR
./.venv/bin/python -m alarm_agent.gate_demo # deterministic gate matrix
./.venv/bin/python -m alarm_agent.demo # end-to-end over event stream
./.venv/bin/python -m alarm_agent.eval # trace-based evaluation
./.venv/bin/python -m alarm_agent.server # serve HTTP endpointcurl -X POST localhost:8080/analyze -H 'Content-Type: application/json' \
-d '{
"event_id":"E1", "asset_id":"MTR-101",
"alarm_code":"A-VIB-SPIKE", "alarm_name":"Vibration Spike",
"location":"unit_3", "serial_group":"SERIAL-GRP-1",
"value":11.2, "unit":"mm/s"
}'Returns the evidence package, the gate decision, the routing target, and the raw
model output. GET /policy prints the live allow/deny lists; GET /health is a
liveness probe.
config.yaml Open model endpoints, gate thresholds, paths
policy.yaml Declarative security policy (the sandbox)
run.sh One-command setup + demo
scripts/patch_agents_sdk.py openai-agents/openai version-compat shim
alarm_agent/
config.py Config loader
llm.py Model factory (Ollama) + embeddings + OCR (tesseract/vision)
data_store.py SQLite warehouse + text-to-SQL
vector_store.py numpy vector store (playbooks + remedies)
seed.py Synthetic plant + OCR'd scanned playbook + indexes
tools.py The seven agent tools (3 phases) with policy gating
agents.py investigator + 2 specialist subagents (agent-as-tool)
pipeline.py analyze_alarm(): agent -> extract -> gate -> route
gate.py Policy/confidence/content-safety decision
gate_demo.py Deterministic decision-matrix demo
trace.py File-based trace processor (eval input)
eval.py Trace-based evaluator
server.py FastAPI HTTP endpoint
demo.py End-to-end batch driver
data/ generated warehouse.db, indexes, traces/, logs/
| original concept | this implementation |
|---|---|
| agent orchestration library | OpenAI Agents SDK |
| closed reasoning model family | google/gemma:12b via Ollama (OpenAI-compatible) |
| fast/nano vs super tiers | one model, two temperature tiers |
| document parse model | Tesseract (+ optional Ollama vision OCR) |
| GPU vector index (reference design) | numpy cosine store + Ollama embeddings (CPU) |
| GPU signal/anomaly libs (reference design) | SciPy FFT + NumPy z-score (CPU) |
| text-to-sql tool | SQLite + LLM/template SQL |
| secure runtime + YAML policy | policy.yaml gate engine |
| content-safety model | auditable heuristic gate |
| trace/eval backend | file trace processor + eval.py |
The agent is the part of the system that improves in cost and performance as more alarms are put through it.