Skip to content

Commit 6704033

Browse files
singankitnick863
andauthored
Users/singankit/evaluator crud (#35001)
* Adding tests to capture groundedness with expected values * Evalutors API * Add tests and minor fixes * Add CHANGELOG * Fix linters * Fix mypy * Reuse model operations code for evaluator operations. * Do not allow creating the different type of a model, if the previous version exists. * Do not allow creation of evaluators by ModelOperations * Fixes * Linter fix * Fix * Fix --------- Co-authored-by: nick863 <[email protected]>
1 parent 8e6ed95 commit 6704033

File tree

16 files changed

+1102
-8
lines changed

16 files changed

+1102
-8
lines changed

sdk/ml/azure-ai-ml/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 1.16.0 (unreleased)
44

55
### Features Added
6+
- Add experimental support for working with Promptflow evaluators: `ml_client.evaluators`.
67
- Many changes to the Connection entity class and its associated operations.
78
- Workspace Connection `list`, `get`, and `create_or_update` operations now include an optional `populate_secrets` input, which causes the operations to try making a secondary call to fill in the returned connections' credential info if possible. Only works with api key-based credentials for now.
89
- Many workspace connection subtypes added. The full list of subclasses is now:

sdk/ml/azure-ai-ml/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/ml/azure-ai-ml",
5-
"Tag": "python/ml/azure-ai-ml_8c61dc0136"
5+
"Tag": "python/ml/azure-ai-ml_ce8aa03671"
66
}

sdk/ml/azure-ai-ml/azure/ai/ml/_artifacts/_artifact_utilities.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
FeatureSetOperations,
5757
IndexOperations,
5858
ModelOperations,
59+
EvaluatorOperations,
5960
)
6061
from azure.ai.ml.operations._code_operations import CodeOperations
6162

