rc/SIENTIAPDE-2166- feat: rich metadata (filter/policy/counter) for mlflow_content_gate
This commit is contained in:
@@ -6,7 +6,13 @@ from sientia_do.notifications.models import NotificationLevel
|
||||
from sientia_do.observability.metrics_controller import MetricsController
|
||||
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
|
||||
|
||||
from laborious.activities.gates import Gates, input_filter_counters, input_filter_functions
|
||||
from laborious.activities.gates import (
|
||||
Gates,
|
||||
input_filter_counters,
|
||||
input_filter_functions,
|
||||
mlflow_content_filter_counters,
|
||||
mlflow_content_filter_functions,
|
||||
)
|
||||
|
||||
|
||||
@fixture(autouse=True)
|
||||
@@ -469,7 +475,7 @@ def test_mlflow_content_gate_invalid_filter(gates_activity):
|
||||
result = gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
# Assert
|
||||
assert result == (None, 0, '')
|
||||
assert result == (None, 0, '', None)
|
||||
|
||||
|
||||
@patch('laborious.activities.gates.mlflow_content_filter_functions')
|
||||
@@ -491,7 +497,7 @@ def test_mlflow_content_gate_filter_exception(mock_mlflow_content_filter_functio
|
||||
result = gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
# Assert
|
||||
assert result == (None, 0, '')
|
||||
assert result == (None, 0, '', None)
|
||||
gates_activity.debug.assert_called()
|
||||
gates_activity.send_notification.assert_called_once_with(
|
||||
metadata=metadata['metadata'],
|
||||
@@ -517,12 +523,12 @@ def test_mlflow_content_gate_no_filters(gates_activity):
|
||||
result = gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
# Assert
|
||||
assert result == (None, 0, '')
|
||||
assert result == (None, 0, '', None)
|
||||
gates_activity.debug.assert_called()
|
||||
|
||||
|
||||
def test_mlflow_content_gate_with_filter(gates_activity):
|
||||
# Arrange
|
||||
# Arrange: 3 rows, all-NaN after the filter's own normalization — triggers NAN_VALUES.
|
||||
input_data = {
|
||||
**metadata,
|
||||
'filters': {'NAN_VALUES': {'POLICY': 'STOP', 'CONFIG': {}}},
|
||||
@@ -532,10 +538,15 @@ def test_mlflow_content_gate_with_filter(gates_activity):
|
||||
}
|
||||
|
||||
# Act
|
||||
result = gates_activity.mlflow_content_gate(input_data)
|
||||
path_flag, confidence, comment, document = gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
# Assert
|
||||
assert result == ('STOP', -1, 'Transformed data not passed the content filter')
|
||||
assert (path_flag, confidence, comment) == (
|
||||
'STOP',
|
||||
-1,
|
||||
'Transformed data not passed the content filter',
|
||||
)
|
||||
assert document == {'filters': [{'name': 'NAN_VALUES', 'policy': 'STOP', 'counter': 3}]}
|
||||
gates_activity.debug.assert_called()
|
||||
gates_activity.send_notification.assert_called()
|
||||
|
||||
@@ -554,7 +565,7 @@ def test_mlflow_content_gate_with_filter_not_caught(gates_activity):
|
||||
result = gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
# Assert
|
||||
assert result == (None, 0, '')
|
||||
assert result == (None, 0, '', None)
|
||||
gates_activity.debug.assert_called()
|
||||
|
||||
|
||||
@@ -569,10 +580,18 @@ def test_mlflow_content_gate_filter_returns_false(gates_activity):
|
||||
|
||||
result = gates_activity.mlflow_content_gate(input_data)
|
||||
|
||||
assert result == (None, 0, '')
|
||||
assert result == (None, 0, '', None)
|
||||
gates_activity.debug.assert_called()
|
||||
|
||||
|
||||
def test_mlflow_content_filter_counters_covers_every_mlflow_content_filter_function():
|
||||
# Same parity guard as `test_input_filter_counters_covers_every_input_filter_function`
|
||||
# (review finding, 2026-09-30): a filter added to `mlflow_content_filter_functions` without
|
||||
# a matching counter here degrades to counter=0 instead of crashing, but this test still
|
||||
# catches the gap before it ships.
|
||||
assert set(mlflow_content_filter_counters) == set(mlflow_content_filter_functions)
|
||||
|
||||
|
||||
def test_get_prediction_store_policy_invalid_policy(gates_activity):
|
||||
# Arrange
|
||||
prediction_store_policy = 'INVALID_POLICY'
|
||||
|
||||
@@ -61,7 +61,7 @@ async def test_run(workflow_mock, prediction_process):
|
||||
# mlflow_response_gate (transform)
|
||||
('continue', 0.95, 'Error', None),
|
||||
# mlflow_content_gate (transform)
|
||||
('continue', 0.95, 'Transformed data not passed the content filter'),
|
||||
('continue', 0.95, 'Transformed data not passed the content filter', None),
|
||||
# mlflow_response_gate (predict)
|
||||
('continue', 0.95, 'Error', None),
|
||||
]
|
||||
@@ -369,7 +369,7 @@ async def test_run_stop_at_mlflow_content_gate(workflow_mock, prediction_process
|
||||
# mlflow_response_gate (transform)
|
||||
('continue', 0.95, 'Error', None),
|
||||
# mlflow_content_gate (transform)
|
||||
('continue', 0.95, 'Transformed data not passed the content filter'),
|
||||
('continue', 0.95, 'Transformed data not passed the content filter', None),
|
||||
]
|
||||
|
||||
# Act
|
||||
@@ -480,7 +480,7 @@ async def test_run_stop_at_mlflow_last_response_gate(workflow_mock, prediction_p
|
||||
# mlflow_response_gate (transform)
|
||||
('continue', 0.95, 'Error', None),
|
||||
# mlflow_content_gate (transform)
|
||||
('continue', 0.95, 'Transformed data not passed the content filter'),
|
||||
('continue', 0.95, 'Transformed data not passed the content filter', None),
|
||||
('continue', 0.95, 'Error', None), # mlflow_response_gate (predict)
|
||||
]
|
||||
|
||||
@@ -695,10 +695,10 @@ async def test_path_flag_handler_repeat(workflow_mock, prediction_process):
|
||||
async def test_path_flag_handler_repeat_no_document_skips_write_activity_call(
|
||||
workflow_mock, prediction_process
|
||||
):
|
||||
# Arrange: no document (e.g. REPEAT triggered by a gate that doesn't build one yet — today,
|
||||
# `mlflow_content_gate`) must not even dispatch the remote write_prediction_metadata
|
||||
# activity — that would be a wasted Temporal round-trip on every REPEAT caused by that
|
||||
# gate, not a rare edge case (review finding, 2026-10-01).
|
||||
# Arrange: no document must not even dispatch the remote write_prediction_metadata
|
||||
# activity — that would be a wasted Temporal round-trip (review finding, 2026-10-01).
|
||||
# Defensive case: all 3 gates now always build a document alongside a non-`None`
|
||||
# `path_flag`, so this guards a caller that doesn't, not today's common case.
|
||||
data = {'test': 'data'}
|
||||
|
||||
# Act
|
||||
@@ -1076,7 +1076,7 @@ async def test_run_with_cleanup_prefixes(workflow_mock, prediction_process):
|
||||
workflow_mock.execute_local_activity_method.side_effect = [
|
||||
('continue', 0.95, 'ok', None), # input_gate
|
||||
('continue', 0.95, '', None), # mlflow_response_gate (transform)
|
||||
('continue', 0.95, ''), # mlflow_content_gate (transform)
|
||||
('continue', 0.95, '', None), # mlflow_content_gate (transform)
|
||||
('continue', 0.95, '', None), # mlflow_response_gate (predict)
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user