mlrun 1.8.0rc18__py3-none-any.whl → 1.8.0rc20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mlrun might be problematic. Click here for more details.

Files changed (31) hide show
  1. mlrun/__main__.py +5 -0
  2. mlrun/common/runtimes/constants.py +17 -0
  3. mlrun/common/schemas/artifact.py +6 -0
  4. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  5. mlrun/common/schemas/model_monitoring/constants.py +16 -0
  6. mlrun/common/schemas/model_monitoring/model_endpoints.py +4 -2
  7. mlrun/config.py +2 -2
  8. mlrun/db/base.py +18 -0
  9. mlrun/db/httpdb.py +118 -1
  10. mlrun/db/nopdb.py +9 -0
  11. mlrun/frameworks/_common/model_handler.py +0 -2
  12. mlrun/model_monitoring/db/tsdb/base.py +116 -8
  13. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +2 -0
  14. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +37 -29
  15. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +46 -26
  16. mlrun/model_monitoring/helpers.py +2 -2
  17. mlrun/model_monitoring/stream_processing.py +21 -0
  18. mlrun/projects/pipelines.py +16 -3
  19. mlrun/projects/project.py +45 -8
  20. mlrun/runtimes/nuclio/serving.py +20 -11
  21. mlrun/serving/v2_serving.py +51 -36
  22. mlrun/utils/helpers.py +163 -1
  23. mlrun/utils/notifications/notification/webhook.py +3 -0
  24. mlrun/utils/notifications/notification_pusher.py +59 -165
  25. mlrun/utils/version/version.json +2 -2
  26. {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/METADATA +1 -1
  27. {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/RECORD +31 -31
  28. {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/LICENSE +0 -0
  29. {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/WHEEL +0 -0
  30. {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/entry_points.txt +0 -0
  31. {mlrun-1.8.0rc18.dist-info → mlrun-1.8.0rc20.dist-info}/top_level.txt +0 -0
mlrun/__main__.py CHANGED
@@ -32,6 +32,7 @@ from tabulate import tabulate
32
32
  import mlrun
33
33
  import mlrun.common.constants as mlrun_constants
34
34
  import mlrun.common.schemas
35
+ import mlrun.utils.helpers
35
36
  from mlrun.common.helpers import parse_versioned_object_uri
36
37
  from mlrun.runtimes.mounts import auto_mount as auto_mount_modifier
37
38
 
@@ -304,6 +305,7 @@ def run(
304
305
  update_in(runtime, "spec.build.code_origin", url_file)
305
306
  elif runtime:
306
307
  runtime = py_eval(runtime)
308
+ runtime = mlrun.utils.helpers.as_dict(runtime)
307
309
  if not isinstance(runtime, dict):
308
310
  print(f"Runtime parameter must be a dict, not {type(runtime)}")
309
311
  exit(1)
@@ -515,6 +517,7 @@ def build(
515
517
 
516
518
  if runtime:
517
519
  runtime = py_eval(runtime)
520
+ runtime = mlrun.utils.helpers.as_dict(runtime)
518
521
  if not isinstance(runtime, dict):
519
522
  print(f"Runtime parameter must be a dict, not {type(runtime)}")
520
523
  exit(1)
@@ -662,6 +665,8 @@ def deploy(
662
665
  runtime = py_eval(spec)
663
666
  else:
664
667
  runtime = {}
668
+
669
+ runtime = mlrun.utils.helpers.as_dict(runtime)
665
670
  if not isinstance(runtime, dict):
666
671
  print(f"Runtime parameter must be a dict, not {type(runtime)}")
667
672
  exit(1)
@@ -214,6 +214,23 @@ class RunStates:
214
214
  RunStates.skipped: mlrun_pipelines.common.models.RunStatuses.skipped,
215
215
  }[run_state]
216
216
 
217
+ @staticmethod
218
+ def pipeline_run_status_to_run_state(pipeline_run_status):
219
+ if pipeline_run_status not in mlrun_pipelines.common.models.RunStatuses.all():
220
+ raise ValueError(f"Invalid pipeline run status: {pipeline_run_status}")
221
+ return {
222
+ mlrun_pipelines.common.models.RunStatuses.succeeded: RunStates.completed,
223
+ mlrun_pipelines.common.models.RunStatuses.failed: RunStates.error,
224
+ mlrun_pipelines.common.models.RunStatuses.running: RunStates.running,
225
+ mlrun_pipelines.common.models.RunStatuses.pending: RunStates.pending,
226
+ mlrun_pipelines.common.models.RunStatuses.canceled: RunStates.aborted,
227
+ mlrun_pipelines.common.models.RunStatuses.canceling: RunStates.aborting,
228
+ mlrun_pipelines.common.models.RunStatuses.skipped: RunStates.skipped,
229
+ mlrun_pipelines.common.models.RunStatuses.runtime_state_unspecified: RunStates.unknown,
230
+ mlrun_pipelines.common.models.RunStatuses.error: RunStates.error,
231
+ mlrun_pipelines.common.models.RunStatuses.paused: RunStates.unknown,
232
+ }[pipeline_run_status]
233
+
217
234
 
218
235
  # TODO: remove this class in 1.9.0 - use only MlrunInternalLabels
219
236
  class RunLabels(enum.Enum):
@@ -51,6 +51,12 @@ class ArtifactCategories(mlrun.common.types.StrEnum):
51
51
  True,
52
52
  )
53
53
 
54
+ @classmethod
55
+ def from_kind(cls, kind: str) -> "ArtifactCategories":
56
+ if kind in [cls.model.value, cls.dataset.value, cls.document.value]:
57
+ return cls(kind)
58
+ return cls.other
59
+
54
60
 
55
61
  class ArtifactIdentifier(pydantic.v1.BaseModel):
56
62
  # artifact kind
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  from .constants import (
16
+ INTERSECT_DICT_KEYS,
16
17
  V3IO_MODEL_MONITORING_DB,
17
18
  ApplicationEvent,
18
19
  ControllerPolicy,
@@ -61,6 +61,7 @@ class ModelEndpointSchema(MonitoringStrEnum):
61
61
  STATE = "state"
62
62
  MONITORING_MODE = "monitoring_mode"
63
63
  FIRST_REQUEST = "first_request"
64
+ SAMPLING_PERCENTAGE = "sampling_percentage"
64
65
 
65
66
  # status - operative
66
67
  LAST_REQUEST = "last_request"
@@ -137,6 +138,10 @@ class EventFieldType:
137
138
  SAMPLE_PARQUET_PATH = "sample_parquet_path"
138
139
  TIME = "time"
139
140
  TABLE_COLUMN = "table_column"
141
+ SAMPLING_PERCENTAGE = "sampling_percentage"
142
+ SAMPLING_RATE = "sampling_rate"
143
+ ESTIMATED_PREDICTION_COUNT = "estimated_prediction_count"
144
+ EFFECTIVE_SAMPLE_COUNT = "effective_sample_count"
140
145
 
141
146
 
142
147
  class FeatureSetFeatures(MonitoringStrEnum):
@@ -248,6 +253,12 @@ class ProjectSecretKeys:
248
253
  ]
249
254
 
250
255
 
256
+ class GetEventsFormat(MonitoringStrEnum):
257
+ SINGLE = "single"
258
+ SEPARATION = "separation"
259
+ INTERSECTION = "intersection"
260
+
261
+
251
262
  class ModelEndpointTargetSchemas(MonitoringStrEnum):
252
263
  V3IO = "v3io"
253
264
  MYSQL = "mysql"
@@ -448,3 +459,8 @@ FQN_REGEX = re.compile(FQN_PATTERN)
448
459
  PROJECT_PATTERN = r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
449
460
 
450
461
  MODEL_ENDPOINT_ID_PATTERN = r"^[a-zA-Z0-9_-]+$"
462
+
463
+ INTERSECT_DICT_KEYS = {
464
+ ModelEndpointMonitoringMetricType.METRIC: "intersect_metrics",
465
+ ModelEndpointMonitoringMetricType.RESULT: "intersect_results",
466
+ }
@@ -160,6 +160,7 @@ class ModelEndpointStatus(ObjectStatus, ModelEndpointParser):
160
160
  state: Optional[str] = "unknown" # will be updated according to the function state
161
161
  first_request: Optional[datetime] = None
162
162
  monitoring_mode: Optional[ModelMonitoringMode] = ModelMonitoringMode.disabled
163
+ sampling_percentage: Optional[float] = 100
163
164
 
164
165
  # operative
165
166
  last_request: Optional[datetime] = None
@@ -177,6 +178,7 @@ class ModelEndpointStatus(ObjectStatus, ModelEndpointParser):
177
178
  "monitoring_mode",
178
179
  "first_request",
179
180
  "last_request",
181
+ "sampling_percentage",
180
182
  ]
181
183
 
182
184
 
@@ -259,12 +261,12 @@ class ModelEndpointMonitoringMetric(BaseModel):
259
261
 
260
262
  def __init__(self, **kwargs):
261
263
  super().__init__(**kwargs)
262
- self.full_name = _compose_full_name(
264
+ self.full_name = compose_full_name(
263
265
  project=self.project, app=self.app, name=self.name, type=self.type
264
266
  )
265
267
 
266
268
 
267
- def _compose_full_name(
269
+ def compose_full_name(
268
270
  *,
269
271
  project: str,
270
272
  app: str,
mlrun/config.py CHANGED
@@ -83,8 +83,8 @@ default_config = {
83
83
  "images_to_enrich_registry": "^mlrun/*,python:3.9",
84
84
  "kfp_url": "",
85
85
  "kfp_ttl": "14400", # KFP ttl in sec, after that completed PODs will be deleted
86
- "kfp_image": "mlrun/mlrun-kfp", # image to use for KFP runner (defaults to mlrun/mlrun-kfp)
87
- "dask_kfp_image": "mlrun/ml-base", # image to use for dask KFP runner (defaults to mlrun/ml-base)
86
+ "kfp_image": "mlrun/mlrun-kfp", # image to use for KFP runner
87
+ "dask_kfp_image": "mlrun/ml-base", # image to use for dask KFP runner
88
88
  "igz_version": "", # the version of the iguazio system the API is running on
89
89
  "iguazio_api_url": "", # the url to iguazio api
90
90
  "spark_app_image": "", # image to use for spark operator app runtime
mlrun/db/base.py CHANGED
@@ -68,6 +68,15 @@ class RunDBInterface(ABC):
68
68
  ):
69
69
  pass
70
70
 
71
+ def push_pipeline_notifications(
72
+ self,
73
+ pipeline_id,
74
+ project="",
75
+ notifications=None,
76
+ timeout=45,
77
+ ):
78
+ pass
79
+
71
80
  @abstractmethod
72
81
  def read_run(
73
82
  self,
@@ -337,6 +346,15 @@ class RunDBInterface(ABC):
337
346
  ) -> list[mm_endpoints.ModelEndpointMonitoringMetric]:
338
347
  pass
339
348
 
349
+ def get_metrics_by_multiple_endpoints(
350
+ self,
351
+ project: str,
352
+ endpoint_ids: Union[str, list[str]],
353
+ type: Literal["results", "metrics", "all"] = "all",
354
+ events_format: mm_constants.GetEventsFormat = mm_constants.GetEventsFormat.SEPARATION,
355
+ ) -> dict[str, list[mm_endpoints.ModelEndpointMonitoringMetric]]:
356
+ pass
357
+
340
358
  @abstractmethod
341
359
  def delete_project(
342
360
  self,
mlrun/db/httpdb.py CHANGED
@@ -780,9 +780,84 @@ class HTTPRunDB(RunDBInterface):
780
780
  )
781
781
  if response.status_code == http.HTTPStatus.ACCEPTED:
782
782
  background_task = mlrun.common.schemas.BackgroundTask(**response.json())
783
- return self._wait_for_background_task_to_reach_terminal_state(
783
+ background_task = self._wait_for_background_task_to_reach_terminal_state(
784
+ background_task.metadata.name, project=project
785
+ )
786
+ if (
787
+ background_task.status.state
788
+ == mlrun.common.schemas.BackgroundTaskState.succeeded
789
+ ):
790
+ logger.info(
791
+ "Notifications for the run have been pushed",
792
+ project=project,
793
+ run_id=uid,
794
+ )
795
+ elif (
796
+ background_task.status.state
797
+ == mlrun.common.schemas.BackgroundTaskState.failed
798
+ ):
799
+ logger.error(
800
+ "Failed to push run notifications",
801
+ project=project,
802
+ run_id=uid,
803
+ error=background_task.status.error,
804
+ )
805
+ return None
806
+
807
+ def push_pipeline_notifications(
808
+ self,
809
+ pipeline_id,
810
+ project="",
811
+ notifications=None,
812
+ timeout=45,
813
+ ):
814
+ """
815
+ Push notifications for a pipeline.
816
+
817
+ :param pipeline_id: Unique ID of the pipeline(KFP).
818
+ :param project: Project that the run belongs to.
819
+ :param notifications: List of notifications to push.
820
+ :returns: :py:class:`~mlrun.common.schemas.BackgroundTask`.
821
+ """
822
+ if notifications is None or type(notifications) is not list:
823
+ raise MLRunInvalidArgumentError(
824
+ "The 'notifications' parameter must be a list."
825
+ )
826
+
827
+ project = project or config.default_project
828
+
829
+ response = self.api_call(
830
+ "POST",
831
+ path=f"projects/{project}/pipelines/{pipeline_id}/push-notifications",
832
+ error="Failed push notifications",
833
+ body=_as_json([notification.to_dict() for notification in notifications]),
834
+ timeout=timeout,
835
+ )
836
+ if response.status_code == http.HTTPStatus.ACCEPTED:
837
+ background_task = mlrun.common.schemas.BackgroundTask(**response.json())
838
+ background_task = self._wait_for_background_task_to_reach_terminal_state(
784
839
  background_task.metadata.name, project=project
785
840
  )
841
+ if (
842
+ background_task.status.state
843
+ == mlrun.common.schemas.BackgroundTaskState.succeeded
844
+ ):
845
+ logger.info(
846
+ "Pipeline notifications have been pushed",
847
+ project=project,
848
+ pipeline_id=pipeline_id,
849
+ )
850
+ elif (
851
+ background_task.status.state
852
+ == mlrun.common.schemas.BackgroundTaskState.failed
853
+ ):
854
+ logger.error(
855
+ "Failed to push pipeline notifications",
856
+ project=project,
857
+ pipeline_id=pipeline_id,
858
+ error=background_task.status.error,
859
+ )
860
+
786
861
  return None
787
862
 
788
863
  def read_run(
@@ -3524,6 +3599,48 @@ class HTTPRunDB(RunDBInterface):
3524
3599
  list[mm_endpoints.ModelEndpointMonitoringMetric], monitoring_metrics
3525
3600
  )
3526
3601
 
3602
+ def get_metrics_by_multiple_endpoints(
3603
+ self,
3604
+ project: str,
3605
+ endpoint_ids: Union[str, list[str]],
3606
+ type: Literal["results", "metrics", "all"] = "all",
3607
+ events_format: mm_constants.GetEventsFormat = mm_constants.GetEventsFormat.SEPARATION,
3608
+ ) -> dict[str, list[mm_endpoints.ModelEndpointMonitoringMetric]]:
3609
+ """Get application metrics/results by endpoint id and project.
3610
+
3611
+ :param project: The name of the project.
3612
+ :param endpoint_ids: The unique id of the model endpoint. Can be a single id or a list of ids.
3613
+ :param type: The type of the metrics to return. "all" means "results" and "metrics".
3614
+ :param events_format: response format:
3615
+
3616
+ separation: {"mep_id1":[...], "mep_id2":[...]}
3617
+ intersection {"intersect_metrics":[], "intersect_results":[]}
3618
+ :return: A dictionary of application metrics and/or results for the model endpoints formatted by events_format.
3619
+ """
3620
+ path = f"projects/{project}/model-endpoints/metrics"
3621
+ params = {
3622
+ "type": type,
3623
+ "endpoint-id": endpoint_ids,
3624
+ "events_format": events_format,
3625
+ }
3626
+ error_message = (
3627
+ f"Failed to get model monitoring metrics,"
3628
+ f" endpoint_ids: {endpoint_ids}, project: {project}"
3629
+ )
3630
+ response = self.api_call(
3631
+ mlrun.common.types.HTTPMethod.GET,
3632
+ path,
3633
+ error_message,
3634
+ params=params,
3635
+ )
3636
+ monitoring_metrics_by_endpoint = response.json()
3637
+ parsed_metrics_by_endpoint = {}
3638
+ for endpoint, metrics in monitoring_metrics_by_endpoint.items():
3639
+ parsed_metrics_by_endpoint[endpoint] = parse_obj_as(
3640
+ list[mm_endpoints.ModelEndpointMonitoringMetric], metrics
3641
+ )
3642
+ return parsed_metrics_by_endpoint
3643
+
3527
3644
  def create_user_secrets(
3528
3645
  self,
3529
3646
  user: str,
mlrun/db/nopdb.py CHANGED
@@ -84,6 +84,15 @@ class NopDB(RunDBInterface):
84
84
  ):
85
85
  pass
86
86
 
87
+ def push_pipeline_notifications(
88
+ self,
89
+ pipeline_id,
90
+ project="",
91
+ notifications=None,
92
+ timeout=45,
93
+ ):
94
+ pass
95
+
87
96
  def list_runtime_resources(
88
97
  self,
89
98
  project: Optional[str] = None,
@@ -976,7 +976,6 @@ class ModelHandler(ABC, Generic[CommonTypes.ModelType, CommonTypes.IOSampleType]
976
976
  custom_objects_map_json,
977
977
  local_path=custom_objects_map_json,
978
978
  artifact_path=self._context.artifact_path,
979
- db_key=False,
980
979
  )
981
980
 
982
981
  # Zip the custom objects directory:
@@ -997,7 +996,6 @@ class ModelHandler(ABC, Generic[CommonTypes.ModelType, CommonTypes.IOSampleType]
997
996
  custom_objects_zip,
998
997
  local_path=custom_objects_zip,
999
998
  artifact_path=self._context.artifact_path,
1000
- db_key=False,
1001
999
  )
1002
1000
 
1003
1001
  return artifacts
@@ -234,14 +234,14 @@ class TSDBConnector(ABC):
234
234
  @abstractmethod
235
235
  def get_metrics_metadata(
236
236
  self,
237
- endpoint_id: str,
237
+ endpoint_id: typing.Union[str, list[str]],
238
238
  start: typing.Optional[datetime] = None,
239
239
  end: typing.Optional[datetime] = None,
240
240
  ) -> pd.DataFrame:
241
241
  """
242
- Fetches distinct metrics metadata from the metrics TSDB table for a specified model endpoint.
242
+ Fetches distinct metrics metadata from the metrics TSDB table for a specified model endpoints.
243
243
 
244
- :param endpoint_id: The model endpoint identifier.
244
+ :param endpoint_id: The model endpoint identifier. Can be a single id or a list of ids.
245
245
  :param start: The start time of the query.
246
246
  :param end: The end time of the query.
247
247
 
@@ -252,14 +252,14 @@ class TSDBConnector(ABC):
252
252
  @abstractmethod
253
253
  def get_results_metadata(
254
254
  self,
255
- endpoint_id: str,
255
+ endpoint_id: typing.Union[str, list[str]],
256
256
  start: typing.Optional[datetime] = None,
257
257
  end: typing.Optional[datetime] = None,
258
258
  ) -> pd.DataFrame:
259
259
  """
260
- Fetches distinct results metadata from the app-results TSDB table for a specified model endpoint.
260
+ Fetches distinct results metadata from the app-results TSDB table for a specified model endpoints.
261
261
 
262
- :param endpoint_id: The model endpoint identifier.
262
+ :param endpoint_id: The model endpoint identifier. Can be a single id or a list of ids.
263
263
  :param start: The start time of the query.
264
264
  :param end: The end time of the query.
265
265
 
@@ -341,7 +341,7 @@ class TSDBConnector(ABC):
341
341
  logger.debug("No metrics", missing_metrics=metrics_without_data.keys())
342
342
  grouped = []
343
343
  for (app_name, name), sub_df in grouped:
344
- full_name = mlrun.model_monitoring.helpers._compose_full_name(
344
+ full_name = mm_schemas.model_endpoints.compose_full_name(
345
345
  project=project,
346
346
  app=app_name,
347
347
  name=name,
@@ -410,7 +410,7 @@ class TSDBConnector(ABC):
410
410
  result_kind = mlrun.model_monitoring.db.tsdb.helpers._get_result_kind(
411
411
  sub_df
412
412
  )
413
- full_name = mlrun.model_monitoring.helpers._compose_full_name(
413
+ full_name = mm_schemas.model_endpoints.compose_full_name(
414
414
  project=project, app=app_name, name=name
415
415
  )
416
416
  try:
@@ -467,6 +467,7 @@ class TSDBConnector(ABC):
467
467
 
468
468
  :return: A list of mm metrics objects.
469
469
  """
470
+
470
471
  return list(
471
472
  map(
472
473
  lambda record: mm_schemas.ModelEndpointMonitoringMetric(
@@ -481,6 +482,113 @@ class TSDBConnector(ABC):
481
482
  )
482
483
  )
483
484
 
485
+ @staticmethod
486
+ def df_to_metrics_grouped_dict(
487
+ *,
488
+ df: pd.DataFrame,
489
+ project: str,
490
+ type: str,
491
+ ) -> dict[str, list[mm_schemas.ModelEndpointMonitoringMetric]]:
492
+ """
493
+ Parse a DataFrame of metrics from the TSDB into a grouped mm metrics objects by endpoint_id.
494
+
495
+ :param df: The DataFrame to parse.
496
+ :param project: The project name.
497
+ :param type: The type of the metrics (either "result" or "metric").
498
+
499
+ :return: A grouped dict of mm metrics/results, using model_endpoints_ids as keys.
500
+ """
501
+
502
+ if df.empty:
503
+ return {}
504
+
505
+ grouped_by_fields = [mm_schemas.WriterEvent.APPLICATION_NAME]
506
+ if type == "result":
507
+ name_column = mm_schemas.ResultData.RESULT_NAME
508
+ grouped_by_fields.append(mm_schemas.ResultData.RESULT_KIND)
509
+ else:
510
+ name_column = mm_schemas.MetricData.METRIC_NAME
511
+
512
+ grouped_by_fields.append(name_column)
513
+ # groupby has different behavior for category columns
514
+ df["endpoint_id"] = df["endpoint_id"].astype(str)
515
+ grouped_by_df = df.groupby("endpoint_id")
516
+ grouped_dict = grouped_by_df.apply(
517
+ lambda group: list(
518
+ map(
519
+ lambda record: mm_schemas.ModelEndpointMonitoringMetric(
520
+ project=project,
521
+ type=type,
522
+ app=record.get(mm_schemas.WriterEvent.APPLICATION_NAME),
523
+ name=record.get(name_column),
524
+ **{"kind": record.get(mm_schemas.ResultData.RESULT_KIND)}
525
+ if type == "result"
526
+ else {},
527
+ ),
528
+ group[grouped_by_fields].to_dict(orient="records"),
529
+ )
530
+ )
531
+ ).to_dict()
532
+ return grouped_dict
533
+
534
+ @staticmethod
535
+ def df_to_events_intersection_dict(
536
+ *,
537
+ df: pd.DataFrame,
538
+ project: str,
539
+ type: typing.Union[str, mm_schemas.ModelEndpointMonitoringMetricType],
540
+ ) -> dict[str, list[mm_schemas.ModelEndpointMonitoringMetric]]:
541
+ """
542
+ Parse a DataFrame of metrics from the TSDB into a dict of intersection metrics/results by name and application
543
+ (and kind in results).
544
+
545
+ :param df: The DataFrame to parse.
546
+ :param project: The project name.
547
+ :param type: The type of the metrics (either "result" or "metric").
548
+
549
+ :return: A dictionary where the key is event type (as defined by `INTERSECT_DICT_KEYS`),
550
+ and the value is a list containing the intersect metrics or results across all endpoint IDs.
551
+
552
+ For example:
553
+ {
554
+ "intersect_metrics": [...]
555
+ }
556
+ """
557
+ dict_key = mm_schemas.INTERSECT_DICT_KEYS[type]
558
+ metrics = []
559
+ if df.empty:
560
+ return {dict_key: []}
561
+
562
+ columns_to_zip = [mm_schemas.WriterEvent.APPLICATION_NAME]
563
+
564
+ if type == "result":
565
+ name_column = mm_schemas.ResultData.RESULT_NAME
566
+ columns_to_zip.append(mm_schemas.ResultData.RESULT_KIND)
567
+ else:
568
+ name_column = mm_schemas.MetricData.METRIC_NAME
569
+ columns_to_zip.insert(1, name_column)
570
+
571
+ # groupby has different behavior for category columns
572
+ df["endpoint_id"] = df["endpoint_id"].astype(str)
573
+ df["event_values"] = list(zip(*[df[col] for col in columns_to_zip]))
574
+ grouped_by_event_values = df.groupby("endpoint_id")["event_values"].apply(set)
575
+ common_event_values_combinations = set.intersection(*grouped_by_event_values)
576
+ result_kind = None
577
+ for data in common_event_values_combinations:
578
+ application_name, event_name = data[0], data[1]
579
+ if len(data) > 2: # in result case
580
+ result_kind = data[2]
581
+ metrics.append(
582
+ mm_schemas.ModelEndpointMonitoringMetric(
583
+ project=project,
584
+ type=type,
585
+ app=application_name,
586
+ name=event_name,
587
+ kind=result_kind,
588
+ )
589
+ )
590
+ return {dict_key: metrics}
591
+
484
592
  @staticmethod
485
593
  def _get_start_end(
486
594
  start: typing.Union[datetime, None],
@@ -298,6 +298,8 @@ class Predictions(TDEngineSchema):
298
298
  mm_schemas.EventFieldType.TIME: _TDEngineColumn.TIMESTAMP,
299
299
  mm_schemas.EventFieldType.LATENCY: _TDEngineColumn.FLOAT,
300
300
  mm_schemas.EventKeyMetrics.CUSTOM_METRICS: _TDEngineColumn.BINARY_1000,
301
+ mm_schemas.EventFieldType.ESTIMATED_PREDICTION_COUNT: _TDEngineColumn.FLOAT,
302
+ mm_schemas.EventFieldType.EFFECTIVE_SAMPLE_COUNT: _TDEngineColumn.INT,
301
303
  }
302
304
  tags = {
303
305
  mm_schemas.WriterEvent.ENDPOINT_ID: _TDEngineColumn.BINARY_64,