azure-ai-evaluation 1.0.1__py3-none-any.whl → 1.2.0__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 azure-ai-evaluation might be problematic. Click here for more details.

Files changed (47) hide show
  1. azure/ai/evaluation/_azure/__init__.py +3 -0
  2. azure/ai/evaluation/_azure/_clients.py +204 -0
  3. azure/ai/evaluation/_azure/_models.py +227 -0
  4. azure/ai/evaluation/_azure/_token_manager.py +118 -0
  5. azure/ai/evaluation/_common/rai_service.py +30 -21
  6. azure/ai/evaluation/_constants.py +19 -0
  7. azure/ai/evaluation/_evaluate/_batch_run/__init__.py +2 -1
  8. azure/ai/evaluation/_evaluate/_batch_run/target_run_context.py +1 -1
  9. azure/ai/evaluation/_evaluate/_eval_run.py +16 -43
  10. azure/ai/evaluation/_evaluate/_evaluate.py +76 -44
  11. azure/ai/evaluation/_evaluate/_utils.py +93 -34
  12. azure/ai/evaluation/_evaluators/_bleu/_bleu.py +46 -25
  13. azure/ai/evaluation/_evaluators/_common/__init__.py +2 -0
  14. azure/ai/evaluation/_evaluators/_common/_base_eval.py +140 -5
  15. azure/ai/evaluation/_evaluators/_common/_base_multi_eval.py +61 -0
  16. azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +12 -1
  17. azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +40 -2
  18. azure/ai/evaluation/_evaluators/_common/_conversation_aggregators.py +49 -0
  19. azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +6 -43
  20. azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +2 -0
  21. azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +2 -0
  22. azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +2 -0
  23. azure/ai/evaluation/_evaluators/_content_safety/_violence.py +2 -0
  24. azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +61 -68
  25. azure/ai/evaluation/_evaluators/_gleu/_gleu.py +45 -23
  26. azure/ai/evaluation/_evaluators/_meteor/_meteor.py +55 -34
  27. azure/ai/evaluation/_evaluators/_qa/_qa.py +32 -27
  28. azure/ai/evaluation/_evaluators/_rouge/_rouge.py +44 -23
  29. azure/ai/evaluation/_evaluators/_similarity/_similarity.py +42 -82
  30. azure/ai/evaluation/_http_utils.py +6 -4
  31. azure/ai/evaluation/_vendor/rouge_score/rouge_scorer.py +0 -4
  32. azure/ai/evaluation/_vendor/rouge_score/scoring.py +0 -4
  33. azure/ai/evaluation/_vendor/rouge_score/tokenize.py +0 -4
  34. azure/ai/evaluation/_version.py +1 -1
  35. azure/ai/evaluation/simulator/_adversarial_scenario.py +2 -0
  36. azure/ai/evaluation/simulator/_adversarial_simulator.py +35 -16
  37. azure/ai/evaluation/simulator/_conversation/__init__.py +128 -7
  38. azure/ai/evaluation/simulator/_conversation/_conversation.py +0 -1
  39. azure/ai/evaluation/simulator/_indirect_attack_simulator.py +1 -0
  40. azure/ai/evaluation/simulator/_model_tools/_rai_client.py +40 -0
  41. azure/ai/evaluation/simulator/_model_tools/_template_handler.py +1 -0
  42. azure/ai/evaluation/simulator/_simulator.py +24 -13
  43. {azure_ai_evaluation-1.0.1.dist-info → azure_ai_evaluation-1.2.0.dist-info}/METADATA +84 -15
  44. {azure_ai_evaluation-1.0.1.dist-info → azure_ai_evaluation-1.2.0.dist-info}/RECORD +47 -41
  45. {azure_ai_evaluation-1.0.1.dist-info → azure_ai_evaluation-1.2.0.dist-info}/NOTICE.txt +0 -0
  46. {azure_ai_evaluation-1.0.1.dist-info → azure_ai_evaluation-1.2.0.dist-info}/WHEEL +0 -0
  47. {azure_ai_evaluation-1.0.1.dist-info → azure_ai_evaluation-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,3 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
@@ -0,0 +1,204 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ from logging import Logger
6
+ from typing import Any, Dict, Final, Optional, Set, Union, cast
7
+ from threading import Lock
8
+ from urllib.parse import quote
9
+ from json.decoder import JSONDecodeError
10
+
11
+ from azure.core.credentials import TokenCredential, AzureSasCredential
12
+ from azure.core.rest import HttpResponse
13
+ from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
14
+ from azure.ai.evaluation._http_utils import HttpPipeline, get_http_client
15
+ from azure.ai.evaluation._azure._token_manager import AzureMLTokenManager
16
+ from azure.ai.evaluation.simulator._model_tools._identity_manager import TokenScope
17
+ from ._models import BlobStoreInfo, Workspace
18
+
19
+
20
+ API_VERSION: Final[str] = "2024-07-01-preview"
21
+ QUERY_KEY_API_VERSION: Final[str] = "api-version"
22
+ PATH_ML_WORKSPACES = ("providers", "Microsoft.MachineLearningServices", "workspaces")
23
+
24
+
25
+ class LiteMLClient:
26
+ """A lightweight Azure ML API client.
27
+
28
+ :param subscription_id: Azure subscription ID
29
+ :type subscription_id: str
30
+ :param resource_group: Azure resource group name
31
+ :type resource_group: str
32
+ :param logger: Logger object
33
+ :type logger: logging.Logger
34
+ :keyword credential: Azure credentials
35
+ :paramtype credential: TokenCredential
36
+ :keyword kwargs: Additional keyword arguments
37
+ :paramtype kwargs: Dict
38
+ :keyword str api_version: The API version. Default is 2024-10-01
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ subscription_id: str,
44
+ resource_group: str,
45
+ logger: Logger,
46
+ credential: Optional[TokenCredential] = None,
47
+ **kwargs: Any,
48
+ ) -> None:
49
+ subscription_id = quote(subscription_id, safe="")
50
+ resource_group = quote(resource_group, safe="")
51
+
52
+ self._base_url: Final[str] = (
53
+ f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
54
+ )
55
+ self._logger: Final[Logger] = logger
56
+ self._api_version: Final[str] = kwargs.get("api_version", API_VERSION)
57
+ self._http_client: Final[HttpPipeline] = get_http_client(**kwargs)
58
+ self._lock: Final[Lock] = Lock()
59
+
60
+ # things that can change under lock
61
+ self._token_manager: Optional[AzureMLTokenManager] = None
62
+ self._credential: Optional[TokenCredential] = credential
63
+
64
+ def get_token(self) -> str:
65
+ return self._get_token_manager().get_token()
66
+
67
+ def get_credential(self) -> TokenCredential:
68
+ # load the token manager to get the credential if needed
69
+ self._get_token_manager()
70
+ return cast(TokenCredential, self._credential)
71
+
72
+ def workspace_get_default_datastore(
73
+ self, workspace_name: str, *, include_credentials: bool = False, **kwargs: Any
74
+ ) -> BlobStoreInfo:
75
+ # 1. Get the default blob store
76
+ # REST API documentation:
77
+ # https://learn.microsoft.com/rest/api/azureml/datastores/list?view=rest-azureml-2024-10-01
78
+ url = self._generate_path( # pylint: disable=specify-parameter-names-in-call
79
+ *PATH_ML_WORKSPACES, workspace_name, "datastores"
80
+ )
81
+ headers = self._get_headers()
82
+
83
+ stores_response = self._http_client.request(
84
+ method="GET",
85
+ url=url,
86
+ params={QUERY_KEY_API_VERSION: self._api_version, "isDefault": True, "count": 1, "orderByAsc": "false"},
87
+ headers=headers,
88
+ )
89
+ self._throw_on_http_error(stores_response, "list default workspace datastore")
90
+
91
+ json = stores_response.json()["value"][0]
92
+ props_json = json["properties"]
93
+ name = json["name"]
94
+ account_name = props_json["accountName"]
95
+ endpoint = props_json["endpoint"]
96
+ container_name = props_json["containerName"]
97
+ credential_type = props_json.get("credentials", {}).get("credentialsType")
98
+
99
+ # 2. Get the SAS token to use for accessing the blob store
100
+ # REST API documentation:
101
+ # https://learn.microsoft.com/rest/api/azureml/datastores/list-secrets?view=rest-azureml-2024-10-01
102
+ blob_store_credential: Optional[Union[AzureSasCredential, TokenCredential, str]]
103
+ if not include_credentials:
104
+ blob_store_credential = None
105
+ elif credential_type and credential_type.lower() == "none":
106
+ # If storage account key access is disabled, and only Microsoft Entra ID authentication is available,
107
+ # the credentialsType will be "None" and we should not attempt to get the secrets.
108
+ blob_store_credential = self.get_credential()
109
+ else:
110
+ url = self._generate_path(
111
+ *PATH_ML_WORKSPACES, workspace_name, "datastores", "workspaceblobstore", "listSecrets"
112
+ )
113
+ secrets_response = self._http_client.request(
114
+ method="POST",
115
+ url=url,
116
+ json={
117
+ "expirableSecret": True,
118
+ "expireAfterHours": int(kwargs.get("key_expiration_hours", 1)),
119
+ },
120
+ params={
121
+ QUERY_KEY_API_VERSION: self._api_version,
122
+ },
123
+ headers=headers,
124
+ )
125
+ self._throw_on_http_error(secrets_response, "workspace datastore secrets")
126
+
127
+ secrets_json = secrets_response.json()
128
+ secrets_type = secrets_json["secretsType"].lower()
129
+
130
+ # As per this website, only SAS tokens, access tokens, or Entra IDs are valid for accessing blob data
131
+ # stores:
132
+ # https://learn.microsoft.com/rest/api/storageservices/authorize-requests-to-azure-storage.
133
+ if secrets_type == "sas":
134
+ blob_store_credential = AzureSasCredential(secrets_json["sasToken"])
135
+ elif secrets_type == "accountkey":
136
+ # To support older versions of azure-storage-blob better, we return a string here instead of
137
+ # an AzureNamedKeyCredential
138
+ blob_store_credential = secrets_json["key"]
139
+ else:
140
+ raise EvaluationException(
141
+ message=f"The '{account_name}' blob store does not use a recognized credential type.",
142
+ internal_message=f"The credential type is '{secrets_type}'",
143
+ target=ErrorTarget.EVALUATE,
144
+ category=ErrorCategory.INVALID_VALUE,
145
+ blame=ErrorBlame.SYSTEM_ERROR,
146
+ )
147
+
148
+ return BlobStoreInfo(name, account_name, endpoint, container_name, blob_store_credential)
149
+
150
+ def workspace_get_info(self, workspace_name: str) -> Workspace:
151
+ # https://learn.microsoft.com/rest/api/azureml/workspaces/get?view=rest-azureml-2024-10-01
152
+ workspace_response = self._http_client.request(
153
+ "GET",
154
+ self._generate_path(*PATH_ML_WORKSPACES, workspace_name),
155
+ params={QUERY_KEY_API_VERSION: self._api_version},
156
+ headers=self._get_headers(),
157
+ )
158
+
159
+ self._throw_on_http_error(workspace_response, f"get '{workspace_name}' workspace")
160
+ workspace = Workspace.deserialize(workspace_response)
161
+ return workspace
162
+
163
+ def _get_token_manager(self) -> AzureMLTokenManager:
164
+ # Lazy init since getting credentials in the constructor can take a long time in some situations
165
+ if self._token_manager is None:
166
+ with self._lock:
167
+ if self._token_manager is None:
168
+ self._token_manager = AzureMLTokenManager(
169
+ TokenScope.DEFAULT_AZURE_MANAGEMENT.value, self._logger, credential=self._credential
170
+ )
171
+ self._credential = self._token_manager.credential
172
+
173
+ return self._token_manager
174
+
175
+ @staticmethod
176
+ def _throw_on_http_error(response: HttpResponse, description: str, valid_status: Optional[Set[int]] = None) -> None:
177
+ if valid_status and (response.status_code in valid_status):
178
+ return
179
+ if response.status_code >= 200 and response.status_code < 300:
180
+ # nothing to see here, move along
181
+ return
182
+
183
+ message = f"The {description} request failed with HTTP {response.status_code}"
184
+ try:
185
+ error_json = response.json()["error"]
186
+ additional_info = f"({error_json['code']}) {error_json['message']}"
187
+ message += f" - {additional_info}"
188
+ except (JSONDecodeError, ValueError, KeyError):
189
+ pass
190
+
191
+ raise EvaluationException(
192
+ message=message,
193
+ target=ErrorTarget.EVALUATE,
194
+ category=ErrorCategory.FAILED_EXECUTION,
195
+ blame=ErrorBlame.SYSTEM_ERROR,
196
+ )
197
+
198
+ def _generate_path(self, *paths: str) -> str:
199
+ sanitized_paths = [quote(path, safe="") for path in paths]
200
+ url = self._base_url + "/" + str.join("/", sanitized_paths)
201
+ return url
202
+
203
+ def _get_headers(self) -> Dict[str, str]:
204
+ return {"Authorization": f"Bearer {self.get_token()}", "Content-Type": "application/json"}
@@ -0,0 +1,227 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+
5
+ # pylint: disable=too-many-instance-attributes
6
+ # pylint: disable=too-many-locals
7
+ # pylint: disable=line-too-long
8
+
9
+ from typing import Dict, List, NamedTuple, Optional, Union
10
+ from msrest.serialization import Model
11
+ from azure.core.credentials import AzureSasCredential, TokenCredential
12
+
13
+
14
+ class BlobStoreInfo(NamedTuple):
15
+ name: str
16
+ account_name: str
17
+ endpoint: str
18
+ container_name: str
19
+ credential: Optional[Union[AzureSasCredential, TokenCredential, str]]
20
+
21
+
22
+ class WorkspaceHubConfig(Model):
23
+ """WorkspaceHub's configuration object."""
24
+
25
+ _attribute_map = {
26
+ "additional_workspace_storage_accounts": {"key": "additionalWorkspaceStorageAccounts", "type": "[str]"},
27
+ "default_workspace_resource_group": {"key": "defaultWorkspaceResourceGroup", "type": "str"},
28
+ }
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ additional_workspace_storage_accounts: Optional[List[str]] = None,
34
+ default_workspace_resource_group: Optional[str] = None,
35
+ **kwargs
36
+ ):
37
+ super(WorkspaceHubConfig, self).__init__(**kwargs)
38
+ self.additional_workspace_storage_accounts = additional_workspace_storage_accounts
39
+ self.default_workspace_resource_group = default_workspace_resource_group
40
+
41
+
42
+ class Workspace(Model):
43
+ """An object that represents a machine learning workspace.
44
+
45
+ Variables are only populated by the server, and will be ignored when sending a request."""
46
+
47
+ _validation = {
48
+ "id": {"readonly": True},
49
+ "name": {"readonly": True},
50
+ "type": {"readonly": True},
51
+ #'system_data': {'readonly': True},
52
+ "agents_endpoint_uri": {"readonly": True},
53
+ "ml_flow_tracking_uri": {"readonly": True},
54
+ #'notebook_info': {'readonly': True},
55
+ "private_endpoint_connections": {"readonly": True},
56
+ #'private_link_count': {'readonly': True},
57
+ "provisioning_state": {"readonly": True},
58
+ "service_provisioned_resource_group": {"readonly": True},
59
+ "storage_hns_enabled": {"readonly": True},
60
+ "tenant_id": {"readonly": True},
61
+ "workspace_id": {"readonly": True},
62
+ }
63
+
64
+ _attribute_map = {
65
+ "id": {"key": "id", "type": "str"},
66
+ "name": {"key": "name", "type": "str"},
67
+ "type": {"key": "type", "type": "str"},
68
+ #'system_data': {'key': 'systemData', 'type': 'SystemData'},
69
+ #'identity': {'key': 'identity', 'type': 'ManagedServiceIdentity'},
70
+ "kind": {"key": "kind", "type": "str"},
71
+ "location": {"key": "location", "type": "str"},
72
+ #'sku': {'key': 'sku', 'type': 'Sku'},
73
+ "tags": {"key": "tags", "type": "{str}"},
74
+ "agents_endpoint_uri": {"key": "properties.agentsEndpointUri", "type": "str"},
75
+ "allow_public_access_when_behind_vnet": {"key": "properties.allowPublicAccessWhenBehindVnet", "type": "bool"},
76
+ "allow_role_assignment_on_rg": {"key": "properties.allowRoleAssignmentOnRG", "type": "bool"},
77
+ "application_insights": {"key": "properties.applicationInsights", "type": "str"},
78
+ "associated_workspaces": {"key": "properties.associatedWorkspaces", "type": "[str]"},
79
+ "container_registries": {"key": "properties.containerRegistries", "type": "[str]"},
80
+ "container_registry": {"key": "properties.containerRegistry", "type": "str"},
81
+ "description": {"key": "properties.description", "type": "str"},
82
+ "discovery_url": {"key": "properties.discoveryUrl", "type": "str"},
83
+ "enable_data_isolation": {"key": "properties.enableDataIsolation", "type": "bool"},
84
+ "enable_service_side_cmk_encryption": {"key": "properties.enableServiceSideCMKEncryption", "type": "bool"},
85
+ "enable_simplified_cmk": {"key": "properties.enableSimplifiedCmk", "type": "bool"},
86
+ "enable_software_bill_of_materials": {"key": "properties.enableSoftwareBillOfMaterials", "type": "bool"},
87
+ #'encryption': {'key': 'properties.encryption', 'type': 'EncryptionProperty'},
88
+ "existing_workspaces": {"key": "properties.existingWorkspaces", "type": "[str]"},
89
+ #'feature_store_settings': {'key': 'properties.featureStoreSettings', 'type': 'FeatureStoreSettings'},
90
+ "friendly_name": {"key": "properties.friendlyName", "type": "str"},
91
+ "hbi_workspace": {"key": "properties.hbiWorkspace", "type": "bool"},
92
+ "hub_resource_id": {"key": "properties.hubResourceId", "type": "str"},
93
+ "image_build_compute": {"key": "properties.imageBuildCompute", "type": "str"},
94
+ "ip_allowlist": {"key": "properties.ipAllowlist", "type": "[str]"},
95
+ "key_vault": {"key": "properties.keyVault", "type": "str"},
96
+ "key_vaults": {"key": "properties.keyVaults", "type": "[str]"},
97
+ #'managed_network': {'key': 'properties.managedNetwork', 'type': 'ManagedNetworkSettings'},
98
+ "ml_flow_tracking_uri": {"key": "properties.mlFlowTrackingUri", "type": "str"},
99
+ #'network_acls': {'key': 'properties.networkAcls', 'type': 'NetworkAcls'},
100
+ #'notebook_info': {'key': 'properties.notebookInfo', 'type': 'NotebookResourceInfo'},
101
+ "primary_user_assigned_identity": {"key": "properties.primaryUserAssignedIdentity", "type": "str"},
102
+ "private_endpoint_connections": {
103
+ "key": "properties.privateEndpointConnections",
104
+ "type": "[PrivateEndpointConnection]",
105
+ },
106
+ "private_link_count": {"key": "properties.privateLinkCount", "type": "int"},
107
+ "provision_network_now": {"key": "properties.provisionNetworkNow", "type": "bool"},
108
+ "provisioning_state": {"key": "properties.provisioningState", "type": "str"},
109
+ #'public_network_access': {'key': 'properties.publicNetworkAccess', 'type': 'str'},
110
+ #'serverless_compute_settings': {'key': 'properties.serverlessComputeSettings', 'type': 'ServerlessComputeSettings'},
111
+ #'service_managed_resources_settings': {'key': 'properties.serviceManagedResourcesSettings', 'type': 'ServiceManagedResourcesSettings'},
112
+ "service_provisioned_resource_group": {"key": "properties.serviceProvisionedResourceGroup", "type": "str"},
113
+ #'shared_private_link_resources': {'key': 'properties.sharedPrivateLinkResources', 'type': '[SharedPrivateLinkResource]'},
114
+ "soft_delete_retention_in_days": {"key": "properties.softDeleteRetentionInDays", "type": "int"},
115
+ "storage_account": {"key": "properties.storageAccount", "type": "str"},
116
+ "storage_accounts": {"key": "properties.storageAccounts", "type": "[str]"},
117
+ "storage_hns_enabled": {"key": "properties.storageHnsEnabled", "type": "bool"},
118
+ #'system_datastores_auth_mode': {'key': 'properties.systemDatastoresAuthMode', 'type': 'str'},
119
+ "tenant_id": {"key": "properties.tenantId", "type": "str"},
120
+ "v1_legacy_mode": {"key": "properties.v1LegacyMode", "type": "bool"},
121
+ "workspace_hub_config": {"key": "properties.workspaceHubConfig", "type": "WorkspaceHubConfig"},
122
+ "workspace_id": {"key": "properties.workspaceId", "type": "str"},
123
+ }
124
+
125
+ def __init__(
126
+ self,
127
+ *,
128
+ # system_data: Optional[SystemData] = None,
129
+ # identity: Optional["ManagedServiceIdentity"] = None,
130
+ kind: Optional[str] = None,
131
+ location: Optional[str] = None,
132
+ # sku: Optional["Sku"] = None,
133
+ tags: Optional[Dict[str, str]] = None,
134
+ allow_public_access_when_behind_vnet: Optional[bool] = None,
135
+ allow_role_assignment_on_rg: Optional[bool] = None,
136
+ application_insights: Optional[str] = None,
137
+ associated_workspaces: Optional[List[str]] = None,
138
+ container_registries: Optional[List[str]] = None,
139
+ container_registry: Optional[str] = None,
140
+ description: Optional[str] = None,
141
+ discovery_url: Optional[str] = None,
142
+ enable_data_isolation: Optional[bool] = None,
143
+ enable_service_side_cmk_encryption: Optional[bool] = None,
144
+ enable_simplified_cmk: Optional[bool] = None,
145
+ enable_software_bill_of_materials: Optional[bool] = None,
146
+ # encryption: Optional["EncryptionProperty"] = None,
147
+ existing_workspaces: Optional[List[str]] = None,
148
+ # feature_store_settings: Optional["FeatureStoreSettings"] = None,
149
+ friendly_name: Optional[str] = None,
150
+ hbi_workspace: Optional[bool] = None,
151
+ hub_resource_id: Optional[str] = None,
152
+ image_build_compute: Optional[str] = None,
153
+ ip_allowlist: Optional[List[str]] = None,
154
+ key_vault: Optional[str] = None,
155
+ key_vaults: Optional[List[str]] = None,
156
+ # managed_network: Optional["ManagedNetworkSettings"] = None,
157
+ # network_acls: Optional["NetworkAcls"] = None,
158
+ primary_user_assigned_identity: Optional[str] = None,
159
+ provision_network_now: Optional[bool] = None,
160
+ # public_network_access: Optional[Union[str, "PublicNetworkAccessType"]] = None,
161
+ # serverless_compute_settings: Optional["ServerlessComputeSettings"] = None,
162
+ # service_managed_resources_settings: Optional["ServiceManagedResourcesSettings"] = None,
163
+ # shared_private_link_resources: Optional[List["SharedPrivateLinkResource"]] = None,
164
+ soft_delete_retention_in_days: Optional[int] = None,
165
+ storage_account: Optional[str] = None,
166
+ storage_accounts: Optional[List[str]] = None,
167
+ # system_datastores_auth_mode: Optional[Union[str, "SystemDatastoresAuthMode"]] = None,
168
+ v1_legacy_mode: Optional[bool] = None,
169
+ workspace_hub_config: Optional["WorkspaceHubConfig"] = None,
170
+ **kwargs
171
+ ):
172
+ super(Workspace, self).__init__(**kwargs)
173
+ self.id: Optional[str] = None
174
+ self.name: Optional[str] = None
175
+ self.type: Optional[str] = None
176
+ # self.system_data = system_data
177
+ # self.identity = identity
178
+ self.kind = kind
179
+ self.location = location
180
+ # self.sku = sku
181
+ self.tags = tags
182
+ self.agents_endpoint_uri = None
183
+ self.allow_public_access_when_behind_vnet = allow_public_access_when_behind_vnet
184
+ self.allow_role_assignment_on_rg = allow_role_assignment_on_rg
185
+ self.application_insights = application_insights
186
+ self.associated_workspaces = associated_workspaces
187
+ self.container_registries = container_registries
188
+ self.container_registry = container_registry
189
+ self.description = description
190
+ self.discovery_url = discovery_url
191
+ self.enable_data_isolation = enable_data_isolation
192
+ self.enable_service_side_cmk_encryption = enable_service_side_cmk_encryption
193
+ self.enable_simplified_cmk = enable_simplified_cmk
194
+ self.enable_software_bill_of_materials = enable_software_bill_of_materials
195
+ # self.encryption = encryption
196
+ self.existing_workspaces = existing_workspaces
197
+ # self.feature_store_settings = feature_store_settings
198
+ self.friendly_name = friendly_name
199
+ self.hbi_workspace = hbi_workspace
200
+ self.hub_resource_id = hub_resource_id
201
+ self.image_build_compute = image_build_compute
202
+ self.ip_allowlist = ip_allowlist
203
+ self.key_vault = key_vault
204
+ self.key_vaults = key_vaults
205
+ # self.managed_network = managed_network
206
+ self.ml_flow_tracking_uri = None
207
+ # self.network_acls = network_acls
208
+ # self.notebook_info = None
209
+ self.primary_user_assigned_identity = primary_user_assigned_identity
210
+ self.private_endpoint_connections = None
211
+ self.private_link_count = None
212
+ self.provision_network_now = provision_network_now
213
+ self.provisioning_state = None
214
+ # self.public_network_access = public_network_access
215
+ # self.serverless_compute_settings = serverless_compute_settings
216
+ # self.service_managed_resources_settings = service_managed_resources_settings
217
+ self.service_provisioned_resource_group = None
218
+ # self.shared_private_link_resources = shared_private_link_resources
219
+ self.soft_delete_retention_in_days = soft_delete_retention_in_days
220
+ self.storage_account = storage_account
221
+ self.storage_accounts = storage_accounts
222
+ self.storage_hns_enabled = None
223
+ # self.system_datastores_auth_mode = system_datastores_auth_mode
224
+ self.tenant_id = None
225
+ self.v1_legacy_mode = v1_legacy_mode
226
+ self.workspace_hub_config = workspace_hub_config
227
+ self.workspace_id = None
@@ -0,0 +1,118 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
4
+ import os
5
+ import logging
6
+ import time
7
+ import inspect
8
+ from typing import cast, Optional, Union
9
+
10
+ from azure.core.credentials import TokenCredential, AccessToken
11
+ from azure.identity import AzureCliCredential, DefaultAzureCredential, ManagedIdentityCredential
12
+ from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
13
+ from ..simulator._model_tools._identity_manager import APITokenManager, AZURE_TOKEN_REFRESH_INTERVAL
14
+
15
+
16
+ class AzureMLTokenManager(APITokenManager):
17
+ """API Token manager for Azure Management API.
18
+
19
+ :param token_scope: Token scopes for Azure endpoint
20
+ :type token_scope: str
21
+ :param logger: Logger object
22
+ :type logger: logging.Logger
23
+ :keyword kwargs: Additional keyword arguments
24
+ :paramtype kwargs: Dict
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ token_scope: str,
30
+ logger: logging.Logger,
31
+ credential: Optional[TokenCredential] = None,
32
+ ):
33
+ super().__init__(logger, credential=credential)
34
+ self.token_scope = token_scope
35
+ self.token_expiry_time: Optional[int] = None
36
+
37
+ def get_aad_credential(self) -> Union[DefaultAzureCredential, ManagedIdentityCredential]:
38
+ """Get the Azure credentials to use for the management APIs.
39
+
40
+ :return: Azure credentials
41
+ :rtype: DefaultAzureCredential or ManagedIdentityCredential
42
+ """
43
+ # Adds some of the additional types credentials that the previous Azure AI ML code used
44
+ # These may or may not be needed but kept here for backwards compatibility
45
+
46
+ if os.getenv("AZUREML_OBO_ENABLED"):
47
+ # using Azure on behalf of credentials requires the use of the azure-ai-ml package
48
+ try:
49
+ from azure.ai.ml.identity import AzureMLOnBehalfOfCredential
50
+
51
+ self.logger.debug("User identity is configured, use OBO credential.")
52
+ return AzureMLOnBehalfOfCredential() # type: ignore
53
+ except (ModuleNotFoundError, ImportError):
54
+ raise EvaluationException( # pylint: disable=raise-missing-from
55
+ message=(
56
+ "The required packages for OBO credentials are missing.\n"
57
+ 'To resolve this, please install them by running "pip install azure-ai-ml".'
58
+ ),
59
+ target=ErrorTarget.EVALUATE,
60
+ category=ErrorCategory.MISSING_PACKAGE,
61
+ blame=ErrorBlame.USER_ERROR,
62
+ )
63
+ elif os.environ.get("PF_USE_AZURE_CLI_CREDENTIAL", "false").lower() == "true":
64
+ self.logger.debug("Use azure cli credential since specified in environment variable.")
65
+ return AzureCliCredential() # type: ignore
66
+ elif os.environ.get("IS_IN_CI_PIPELINE", "false").lower() == "true":
67
+ # use managed identity when executing in CI pipeline.
68
+ self.logger.debug("Use azure cli credential since in CI pipeline.")
69
+ return AzureCliCredential() # type: ignore
70
+ else:
71
+ # Fall back to using the parent implementation
72
+ return super().get_aad_credential()
73
+
74
+ def get_token(self) -> str:
75
+ """Get the API token. If the token is not available or has expired, refresh the token.
76
+
77
+ :return: API token
78
+ :rtype: str
79
+ """
80
+ if self._token_needs_update():
81
+ credential = cast(TokenCredential, self.credential)
82
+ access_token = credential.get_token(self.token_scope)
83
+ self._update_token(access_token)
84
+
85
+ return cast(str, self.token) # check for none is hidden in the _token_needs_update method
86
+
87
+ async def get_token_async(self) -> str:
88
+ """Get the API token asynchronously. If the token is not available or has expired, refresh it.
89
+
90
+ :return: API token
91
+ :rtype: str
92
+ """
93
+ if self._token_needs_update():
94
+ credential = cast(TokenCredential, self.credential)
95
+ get_token_method = credential.get_token(self.token_scope)
96
+ if inspect.isawaitable(get_token_method):
97
+ access_token = await get_token_method
98
+ else:
99
+ access_token = get_token_method
100
+ self._update_token(access_token)
101
+
102
+ return cast(str, self.token) # check for none is hidden in the _token_needs_update method
103
+
104
+ def _token_needs_update(self) -> bool:
105
+ current_time = time.time()
106
+ return (
107
+ self.token is None
108
+ or self.last_refresh_time is None
109
+ or self.token_expiry_time is None
110
+ or self.token_expiry_time - current_time < AZURE_TOKEN_REFRESH_INTERVAL
111
+ or current_time - self.last_refresh_time > AZURE_TOKEN_REFRESH_INTERVAL
112
+ )
113
+
114
+ def _update_token(self, access_token: AccessToken) -> None:
115
+ self.token = cast(str, access_token.token)
116
+ self.token_expiry_time = access_token.expires_on
117
+ self.last_refresh_time = time.time()
118
+ self.logger.info("Refreshed Azure management token.")
@@ -72,18 +72,21 @@ def get_formatted_template(data: dict, annotation_task: str) -> str:
72
72
  return user_text.replace("'", '\\"')
73
73
 
74
74
 
75
- def get_common_headers(token: str) -> Dict:
75
+ def get_common_headers(token: str, evaluator_name: Optional[str] = None) -> Dict:
76
76
  """Get common headers for the HTTP request
