beekeeper-monitors-watsonx 1.0.5.post1__py3-none-any.whl → 1.0.7__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.
@@ -0,0 +1,645 @@
1
+ import datetime
2
+ import logging
3
+ import uuid
4
+ from typing import Any, Dict, List, Literal, Union
5
+
6
+ from beekeeper.monitors.watsonx.supporting_classes.credentials import (
7
+ CloudPakforDataCredentials,
8
+ IntegratedSystemCredentials,
9
+ )
10
+ from beekeeper.monitors.watsonx.supporting_classes.enums import Region
11
+ from beekeeper.monitors.watsonx.supporting_classes.metric import (
12
+ WatsonxLocalMetric,
13
+ WatsonxMetric,
14
+ )
15
+ from beekeeper.monitors.watsonx.utils.data_utils import validate_and_filter_dict
16
+ from beekeeper.monitors.watsonx.utils.instrumentation import suppress_output
17
+ from deprecated import deprecated
18
+
19
+
20
+ class WatsonxCustomMetricsManager:
21
+ """
22
+ Provides functionality to set up a custom metric to measure your model's performance with IBM watsonx.governance.
23
+
24
+ Attributes:
25
+ api_key (str): The API key for IBM watsonx.governance.
26
+ region (Region, optional): The region where watsonx.governance is hosted when using IBM Cloud.
27
+ Defaults to `us-south`.
28
+ cpd_creds (CloudPakforDataCredentials, optional): IBM Cloud Pak for Data environment credentials.
29
+
30
+ Example:
31
+ ```python
32
+ from beekeeper.monitors.watsonx import (
33
+ WatsonxCustomMetricsManager,
34
+ CloudPakforDataCredentials,
35
+ )
36
+
37
+ # watsonx.governance (IBM Cloud)
38
+ wxgov_client = WatsonxCustomMetricsManager(api_key="API_KEY")
39
+
40
+ # watsonx.governance (CP4D)
41
+ cpd_creds = CloudPakforDataCredentials(
42
+ url="CPD_URL",
43
+ username="USERNAME",
44
+ password="PASSWORD",
45
+ version="5.0",
46
+ instance_id="openshift",
47
+ )
48
+
49
+ wxgov_client = WatsonxCustomMetricsManager(cpd_creds=cpd_creds)
50
+ ```
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ api_key: str = None,
56
+ region: Union[Region, str] = Region.US_SOUTH,
57
+ cpd_creds: Union[CloudPakforDataCredentials, Dict] = None,
58
+ ) -> None:
59
+ from ibm_cloud_sdk_core.authenticators import IAMAuthenticator # type: ignore
60
+ from ibm_watson_openscale import APIClient as WosAPIClient # type: ignore
61
+
62
+ self.region = Region.from_value(region)
63
+ self._api_key = api_key
64
+ self._wos_client = None
65
+
66
+ if cpd_creds:
67
+ self._wos_cpd_creds = validate_and_filter_dict(
68
+ cpd_creds.to_dict(),
69
+ ["username", "password", "api_key", "disable_ssl_verification"],
70
+ ["url"],
71
+ )
72
+
73
+ if not self._wos_client:
74
+ try:
75
+ if hasattr(self, "_wos_cpd_creds") and self._wos_cpd_creds:
76
+ from ibm_cloud_sdk_core.authenticators import (
77
+ CloudPakForDataAuthenticator, # type: ignore
78
+ )
79
+
80
+ authenticator = CloudPakForDataAuthenticator(**self._wos_cpd_creds)
81
+
82
+ self._wos_client = WosAPIClient(
83
+ authenticator=authenticator,
84
+ service_url=self._wos_cpd_creds["url"],
85
+ )
86
+
87
+ else:
88
+ from ibm_cloud_sdk_core.authenticators import (
89
+ IAMAuthenticator, # type: ignore
90
+ )
91
+
92
+ authenticator = IAMAuthenticator(apikey=self._api_key)
93
+ self._wos_client = WosAPIClient(
94
+ authenticator=authenticator,
95
+ service_url=self.region.openscale,
96
+ )
97
+
98
+ except Exception as e:
99
+ logging.error(
100
+ f"Error connecting to IBM watsonx.governance (openscale): {e}",
101
+ )
102
+ raise
103
+
104
+ def _add_integrated_system(
105
+ self,
106
+ credentials: IntegratedSystemCredentials,
107
+ name: str,
108
+ endpoint: str,
109
+ ) -> str:
110
+ custom_metrics_integrated_system = self._wos_client.integrated_systems.add(
111
+ name=name,
112
+ description="Integrated system created by Beekeeper.",
113
+ type="custom_metrics_provider",
114
+ credentials=credentials.to_dict(),
115
+ connection={"display_name": name, "endpoint": endpoint},
116
+ ).result
117
+
118
+ return custom_metrics_integrated_system.metadata.id
119
+
120
+ def _add_monitor_definitions(
121
+ self,
122
+ name: str,
123
+ metrics: List[WatsonxMetric],
124
+ schedule: bool,
125
+ ):
126
+ from ibm_watson_openscale.base_classes.watson_open_scale_v2 import (
127
+ ApplicabilitySelection,
128
+ MonitorInstanceSchedule,
129
+ MonitorMetricRequest,
130
+ MonitorRuntime,
131
+ ScheduleStartTime,
132
+ )
133
+
134
+ _metrics = [MonitorMetricRequest(**metric.to_dict()) for metric in metrics]
135
+ _monitor_runtime = None
136
+ _monitor_schedule = None
137
+
138
+ if schedule:
139
+ _monitor_runtime = MonitorRuntime(type="custom_metrics_provider")
140
+ _monitor_schedule = MonitorInstanceSchedule(
141
+ repeat_interval=1,
142
+ repeat_unit="hour",
143
+ start_time=ScheduleStartTime(
144
+ type="relative",
145
+ delay_unit="minute",
146
+ delay=30,
147
+ ),
148
+ )
149
+
150
+ custom_monitor_details = self._wos_client.monitor_definitions.add(
151
+ name=name,
152
+ metrics=_metrics,
153
+ tags=[],
154
+ schedule=_monitor_schedule,
155
+ applies_to=ApplicabilitySelection(input_data_type=["unstructured_text"]),
156
+ monitor_runtime=_monitor_runtime,
157
+ background_mode=False,
158
+ ).result
159
+
160
+ return custom_monitor_details.metadata.id
161
+
162
+ def _get_monitor_instance(self, subscription_id: str, monitor_definition_id: str):
163
+ monitor_instances = self._wos_client.monitor_instances.list(
164
+ monitor_definition_id=monitor_definition_id,
165
+ target_target_id=subscription_id,
166
+ ).result.monitor_instances
167
+
168
+ if len(monitor_instances) == 1:
169
+ return monitor_instances[0]
170
+ else:
171
+ return None
172
+
173
+ def _update_monitor_instance(
174
+ self,
175
+ integrated_system_id: str,
176
+ custom_monitor_id: str,
177
+ ):
178
+ payload = [
179
+ {
180
+ "op": "replace",
181
+ "path": "/parameters",
182
+ "value": {
183
+ "custom_metrics_provider_id": integrated_system_id,
184
+ "custom_metrics_wait_time": 60,
185
+ "enable_custom_metric_runs": True,
186
+ },
187
+ },
188
+ ]
189
+
190
+ return self._wos_client.monitor_instances.update(
191
+ custom_monitor_id,
192
+ payload,
193
+ update_metadata_only=True,
194
+ ).result
195
+
196
+ def _get_patch_request_field(
197
+ self,
198
+ field_path: str,
199
+ field_value: Any,
200
+ op_name: str = "replace",
201
+ ) -> Dict:
202
+ return {"op": op_name, "path": field_path, "value": field_value}
203
+
204
+ def _get_dataset_id(
205
+ self,
206
+ subscription_id: str,
207
+ data_set_type: Literal["feedback", "payload_logging"],
208
+ ) -> str:
209
+ data_sets = self._wos_client.data_sets.list(
210
+ target_target_id=subscription_id,
211
+ type=data_set_type,
212
+ ).result.data_sets
213
+ data_set_id = None
214
+ if len(data_sets) > 0:
215
+ data_set_id = data_sets[0].metadata.id
216
+ return data_set_id
217
+
218
+ def _get_dataset_data(self, data_set_id: str):
219
+ json_data = self._wos_client.data_sets.get_list_of_records(
220
+ data_set_id=data_set_id,
221
+ format="list",
222
+ ).result
223
+
224
+ if not json_data.get("records"):
225
+ return None
226
+
227
+ return json_data["records"][0]
228
+
229
+ def _get_existing_data_mart(self):
230
+ data_marts = self._wos_client.data_marts.list().result.data_marts
231
+ if len(data_marts) == 0:
232
+ raise Exception(
233
+ "No data marts found. Please ensure at least one data mart is available.",
234
+ )
235
+
236
+ return data_marts[0].metadata.id
237
+
238
+ # ===== Global Custom Metrics =====
239
+ @deprecated(
240
+ reason="'add_metric_definition()' is deprecated and will be removed in a future version. Use 'create_metric_definition()' instead.",
241
+ version="1.0.6",
242
+ action="always",
243
+ )
244
+ def add_metric_definition(
245
+ self,
246
+ name: str,
247
+ metrics: List[WatsonxMetric],
248
+ integrated_system_url: str,
249
+ integrated_system_credentials: IntegratedSystemCredentials,
250
+ schedule: bool = False,
251
+ ):
252
+ return self.create_metric_definition(
253
+ name=name,
254
+ metrics=metrics,
255
+ integrated_system_url=integrated_system_url,
256
+ integrated_system_credentials=integrated_system_credentials,
257
+ schedule=schedule,
258
+ )
259
+
260
+ def create_metric_definition(
261
+ self,
262
+ name: str,
263
+ metrics: List[WatsonxMetric],
264
+ integrated_system_url: str,
265
+ integrated_system_credentials: IntegratedSystemCredentials,
266
+ schedule: bool = False,
267
+ ):
268
+ """
269
+ Creates a custom metric definition for IBM watsonx.governance.
270
+
271
+ This must be done before using custom metrics.
272
+
273
+ Args:
274
+ name (str): The name of the custom metric group.
275
+ metrics (List[WatsonxMetric]): A list of metrics to be measured.
276
+ schedule (bool, optional): Enable or disable the scheduler. Defaults to `False`.
277
+ integrated_system_url (str): The URL of the external metric provider.
278
+ integrated_system_credentials (IntegratedSystemCredentials): The credentials for the integrated system.
279
+
280
+ Example:
281
+ ```python
282
+ from beekeeper.monitors.watsonx import (
283
+ WatsonxMetric,
284
+ IntegratedSystemCredentials,
285
+ WatsonxMetricThreshold,
286
+ )
287
+
288
+ wxgov_client.create_metric_definition(
289
+ name="Custom Metric - Custom LLM Quality",
290
+ metrics=[
291
+ WatsonxMetric(
292
+ name="context_quality",
293
+ applies_to=[
294
+ "retrieval_augmented_generation",
295
+ "summarization",
296
+ ],
297
+ thresholds=[
298
+ WatsonxMetricThreshold(
299
+ threshold_type="lower_limit", default_value=0.75
300
+ )
301
+ ],
302
+ )
303
+ ],
304
+ integrated_system_url="IS_URL", # URL to the endpoint computing the metric
305
+ integrated_system_credentials=IntegratedSystemCredentials(
306
+ auth_type="basic", username="USERNAME", password="PASSWORD"
307
+ ),
308
+ )
309
+ ```
310
+ """
311
+ integrated_system_id = self._add_integrated_system(
312
+ integrated_system_credentials,
313
+ name,
314
+ integrated_system_url,
315
+ )
316
+
317
+ external_monitor_id = suppress_output(
318
+ self._add_monitor_definitions,
319
+ name,
320
+ metrics,
321
+ schedule,
322
+ )
323
+
324
+ # Associate the external monitor with the integrated system
325
+ payload = [
326
+ {
327
+ "op": "add",
328
+ "path": "/parameters",
329
+ "value": {"monitor_definition_ids": [external_monitor_id]},
330
+ },
331
+ ]
332
+
333
+ self._wos_client.integrated_systems.update(integrated_system_id, payload)
334
+
335
+ return {
336
+ "integrated_system_id": integrated_system_id,
337
+ "monitor_definition_id": external_monitor_id,
338
+ }
339
+
340
+ @deprecated(
341
+ reason="'add_observer_instance()' is deprecated and will be removed in a future version. Use 'attach_monitor_instance()' from 'beekeeper-monitors-watsonx' instead.",
342
+ version="1.0.5",
343
+ action="always",
344
+ )
345
+ def add_observer_instance(
346
+ self,
347
+ integrated_system_id: str,
348
+ monitor_definition_id: str,
349
+ subscription_id: str,
350
+ ):
351
+ return self.attach_monitor_instance(
352
+ integrated_system_id=integrated_system_id,
353
+ monitor_definition_id=monitor_definition_id,
354
+ subscription_id=subscription_id,
355
+ )
356
+
357
+ @deprecated(
358
+ reason="'add_monitor_instance()' is deprecated and will be removed in a future version. Use 'attach_monitor_instance()' from 'beekeeper-monitors-watsonx' instead.",
359
+ version="1.0.6",
360
+ action="always",
361
+ )
362
+ def add_monitor_instance(
363
+ self,
364
+ integrated_system_id: str,
365
+ monitor_definition_id: str,
366
+ subscription_id: str,
367
+ ):
368
+ return self.attach_monitor_instance(
369
+ integrated_system_id=integrated_system_id,
370
+ monitor_definition_id=monitor_definition_id,
371
+ subscription_id=subscription_id,
372
+ )
373
+
374
+ def attach_monitor_instance(
375
+ self,
376
+ integrated_system_id: str,
377
+ monitor_definition_id: str,
378
+ subscription_id: str,
379
+ ):
380
+ """
381
+ Attaches the specified monitor definition to the specified subscription.
382
+
383
+ Args:
384
+ integrated_system_id (str): The ID of the integrated system.
385
+ monitor_definition_id (str): The ID of the custom metric monitor instance.
386
+ subscription_id (str): The ID of the subscription to associate the monitor with.
387
+
388
+ Example:
389
+ ```python
390
+ wxgov_client.attach_monitor_instance(
391
+ integrated_system_id="019667ca-5687-7838-8d29-4ff70c2b36b0",
392
+ monitor_definition_id="custom_llm_quality",
393
+ subscription_id="0195e95d-03a4-7000-b954-b607db10fe9e",
394
+ )
395
+ ```
396
+ """
397
+ from ibm_watson_openscale.base_classes.watson_open_scale_v2 import Target
398
+
399
+ data_marts = self._wos_client.data_marts.list().result.data_marts
400
+ if len(data_marts) == 0:
401
+ raise Exception(
402
+ "No data marts found. Please ensure at least one data mart is available.",
403
+ )
404
+
405
+ data_mart_id = data_marts[0].metadata.id
406
+ existing_monitor_instance = self._get_monitor_instance(
407
+ subscription_id,
408
+ monitor_definition_id,
409
+ )
410
+
411
+ if existing_monitor_instance is None:
412
+ target = Target(target_type="subscription", target_id=subscription_id)
413
+
414
+ parameters = {
415
+ "custom_metrics_provider_id": integrated_system_id,
416
+ "custom_metrics_wait_time": 60,
417
+ "enable_custom_metric_runs": True,
418
+ }
419
+
420
+ monitor_instance_details = suppress_output(
421
+ self._wos_client.monitor_instances.create,
422
+ data_mart_id=data_mart_id,
423
+ background_mode=False,
424
+ monitor_definition_id=monitor_definition_id,
425
+ target=target,
426
+ parameters=parameters,
427
+ ).result
428
+ else:
429
+ existing_instance_id = existing_monitor_instance.metadata.id
430
+ monitor_instance_details = self._update_monitor_instance(
431
+ integrated_system_id,
432
+ existing_instance_id,
433
+ )
434
+
435
+ return monitor_instance_details
436
+
437
+ def publish_metrics(
438
+ self,
439
+ monitor_instance_id: str,
440
+ run_id: str,
441
+ request_records: Dict[str, Union[float, int]],
442
+ ):
443
+ """
444
+ Publishes computed metrics to the specified global monitor instance.
445
+
446
+ Args:
447
+ monitor_instance_id (str): The unique ID of the monitor instance.
448
+ run_id (str): The ID of the monitor run that generated the metrics.
449
+ request_records (Dict[str | float | int]): Dict containing the metrics to be published.
450
+
451
+ Example:
452
+ ```python
453
+ wxgov_client.publish_metrics(
454
+ monitor_instance_id="01966801-f9ee-7248-a706-41de00a8a998",
455
+ run_id="RUN_ID",
456
+ request_records={"context_quality": 0.914, "sensitivity": 0.85},
457
+ )
458
+ ```
459
+ """
460
+ from ibm_watson_openscale.base_classes.watson_open_scale_v2 import (
461
+ MonitorMeasurementRequest,
462
+ Runs,
463
+ )
464
+
465
+ measurement_request = MonitorMeasurementRequest(
466
+ timestamp=datetime.datetime.now(datetime.timezone.utc).strftime(
467
+ "%Y-%m-%dT%H:%M:%S.%fZ",
468
+ ),
469
+ run_id=run_id,
470
+ metrics=[request_records],
471
+ )
472
+
473
+ self._wos_client.monitor_instances.add_measurements(
474
+ monitor_instance_id=monitor_instance_id,
475
+ monitor_measurement_request=[measurement_request],
476
+ ).result
477
+
478
+ run = Runs(watson_open_scale=self._wos_client)
479
+ patch_payload = []
480
+ patch_payload.append(self._get_patch_request_field("/status/state", "finished"))
481
+ patch_payload.append(
482
+ self._get_patch_request_field(
483
+ "/status/completed_at",
484
+ datetime.datetime.now(datetime.timezone.utc).strftime(
485
+ "%Y-%m-%dT%H:%M:%S.%fZ",
486
+ ),
487
+ ),
488
+ )
489
+
490
+ return run.update(
491
+ monitor_instance_id=monitor_instance_id,
492
+ monitoring_run_id=run_id,
493
+ json_patch_operation=patch_payload,
494
+ ).result
495
+
496
+ # ===== Local Custom Metrics =====
497
+ @deprecated(
498
+ reason="'add_local_metric_definition()' is deprecated and will be removed in a future version. Use 'create_local_metric_definition()' from 'beekeeper-monitors-watsonx' instead.",
499
+ version="1.0.6",
500
+ action="always",
501
+ )
502
+ def add_local_metric_definition(
503
+ self,
504
+ name: str,
505
+ metrics: List[WatsonxMetric],
506
+ subscription_id: str,
507
+ ):
508
+ return self.create_local_metric_definition(
509
+ name=name,
510
+ metrics=metrics,
511
+ subscription_id=subscription_id,
512
+ )
513
+
514
+ def create_local_metric_definition(
515
+ self,
516
+ name: str,
517
+ metrics: List[WatsonxLocalMetric],
518
+ subscription_id: str,
519
+ ) -> str:
520
+ """
521
+ Creates a custom metric definition to compute metrics at the local (transaction) level for IBM watsonx.governance.
522
+
523
+ Args:
524
+ name (str): The name of the custom transaction metric group.
525
+ metrics (List[WatsonxLocalMetric]): A list of metrics to be monitored at the local (transaction) level.
526
+ subscription_id (str): The IBM watsonx.governance subscription ID associated with the metric definition.
527
+
528
+ Example:
529
+ ```python
530
+ from beekeeper.monitors.watsonx import WatsonxLocalMetric
531
+
532
+ wxgov_client.create_local_metric_definition(
533
+ name="Custom LLM Local Metric",
534
+ subscription_id="019674ca-0c38-745f-8e9b-58546e95174e",
535
+ metrics=[
536
+ WatsonxLocalMetric(name="context_quality", data_type="double")
537
+ ],
538
+ )
539
+ ```
540
+ """
541
+ from ibm_watson_openscale.base_classes.watson_open_scale_v2 import (
542
+ LocationTableName,
543
+ SparkStruct,
544
+ SparkStructFieldPrimitive,
545
+ Target,
546
+ )
547
+
548
+ target = Target(target_id=subscription_id, target_type="subscription")
549
+ data_mart_id = self._get_existing_data_mart()
550
+ metrics = [SparkStructFieldPrimitive(**metric.to_dict()) for metric in metrics]
551
+
552
+ schema_fields = [
553
+ SparkStructFieldPrimitive(
554
+ name="scoring_id",
555
+ type="string",
556
+ nullable=False,
557
+ ),
558
+ SparkStructFieldPrimitive(
559
+ name="run_id",
560
+ type="string",
561
+ nullable=True,
562
+ ),
563
+ SparkStructFieldPrimitive(
564
+ name="computed_on",
565
+ type="string",
566
+ nullable=False,
567
+ ),
568
+ ]
569
+
570
+ schema_fields.extend(metrics)
571
+
572
+ data_schema = SparkStruct(type="struct", fields=schema_fields)
573
+
574
+ return self._wos_client.data_sets.add(
575
+ target=target,
576
+ name=name,
577
+ type="custom",
578
+ data_schema=data_schema,
579
+ data_mart_id=data_mart_id,
580
+ location=LocationTableName(
581
+ table_name=name.lower().replace(" ", "_") + "_" + str(uuid.uuid4())[:8],
582
+ ),
583
+ background_mode=False,
584
+ ).result.metadata.id
585
+
586
+ def publish_local_metrics(
587
+ self,
588
+ metric_instance_id: str,
589
+ request_records: List[Dict],
590
+ ):
591
+ """
592
+ Publishes computed metrics to the specified transaction record.
593
+
594
+ Args:
595
+ metric_instance_id (str): The unique ID of the custom transaction metric.
596
+ request_records (List[Dict]): A list of dictionaries containing the records to be stored.
597
+
598
+ Example:
599
+ ```python
600
+ wxgov_client.publish_local_metrics(
601
+ metric_instance_id="0196ad39-1b75-7e77-bddb-cc5393d575c2",
602
+ request_records=[
603
+ {
604
+ "scoring_id": "304a9270-44a1-4c4d-bfd4-f756541011f8",
605
+ "run_id": "RUN_ID",
606
+ "computed_on": "payload",
607
+ "context_quality": 0.786,
608
+ }
609
+ ],
610
+ )
611
+ ```
612
+ """
613
+ return self._wos_client.data_sets.store_records(
614
+ data_set_id=metric_instance_id,
615
+ request_body=request_records,
616
+ ).result
617
+
618
+ def list_local_metrics(
619
+ self,
620
+ metric_instance_id: str,
621
+ ):
622
+ """
623
+ Lists records from a custom local metric definition.
624
+
625
+ Args:
626
+ metric_instance_id (str): The unique ID of the custom transaction metric.
627
+
628
+ Example:
629
+ ```python
630
+ wxgov_client.list_local_metrics(
631
+ metric_instance_id="0196ad47-c505-73c0-9d7b-91c082b697e3"
632
+ )
633
+ ```
634
+ """
635
+ return self._get_dataset_data(metric_instance_id)
636
+
637
+
638
+ @deprecated(
639
+ reason="'WatsonxCustomMetric()' is deprecated and will be removed in a future version. "
640
+ "Use 'WatsonxCustomMetricsManager' instead.",
641
+ version="1.0.6",
642
+ action="always",
643
+ )
644
+ class WatsonxCustomMetric(WatsonxCustomMetricsManager):
645
+ pass