SIENTIAPDE-1182

Implement prediction store policy handling in Gates activity

- Added a new method `get_prediction_store_policy` to validate and parse the prediction store policy.
- Updated `format_prediction` method to utilize the new policy handling, allowing for sorting of predictions based on the specified policy.
- Enhanced test coverage for the new policy handling, including various scenarios for valid and invalid policies.
- Removed the obsolete `coverage.sh` script.
This commit is contained in:
vitor-aignosi
2025-08-28 16:31:55 -03:00
parent 7f3ecc9add
commit 30c1d6746a
6 changed files with 201 additions and 8 deletions

View File

@@ -1 +0,0 @@
pytest --cov=laborious --cov-report=html && xdg-open htmlcov/index.html

View File

@@ -242,6 +242,28 @@ class Gates(BaseActivity):
self.info("Nothing was filtered by the mlflow content gate", metadata) self.info("Nothing was filtered by the mlflow content gate", metadata)
return None, 0, "" return None, 0, ""
def get_prediction_store_policy(self,
prediction_store_policy: str,
metadata: dict[str, Any]) -> tuple[str, int]:
policy_elements = prediction_store_policy.split(':')
if len(policy_elements) < 2:
self.error(
f"Invalid prediction store policy: {prediction_store_policy}, using default policy", metadata)
return 'lts', 1
policy_type = policy_elements[0]
policy_value = policy_elements[1]
# If the policy_type is not lts or erl, we use the default policy
# If the policty_value is not a number or 0, we use the default policy
if policy_type not in ['lts', 'erl'] or not policy_value.isdigit() or int(policy_value) == 0:
self.error(
f"Invalid prediction store policy: {prediction_store_policy}, using default policy", metadata)
return 'lts', 1
return policy_type, int(policy_value)
@activity.defn(name="format_prediction") @activity.defn(name="format_prediction")
async def format_prediction(self, input_data: dict[str, Any]) -> dict[Any, Any]: async def format_prediction(self, input_data: dict[str, Any]) -> dict[Any, Any]:
""" """
@@ -252,21 +274,58 @@ class Gates(BaseActivity):
- timestamp (str): The timestamp of the data. - timestamp (str): The timestamp of the data.
- model_id (str): The id of the model. - model_id (str): The id of the model.
- prediction_confidence (float): The confidence of the prediction. - prediction_confidence (float): The confidence of the prediction.
- prediction_store_policy (str): The policy to store the prediction.
Returns: Returns:
dict: The formatted data. dict: The formatted data.
""" """
metadata = input_data['metadata'] metadata = input_data['metadata']
prediction_store_policy = input_data['prediction_store_policy']
self.info("Formatting prediction...", metadata) self.info("Formatting prediction...", metadata)
data = DataFrame(input_data['data']) data = DataFrame(input_data['data'])
data['timestamp'] = input_data['timestamp']
self.debug(
f"Prediction store policy: {prediction_store_policy}", metadata)
policy_type, policy_value = self.get_prediction_store_policy(
prediction_store_policy, metadata)
# If data has no timestamp, we use the default timestamp and not sort the data
if 'timestamp' not in data.columns:
self.warning(
"Data has no timestamp, using default timestamp", metadata)
data['timestamp'] = input_data['timestamp']
else:
self.debug(
f"Data has timestamp, sorting data by timestamp", metadata)
# If policy_type is lts, we need to sort the data by timestamp descending and take the first policy_value rows
if policy_type == 'lts':
self.debug(
f"Sorting data by timestamp descending", metadata)
data = data.sort_values(by='timestamp', ascending=False)
# If policy_type is erl, we need to sort the data by timestamp ascending and take the first policy_value rows
elif policy_type == 'erl':
self.debug(
f"Sorting data by timestamp ascending", metadata)
data = data.sort_values(by='timestamp', ascending=True)
else:
self.error(
f"Invalid policy type: {policy_type}, using default policy", metadata)
raise ValueError(
f"Invalid policy type: {policy_type}")
data = data.head(int(policy_value))
data['model_id'] = input_data['model_id'] data['model_id'] = input_data['model_id']
data['prediction_confidence'] = input_data['prediction_confidence'] data['prediction_confidence'] = input_data['prediction_confidence']
data['prediction_status'] = 'Good' data['prediction_status'] = 'Good'
data['comments'] = "" data['comments'] = ""
data = data.sort_values(by='timestamp') data = data.sort_values(by='timestamp', ascending=False)
data = data.reset_index(drop=True)
self.info(f"Prediction formatted: {data.size} rows", metadata) self.info(f"Prediction formatted: {data.size} rows", metadata)
self.debug(f"Prediction data: {data.to_string()}", metadata)
return data.to_dict() return data.to_dict()

