Repository navigation
feat: add configurable smart alert rules (#183) - #206
Merged
Devnil434 merged 2 commits intoSep 18, 2026
Merged
Conversation
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (21)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
object_types: [person], matched against the tracked object's classmin_confidence, checked against the highest confidence in the sequencezones, using the names already defined inconfig/zones.yamltime_windowswithHH:MMranges that may cross midnight, plus optionaldaysenabled: falseswitches a rule off in place, applied liveobject_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.pydrops every non-person detection before DeepSORT and hardcodeslabel = "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:
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_typesaccepts the group aliasesperson,vehicle,bag,deviceor any raw COCO class.API
GET /rulesreports 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.GET /rulesGET /rules?enabled_only=trueGET /rules/{rule_id}404if unknownPicked up automatically by the existing router auto-discovery — no registration edit.
Design
services/rules/engine.pyis 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 inlibs/config/rule_loader.py, andservices/rules/provider.pyholds the process-wide instance, keeping filesystem concerns out of the matcher.Three behaviours worth reviewing:
statat most everyRULES_RELOAD_SECONDS. This deviates fromzone_loader.py's thread; a shared base class would have forced one strategy onto both.RULES_TIMEZONEsets 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
TrackEventhad no field for it — so this PR addsTrackEvent.label, populated inservices/memory/pipeline.pyfrom the tracked object's class and inPOST /ingestfrom the request body. It defaults toNone, so events already in Redis stay readable; they simply never match a rule that namesobject_types, which is pinned by a test.Why the config ships as
.exampleShipping a populated
config/alert_rules.yamlwould activate the engine for everyone on upgrade, suppressing alerts that fire today. It ships asconfig/alert_rules.example.yamlfor operators to copy, following the.env.exampleconvention, andconfig/alert_rules.yamlis 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, theALERT_RULES_PATHoverride, 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, including404and 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 legacyNonedefault.341 passedlocally, excluding eight modules needing heavy ML dependencies (ultralytics,deep_sort_realtime,matplotlib, Kafka) andtests/integration/test_backend.py.ruff checkis 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 andGET /rulesboth tracking the file live.Notes for reviewers
services/memory/pipeline.pyimportedconfluent_kafkaunconditionally, though it is undeclared in every requirements file and only used whenUSE_KAFKAis set — that made the module unimportable in CI, so it is now imported only when Kafka is enabled. Separately,services/rules/__init__.pyexports only the pure engine, not the YAML-reading provider, so PyYAML doesn't become a hard import for the memory package thatphase3-tests.ymldoesn't install.tests/integration/test_backend.pyalready fails onmain(11 errors — it patches abackend._redisattribute that doesn't exist). Unrelated to this PR, and no workflow runs that file.GET /rulesdirectly; I left it out to keep this reviewable and because writes would need a second source of truth alongside the YAML file..env.example:ALERT_RULES_PATH,RULES_TIMEZONE,RULES_RELOAD_SECONDS.Summary by CodeRabbit