Skip to content

Commit c849543

Browse files
brandon-wadaAuto-format Bot
andauthored
Create multiple actions on alert (#302)
Create multiple actions on alert --------- Co-authored-by: Auto-format Bot <[email protected]>
1 parent e10713a commit c849543

File tree

3 files changed

+170
-3
lines changed

3 files changed

+170
-3
lines changed

src/groundlight/experimental_api.py

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,23 @@
2727
from groundlight_openapi_client.model.rule_request import RuleRequest
2828
from groundlight_openapi_client.model.status_enum import StatusEnum
2929
from groundlight_openapi_client.model.verb_enum import VerbEnum
30-
from model import ROI, BBoxGeometry, Detector, DetectorGroup, ModeEnum, PaginatedRuleList, Rule
30+
from model import (
31+
ROI,
32+
Action,
33+
ActionList,
34+
BBoxGeometry,
35+
Condition,
36+
Detector,
37+
DetectorGroup,
38+
ModeEnum,
39+
PaginatedRuleList,
40+
Rule,
41+
)
3142

3243
from groundlight.images import parse_supported_image_types
3344
from groundlight.optional_imports import Image, np
3445

35-
from .client import DEFAULT_REQUEST_TIMEOUT, Groundlight
46+
from .client import DEFAULT_REQUEST_TIMEOUT, Groundlight, logger
3647

3748

3849
class ExperimentalApi(Groundlight):
@@ -93,6 +104,142 @@ def __init__(
93104

94105
ITEMS_PER_PAGE = 100
95106

107+
def make_condition(self, verb: str, parameters: dict) -> Condition:
108+
"""
109+
Creates a Condition object for use in creating alerts
110+
111+
This function serves as a convenience method; Condition objects can also be created directly.
112+
113+
**Example usage**::
114+
115+
gl = ExperimentalApi()
116+
117+
# Create a condition for a rule
118+
condition = gl.make_condition("CHANGED_TO", {"label": "YES"})
119+
120+
:param verb: The condition verb to use. One of "ANSWERED_CONSECUTIVELY", "ANSWERED_WITHIN_TIME",
121+
"CHANGED_TO", "NO_CHANGE", "NO_QUERIES"
122+
:param condition_parameters: Additional parameters for the condition, dependant on the verb:
123+
- For ANSWERED_CONSECUTIVELY: {"num_consecutive_labels": N, "label": "YES/NO"}
124+
- For CHANGED_TO: {"label": "YES/NO"}
125+
- For ANSWERED_WITHIN_TIME: {"time_value": N, "time_unit": "MINUTES/HOURS/DAYS"}
126+
127+
:return: The created Condition object
128+
"""
129+
return Condition(verb=verb, parameters=parameters)
130+
131+
def make_action(
132+
self,
133+
channel: str,
134+
recipient: str,
135+
include_image: bool,
136+
) -> Action:
137+
"""
138+
Creates an Action object for use in creating alerts
139+
140+
This function serves as a convenience method; Action objects can also be created directly.
141+
142+
**Example usage**::
143+
144+
gl = ExperimentalApi()
145+
146+
# Create an action for an alert
147+
action = gl.make_action("EMAIL", "[email protected]", include_image=True)
148+
149+
:param channel: The notification channel to use. One of "EMAIL" or "TEXT"
150+
:param recipient: The email address or phone number to send notifications to
151+
:param include_image: Whether to include the triggering image in action message
152+
"""
153+
return Action(
154+
channel=channel,
155+
recipient=recipient,
156+
include_image=include_image,
157+
)
158+
159+
def create_alert( # pylint: disable=too-many-locals # noqa: PLR0913
160+
self,
161+
detector: Union[str, Detector],
162+
name,
163+
condition: Condition,
164+
actions: Union[Action, List[Action], ActionList],
165+
*,
166+
enabled: bool = True,
167+
snooze_time_enabled: bool = False,
168+
snooze_time_value: int = 3600,
169+
snooze_time_unit: str = "SECONDS",
170+
human_review_required: bool = False,
171+
) -> Rule:
172+
"""
173+
Creates an alert for a detector that will trigger actions based on specified conditions.
174+
175+
An alert allows you to configure automated actions when certain conditions are met,
176+
such as when a detector's prediction changes or maintains a particular state.
177+
178+
.. note::
179+
Currently, only binary mode detectors (YES/NO answers) are supported for alerts.
180+
181+
**Example usage**::
182+
183+
gl = ExperimentalApi()
184+
185+
# Create a rule to send email alerts when door is detected as open
186+
condition = gl.make_condition(
187+
verb="CHANGED_TO",
188+
parameters={"label": "YES"}
189+
)
190+
action1 = gl.make_action(
191+
"EMAIL",
192+
193+
include_image=True
194+
)
195+
action2 = gl.make_action(
196+
"TEXT",
197+
"+1234567890",
198+
include_image=False
199+
)
200+
alert = gl.create_alert(
201+
detector="det_idhere",
202+
name="Door Open Alert",
203+
condition=condition,
204+
actions=[action1, action2]
205+
)
206+
207+
:param detector: The detector ID or Detector object to add the alert to
208+
:param name: A unique name to identify this alert
209+
:param enabled: Whether the alert should be active when created (default True)
210+
:param snooze_time_enabled: Enable notification snoozing to prevent alert spam (default False)
211+
:param snooze_time_value: Duration of snooze period (default 3600)
212+
:param snooze_time_unit: Unit for snooze duration - "SECONDS", "MINUTES", "HOURS", or "DAYS" (default "SECONDS")
213+
:param human_review_required: Require human verification before sending alerts (default False)
214+
215+
:return: The created Alert object
216+
"""
217+
if isinstance(actions, Action):
218+
actions = [actions]
219+
elif isinstance(actions, ActionList):
220+
actions = actions.root
221+
if isinstance(detector, Detector):
222+
detector = detector.id
223+
# translate pydantic type to the openapi type
224+
actions = [
225+
ActionRequest(
226+
channel=ChannelEnum(action.channel), recipient=action.recipient, include_image=action.include_image
227+
)
228+
for action in actions
229+
]
230+
rule_input = RuleRequest(
231+
detector_id=detector,
232+
name=name,
233+
enabled=enabled,
234+
action=actions,
235+
condition=ConditionRequest(verb=VerbEnum(condition.verb), parameters=condition.parameters),
236+
snooze_time_enabled=snooze_time_enabled,
237+
snooze_time_value=snooze_time_value,
238+
snooze_time_unit=snooze_time_unit,
239+
human_review_required=human_review_required,
240+
)
241+
return Rule.model_validate(self.actions_api.create_rule(detector, rule_input).to_dict())
242+
96243
def create_rule( # pylint: disable=too-many-locals # noqa: PLR0913
97244
self,
98245
detector: Union[str, Detector],
@@ -168,6 +315,9 @@ def create_rule( # pylint: disable=too-many-locals # noqa: PLR0913
168315
169316
:return: The created Rule object
170317
"""
318+
319+
logger.warning("create_rule is no longer supported. Please use create_alert instead.")
320+
171321
if condition_parameters is None:
172322
condition_parameters = {}
173323
if isinstance(alert_on, str):

test/integration/test_groundlight.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ def is_valid_display_result(result: Any) -> bool:
3737
and not isinstance(result, MultiClassificationResult)
3838
):
3939
return False
40-
if not is_valid_display_label(result.label):
40+
41+
if isinstance(result, BinaryClassificationResult) and not is_valid_display_label(result.label):
4142
return False
4243
return True
4344

test/unit/test_actions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,19 @@ def test_delete_action(gl_experimental: ExperimentalApi):
5252
gl_experimental.delete_rule(rule.id)
5353
with pytest.raises(NotFoundException) as _:
5454
gl_experimental.get_rule(rule.id)
55+
56+
57+
def test_create_alert_multiple_actions(gl_experimental: ExperimentalApi):
58+
name = f"Test {datetime.utcnow()}"
59+
det = gl_experimental.get_or_create_detector(name, "test_query")
60+
condition = gl_experimental.make_condition("CHANGED_TO", {"label": "YES"})
61+
action1 = gl_experimental.make_action("EMAIL", "[email protected]", False)
62+
action2 = gl_experimental.make_action("EMAIL", "[email protected]", False)
63+
actions = [action1, action2]
64+
alert = gl_experimental.create_alert(
65+
det,
66+
f"test_alert_{name}",
67+
condition,
68+
actions,
69+
)
70+
assert len(alert.action.root) == len(actions)

0 commit comments

Comments
 (0)