77
77
 
78
78
  :param token: The Azure authentication token.
79
79
  :type token: str
80
+ :param evaluator_name: The evaluator name. Default is None.
81
+ :type evaluator_name: str
80
82
  :return: The common headers.
81
83
  :rtype: Dict
82
84
  """
85
+ user_agent = f"{USER_AGENT} (type=evaluator; subtype={evaluator_name})" if evaluator_name else USER_AGENT
83
86
  return {
84
87
  "Authorization": f"Bearer {token}",
85
88
  "Content-Type": "application/json",
86
- "User-Agent": USER_AGENT,
89
+ "User-Agent": user_agent,
87
90
  # Handle "RuntimeError: Event loop is closed" from httpx AsyncClient
88
91
  # https://github.com/encode/httpx/discussions/2959
89
92
  "Connection": "close",
@@ -175,7 +178,9 @@ def generate_payload(normalized_user_text: str, metric: str, annotation_task: st
175
178
  )
176
179
 
177
180
 
178
- async def submit_request(data: dict, metric: str, rai_svc_url: str, token: str, annotation_task: str) -> str:
181
+ async def submit_request(
182
+ data: dict, metric: str, rai_svc_url: str, token: str, annotation_task: str, evaluator_name: str
183
+ ) -> str:
179
184
  """Submit request to Responsible AI service for evaluation and return operation ID