View File

@@ -52,6 +52,8 @@ class FormatAndExportPrediction():
'timestamp': input_data['timestamp'], 'timestamp': input_data['timestamp'],
'model_id': input_data['model_id'], 'model_id': input_data['model_id'],
'prediction_confidence': prediction_confidence, 'prediction_confidence': prediction_confidence,
'prediction_store_policy': input_data.get(
'prediction_store_policy', 'lts:1')
}, },
retry_policy=retry_policy, retry_policy=retry_policy,
start_to_close_timeout=timedelta(seconds=60) start_to_close_timeout=timedelta(seconds=60)

View File

@@ -322,15 +322,68 @@ async def test_mlflow_content_gate_with_filter(gates_activity):
gates_activity.send_notification.assert_called() gates_activity.send_notification.assert_called()
def test_get_prediction_store_policy_invalid_policy(gates_activity):
# Arrange
prediction_store_policy = 'INVALID_POLICY'
# Act
policy_type, policy_value = gates_activity.get_prediction_store_policy(
prediction_store_policy, metadata)
# Assert
assert policy_type == 'lts'
assert policy_value == 1
def test_get_prediction_store_policy_invalid_policy_value(gates_activity):
# Arrange
prediction_store_policy = 'abc:INVALID_VALUE'
# Act
policy_type, policy_value = gates_activity.get_prediction_store_policy(
prediction_store_policy, metadata)
# Assert
assert policy_type == 'lts'
assert policy_value == 1
def test_get_prediction_store_policy_valid_policy_type(gates_activity):
# Arrange
prediction_store_policy = 'abc:1'
# Act
policy_type, policy_value = gates_activity.get_prediction_store_policy(
prediction_store_policy, metadata)
# Assert
assert policy_type == 'lts'
assert policy_value == 1
def test_get_prediction_store_policy_valid_policy(gates_activity):
# Arrange
prediction_store_policy = 'erl:1'
# Act
policy_type, policy_value = gates_activity.get_prediction_store_policy(
prediction_store_policy, metadata)
# Assert
assert policy_type == 'erl'
assert policy_value == 1
@mark.asyncio @mark.asyncio
async def test_format_prediction(gates_activity): async def test_format_prediction_no_timestamp(gates_activity):
# Arrange # Arrange
input_data = { input_data = {
**metadata, **metadata,
'data': {'prediction': [1], 'response_time': [0.1]}, 'data': {'prediction': [1], 'response_time': [0.1]},
'timestamp': '2023-05-26 11:12:27', 'timestamp': '2023-05-26 11:12:27',
'model_id': 'test_model', 'model_id': 'test_model',
'prediction_confidence': 0.9 'prediction_confidence': 0.9,
'prediction_store_policy': 'lts:1'
} }
# Act # Act
@@ -346,6 +399,83 @@ async def test_format_prediction(gates_activity):
assert result['comments'] == {0: ""} assert result['comments'] == {0: ""}
@mark.asyncio
async def test_format_prediction_with_timestamp_erl(gates_activity):
# Arrange
input_data = {
**metadata,
'data': {'prediction': [1, 2, 3],
'response_time': [0.1, 0.2, 0.3],
'timestamp': ['2023-05-26 11:12:27', '2023-05-26 11:12:28', '2023-05-26 11:12:29']},
'model_id': 'test_model',
'prediction_confidence': 0.9,
'prediction_store_policy': 'erl:2'
}
# Act
result = await gates_activity.format_prediction(input_data)
# Assert
assert result['prediction'] == {0: 2, 1: 1}
assert result['response_time'] == {0: 0.2, 1: 0.1}
assert result['timestamp'] == {
0: '2023-05-26 11:12:28', 1: '2023-05-26 11:12:27'}
assert result['model_id'] == {0: 'test_model', 1: 'test_model'}
assert result['prediction_confidence'] == {0: 0.9, 1: 0.9}
assert result['prediction_status'] == {0: 'Good', 1: 'Good'}
assert result['comments'] == {0: "", 1: ""}
@mark.asyncio
async def test_format_prediction_with_timestamp_lts(gates_activity):
# Arrange
input_data = {
**metadata,
'data': {'prediction': [1, 2, 3],
'response_time': [0.1, 0.2, 0.3],
'timestamp': ['2023-05-26 11:12:27', '2023-05-26 11:12:28', '2023-05-26 11:12:29']},
'model_id': 'test_model',
'prediction_confidence': 0.9,
'prediction_store_policy': 'lts:2'
}
# Act
result = await gates_activity.format_prediction(input_data)
# Assert
assert result['prediction'] == {0: 3, 1: 2}
assert result['response_time'] == {0: 0.3, 1: 0.2}
assert result['timestamp'] == {
0: '2023-05-26 11:12:29', 1: '2023-05-26 11:12:28'}
assert result['model_id'] == {0: 'test_model', 1: 'test_model'}
assert result['prediction_confidence'] == {0: 0.9, 1: 0.9}
assert result['prediction_status'] == {0: 'Good', 1: 'Good'}
assert result['comments'] == {0: "", 1: ""}
@mark.asyncio
async def test_format_prediction_with_timestamp_invalid_policy(gates_activity):
# Arrange
input_data = {
**metadata,
'data': {'prediction': [1, 2, 3],
'response_time': [0.1, 0.2, 0.3],
'timestamp': ['2023-05-26 11:12:27', '2023-05-26 11:12:28', '2023-05-26 11:12:29']},
'model_id': 'test_model',
'prediction_confidence': 0.9,
'prediction_store_policy': 'lts:2'
}
gates_activity.get_prediction_store_policy = MagicMock(
return_value=('invalid', 1))
try:
result = await gates_activity.format_prediction(input_data)
except ValueError as e:
assert str(e) == "Invalid policy type: invalid"
else:
assert False, "Expected ValueError"
@mark.asyncio @mark.asyncio
async def test_format_default_prediction(gates_activity): async def test_format_default_prediction(gates_activity):
# Arrange # Arrange

