Skip to content

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

Open
Antra1705 wants to merge 1 commit into
Devnil434:mainfrom
Antra1705:feature-183-configurable-alert-rules
Open

feat: add configurable smart alert rules (#183)#206
Antra1705 wants to merge 1 commit into
Devnil434:mainfrom
Antra1705:feature-183-configurable-alert-rules

Conversation

@Antra1705

Copy link
Copy Markdown

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 alertsobject_types: [person], matched against the tracked object's class
  • Confidence thresholdmin_confidence, checked against the highest confidence in the sequence
  • Zone-based alertszones, using the names already defined in config/zones.yaml
  • Time-based rulestime_windows with HH:MM ranges that may cross midnight, plus optional days
  • Enable/Disable rulesenabled: false switches a rule off in place, applied live
  • Vehicle alertsexpressible 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.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Antra1705, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f6b3b33-28aa-4d8e-939f-6b741bc5d919

📥 Commits

Reviewing files that changed from the base of the PR and between 578a7d1 and 52a6a55.

📒 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

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.

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.

1 participant