Skip to content

feat: add configurable smart alert rules (#183) - #206

Merged
Devnil434 merged 2 commits into
Devnil434:mainfrom
Antra1705:feature-183-configurable-alert-rules
Sep 18, 2026
Merged

Devnil434 merged 2 commits into
Devnil434:mainfrom
Antra1705:feature-183-configurable-alert-rules

Conversation

@Antra1705

@Antra1705 Antra1705 commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a rule engine so operators can decide which suspicious activity actually raises an alert, instead of every zone-entry-plus-dwell event reaching the reasoning layer. Rules live in config/alert_rules.yaml, are validated on load, and are re-read when the file changes — no restart, no code edit.

The engine narrows the existing trigger; it never widens it. Activity must still clear the zone, dwell, and suspicious-action gates, so a permissive rule cannot manufacture an alert. With no rule enabled the engine abstains rather than matching nothing, which is what makes this safe to merge: an absent or fully disabled config leaves the pipeline behaving exactly as it does today.

Closes #183

Deliverables from the issue

  • Person detection alerts — object_types: [person], matched against the tracked object's class
  • Confidence threshold — min_confidence, checked against the highest confidence in the sequence
  • Zone-based alerts — zones, using the names already defined in config/zones.yaml
  • Time-based rules — time_windows with HH:MM ranges that may cross midnight, plus optional days
  • Enable/Disable rules — enabled: false switches a rule off in place, applied live
  • Vehicle alerts — expressible but inert today, see below

object_types: [vehicle] parses, validates, and matches correctly in the engine (there are tests for it), but it cannot fire in the live pipeline yet: services/tracking/tracker.py drops every non-person detection before DeepSORT and hardcodes label = "person". Making vehicle alerts real means multi-class tracking — ReID, action hints, and dwell semantics all currently assume people — which is a substantial feature of its own and would change what gets tracked for every existing deployment. Rather than add vehicle classes to the detector and ship a rule that silently never matches, the taxonomy is in place and the limitation is documented in the README and the example config. Happy to open a follow-up issue for multi-class tracking if you'd like.

How a rule looks

Rules are off until the file exists:

cp config/alert_rules.example.yaml config/alert_rules.yaml
rules:
  - id: restricted_door_person
    description: Person lingering or repeatedly approaching the secure door.
    object_types: [person]
    zones: [restricted_door]
    action_hints: [lingering, near_keypad, repeated_approach]
    min_confidence: 0.6

  - id: after_hours_corridor
    zones: [safe_corridor]
    time_windows:
      - start: "19:00"      # crosses midnight
        end: "07:00"
    cooldown_seconds: 120    # overrides REASONING_COOLDOWN_SECONDS for this rule

  - id: daytime_corridor_person
    enabled: false           # kept on file, switched off
    zones: [safe_corridor]

Every dimension is optional and an omitted one means "any". Populated dimensions are ANDed, rules are tried top to bottom, and the first match wins — so specific rules go first. object_types accepts the group aliases person, vehicle, bag, device or any raw COCO class.

API

GET /rules reports what the pipeline is currently enforcing, in evaluation order. It's read-only on purpose: the YAML file is the source of truth, and it hot-reloads, so there's no second write path to keep consistent.

Endpoint Purpose
GET /rules All rules, in evaluation order
GET /rules?enabled_only=true Only the rules currently in force
GET /rules/{rule_id} One rule, 404 if unknown

Picked up automatically by the existing router auto-discovery — no registration edit.

Design

services/rules/engine.py is pure: the rules, the activity, and the current time all arrive as arguments, so a decision is reproducible in a test with no config file and no frozen clock. YAML parsing and reload live in libs/config/rule_loader.py, and services/rules/provider.py holds the process-wide instance, keeping filesystem concerns out of the matcher.

Three behaviours worth reviewing:

  • A malformed edit keeps the last valid rules in force, logging the reason. A typo mid-edit must not silently widen or narrow what gets alerted on. A file that is already broken at startup degrades to empty rather than stopping the process.
  • Reload is driven by mtime, not a background thread. Rules are read on the request path, so there's no thread to supervise, and the freshness check costs one stat at most every RULES_RELOAD_SECONDS. This deviates from zone_loader.py's thread; a shared base class would have forced one strategy onto both.
  • Time windows are evaluated at the event's timestamp, not at evaluation time, so a late-arriving event is judged by when it actually happened. RULES_TIMEZONE sets the site's timezone; an unknown value falls back to UTC with a warning.

Prerequisite: the object class was never persisted

Rules match on object type, but TrackEvent had no field for it — so this PR adds TrackEvent.label, populated in services/memory/pipeline.py from the tracked object's class and in POST /ingest from the request body. It defaults to None, so events already in Redis stay readable; they simply never match a rule that names object_types, which is pinned by a test.

Why the config ships as .example

Shipping a populated config/alert_rules.yaml would activate the engine for everyone on upgrade, suppressing alerts that fire today. It ships as config/alert_rules.example.yaml for operators to copy, following the .env.example convention, and config/alert_rules.yaml is gitignored so local rules can't be committed by accident.

Testing

78 new tests, all passing:

  • tests/test_alert_rules.py (39) — group expansion, midnight-crossing and weekday windows, per-dimension matching, AND semantics, first-match-wins, abstention with no or all-disabled rules, and the inclusive confidence boundary.
  • tests/test_rule_loader.py (18) — valid load and ordering, missing/empty/null-key files, malformed YAML, duplicate ids, the ALERT_RULES_PATH override, hot reload, keeping the last good rules through a broken edit, and a check that the shipped example config actually loads.
  • tests/integration/test_rules.py (7) — the endpoints over HTTP, including 404 and the no-config-file case.
  • tests/test_trigger.py (+10) — the gate itself: default behaviour preserved with no rules, suppression on non-match, that a permissive rule cannot bypass the dwell threshold, per-rule cooldown, and event-timestamp window evaluation.
  • tests/test_memory.py (+4) — the object class reaching storage, including a non-person class and the legacy None default.

341 passed locally, excluding eight modules needing heavy ML dependencies (ultralytics, deep_sort_realtime, matplotlib, Kafka) and tests/integration/test_backend.py. ruff check is clean on every touched path. Also verified end to end against the real config path: creating, editing, disabling, and breaking the rules file while running, with the trigger and GET /rules both tracking the file live.

Notes for reviewers

  • Two incidental fixes were needed to keep CI green. services/memory/pipeline.py imported confluent_kafka unconditionally, though it is undeclared in every requirements file and only used when USE_KAFKA is set — that made the module unimportable in CI, so it is now imported only when Kafka is enabled. Separately, services/rules/__init__.py exports only the pure engine, not the YAML-reading provider, so PyYAML doesn't become a hard import for the memory package that phase3-tests.yml doesn't install.
  • tests/integration/test_backend.py already fails on main (11 errors — it patches a backend._redis attribute that doesn't exist). Unrelated to this PR, and no workflow runs that file.
  • No dashboard UI here. The follow-up toggle can read GET /rules directly; I left it out to keep this reviewable and because writes would need a second source of truth alongside the YAML file.
  • New env vars are documented in .env.example: ALERT_RULES_PATH, RULES_TIMEZONE, RULES_RELOAD_SECONDS.

Summary by CodeRabbit

  • New Features
    • Added configurable YAML alert rules with matching by object type, zone, action, confidence, time window, and cooldown.
    • Added live rule reloading without restarting the service.
    • Added read-only APIs to list, filter, and inspect alert rules.
    • Events now retain tracked object labels for more precise alert matching.
  • Documentation
    • Added configuration guidance, rule examples, matching behavior, and API documentation.
  • Bug Fixes
    • Preserved the last valid rule configuration when edited rules contain errors.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 29d3e979-2506-43de-9a5f-e79e0e4b083b

📥 Commits

Reviewing files that changed from the base of the PR and between 753604b and e10cf8b.

📒 Files selected for processing (21)
  • .env.example
  • .gitignore
  • README.md
  • apps/backend/routes/ingest.py
  • apps/backend/routes/rules.py
  • config/alert_rules.example.yaml
  • docs/ARCHITECTURE.md
  • libs/config/rule_loader.py
  • libs/config/settings.py
  • libs/schemas/memory.py
  • libs/schemas/rules.py
  • services/memory/pipeline.py
  • services/memory/trigger.py
  • services/rules/__init__.py
  • services/rules/engine.py
  • services/rules/provider.py
  • tests/integration/test_rules.py
  • tests/test_alert_rules.py
  • tests/test_memory.py
  • tests/test_rule_loader.py
  • tests/test_trigger.py
 ________________________________________________
< I raised Series-B funding to review your code. >
 ------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Devnil434
Devnil434 merged commit 115ef5d into Devnil434:main Sep 18, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add Configurable Smart Alert Rules

2 participants