View File

@@ -82,7 +82,8 @@ async def test_request_transform(mock_max, mock_dataframe, mlflow):
} }
# Mock the transform response # Mock the transform response
expected_response = {'prediction': [0.5, 0.6]} expected_response = {'prediction': [0.5, 0.6], 'timestamp': [
'2024-01-01', '2024-01-02']}
mlflow.model_monitoring_repository.transform.return_value = expected_response mlflow.model_monitoring_repository.transform.return_value = expected_response
mock_dataframe.return_value.sort_values.return_value = mock_dataframe.return_value mock_dataframe.return_value.sort_values.return_value = mock_dataframe.return_value
@@ -98,7 +99,7 @@ async def test_request_transform(mock_max, mock_dataframe, mlflow):
) )
mock_dataframe = mock_dataframe.return_value.pivot.return_value mock_dataframe = mock_dataframe.return_value.pivot.return_value
mock_dataframe.fillna.assert_called_once_with(np.nan, inplace=True) mock_dataframe.fillna.assert_called_once_with(np.nan, inplace=True)
mock_dataframe.reset_index.assert_called_once() # mock_dataframe.reset_index.assert_called_once()
mock_dataframe.columns.name = None mock_dataframe.columns.name = None
# Verify the response # Verify the response

View File

@@ -35,7 +35,8 @@ async def test_run_none_path_flag(workflow_mock, format_and_export_prediction):
"schema": "test_schema", "schema": "test_schema",
"table_name": "test_table", "table_name": "test_table",
"opc_servers": ["test_server"], "opc_servers": ["test_server"],
"opc_output_config": {"test": "config"} "opc_output_config": {"test": "config"},
"prediction_store_policy": "erl:1"
} }
await format_and_export_prediction.run(input_data) await format_and_export_prediction.run(input_data)
@@ -48,6 +49,7 @@ async def test_run_none_path_flag(workflow_mock, format_and_export_prediction):
'timestamp': input_data['timestamp'], 'timestamp': input_data['timestamp'],
'model_id': input_data['model_id'], 'model_id': input_data['model_id'],
'prediction_confidence': input_data['prediction_confidence'], 'prediction_confidence': input_data['prediction_confidence'],
'prediction_store_policy': input_data['prediction_store_policy'],
**metadata **metadata
}, },
retry_policy=ANY, retry_policy=ANY,