Back in 2019 a vendor shut down the cloud behind my cameras and bricked them; I repurposed what was left instead of throwing the hardware out, and the fleet never left. So when this series needed a real workload instead of a generic AI sample, the camera fleet was the obvious pick. Availability, RTSP loss, stale heartbeats, motion bursts, command acknowledgements, config drift, and connector-fronted cameras all flow through the existing cameras/# contract already.

The goal is not asking a model to read random logs. The goal is turning structured edge events into operator guidance without changing the camera control plane. Those are two very different projects, and I only signed up for one of them.

What an Operator Actually Needs

Edge operators do not need another dashboard full of raw events. I have stared at enough of those to know they mostly teach you where to look next. What an operator wants when a site goes quiet is simple: what happened, what is impacted, what evidence supports that conclusion, and what action should happen first.

The camera control plane already has the right shape for this:

  • Cameras or gateways connect outbound.
  • They publish status, inventory, availability, events, metrics, command acknowledgements, and logs.
  • The topic tree is consistent: cameras/<site>/<camera>/<channel>.
  • Azure IoT Operations can sit under that topic contract as the MQTT broker and edge data plane.
  • A data flow can forward the same cameras/# stream northbound without changing producers.

So the operations assistant builds on that contract. It does not create a second camera model. One camera model in this house is plenty.

Three Ways In, One Event Shape

Camera operations assistant architecture: simulated replay, MQTT cameras/#, and Event Hubs telemetry feed event normalization, fleet state, incident building, runbook retrieval, prompt building, and operator output with LocalOnly route evidence

In the design, AiOnTheEdge.OperationsAssistant takes input three ways. Simulated mode replays JSON from samples/camera-events, which is the reliable presentation path. MQTT mode subscribes to cameras/#, which is the local edge path. Event Hubs mode consumes forwarded camera telemetry, which is the cloud analytics path.

Every input mode normalizes events into one shape. The sample below is synthetic, and so is the fleet around it: garage-east is the Class B agent camera the camera series describes on remote1, while driveway-west and front-door are invented siblings on the same site so the incident grouping has something to group.

{
  "eventId": "evt-001",
  "timestampUtc": "2026-06-30T12:00:00Z",
  "siteId": "remote1",
  "cameraId": "garage-east",
  "channel": "event",
  "eventType": "rtsp_loss",
  "severity": "warning",
  "payload": {
    "streamUrl": "rtsp://camera/stream1",
    "durationSeconds": 90,
    "lastFrameUtc": "2026-06-30T11:58:30Z"
  }
}

From there the service builds incidents from facts. Six pieces split the work:

  • CameraEventIngestWorker reads replay files, MQTT, or Event Hubs.
  • FleetStateStore tracks latest status, inventory, availability, version, config hash, and recent events.
  • IncidentBuilder groups related events by site, camera, event type, time window, and severity.
  • RunbookRetriever pulls relevant local runbook chunks from the Knowledge Assistant.
  • OperatorPromptBuilder creates a compact prompt from structured facts and runbook snippets.
  • OperationsAssistantController exposes incident summaries and fleet questions.

And whatever the assistant answers, evidence rides along with it:

{
  "summary": "Remote property 1 likely has a site-level network issue.",
  "impact": [
    "garage-east offline",
    "driveway-west offline",
    "front-door heartbeat stale"
  ],
  "evidence": [
    {
      "timestampUtc": "2026-06-30T11:58:30Z",
      "cameraId": "garage-east",
      "eventType": "rtsp_loss"
    }
  ],
  "recommendedActions": [
    "Check the remote1 camera VLAN gateway and VPN tunnel.",
    "Verify NTP and DNS availability for the camera VLAN.",
    "Avoid rebooting individual cameras until site connectivity is confirmed."
  ],
  "routeDecision": {
    "policy": "LocalOnly",
    "selectedBackend": "foundry-local"
  }
}

How the Talk Actually Runs

The moment worth staging takes a raw event through incident grouping to a first action. Here is the run order I actually use:

  1. Start replay: site-network-partition.
  2. Show raw events arriving, each one displayed under the cameras/<site>/<camera>/<channel> topic composed from its site, camera, and channel.
  3. Show normalized events updating fleet state.
  4. Ask: What happened at remote1 in the last 15 minutes?
  5. Show the assistant summary, impacted cameras, evidence, runbook citations, and first actions.
  6. Open the route trace and show local-only inference due to operational camera telemetry.
  7. Trigger config-drift.
  8. Ask: Which cameras need config remediation?
  9. Show desired versus reported config hash and the apply_config recommendation.
  10. Show that the same raw events can also flow through Azure IoT Operations and Event Hubs in the full Azure path.

