Skip to main content
Glama
README.md
# PLC AI Diagnostic & Code Agent

[![CI](https://github.com/eLSeR17/plc-ai-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/eLSeR17/plc-ai-agent/actions/workflows/ci.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

**Local AI agent (Ollama + MCP) for PLC diagnostics and SCL code generation via
OPC UA and TIA Portal Openness.** A two-layer system that reads industrial PLC
data and drafts IEC 61131-3 SCL code — 100 % local, no cloud, no API keys.

> Industrial AI portfolio project by [eLSeR17](https://github.com/eLSeR17).
> Demonstrates the full IT/OT bridge: an OPC UA monitoring layer feeding a
> Model Context Protocol (MCP) server, and a code-generation layer that produces
> validated SCL with a mandatory human review gate.

## Problem

Industrial plants run on Siemens S7-1500 PLCs that publish live process data
over OPC UA. Turning that data into **predictive diagnostics** and
**maintainable control code** still requires a specialist staring at tag tables
and a separate engineer hand-writing SCL. LLM agents could help with both — if
they could *read* the PLC and *write* valid, reviewable SCL.

Official vendor AI offerings (e.g. Siemens' Eigen Engineering Agent) are
cloud-based, come with licensing limits, and their terms prohibit scripted
automation — and many plants cannot let process data leave the shop floor at
all. The answer is a **local, open-source agent**: the LLM runs on your own
hardware and your data never leaves your network.

## Solution (two layers)

| Layer | Purpose | Status |
|-------|---------|--------|
| **Layer 1 — Monitoring** | OPC UA client reads PLC tags; an MCP server exposes 7 read-only tools (`leer_tags`, `leer_estado`, `leer_alarmas`, `historial_sensores`, `crear_orden_trabajo`, `generar_scl`, `validar_scl`) to the agent. Works against the bundled deterministic simulator **or a real S7-1500** (native OPC UA server, port 4840). | ✅ Phases 1–2 |
| **Layer 2 — Code generation** | A local ReAct agent (Ollama) instructs a *deterministic* SCL generator: the LLM selects a template, never writes SCL freely. Output is validated syntactically and, on Windows, can be compiled through an optional TIA Portal Openness bridge. **Never downloads to a controller without human review.** | ✅ Phases 3–6 |

**Non-negotiable safety invariant:** the agent is *read-only* on the PLC in
Layer 1, and in Layer 2 its output is validated and reviewed by a human before
anything reaches a controller. See [Security](#security).

## Demo

All outputs below are real, produced by `scripts/cli.py` against the bundled
simulator (no hardware, no network, fully reproducible).

### 1. PLC diagnostics (live tags + status)

```text
$ python3 scripts/cli.py estado --json
{
  "plc": "opc.tcp://127.0.0.1:4840",
  "mode": "SIMULATOR",
  "last_scan_ms": 50.0,
  "uptime_s": 0.0
}

$ python3 scripts/cli.py tags motor.temp_C --json
{
  "tags": [
    {
      "name": "motor.temp_C",
      "value": 62.47086715698242,
      "unit": "°C",
      "timestamp": "2026-09-11T15:55:16.172664+00:00",
      "quality": 1
    }
  ]
}

$ python3 scripts/cli.py alarmas --json
{ "alarms": [] }   # all 10 monitored tags inside their thresholds
```

### 2. Historical series (local CSV historian)

```text
$ PYTHONPATH=src python3 -m plc_ai.data_gen --days 3   # generate history CSV
wrote 4320 rows -> data/historical/history.csv

$ python3 scripts/cli.py historico motor.temp_C --json
{
  "series": [
    {
      "tag": "motor.temp_C",
      "points": [
        {"t": "2026-09-09T15:50:00+00:00", "v": 62.47086728988105},
        {"t": "2026-09-09T16:00:00+00:00", "v": 64.58467365158069},
        ...   (432 points total for 3 days)
      ]
    }
  ]
}
```

### 3. CMMS work-order lifecycle (JSON + SAP-PM-style CSV)

```text
$ python3 scripts/cli.py crear-ot --maquina motor --prioridad alta \
    --descripcion "Motor 3 temp high" --accion "Check cooling fan" --json
{ "id_ot": "e4077595", "maquina": "motor", "prioridad": "alta",
  "estado": "abierta", "accion": "Check cooling fan" }

$ python3 scripts/cli.py ot list --json
[ { "id_ot": "e4077595", "maquina": "motor", "prioridad": "alta",
    "estado": "cerrada", "resultado": "reparado" },
  { "id_ot": "3add8851", "maquina": "bomba", "prioridad": "media",
    "estado": "abierta" } ]

$ python3 scripts/cli.py ot close 3add8851 --resultado reparado \
    --observaciones "Seal replaced, no leak" --json
{ "id_ot": "3add8851", "estado": "cerrada", "resultado": "reparado" }

$ python3 scripts/cli.py export-csv --output cmms_export.csv
Exported 2 orders to cmms_export.csv
$ cat cmms_export.csv
id_ot,maquina,prioridad,estado,creada_en,cerrada_en,resultado
e4077595,motor,alta,cerrada,2026-09-11T15:46:51,2026-09-11T15:46:57,reparado
3add8851,bomba,media,cerrada,2026-09-11T15:46:52,2026-09-11T16:03:17,reparado
```

### 4. SCL generation + validation

```text
$ python3 scripts/cli.py scl generador --plantilla proteccion_termica \
    --objetivo "Protect conveyor motor from overheating" \
    --parametros '{"limite": 85, "histeresis": 3}' --json
{
  "scl": "// ------------------------------------------------------------------\n// FB_ProteccionTermica - thermal protection with hysteresis\n// GENERATED by plc-ai-agent (template). Review before download.\n// Objetivo: Protect conveyor motor from overheating\n// parametros: limite=85, histeresis=3\n// ------------------------------------------------------------------\nFUNCTION_BLOCK FB_ProteccionTermica\n\nVAR_INPUT\n    temp_actual : REAL;        // current measured temperature\n    temp_max : REAL;           // alarm trip threshold\n    temp_hyst : REAL;          // hysteresis band (e.g. 2.0)\n    hist_reset : BOOL;         // manual reset of the latched alarm\nEND_VAR\n\nVAR_OUTPUT\n    alarma : BOOL;             // latched high-temperature alarm\n    en_servicio : BOOL;        // TRUE while block is armed\nEND_VAR\n\nVAR\n    alarma_interna : BOOL;     // raw trip flag\nEND_VAR\n\nBEGIN\n    IF temp_actual > temp_max THEN\n        alarma_interna := TRUE;\n    ELSIF temp_actual < (temp_max - temp_hyst) THEN\n        alarma_interna := FALSE;\n    END_IF;\n    IF alarma_interna THEN\n        alarma := TRUE;\n    END_IF;\n    IF hist_reset THEN\n        alarma := FALSE;\n    END_IF;\n    en_servicio := TRUE;\nEND_FUNCTION_BLOCK",
  "advertencias": [
    "Code generated from template 'proteccion_termica': review the logic against the real machine before downloading to the PLC.",
    "The LLM selects the template and parameters; the generator produces exact SCL. The LLM never writes SCL freely (ADR-015)."
  ]
}

$ python3 scripts/cli.py scl validar data/fixtures/scl_good.scl --json
{ "valido": true, "errores": [], "warnings": [...] }

$ python3 scripts/cli.py scl validar data/fixtures/scl_bad.scl --json
{ "valido": false,
  "errores": [
    "unbalanced IF/END_IF (1 vs 0)",
    "missing ';' at end of assignment line: 'alarma := TRUE'",
    "found '=' where ':=' may be required: 'alarma = FALSE'"
  ], "warnings": [...] }
```

> The validator catches *exactly* the three intentional errors in the bad
> fixture — deterministic, no LLM involved.

### 5. TIA Openness bridge (optional, Windows) — honest failure mode

```text
# Bridge URL not configured -> controlled error, never a crash
$ PYTHONPATH=src python3 -c "from plc_ai.openness import OpennessBridgeClient; \
  print(OpennessBridgeClient().health_check())"
{'ok': False, 'error': 'Openness bridge not configured'}
```

## Architecture

```
                ┌───────────────── Layer 1 · MONITORING ─────────────────┐
                │                                                         │
  S7-1500 ◄─────┤  OPC UA (port 4840)         asyncua simulator ◄────────┤
  (real PLC,    │       │                    (3 machines / 10 tags,       │
  native UA)    │       ▼                      deterministic, seed=42)    │
                │  OpcUaClient.read_tags()  ── value + unit + QUALITY     │
                │       │                                                 │
                │       ▼                                                 │
                │  MCP server (stdio)  ◄── ReAct agent (Ollama LOCAL)     │
                │  7 tools               qwen2.5-coder:7b, urllib-only    │
                └─────────────────────────────────────────────────────────┘
                                             │ selects template (+params)
                                             ▼
                ┌───────────────── Layer 2 · CODE GENERATION ─────────────┐
                │  generate_scl()  →  validate_scl()                      │
                │     (deterministic templates,   (IEC 61131-3 structural │
                │      LLM NEVER writes SCL freely)   validator)          │
                │      │                                                  │
                │      ▼  (optional, Windows only)                        │
                │  Openness bridge (HTTP/JSON) → TIA Portal → PLCSIM      │
                │      │                                                  │
                │      ▼                                                  │
                │  HUMAN REVIEW ──► download to controller (never auto)   │
                └─────────────────────────────────────────────────────────┘
```

- Real-PLC integration notes: [`docs/REAL_PLC_GUIDE.md`](docs/REAL_PLC_GUIDE.md)
- TIA Openness integration contract: [`docs/TIA_OPENNESS_GUIDE.md`](docs/TIA_OPENNESS_GUIDE.md)
- Full architecture decision log: [`docs/DECISIONS.md`](docs/DECISIONS.md)

## Stack

- **Python 3.11+** (CI runs 3.11 and 3.12)
- **asyncua** — OPC UA client/server (the standard Python OPC UA implementation)
- **MCP SDK** (`mcp>=2.0`, `MCPServer`, stdio transport)
- **Ollama** — local LLM (`qwen2.5-coder:7b`), reachable only inside the Docker
  network as `http://ollama:11434`
- **urllib only** for the LLM/HTTP clients — no heavyweight LLM SDK
- Standard library for the CLI, CMMS and history layers

## Getting started

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

pytest tests/ -q                       # 165 tests, no network, no LLM needed

# Diagnostics against the bundled simulator
python3 scripts/cli.py estado
python3 scripts/cli.py tags all
python3 scripts/cli.py alarmas
python3 scripts/cli.py historico motor.temp_C

# Generate history data for historico (example CSV also committed in data/fixtures/)
PYTHONPATH=src python3 -m plc_ai.data_gen --days 7
```

### Full agent run (local LLM via Ollama)

The agent simulates the MCP client: it decides which tools to call, reads the
results and answers with grounded diagnostics:

```bash
# Inside the Docker network where Ollama lives (see .env.example)
OLLAMA_HOST=ollama:11434 OLLAMA_MODEL=qwen2.5-coder:7b \
    python3 scripts/e2e_agent.py "Mide la temperatura del motor y si es
    alta, genera un bloque SCL de proteccion termica."
```

No Ollama running? The agent degrades gracefully to a controlled error answer
instead of hanging — see `docs/LIVE_EVAL.md`.

### MCP server (for any MCP-capable client)

```bash
export PLC_USE_SIMULATOR=true
PYTHONPATH=src python3 -m plc_ai.mcp.server     # stdio
```

The seven tools:

| Tool | Input | Output |
|------|-------|--------|
| `leer_tags` | `tags: [str]` | `{tags: [{name, value, unit, timestamp, quality}]}` |
| `leer_estado` | — | `{plc, mode, last_scan_ms, uptime_s}` |
| `leer_alarmas` | `active_only: bool` | `{alarms: [{id, tag, level, text, since}]}` |
| `historial_sensores` | `tags, start, end, agg` | `{series: [{tag, points: [{t, v}]}]}` |
| `crear_orden_trabajo` | `maquina, prioridad, descripcion, accion` | `{id_ot, estado}` |
| `generar_scl` | `objetivo, contexto, plantilla` | `{scl, advertencias}` |
| `validar_scl` | `scl` | `{valido, errores, warnings}` |

Configuration is 100 % env-driven — see [`.env.example`](.env.example):
`PLC_ENDPOINT`/`OPCUA_ENDPOINT`, `PLC_NAMESPACE`/`OPCUA_NS`,
`HIST_DATA_DIR`, `OT_DIR`, `PLC_USE_SIMULATOR`, `ALLOW_WRITE_TO_PLC`,
`OLLAMA_HOST`, `OLLAMA_MODEL`, `AGENT_MAX_STEPS`, `OPENNESS_BASE_URL`.

## Facts / Hypotheses / Estimates

Honest engineering — this is an AI/OT bridge; hardware validation is pending.

| Type | Claim | Evidence |
|------|-------|----------|
| **FACT** | 165 tests pass, deterministic | `pytest tests/ -q` → 165 passed (29 Phase 1 + 24 Phase 2 + 82 Phases 3–4 + 21 CMMS + 9 Openness) |
| **FACT** | A real local LLM (Ollama `qwen2.5-coder:7b`) drove the full loop: read live tags, then generated a valid `FB_ProteccionTermica` | Live e2e documented in `docs/LIVE_EVAL.md` (4 real bugs found & fixed) |
| **FACT** | The SCL validator detects exactly the 3 intentional errors of `scl_bad.scl` | `scripts/cli.py scl validar` output above |
| **FACT** | The simulator speaks standard OPC UA on `opc.tcp://127.0.0.1:4840`; the client reads it with the asyncua library | `tests/test_simulator.py` + `tests/test_client.py` (real loopback TCP, in-process) |
| **FACT** | TIA Portal Openness requires Windows + a license (V18+); its vendor terms prohibit scripted automation | Reviewed in `docs/TIA_OPENNESS_GUIDE.md` (official Siemens documentation) |
| **HYPOTHESIS** | The same OPC UA client works against a real S7-1500 with the same tag configuration | Not tested — no PLC in the dev environment (tag/namespace mapping documented in `docs/REAL_PLC_GUIDE.md`) |
| **HYPOTHESIS** | The Openness bridge compiles generated SCL in Windows + TIA V20 without changes | Not tested — requires Windows + TIA license |
| **HYPOTHESIS** | Simulated tag curves are representative of real machine signals | Validated for signal shapes (sinusoid + drift + injected faults); not against real vibration/temperature spectra |
| **ESTIMATE** | OPC UA read latency ~30–100 ms | From asyncua loop timings on a local network (assumption: LAN, low load) |
| **ESTIMATE** | One FB compiles in TIA in seconds (order of magnitude) | Not measured — no Windows environment |

## Limitations

- **No real hardware tested.** Everything runs against the deterministic
  simulator; the real-PLC path is implemented but unverified (see
  `docs/REAL_PLC_GUIDE.md` for the exact steps).
- **Compilation stops at syntax on Linux.** Layer 2 ends at the SCL validator;
  a real compile needs Windows + TIA Portal Openness + license.
- **Simulator scope.** 3 machines / 10 tags with 9 alarm rules — not every
  failure mode of a real plant.
- **Local LLMs can hallucinate.** Mitigated three ways: the LLM *selects* one of
  8 deterministic templates (it never writes SCL freely), readings are only
  trusted when `quality == 1` **and** the timestamp is fresh, and the agent is
  trained (system prompt) to answer with HECHOS/HIPÓTESIS instead of inventing.
- **Single-user, stdio MCP.** No HTTP transport, no multi-user session model.

## Security

- **No cloud, no API keys.** The LLM runs locally (Ollama in the Docker
  network); nothing leaves the shop floor.
- **Read-only by default.** `ALLOW_WRITE_TO_PLC=false`; there is no write path
  implemented at all.
- **Never downloads to production autonomously.** SCL output requires human
  review; the optional Openness bridge has no scheduler and no auto-deploy.
- Only placeholders in `.env.example`; real `.env` is gitignored.

## Roadmap

- Validate against a real S7-1500 (tag mapping + namespace; checklist ready in
  `docs/REAL_PLC_GUIDE.md`).
- Close the compile loop on Windows (Openness bridge + PLCSIM).
- More SCL templates (PID, interlocking), alarm-correlation diagnostics,
  multi-session MCP (HTTP transport).

## Related portfolio projects

`plc-ai-agent` is part of a public AI-engineering portfolio by
[eLSeR17](https://github.com/eLSeR17). Sibling projects demonstrate
complementary skills:

- [`alpha-agent`](https://github.com/eLSeR17/alpha-agent) — LLM agent with
  function-calling, guardrails and external evaluations.
- [`smart-contract-rag`](https://github.com/eLSeR17/smart-contract-rag) —
  production RAG over smart-contract audits with golden-dataset evals and a
  regression guard.
- [`evalforge`](https://github.com/eLSeR17/evalforge) — standalone evaluation
  framework (dual judge, regression guard) that black-box-evaluates the agent
  projects.
- [`pdm-agent`](https://github.com/eLSeR17/pdm-agent) — predictive-maintenance
  agent: sensor ML pipelines feeding an LLM work-order layer.

## Contributing

Issues and PRs are welcome. Keep changes small and testable: every PR must
keep `pytest` green and `ruff check` clean. Both run in CI.

## License

MIT — see [LICENSE](LICENSE). The generated SCL is yours; review it before
download (that is not a license term, it is the safe thing to do).