@@ -463,7 +464,12 @@ def _update_gen2_metadata(name, version, indicator_file, storage_client) -> None
463464
def _check_and_upload_path(
464465
artifact: T,
465466
asset_operations: Union[
466-
"DataOperations", "ModelOperations", "CodeOperations", "FeatureSetOperations", "IndexOperations"
467+
"DataOperations",
468+
"ModelOperations",
469+
"EvaluatorOperations",
470+
"CodeOperations",
471+
"FeatureSetOperations",
472+
"IndexOperations",
467473
],
468474
artifact_type: str,
469475
datastore_name: Optional[str] = None,

sdk/ml/azure-ai-ml/azure/ai/ml/_ml_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
JobOperations,
8787
MarketplaceSubscriptionOperations,
8888
ModelOperations,
89+
EvaluatorOperations,
8990
OnlineDeploymentOperations,
9091
OnlineEndpointOperations,
9192
RegistryOperations,
@@ -537,6 +538,25 @@ def __init__(
537538
registry_reference=registry_reference,
538539
**app_insights_handler_kwargs, # type: ignore[arg-type]
539540
)
541+
# Evaluators
542+
self._evaluators = EvaluatorOperations(
543+
self._operation_scope,
544+
self._operation_config,
545+
(
546+
self._service_client_10_2021_dataplanepreview
547+
if registry_name or registry_reference
548+
else self._service_client_08_2023_preview
549+
),
550+
self._datastores,
551+
self._operation_container,
552+
requests_pipeline=self._requests_pipeline,
553+
control_plane_client=self._service_client_08_2023_preview,
554+
workspace_rg=self._ws_rg,
555+
workspace_sub=self._ws_sub,
556+
registry_reference=registry_reference,
557+
**app_insights_handler_kwargs, # type: ignore[arg-type]
558+
)
559+
540560
self._operation_container.add(AzureMLResourceType.MODEL, self._models)
541561
self._code = CodeOperations(
542562
self._ws_operation_scope if registry_reference else self._operation_scope,
@@ -948,6 +968,16 @@ def models(self) -> ModelOperations:
948968
"""
949969
return self._models
950970

971+
@property
972+
@experimental
973+
def evaluators(self) -> EvaluatorOperations:
974+
"""A collection of model related operations.
975+
976+
:return: Model operations
977+
:rtype: ~azure.ai.ml.operations.ModelOperations
978+
"""
979+
return self._evaluators
980+
951981
@property
952982
def online_endpoints(self) -> OnlineEndpointOperations:
953983
"""A collection of online endpoint related operations.

sdk/ml/azure-ai-ml/azure/ai/ml/_utils/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,3 +1419,11 @@ def extract_name_and_version(azureml_id: str) -> Dict[str, str]:
14191419
"name": name,
14201420
"version": version,
14211421
}
1422+
1423+
1424+
def _get_evaluator_properties():
1425+
return {"is-promptflow": "true", "is-evaluator": "true"}
1426+
1427+
1428+
def _is_evaluator(properties: Dict[str, str]) -> bool:
1429+
return properties.get("is-evaluator") == "true" and properties.get("is-promptflow") == "true"

sdk/ml/azure-ai-ml/azure/ai/ml/operations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from ._connections_operations import ConnectionsOperations
3131
from ._workspace_operations import WorkspaceOperations
3232
from ._workspace_outbound_rule_operations import WorkspaceOutboundRuleOperations
33+
from ._evaluator_operations import EvaluatorOperations
3334
from ._serverless_endpoint_operations import ServerlessEndpointOperations
3435
from ._marketplace_subscription_operations import MarketplaceSubscriptionOperations
3536

@@ -38,6 +39,7 @@
3839
"DatastoreOperations",
3940
"JobOperations",
4041
"ModelOperations",
42+
"EvaluatorOperations",
4143
"WorkspaceOperations",
4244
"RegistryOperations",
4345
"OnlineEndpointOperations",
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
5+
# pylint: disable=protected-access,no-value-for-parameter,disable=docstring-missing-return,docstring-missing-param,docstring-missing-rtype,ungrouped-imports,line-too-long,too-many-statements
6+
7+
from os import PathLike
8+
from typing import Any, Dict, Iterable, Optional, Union, cast
9+
from azure.ai.ml._restclient.v2021_10_01_dataplanepreview import (
10+
AzureMachineLearningWorkspaces as ServiceClient102021Dataplane,
11+
)
12+
from azure.ai.ml._restclient.v2023_08_01_preview import (
13+
AzureMachineLearningWorkspaces as ServiceClient082023Preview,
14+
)
15+
from azure.ai.ml._restclient.v2023_08_01_preview.models import (
16+
ListViewType,
17+
)
18+
from azure.ai.ml._scope_dependent_operations import (
19+
OperationConfig,
20+
OperationsContainer,
21+
OperationScope,
22+
_ScopeDependentOperations,
23+
)
24+
from azure.ai.ml._telemetry import ActivityType, monitor_with_activity
25+
from azure.ai.ml._utils._logger_utils import OpsLogger
26+
from azure.ai.ml._utils.utils import (
27+
_get_evaluator_properties,
28+
_is_evaluator,
29+
)
30+
from azure.ai.ml.entities._assets import Model
31+
from azure.ai.ml.entities._assets.workspace_asset_reference import (
32+
WorkspaceAssetReference,
33+
)
34+
from azure.ai.ml.exceptions import (
35+
UnsupportedOperationError,
36+
)
37+
from azure.ai.ml.operations._datastore_operations import DatastoreOperations
38+
from azure.core.exceptions import ResourceNotFoundError
39+
40+
from azure.ai.ml.operations._model_operations import ModelOperations
41+
42+
ops_logger = OpsLogger(__name__)
43+
module_logger = ops_logger.module_logger
44+
45+
46+
class EvaluatorOperations(_ScopeDependentOperations):
47+
"""EvaluatorOperations.
48+
49+
You should not instantiate this class directly. Instead, you should create an MLClient instance that instantiates it
50+
for you and attaches it as an attribute.
51+
52+
:param operation_scope: Scope variables for the operations classes of an MLClient object.
53+
:type operation_scope: ~azure.ai.ml._scope_dependent_operations.OperationScope
54+
:param operation_config: Common configuration for operations classes of an MLClient object.
55+
:type operation_config: ~azure.ai.ml._scope_dependent_operations.OperationConfig
56+
:param service_client: Service client to allow end users to operate on Azure Machine Learning Workspace
57+
resources (ServiceClient082023Preview or ServiceClient102021Dataplane).
58+
:type service_client: typing.Union[
59+
azure.ai.ml._restclient.v2023_04_01_preview._azure_machine_learning_workspaces.AzureMachineLearningWorkspaces,
60+
azure.ai.ml._restclient.v2021_10_01_dataplanepreview._azure_machine_learning_workspaces.
61+
AzureMachineLearningWorkspaces]
62+
:param datastore_operations: Represents a client for performing operations on Datastores.
63+
:type datastore_operations: ~azure.ai.ml.operations._datastore_operations.DatastoreOperations
64+
:param all_operations: All operations classes of an MLClient object.
65+
:type all_operations: ~azure.ai.ml._scope_dependent_operations.OperationsContainer
66+
"""
67+
68+
# pylint: disable=unused-argument
69+
def __init__(
70+
self,
71+
operation_scope: OperationScope,
72+
operation_config: OperationConfig,
73+
service_client: Union[ServiceClient082023Preview, ServiceClient102021Dataplane],
74+
datastore_operations: DatastoreOperations,
75+
all_operations: Optional[OperationsContainer] = None,
76+
**kwargs,
77+
):
78+
super(EvaluatorOperations, self).__init__(operation_scope, operation_config)
79+
80+
ops_logger.update_info(kwargs)
81+
self._model_op = ModelOperations(
82+
operation_scope=operation_scope,
83+
operation_config=operation_config,
84+
service_client=service_client,
85+
datastore_operations=datastore_operations,
86+
all_operations=all_operations,
87+
**{ModelOperations._IS_EVALUATOR: True},
88+
**kwargs,
89+
)
90+
self._operation_scope = self._model_op._operation_scope
91+
self._datastore_operation = self._model_op._datastore_operation
92+
93+
@monitor_with_activity(ops_logger, "Evaluator.CreateOrUpdate", ActivityType.PUBLICAPI)
94+
def create_or_update( # type: ignore
95+
self, model: Union[Model, WorkspaceAssetReference]
96+
) -> Model: # TODO: Are we going to implement job_name?
97+
"""Returns created or updated model asset.
98+
99+
:param model: Model asset object.
100+
:type model: ~azure.ai.ml.entities.Model
101+
:raises ~azure.ai.ml.exceptions.AssetPathException: Raised when the Model artifact path is
102+
already linked to another asset
103+
:raises ~azure.ai.ml.exceptions.ValidationException: Raised if Model cannot be successfully validated.
104+
Details will be provided in the error message.
105+
:raises ~azure.ai.ml.exceptions.EmptyDirectoryError: Raised if local path provided points to an empty directory.
106+
:return: Model asset object.
107+
:rtype: ~azure.ai.ml.entities.Model
108+
"""
109+
model.properties.update(_get_evaluator_properties())
110+
return self._model_op.create_or_update(model)
111+
112+
def _raise_if_not_evaluator(self, properties: Optional[Dict[str, Any]], message: str) -> None:
113+
"""
114+
:param properties: The properties of a model.
115+
:type properties: dict[str, str]
116+
:param message: The message to be set on exception.
117+
:type message: str
118+
:raises ~azure.ai.ml.exceptions.ValidationException: Raised if model is not an
119+
evaluator.
120+
"""
121+
if properties is not None and not _is_evaluator(properties):
122+
raise ResourceNotFoundError(
123+
message=message,
124+
response=None,
125+
)
126+
127+
@monitor_with_activity(ops_logger, "Evaluator.Get", ActivityType.PUBLICAPI)
128+
def get(self, name: str, version: Optional[str] = None, label: Optional[str] = None) -> Model:
129+
"""Returns information about the specified model asset.
130+
131+
:param name: Name of the model.
132+
:type name: str
133+
:param version: Version of the model.
134+
:type version: str
135+
:param label: Label of the model. (mutually exclusive with version)
136+
:type label: str
137+
:raises ~azure.ai.ml.exceptions.ValidationException: Raised if Model cannot be successfully validated.
138+
Details will be provided in the error message.
139+
:return: Model asset object.
140+
:rtype: ~azure.ai.ml.entities.Model
141+
"""
142+
model = self._model_op.get(name, version, label)
143+
144+
properties = None if model is None else model.properties
145+
self._raise_if_not_evaluator(
146+
properties,
147+
f"Evaluator {name} with version {version} not found.",
148+
)
149+
150+
return model
151+
152+
@monitor_with_activity(ops_logger, "Evaluator.Download", ActivityType.PUBLICAPI)
153+
def download(self, name: str, version: str, download_path: Union[PathLike, str] = ".") -> None:
154+
"""Download files related to a model.
155+
156+
:param name: Name of the model.
157+
:type name: str
158+
:param version: Version of the model.
159+
:type version: str
160+
:param download_path: Local path as download destination, defaults to current working directory of the current
161+
user. Contents will be overwritten.
162+
:type download_path: Union[PathLike, str]
163+
:raises ResourceNotFoundError: if can't find a model matching provided name.
164+
"""
165+
self._model_op.download(name, version, download_path)
166+
167+
@monitor_with_activity(ops_logger, "Evaluator.List", ActivityType.PUBLICAPI)
168+
def list(
169+
self,
170+
name: str,
171+
stage: Optional[str] = None,
172+
*,
173+
list_view_type: ListViewType = ListViewType.ACTIVE_ONLY,
174+
) -> Iterable[Model]:
175+
"""List all model assets in workspace.
176+
177+
:param name: Name of the model.
178+
:type name: str
179+
:param stage: The Model stage
180+
:type stage: Optional[str]
181+
:keyword list_view_type: View type for including/excluding (for example) archived models.
182+
Defaults to :attr:`ListViewType.ACTIVE_ONLY`.
183+
:paramtype list_view_type: ListViewType
184+
:return: An iterator like instance of Model objects
185+
:rtype: ~azure.core.paging.ItemPaged[~azure.ai.ml.entities.Model]
186+
"""
187+
properties_str = "is-promptflow=true,is-evaluator=true"
188+
if name:
189+
return cast(
190+
Iterable[Model],
191+
(
192+
self._model_op._model_versions_operation.list(
193+
name=name,
194+
registry_name=self._model_op._registry_name,
195+
cls=lambda objs: [Model._from_rest_object(obj) for obj in objs],
196+
properties=properties_str,
197+
**self._model_op._scope_kwargs,
198+
)
199+
if self._registry_name
200+
else self._model_op._model_versions_operation.list(
201+
name=name,
202+
workspace_name=self._model_op._workspace_name,
203+
cls=lambda objs: [Model._from_rest_object(obj) for obj in objs],
204+
list_view_type=list_view_type,
205+
properties=properties_str,
206+
stage=stage,
207+
**self._model_op._scope_kwargs,
208+
)
209+
),
210+
)
211+
# ModelContainer object does not carry properties.
212+
raise UnsupportedOperationError("list on evaluation operations without name provided")
213+
# TODO: Implement filtering of the ModelContainerOperations list output
214+
# return cast(
215+
# Iterable[Model], (
216+
# self._model_container_operation.list(
217+
# registry_name=self._registry_name,
218+
# cls=lambda objs: [Model._from_container_rest_object(obj) for obj in objs],
219+
# list_view_type=list_view_type,
220+
# **self._scope_kwargs,
221+
# )
222+
# if self._registry_name
223+
# else self._model_container_operation.list(
224+
# workspace_name=self._workspace_name,
225+
# cls=lambda objs: [Model._from_container_rest_object(obj) for obj in objs],
226+
# list_view_type=list_view_type,
227+
# **self._scope_kwargs,
228+
# )
229+
# )
230+
# )

0 commit comments

Comments
 (0)