Notice what the model never does: invent cameras, sites, or causes. It summarizes only from structured incident facts and retrieved runbook text. If the facts do not name a cause, neither does the assistant.

Standing on the Camera Series

The three camera posts already define the control plane, and this demo does not reopen it:

  • Part 1 defines the topic tree, network model, command set, desired/reported state, and camera classes.
  • Part 2 swaps the broker under the same contract to Azure IoT Operations.
  • Part 3 forwards cameras/# to Event Hubs and adds the ONVIF connector bridge path.

Those posts are prerequisites here; this one adds a local AI operations layer on top and stops arguing about cameras. The new build is just the operations surface:

POST /camera-events
GET  /fleet/sites
GET  /fleet/sites/{siteId}
GET  /fleet/cameras/{siteId}/{cameraId}
GET  /incidents
GET  /incidents/{incidentId}
POST /incidents/{incidentId}/summarize
POST /fleet/query
POST /admin/demo/replay/{scenarioName}
Camera operations seeded incident scenarios: RTSP loss, site partition, stale agent version, config drift, motion burst, and connector-fronted camera events feed normalization, incident grouping, hallucination guards, and operator guidance with summary, evidence, and first actions

The diagram above traces one scenario the whole way to operator guidance. The table below is the cast list - six seeded scenarios that give the demo its plot:

Scenario Events
rtsp-loss-single-camera One camera reachable but RTSP failing.
site-network-partition Multiple cameras offline at one site within 90 seconds.
stale-agent-version Camera healthy but agent version behind desired version.
config-drift Desired config hash differs from reported config hash.
motion-burst Many motion events across cameras at one site.
connector-fronted-camera AIO connector event mapped back into the camera contract.

Each scenario lives in AiOnTheEdge.DemoScenarios as a deterministic script of normalized events, so the demo behaves the same way in a hotel ballroom as it did on my desk. For example, the shape of the site-network-partition scenario file is:

{
  "scenarioName": "site-network-partition",
  "description": "Cameras at one site drop inside a 90-second window.",
  "events": [
    {
      "offsetSeconds": 0,
      "event": {
        "eventId": "evt-partition-001",
        "siteId": "remote1",
        "cameraId": "garage-east",
        "channel": "event",
        "eventType": "rtsp_loss",
        "severity": "warning"
      }
    },
    {
      "offsetSeconds": 45,
      "event": {
        "eventId": "evt-partition-002",
        "siteId": "remote1",
        "cameraId": "driveway-west",
        "channel": "availability",
        "eventType": "offline",
        "severity": "error"
      }
    }
  ]
}

The Hallucination Trap

A confident hallucinated incident is the outcome this whole design is built to avoid. An assistant that infers a site outage because it sounds plausible is worse than no assistant at all; somebody will act on that answer and start rebooting the wrong things. It should only name a cause when the structured facts and runbook snippets support it.

For a live demo, the simulator is the default. Real cameras and Azure IoT Operations are valuable, but the presentation should not depend on a camera or a VPN behaving perfectly. Talks provide enough surprises on their own.

What Demo 3 Has to Prove

Replay has to produce the same fleet state every time, because a demo that drifts is a demo that argues with me on stage. On top of that determinism, the assistant has to summarize several seeded incident types and cite structured evidence and runbook snippets for each one, and the screen has to show the whole chain at once: original topic, normalized event, grouped incident, and the answer built from them. The hard constraint sits at the end of that chain - the answer never names a camera or a site that is not already in the event store.

The other two input modes are there to prove the shape generalizes. MQTT mode subscribes to cameras/# when a broker is present, Event Hubs mode consumes forwarded telemetry when Azure is connected, and neither one changes a line of the assistant’s logic. All of it runs without a real camera in the room. If the demo needed real cameras, I would be doing IT support on stage instead of showing an architecture.

This is the direct continuation of the Azure IoT Operations camera series. The existing control-plane post owns the cameras/# contract; this post uses that contract as the AI workload.

References