Skip to content

Commit 6916c3d

Browse files
authored
Add missing endpoints for Monitors: (#156)
* Add missing endpoints for Monitors: - Update & delete monitor endpoint - Updated Streamlit to reflect new changes - New endpoind tests - Bug fixes in Streamlit - Chnages in Model update endpoint * Tests and documentation
1 parent 3e3ac92 commit 6916c3d

File tree

16 files changed

+491
-70
lines changed

16 files changed

+491
-70
lines changed

.coverage

0 Bytes
Binary file not shown.

docs/mkdocs/docs/sdk-docs.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,27 @@ Creates a monitor for a specific metric.
100100

101101
Some metrics like the data drift don't use a threshold so the feature that will be monitored should be inserted. In any case, both `feature` and `lower_threshold` can't be `None` at the same time.
102102

103+
**_update_model_monitor_**_(model_monitor_id, name=None, status=None, severity=None, email=None, lower_threshold=None)_
104+
105+
Updates a model monitor with a specific ID.
106+
107+
| Parameter | Type | Description |
108+
| -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
109+
| **model_monitor_id** | `str` | The ID of the model monitor to update. |
110+
| **name** | `str` | The name of the monitor. Defaults to `None`. |
111+
| **status** | `MonitorStatus` | The status of the monitor. Possible values for `MonitorStatus`: `active`, `inactive`. Defaults to `None`. |
112+
| **severity** | `AlertSeverity` | The severity of the alert the monitor produces. Possible values for `AlertSeverity`: `low`, `mid`, `high`. Defaults to `None`. |
113+
| **email** | `str` | The email to which the alert will be sent. Defaults to `None`. |
114+
| **lower_threshold** | `float` | The threshold below which an alert will be produced. Defaults to `None`. |
115+
116+
**_delete_model_monitor_**_(model_monitor_id)_
117+
118+
Deletes a model monitor with a specific ID.
119+
120+
| Parameter | Type | Description |
121+
| -------------------- | ----- | -------------------------------------- |
122+
| **model_monitor_id** | `str` | The ID of the model monitor to update. |
123+
103124
## Metrics
104125

105126
**_get_drifting_metrics_**_(model_id)_

whitebox/api/v1/docs.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@
3838
"name": "Alerts",
3939
"description": "This set of endpoints handles a model's alerts.",
4040
},
41-
{
42-
"name": "Users",
43-
"description": "This is a helper endpoint to create and delete an admin user in tests.",
44-
},
4541
{
4642
"name": "Cron Tasks",
4743
"description": "This is a helper endpoint to trigger cron tasks for tests.",

whitebox/api/v1/model_monitors.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
from typing import List, Union
22
from whitebox.middleware.auth import authenticate_user
3-
from whitebox.schemas.modelMonitor import ModelMonitor, ModelMonitorCreateDto
3+
from whitebox.schemas.modelMonitor import (
4+
ModelMonitor,
5+
ModelMonitorCreateDto,
6+
ModelMonitorUpdateDto,
7+
MonitorMetrics,
8+
)
49
from fastapi import APIRouter, Depends, status
510
from whitebox import crud
611
from sqlalchemy.orm import Session
712
from whitebox.core.db import get_db
813
from whitebox.schemas.user import User
14+
from whitebox.schemas.utils import StatusCode
915
from whitebox.utils.errors import add_error_responses, errors
1016

1117

@@ -27,6 +33,39 @@ async def create_model_monitor(
2733
) -> ModelMonitor:
2834
"""Inserts a model monitor into the database."""
2935

36+
model = crud.models.get(db, body.model_id)
37+
if not model:
38+
return errors.not_found("Model not found!")
39+
40+
if body.metric in [MonitorMetrics.concept_drift, MonitorMetrics.data_drift]:
41+
if body.metric == MonitorMetrics.concept_drift:
42+
body.feature = model.target_column
43+
else:
44+
if not body.feature:
45+
return errors.bad_request(f"Please set a feature for the monitor!")
46+
# TODO This should get the feature columns from model.features when this field is
47+
# automatically updated from the training dataset.
48+
dataset_row = crud.dataset_rows.get_first_by_filter(db, model_id=model.id)
49+
if not dataset_row:
50+
return errors.not_found(
51+
f"No training dataset found for model: {model.id}!\
52+
Insert the taining dataset and then create a monitor!"
53+
)
54+
features = dataset_row.processed
55+
if body.feature not in features:
56+
return errors.bad_request(
57+
f"Monitored featured must be in the dataset's features!"
58+
)
59+
if body.feature == model.target_column:
60+
return errors.bad_request(
61+
f"Monitored featured cannot be the target column in data drift!"
62+
)
63+
body.lower_threshold = None
64+
else:
65+
if body.lower_threshold is None:
66+
return errors.bad_request(f"Please set a lower threshold for the monitor!")
67+
body.feature = None
68+
3069
new_model_monitor = crud.model_monitors.create(db=db, obj_in=body)
3170
return new_model_monitor
3271

@@ -60,3 +99,59 @@ async def get_all_models_model_monitors(
6099
return errors.not_found("Model not found")
61100
else:
62101
return crud.model_monitors.get_all(db=db)
102+
103+
104+
@model_monitors_router.put(
105+
"/model-monitors/{model_monitor_id}",
106+
tags=["Model Monitors"],
107+
response_model=ModelMonitor,
108+
summary="Update model monitor",
109+
status_code=status.HTTP_200_OK,
110+
responses=add_error_responses([400, 401, 404]),
111+
)
112+
async def update_model_monitor(
113+
model_monitor_id: str,
114+
body: ModelMonitorUpdateDto,
115+
db: Session = Depends(get_db),
116+
authenticated_user: User = Depends(authenticate_user),
117+
) -> ModelMonitor:
118+
"""Updates record of the model monitor with the specified id."""
119+
120+
# Remove all unset properties (with None values) from the update object
121+
filtered_body = {k: v for k, v in dict(body).items() if v is not None}
122+
123+
model_monitor = crud.model_monitors.get(db=db, _id=model_monitor_id)
124+
125+
if not model_monitor:
126+
return errors.not_found("Model monitor not found!")
127+
128+
if model_monitor.metric in [
129+
MonitorMetrics.concept_drift,
130+
MonitorMetrics.data_drift,
131+
]:
132+
filtered_body["lower_threshold"] = None
133+
134+
return crud.model_monitors.update(db=db, db_obj=model_monitor, obj_in=filtered_body)
135+
136+
137+
@model_monitors_router.delete(
138+
"/model-monitors/{model_monitor_id}",
139+
tags=["Model Monitors"],
140+
response_model=StatusCode,
141+
summary="Delete model monitor",
142+
status_code=status.HTTP_200_OK,
143+
responses=add_error_responses([401, 404]),
144+
)
145+
async def delete_model_monitor(
146+
model_monitor_id: str,
147+
db: Session = Depends(get_db),
148+
authenticated_user: User = Depends(authenticate_user),
149+
) -> StatusCode:
150+
"""Deletes the model monitor with the specified id from the database."""
151+
152+
model_monitor = crud.model_monitors.get(db=db, _id=model_monitor_id)
153+
if not model_monitor:
154+
return errors.not_found("Model monitor not found")
155+
156+
crud.model_monitors.remove(db=db, _id=model_monitor_id)
157+
return {"status_code": status.HTTP_200_OK}

whitebox/api/v1/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,22 @@ async def update_model(
106106
) -> Model:
107107
"""Updates record of the model with the specified id"""
108108

109+
# Remove all unset properties (with None values) from the update object
110+
filtered_body = {k: v for k, v in dict(body).items() if v is not None}
111+
109112
model = crud.models.get(db=db, _id=model_id)
110113

111114
if not model:
112115
return errors.not_found("Model not found")
113116

114-
return crud.models.update(db=db, db_obj=model, obj_in=body)
117+
return crud.models.update(db=db, db_obj=model, obj_in=filtered_body)
115118

116119

117120
@models_router.delete(
118121
"/models/{model_id}",
119122
tags=["Models"],
120123
response_model=StatusCode,
121-
summary="Delete user",
124+
summary="Delete model",
122125
status_code=status.HTTP_200_OK,
123126
responses=add_error_responses([401, 404]),
124127
)

whitebox/schemas/model.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,3 @@ class ModelCreateDto(ModelBase):
3030
class ModelUpdateDto(BaseModel):
3131
name: Optional[str]
3232
description: Optional[str]
33-
type: Optional[ModelType]

whitebox/schemas/modelMonitor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ class ModelMonitor(ModelMonitorBase, ItemBase):
4747

4848
class ModelMonitorCreateDto(ModelMonitorBase):
4949
pass
50+
51+
52+
class ModelMonitorUpdateDto(BaseModel):
53+
name: Optional[str]
54+
status: Optional[MonitorStatus]
55+
severity: Optional[AlertSeverity]
56+
email: Optional[str]
57+
lower_threshold: Optional[float]

0 commit comments

Comments
 (0)