Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Per-Alarm Analysis AI Agent for Industrial Alarm Management

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.

Why per-alarm work is hard (recap)

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.

Architecture

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.

1. End-to-end request flow

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}.

2. Agent topology (OpenAI Agents SDK)

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).

3. The three phases (detail)

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].

4. Policy / confidence / content-safety gate

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"            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

5. Security layer β€” the declarative policy sandbox

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 ]

6. Tracing and learning loop

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.

Results

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.

Getting started

Prerequisites

  • 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 tesseract on macOS).

  • Python 3.11+ and a network connection to the Ollama HTTP endpoint (http://11434 by default; it exposes an OpenAI-compatible /v1).

One-command run

./run.sh

It 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.

Manual steps

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 endpoint

Calling the endpoint

curl -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.

Layout

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/

Mapping to the original pattern

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.