180
185
 
181
186
  :param data: The data to evaluate.
@@ -188,6 +193,8 @@ async def submit_request(data: dict, metric: str, rai_svc_url: str, token: str,
188
193
  :type token: str
189
194
  :param annotation_task: The annotation task to use.
190
195
  :type annotation_task: str
196
+ :param evaluator_name: The evaluator name.
197
+ :type evaluator_name: str
191
198
  :return: The operation ID.
192
199
  :rtype: str
193
200
  """
@@ -195,7 +202,7 @@ async def submit_request(data: dict, metric: str, rai_svc_url: str, token: str,
195
202
  payload = generate_payload(normalized_user_text, metric, annotation_task=annotation_task)
196
203
 
197
204
  url = rai_svc_url + "/submitannotation"
198
- headers = get_common_headers(token)
205
+ headers = get_common_headers(token, evaluator_name)
199
206
 
200
207
  async with get_async_http_client_with_timeout() as client:
201
208
  http_response = await client.post(url, json=payload, headers=headers)
@@ -493,24 +500,26 @@ async def evaluate_with_rai_service(
493
500
  credential: TokenCredential,
494
501
  annotation_task: str = Tasks.CONTENT_HARM,
495
502
  metric_display_name=None,
503
+ evaluator_name=None,
496
504
  ) -> Dict[str, Union[str, float]]:
497
- """ "Evaluate the content safety of the response using Responsible AI service
505
+ """Evaluate the content safety of the response using Responsible AI service
498
506
 
499
- :param data: The data to evaluate.
500
- :type data: dict
501
- :param metric_name: The evaluation metric to use.
502
- :type metric_name: str
503
- :param project_scope: The Azure AI project scope details.
504
- :type project_scope: Dict
505
- :param credential: The Azure authentication credential.
506
- :type credential:
507
- ~azure.core.credentials.TokenCredential
508
- :param annotation_task: The annotation task to use.
509
- :type annotation_task: str
510
- :param metric_display_name: The display name of metric to use.
511
- :type metric_display_name: str
512
- :return: The parsed annotation result.
513
- :rtype: Dict[str, Union[str, float]]
507
+ :param data: The data to evaluate.
508
+ :type data: dict
509
+ :param metric_name: The evaluation metric to use.
510
+ :type metric_name: str
511
+ :param project_scope: The Azure AI project scope details.
512
+ :type project_scope: Dict
513
+ :param credential: The Azure authentication credential.
514
+ :type credential: ~azure.core.credentials.TokenCredential
515
+ :param annotation_task: The annotation task to use.
516
+ :type annotation_task: str
517
+ :param metric_display_name: The display name of metric to use.
518
+ :type metric_display_name: str
519
+ :param evaluator_name: The evaluator name to use.
520
+ :type evaluator_name: str
521
+ :return: The parsed annotation result.
522
+ :rtype: Dict[str, Union[str, float]]
514
523
  """
515
524
 
516
525
  # Get RAI service URL from discovery service and check service availability
@@ -519,7 +528,7 @@ async def evaluate_with_rai_service(
519
528
  await ensure_service_availability(rai_svc_url, token, annotation_task)
520
529
 
521
530
  # Submit annotation request and fetch result
522
- operation_id = await submit_request(data, metric_name, rai_svc_url, token, annotation_task)
531
+ operation_id = await submit_request(data, metric_name, rai_svc_url, token, annotation_task, evaluator_name)
523
532
  annotation_response = cast(List[Dict], await fetch_result(operation_id, rai_svc_url, credential, token))
524
533
  result = parse_response(annotation_response, metric_name, metric_display_name)
525
534