azure-ai-evaluation 1.5.0__py3-none-any.whl → 1.7.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.
- azure/ai/evaluation/__init__.py +10 -0
- azure/ai/evaluation/_aoai/__init__.py +10 -0
- azure/ai/evaluation/_aoai/aoai_grader.py +89 -0
- azure/ai/evaluation/_aoai/label_grader.py +66 -0
- azure/ai/evaluation/_aoai/string_check_grader.py +65 -0
- azure/ai/evaluation/_aoai/text_similarity_grader.py +88 -0
- azure/ai/evaluation/_azure/_clients.py +4 -4
- azure/ai/evaluation/_azure/_envs.py +208 -0
- azure/ai/evaluation/_azure/_token_manager.py +12 -7
- azure/ai/evaluation/_common/__init__.py +7 -0
- azure/ai/evaluation/_common/evaluation_onedp_client.py +163 -0
- azure/ai/evaluation/_common/onedp/__init__.py +32 -0
- azure/ai/evaluation/_common/onedp/_client.py +139 -0
- azure/ai/evaluation/_common/onedp/_configuration.py +73 -0
- azure/ai/evaluation/_common/onedp/_model_base.py +1232 -0
- azure/ai/evaluation/_common/onedp/_patch.py +21 -0
- azure/ai/evaluation/_common/onedp/_serialization.py +2032 -0
- azure/ai/evaluation/_common/onedp/_types.py +21 -0
- azure/ai/evaluation/_common/onedp/_validation.py +50 -0
- azure/ai/evaluation/_common/onedp/_vendor.py +50 -0
- azure/ai/evaluation/_common/onedp/_version.py +9 -0
- azure/ai/evaluation/_common/onedp/aio/__init__.py +29 -0
- azure/ai/evaluation/_common/onedp/aio/_client.py +143 -0
- azure/ai/evaluation/_common/onedp/aio/_configuration.py +75 -0
- azure/ai/evaluation/_common/onedp/aio/_patch.py +21 -0
- azure/ai/evaluation/_common/onedp/aio/_vendor.py +40 -0
- azure/ai/evaluation/_common/onedp/aio/operations/__init__.py +39 -0
- azure/ai/evaluation/_common/onedp/aio/operations/_operations.py +4494 -0
- azure/ai/evaluation/_common/onedp/aio/operations/_patch.py +21 -0
- azure/ai/evaluation/_common/onedp/models/__init__.py +142 -0
- azure/ai/evaluation/_common/onedp/models/_enums.py +162 -0
- azure/ai/evaluation/_common/onedp/models/_models.py +2228 -0
- azure/ai/evaluation/_common/onedp/models/_patch.py +21 -0
- azure/ai/evaluation/_common/onedp/operations/__init__.py +39 -0
- azure/ai/evaluation/_common/onedp/operations/_operations.py +5655 -0
- azure/ai/evaluation/_common/onedp/operations/_patch.py +21 -0
- azure/ai/evaluation/_common/onedp/py.typed +1 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/__init__.py +1 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/aio/__init__.py +1 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/aio/operations/__init__.py +25 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/aio/operations/_operations.py +34 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/aio/operations/_patch.py +20 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/__init__.py +1 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/__init__.py +1 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/operations/__init__.py +22 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/operations/_operations.py +29 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/operations/_patch.py +20 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/operations/__init__.py +22 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/operations/_operations.py +29 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/operations/_patch.py +20 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/operations/__init__.py +25 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/operations/_operations.py +34 -0
- azure/ai/evaluation/_common/onedp/servicepatterns/operations/_patch.py +20 -0
- azure/ai/evaluation/_common/rai_service.py +165 -34
- azure/ai/evaluation/_common/raiclient/_version.py +1 -1
- azure/ai/evaluation/_common/utils.py +79 -1
- azure/ai/evaluation/_constants.py +16 -0
- azure/ai/evaluation/_converters/_ai_services.py +162 -118
- azure/ai/evaluation/_converters/_models.py +76 -6
- azure/ai/evaluation/_eval_mapping.py +73 -0
- azure/ai/evaluation/_evaluate/_batch_run/_run_submitter_client.py +30 -16
- azure/ai/evaluation/_evaluate/_batch_run/eval_run_context.py +8 -0
- azure/ai/evaluation/_evaluate/_batch_run/proxy_client.py +5 -0
- azure/ai/evaluation/_evaluate/_batch_run/target_run_context.py +17 -1
- azure/ai/evaluation/_evaluate/_eval_run.py +1 -1
- azure/ai/evaluation/_evaluate/_evaluate.py +325 -76
- azure/ai/evaluation/_evaluate/_evaluate_aoai.py +553 -0
- azure/ai/evaluation/_evaluate/_utils.py +117 -4
- azure/ai/evaluation/_evaluators/_bleu/_bleu.py +11 -1
- azure/ai/evaluation/_evaluators/_code_vulnerability/_code_vulnerability.py +9 -1
- azure/ai/evaluation/_evaluators/_coherence/_coherence.py +12 -2
- azure/ai/evaluation/_evaluators/_common/_base_eval.py +12 -3
- azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +12 -3
- azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +2 -2
- azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +12 -2
- azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +14 -4
- azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +9 -8
- azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +10 -0
- azure/ai/evaluation/_evaluators/_content_safety/_violence.py +10 -0
- azure/ai/evaluation/_evaluators/_document_retrieval/__init__.py +11 -0
- azure/ai/evaluation/_evaluators/_document_retrieval/_document_retrieval.py +469 -0
- azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +10 -0
- azure/ai/evaluation/_evaluators/_fluency/_fluency.py +11 -1
- azure/ai/evaluation/_evaluators/_gleu/_gleu.py +10 -0
- azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +11 -1
- azure/ai/evaluation/_evaluators/_intent_resolution/_intent_resolution.py +16 -2
- azure/ai/evaluation/_evaluators/_meteor/_meteor.py +10 -0
- azure/ai/evaluation/_evaluators/_protected_material/_protected_material.py +11 -0
- azure/ai/evaluation/_evaluators/_qa/_qa.py +10 -0
- azure/ai/evaluation/_evaluators/_relevance/_relevance.py +11 -1
- azure/ai/evaluation/_evaluators/_response_completeness/_response_completeness.py +20 -2
- azure/ai/evaluation/_evaluators/_response_completeness/response_completeness.prompty +31 -46
- azure/ai/evaluation/_evaluators/_retrieval/_retrieval.py +10 -0
- azure/ai/evaluation/_evaluators/_rouge/_rouge.py +10 -0
- azure/ai/evaluation/_evaluators/_service_groundedness/_service_groundedness.py +10 -0
- azure/ai/evaluation/_evaluators/_similarity/_similarity.py +11 -1
- azure/ai/evaluation/_evaluators/_task_adherence/_task_adherence.py +16 -2
- azure/ai/evaluation/_evaluators/_tool_call_accuracy/_tool_call_accuracy.py +86 -12
- azure/ai/evaluation/_evaluators/_ungrounded_attributes/_ungrounded_attributes.py +10 -0
- azure/ai/evaluation/_evaluators/_xpia/xpia.py +11 -0
- azure/ai/evaluation/_exceptions.py +2 -0
- azure/ai/evaluation/_legacy/_adapters/__init__.py +0 -14
- azure/ai/evaluation/_legacy/_adapters/_check.py +17 -0
- azure/ai/evaluation/_legacy/_adapters/_flows.py +1 -1
- azure/ai/evaluation/_legacy/_batch_engine/_engine.py +51 -32
- azure/ai/evaluation/_legacy/_batch_engine/_openai_injector.py +114 -8
- azure/ai/evaluation/_legacy/_batch_engine/_result.py +6 -0
- azure/ai/evaluation/_legacy/_batch_engine/_run.py +6 -0
- azure/ai/evaluation/_legacy/_batch_engine/_run_submitter.py +69 -29
- azure/ai/evaluation/_legacy/_batch_engine/_trace.py +54 -62
- azure/ai/evaluation/_legacy/_batch_engine/_utils.py +19 -1
- azure/ai/evaluation/_legacy/_common/__init__.py +3 -0
- azure/ai/evaluation/_legacy/_common/_async_token_provider.py +124 -0
- azure/ai/evaluation/_legacy/_common/_thread_pool_executor_with_context.py +15 -0
- azure/ai/evaluation/_legacy/prompty/_connection.py +11 -74
- azure/ai/evaluation/_legacy/prompty/_exceptions.py +80 -0
- azure/ai/evaluation/_legacy/prompty/_prompty.py +119 -9
- azure/ai/evaluation/_legacy/prompty/_utils.py +72 -2
- azure/ai/evaluation/_safety_evaluation/_safety_evaluation.py +114 -22
- azure/ai/evaluation/_version.py +1 -1
- azure/ai/evaluation/red_team/_attack_strategy.py +1 -1
- azure/ai/evaluation/red_team/_red_team.py +976 -546
- azure/ai/evaluation/red_team/_utils/metric_mapping.py +23 -0
- azure/ai/evaluation/red_team/_utils/strategy_utils.py +1 -1
- azure/ai/evaluation/simulator/_adversarial_simulator.py +63 -39
- azure/ai/evaluation/simulator/_constants.py +1 -0
- azure/ai/evaluation/simulator/_conversation/__init__.py +13 -6
- azure/ai/evaluation/simulator/_conversation/_conversation.py +2 -1
- azure/ai/evaluation/simulator/_conversation/constants.py +1 -1
- azure/ai/evaluation/simulator/_direct_attack_simulator.py +38 -25
- azure/ai/evaluation/simulator/_helpers/_language_suffix_mapping.py +1 -0
- azure/ai/evaluation/simulator/_indirect_attack_simulator.py +43 -28
- azure/ai/evaluation/simulator/_model_tools/__init__.py +2 -1
- azure/ai/evaluation/simulator/_model_tools/_generated_rai_client.py +26 -18
- azure/ai/evaluation/simulator/_model_tools/_identity_manager.py +5 -10
- azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +65 -41
- azure/ai/evaluation/simulator/_model_tools/_template_handler.py +15 -10
- azure/ai/evaluation/simulator/_model_tools/models.py +20 -17
- {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.7.0.dist-info}/METADATA +49 -3
- {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.7.0.dist-info}/RECORD +144 -86
- /azure/ai/evaluation/_legacy/{_batch_engine → _common}/_logging.py +0 -0
- {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.7.0.dist-info}/NOTICE.txt +0 -0
- {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.7.0.dist-info}/WHEEL +0 -0
- {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.7.0.dist-info}/top_level.txt +0 -0
|
@@ -4,12 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# Pretty much all this code will be removed
|
|
6
6
|
|
|
7
|
-
import logging
|
|
8
|
-
import os
|
|
9
7
|
from typing import Any, Dict, Optional
|
|
10
8
|
|
|
11
|
-
from ._openai_injector import inject_openai_api
|
|
12
|
-
|
|
13
9
|
|
|
14
10
|
def start_trace(
|
|
15
11
|
*,
|
|
@@ -17,27 +13,24 @@ def start_trace(
|
|
|
17
13
|
collection: Optional[str] = None,
|
|
18
14
|
**kwargs: Any,
|
|
19
15
|
) -> None:
|
|
20
|
-
"""
|
|
16
|
+
"""Starts a trace.
|
|
21
17
|
|
|
22
18
|
:param resource_attributes: Specify the resource attributes for current process.
|
|
23
19
|
:type resource_attributes: typing.Optional[dict]
|
|
24
20
|
:param collection: Specify the collection for current tracing.
|
|
25
21
|
:type collection: typing.Optional[str]
|
|
26
22
|
"""
|
|
23
|
+
pass
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
logging.debug("
|
|
31
|
-
|
|
32
|
-
res_attrs: Dict[str, str] = {"service.name": "promptflow"}
|
|
33
|
-
if resource_attributes:
|
|
34
|
-
logging.debug("specified resource attributes: %s", resource_attributes)
|
|
35
|
-
res_attrs.update(resource_attributes)
|
|
25
|
+
# res_attrs: Dict[str, str] = {"service.name": "promptflow"}
|
|
26
|
+
# if resource_attributes:
|
|
27
|
+
# logging.debug("specified resource attributes: %s", resource_attributes)
|
|
28
|
+
# res_attrs.update(resource_attributes)
|
|
36
29
|
|
|
37
|
-
# determine collection
|
|
38
|
-
collection_user_specified = collection is not None
|
|
39
|
-
if not collection_user_specified:
|
|
40
|
-
|
|
30
|
+
# # determine collection
|
|
31
|
+
# collection_user_specified = collection is not None
|
|
32
|
+
# if not collection_user_specified:
|
|
33
|
+
# collection = kwargs.get("_collection", _get_collection_from_cwd())
|
|
41
34
|
# logging.debug("collection is not user specified")
|
|
42
35
|
# if is_collection_writeable():
|
|
43
36
|
# # internal parameter for devkit call
|
|
@@ -53,53 +46,52 @@ def start_trace(
|
|
|
53
46
|
# # logging.debug("collection is protected, will directly use that...")
|
|
54
47
|
# # tracer_provider: TracerProvider = trace.get_tracer_provider()
|
|
55
48
|
# # collection = tracer_provider.resource.attributes["collection"]
|
|
56
|
-
logging.info("collection: %s", collection)
|
|
57
|
-
res_attrs["collection"] = collection or "default"
|
|
58
|
-
logging.info("resource attributes: %s", res_attrs)
|
|
49
|
+
# logging.info("collection: %s", collection)
|
|
50
|
+
# res_attrs["collection"] = collection or "default"
|
|
51
|
+
# logging.info("resource attributes: %s", res_attrs)
|
|
59
52
|
|
|
60
|
-
# if user specifies collection, we will add a flag on tracer provider to avoid override
|
|
61
|
-
_set_tracer_provider(res_attrs, protected_collection=collection_user_specified)
|
|
53
|
+
# # if user specifies collection, we will add a flag on tracer provider to avoid override
|
|
54
|
+
# _set_tracer_provider(res_attrs, protected_collection=collection_user_specified)
|
|
62
55
|
|
|
63
56
|
# Rest of code is removed since we are removing promptflow-devkit dependency
|
|
64
57
|
|
|
65
58
|
|
|
66
|
-
def is_collection_writeable() -> bool:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _get_collection_from_cwd() -> str:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _set_tracer_provider(res_attrs: Dict[str, str], protected_collection: bool) -> None:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# setattr(trace.get_tracer_provider(), TRACER_PROVIDER_PROTECTED_COLLECTION_ATTR, True)
|
|
59
|
+
# def is_collection_writeable() -> bool:
|
|
60
|
+
# # TODO ralphe: This has OpenTelemetry dependency. That is a future task to resolve.
|
|
61
|
+
# # return not getattr(trace.get_tracer_provider(), TRACER_PROVIDER_PROTECTED_COLLECTION_ATTR, False)
|
|
62
|
+
# return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# def _get_collection_from_cwd() -> str:
|
|
66
|
+
# """Try to use cwd folder name as collection name; will fall back to default value if run into exception."""
|
|
67
|
+
# cur_folder_name = ""
|
|
68
|
+
# try:
|
|
69
|
+
# cwd = os.getcwd()
|
|
70
|
+
# cur_folder_name = os.path.basename(cwd)
|
|
71
|
+
# except Exception: # pylint: disable=broad-except
|
|
72
|
+
# # possible exception: PermissionError, FileNotFoundError, OSError, etc.
|
|
73
|
+
# pass
|
|
74
|
+
# collection = cur_folder_name or "default"
|
|
75
|
+
# return collection
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# def _set_tracer_provider(res_attrs: Dict[str, str], protected_collection: bool) -> None:
|
|
79
|
+
# # TODO ralphe: OpenTelemetry dependency. This is a future task to resolve.
|
|
80
|
+
# # res = Resource(attributes=res_attrs)
|
|
81
|
+
# # tracer_provider = TracerProvider(resource=res)
|
|
82
|
+
|
|
83
|
+
# # cur_tracer_provider = trace.get_tracer_provider()
|
|
84
|
+
# # if isinstance(cur_tracer_provider, TracerProvider):
|
|
85
|
+
# # logging.info("tracer provider is already set, will merge the resource attributes...")
|
|
86
|
+
# # cur_res = cur_tracer_provider.resource
|
|
87
|
+
# # logging.debug("current resource: %s", cur_res.attributes)
|
|
88
|
+
# # new_res = cur_res.merge(res)
|
|
89
|
+
# # cur_tracer_provider._resource = new_res
|
|
90
|
+
# # logging.info("tracer provider is updated with resource attributes: %s", new_res.attributes)
|
|
91
|
+
# # else:
|
|
92
|
+
# # trace.set_tracer_provider(tracer_provider)
|
|
93
|
+
# # logging.info("tracer provider is set with resource attributes: %s", res.attributes)
|
|
94
|
+
|
|
95
|
+
# # if protected_collection:
|
|
96
|
+
# # logging.info("user specifies collection, will add a flag on tracer provider to avoid override...")
|
|
97
|
+
# # setattr(trace.get_tracer_provider(), TRACER_PROVIDER_PROTECTED_COLLECTION_ATTR, True)
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
# ---------------------------------------------------------
|
|
4
4
|
|
|
5
|
+
import inspect
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
|
-
from typing import Any, Mapping, Sequence, Tuple
|
|
8
|
+
from typing import Any, Final, Mapping, Sequence, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULTS_KEY: Final[str] = "$defaults$"
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
def normalize_identifier_name(name: str) -> str:
|
|
@@ -80,3 +84,17 @@ def get_value_from_path(path: str, data: Mapping[str, Any]) -> Tuple[bool, Any]:
|
|
|
80
84
|
if len(parts) == 0:
|
|
81
85
|
return False, None
|
|
82
86
|
return _get_value(data, parts)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_async_callable(obj: Any) -> bool:
|
|
90
|
+
"""Check if the object is an async callable. This will be true if the object is a coroutine function,
|
|
91
|
+
or if the object has
|
|
92
|
+
|
|
93
|
+
:param Any obj: The object to check.
|
|
94
|
+
:return: True if the object is an async callable.
|
|
95
|
+
:rtype: bool
|
|
96
|
+
"""
|
|
97
|
+
return (
|
|
98
|
+
inspect.iscoroutinefunction(obj)
|
|
99
|
+
or inspect.iscoroutinefunction(getattr(obj, "__call__", None))
|
|
100
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# ---------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, AsyncContextManager, Optional
|
|
7
|
+
|
|
8
|
+
from azure.core.credentials import AccessToken, TokenCredential
|
|
9
|
+
from azure.identity import AzureCliCredential, DefaultAzureCredential, ManagedIdentityCredential
|
|
10
|
+
|
|
11
|
+
from azure.ai.evaluation._exceptions import EvaluationException, ErrorBlame, ErrorCategory, ErrorTarget
|
|
12
|
+
from azure.ai.evaluation._azure._envs import AzureEnvironmentClient
|
|
13
|
+
|
|
14
|
+
class AsyncAzureTokenProvider(AsyncContextManager["AsyncAzureTokenProvider"]):
|
|
15
|
+
"""Asynchronous token provider for Azure services that supports non-default Azure clouds
|
|
16
|
+
(e.g. Azure China, Azure US Government, etc.)."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
base_url: Optional[str] = None,
|
|
22
|
+
**kwargs: Any
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Initialize the AsyncAzureTokenProvider."""
|
|
25
|
+
self._credential: Optional[TokenCredential] = None
|
|
26
|
+
self._env_client: Optional[AzureEnvironmentClient] = AzureEnvironmentClient(
|
|
27
|
+
base_url=base_url,
|
|
28
|
+
**kwargs)
|
|
29
|
+
|
|
30
|
+
async def close(self) -> None:
|
|
31
|
+
if self._env_client:
|
|
32
|
+
await self._env_client.close()
|
|
33
|
+
self._env_client = None
|
|
34
|
+
|
|
35
|
+
self._credential = None
|
|
36
|
+
|
|
37
|
+
async def get_token(
|
|
38
|
+
self,
|
|
39
|
+
*scopes: str,
|
|
40
|
+
claims: Optional[str] = None,
|
|
41
|
+
tenant_id: Optional[str] = None,
|
|
42
|
+
enable_cae: bool = False,
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
) -> AccessToken:
|
|
45
|
+
if self._credential is None:
|
|
46
|
+
self._credential = await self._initialize_async(self._env_client)
|
|
47
|
+
|
|
48
|
+
if self._credential is None:
|
|
49
|
+
raise EvaluationException(
|
|
50
|
+
f"{self.__class__.__name__} could not determine the credential to use.",
|
|
51
|
+
target=ErrorTarget.UNKNOWN,
|
|
52
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
53
|
+
blame=ErrorBlame.SYSTEM_ERROR)
|
|
54
|
+
|
|
55
|
+
return self._credential.get_token(
|
|
56
|
+
*scopes,
|
|
57
|
+
claims=claims,
|
|
58
|
+
tenant_id=tenant_id,
|
|
59
|
+
enable_cae=enable_cae,
|
|
60
|
+
**kwargs)
|
|
61
|
+
|
|
62
|
+
async def __aenter__(self) -> "AsyncAzureTokenProvider":
|
|
63
|
+
self._credential = await self._initialize_async(self._env_client)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
async def __aexit__(
|
|
67
|
+
self,
|
|
68
|
+
exc_type: Optional[type] = None,
|
|
69
|
+
exc_value: Optional[BaseException] = None,
|
|
70
|
+
traceback: Optional[Any] = None
|
|
71
|
+
) -> None:
|
|
72
|
+
await self.close()
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
async def _initialize_async(client: Optional[AzureEnvironmentClient]) -> TokenCredential:
|
|
76
|
+
# Determine which credential to use based on the configured Azure cloud environment variables
|
|
77
|
+
# and possibly making network calls to Azure to get the correct Azure cloud metadata.
|
|
78
|
+
if client is None:
|
|
79
|
+
raise EvaluationException(
|
|
80
|
+
f"{AsyncAzureTokenProvider.__name__} instance has already been closed.",
|
|
81
|
+
target=ErrorTarget.UNKNOWN,
|
|
82
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
83
|
+
blame=ErrorBlame.USER_ERROR)
|
|
84
|
+
|
|
85
|
+
cloud_name: str = await client.get_default_cloud_name_async()
|
|
86
|
+
if cloud_name != client.DEFAULT_AZURE_CLOUD_NAME:
|
|
87
|
+
# If the cloud name is not the default, we need to get the metadata for the specified cloud
|
|
88
|
+
# and set it in the environment client.
|
|
89
|
+
metadata = await client.get_cloud_async(cloud_name)
|
|
90
|
+
if metadata is None:
|
|
91
|
+
raise EvaluationException(
|
|
92
|
+
f"Failed to get metadata for cloud '{cloud_name}'.",
|
|
93
|
+
target=ErrorTarget.UNKNOWN,
|
|
94
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
95
|
+
blame=ErrorBlame.USER_ERROR)
|
|
96
|
+
|
|
97
|
+
authority = metadata.get("active_directory_endpoint")
|
|
98
|
+
return DefaultAzureCredential(authority=authority, exclude_shared_token_cache_credential=True)
|
|
99
|
+
elif os.getenv("AZUREML_OBO_ENABLED"):
|
|
100
|
+
# using Azure on behalf of credentials requires the use of the azure-ai-ml package
|
|
101
|
+
try:
|
|
102
|
+
from azure.ai.ml.identity import AzureMLOnBehalfOfCredential
|
|
103
|
+
return AzureMLOnBehalfOfCredential() # type: ignore
|
|
104
|
+
except (ModuleNotFoundError, ImportError):
|
|
105
|
+
raise EvaluationException( # pylint: disable=raise-missing-from
|
|
106
|
+
message=(
|
|
107
|
+
"The required packages for OBO credentials are missing.\n"
|
|
108
|
+
'To resolve this, please install them by running "pip install azure-ai-ml".'
|
|
109
|
+
),
|
|
110
|
+
target=ErrorTarget.EVALUATE,
|
|
111
|
+
category=ErrorCategory.MISSING_PACKAGE,
|
|
112
|
+
blame=ErrorBlame.USER_ERROR,
|
|
113
|
+
)
|
|
114
|
+
elif os.environ.get("PF_USE_AZURE_CLI_CREDENTIAL", "false").lower() == "true":
|
|
115
|
+
# TODO ralphe: Is this still needed? DefaultAzureCredential already includes CLI credentials
|
|
116
|
+
# albeit with a lower priority
|
|
117
|
+
return AzureCliCredential()
|
|
118
|
+
elif os.environ.get("IS_IN_CI_PIPELINE", "false").lower() == "true":
|
|
119
|
+
# use managed identity when executing in CI pipeline.
|
|
120
|
+
return AzureCliCredential()
|
|
121
|
+
elif identity_client_id := os.environ.get("DEFAULT_IDENTITY_CLIENT_ID"):
|
|
122
|
+
return ManagedIdentityCredential(client_id=identity_client_id)
|
|
123
|
+
else:
|
|
124
|
+
return DefaultAzureCredential()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ---------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import contextvars
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from functools import partial
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
class ThreadPoolExecutorWithContext(ThreadPoolExecutor):
|
|
11
|
+
"""ThreadPoolExecutor that preserves context variables across threads."""
|
|
12
|
+
@override
|
|
13
|
+
def submit(self, fn, *args, **kwargs):
|
|
14
|
+
context = contextvars.copy_context()
|
|
15
|
+
return super().submit(context.run, partial(fn, *args, **kwargs))
|
|
@@ -3,40 +3,16 @@
|
|
|
3
3
|
# ---------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
import re
|
|
7
6
|
from abc import ABC, abstractmethod
|
|
8
7
|
from dataclasses import dataclass
|
|
9
|
-
from typing import Any, ClassVar, Mapping, Optional
|
|
8
|
+
from typing import Any, ClassVar, Mapping, Optional
|
|
10
9
|
|
|
11
10
|
from azure.ai.evaluation._legacy.prompty._exceptions import MissingRequiredInputError
|
|
12
11
|
from azure.ai.evaluation._legacy.prompty._utils import dataclass_from_dict
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
ENV_VAR_PATTERN = re.compile(r"^\$\{env:(.*)\}$")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _parse_environment_variable(value: Union[str, Any]) -> Union[str, Any]:
|
|
19
|
-
"""Get environment variable from ${env:ENV_NAME}. If not found, return original value.
|
|
20
|
-
|
|
21
|
-
:param value: The value to parse.
|
|
22
|
-
:type value: str | Any
|
|
23
|
-
:return: The parsed value
|
|
24
|
-
:rtype: str | Any"""
|
|
25
|
-
if not isinstance(value, str):
|
|
26
|
-
return value
|
|
27
|
-
|
|
28
|
-
result = re.match(ENV_VAR_PATTERN, value)
|
|
29
|
-
if result:
|
|
30
|
-
env_name = result.groups()[0]
|
|
31
|
-
return os.environ.get(env_name, value)
|
|
32
|
-
|
|
33
|
-
return value
|
|
34
|
-
|
|
35
|
-
|
|
36
14
|
def _is_empty_connection_config(connection_dict: Mapping[str, Any]) -> bool:
|
|
37
|
-
|
|
38
|
-
keys = {k for k, v in connection_dict.items() if v}
|
|
39
|
-
return len(keys - ignored_fields) == 0
|
|
15
|
+
return any(key not in {"azure_deployment", "model", "type"} for key in connection_dict.keys())
|
|
40
16
|
|
|
41
17
|
|
|
42
18
|
@dataclass
|
|
@@ -52,16 +28,6 @@ class Connection(ABC):
|
|
|
52
28
|
:rtype: str"""
|
|
53
29
|
...
|
|
54
30
|
|
|
55
|
-
@abstractmethod
|
|
56
|
-
def is_valid(self, missing_fields: Optional[Set[str]] = None) -> bool:
|
|
57
|
-
"""Check if the connection is valid.
|
|
58
|
-
|
|
59
|
-
:param missing_fields: If set, this will be populated with the missing required fields.
|
|
60
|
-
:type missing_fields: Set[str] | None
|
|
61
|
-
:return: True if the connection is valid, False otherwise.
|
|
62
|
-
:rtype: bool"""
|
|
63
|
-
...
|
|
64
|
-
|
|
65
31
|
@staticmethod
|
|
66
32
|
def parse_from_config(model_configuration: Mapping[str, Any]) -> "Connection":
|
|
67
33
|
"""Parse a connection from a model configuration.
|
|
@@ -71,18 +37,18 @@ class Connection(ABC):
|
|
|
71
37
|
:return: The connection.
|
|
72
38
|
:rtype: Connection
|
|
73
39
|
"""
|
|
74
|
-
|
|
75
|
-
connection_dict = {k: _parse_environment_variable(v) for k, v in model_configuration.items()}
|
|
40
|
+
connection_dict = {**model_configuration}
|
|
76
41
|
connection_type = connection_dict.pop("type", "")
|
|
77
42
|
|
|
43
|
+
connection: Connection
|
|
78
44
|
if connection_type in [AzureOpenAIConnection.TYPE, "azure_openai"]:
|
|
79
|
-
if _is_empty_connection_config(connection_dict):
|
|
45
|
+
if not _is_empty_connection_config(connection_dict):
|
|
80
46
|
connection = AzureOpenAIConnection.from_env()
|
|
81
47
|
else:
|
|
82
48
|
connection = dataclass_from_dict(AzureOpenAIConnection, connection_dict)
|
|
83
49
|
|
|
84
50
|
elif connection_type in [OpenAIConnection.TYPE, "openai"]:
|
|
85
|
-
if _is_empty_connection_config(connection_dict):
|
|
51
|
+
if not _is_empty_connection_config(connection_dict):
|
|
86
52
|
connection = OpenAIConnection.from_env()
|
|
87
53
|
else:
|
|
88
54
|
connection = dataclass_from_dict(OpenAIConnection, connection_dict)
|
|
@@ -94,13 +60,6 @@ class Connection(ABC):
|
|
|
94
60
|
)
|
|
95
61
|
raise MissingRequiredInputError(error_message)
|
|
96
62
|
|
|
97
|
-
missing_fields: Set[str] = set()
|
|
98
|
-
if not connection.is_valid(missing_fields):
|
|
99
|
-
raise MissingRequiredInputError(
|
|
100
|
-
f"The following required fields are missing for connection {connection.type}: "
|
|
101
|
-
f"{', '.join(missing_fields)}"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
63
|
return connection
|
|
105
64
|
|
|
106
65
|
|
|
@@ -109,6 +68,7 @@ class OpenAIConnection(Connection):
|
|
|
109
68
|
"""Connection class for OpenAI endpoints."""
|
|
110
69
|
|
|
111
70
|
base_url: str
|
|
71
|
+
model: str
|
|
112
72
|
api_key: Optional[str] = None
|
|
113
73
|
organization: Optional[str] = None
|
|
114
74
|
|
|
@@ -122,29 +82,19 @@ class OpenAIConnection(Connection):
|
|
|
122
82
|
def from_env(cls) -> "OpenAIConnection":
|
|
123
83
|
return cls(
|
|
124
84
|
base_url=os.environ.get("OPENAI_BASE_URL", ""),
|
|
85
|
+
model=os.environ.get("OPENAI_MODEL", ""),
|
|
125
86
|
api_key=os.environ.get("OPENAI_API_KEY"),
|
|
126
87
|
organization=os.environ.get("OPENAI_ORG_ID"),
|
|
127
88
|
)
|
|
128
89
|
|
|
129
|
-
def is_valid(self, missing_fields: Optional[Set[str]] = None) -> bool:
|
|
130
|
-
if missing_fields is None:
|
|
131
|
-
missing_fields = set()
|
|
132
|
-
if not self.base_url:
|
|
133
|
-
missing_fields.add("base_url")
|
|
134
|
-
if not self.api_key:
|
|
135
|
-
missing_fields.add("api_key")
|
|
136
|
-
if not self.organization:
|
|
137
|
-
missing_fields.add("organization")
|
|
138
|
-
return not bool(missing_fields)
|
|
139
|
-
|
|
140
90
|
|
|
141
91
|
@dataclass
|
|
142
92
|
class AzureOpenAIConnection(Connection):
|
|
143
93
|
"""Connection class for Azure OpenAI endpoints."""
|
|
144
94
|
|
|
145
95
|
azure_endpoint: str
|
|
146
|
-
|
|
147
|
-
|
|
96
|
+
azure_deployment: str
|
|
97
|
+
api_key: Optional[str] = None
|
|
148
98
|
api_version: Optional[str] = None
|
|
149
99
|
resource_id: Optional[str] = None
|
|
150
100
|
|
|
@@ -158,8 +108,8 @@ class AzureOpenAIConnection(Connection):
|
|
|
158
108
|
def from_env(cls) -> "AzureOpenAIConnection":
|
|
159
109
|
return cls(
|
|
160
110
|
azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT", ""),
|
|
111
|
+
azure_deployment=os.environ.get("AZURE_OPENAI_DEPLOYMENT", ""),
|
|
161
112
|
api_key=os.environ.get("AZURE_OPENAI_API_KEY"),
|
|
162
|
-
azure_deployment=os.environ.get("AZURE_OPENAI_DEPLOYMENT"),
|
|
163
113
|
api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-02-01"),
|
|
164
114
|
)
|
|
165
115
|
|
|
@@ -167,16 +117,3 @@ class AzureOpenAIConnection(Connection):
|
|
|
167
117
|
# set default API version
|
|
168
118
|
if not self.api_version:
|
|
169
119
|
self.api_version = "2024-02-01"
|
|
170
|
-
|
|
171
|
-
def is_valid(self, missing_fields: Optional[Set[str]] = None) -> bool:
|
|
172
|
-
if missing_fields is None:
|
|
173
|
-
missing_fields = set()
|
|
174
|
-
if not self.azure_endpoint:
|
|
175
|
-
missing_fields.add("azure_endpoint")
|
|
176
|
-
if not self.api_key:
|
|
177
|
-
missing_fields.add("api_key")
|
|
178
|
-
if not self.azure_deployment:
|
|
179
|
-
missing_fields.add("azure_deployment")
|
|
180
|
-
if not self.api_version:
|
|
181
|
-
missing_fields.add("api_version")
|
|
182
|
-
return not bool(missing_fields)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
# ---------------------------------------------------------
|
|
4
4
|
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from openai import OpenAIError
|
|
5
7
|
from azure.ai.evaluation._exceptions import ErrorCategory, ErrorBlame, ErrorTarget, EvaluationException
|
|
6
8
|
|
|
7
9
|
|
|
@@ -57,3 +59,81 @@ class NotSupportedError(PromptyException):
|
|
|
57
59
|
kwargs.setdefault("target", ErrorTarget.UNKNOWN)
|
|
58
60
|
kwargs.setdefault("blame", ErrorBlame.SYSTEM_ERROR)
|
|
59
61
|
super().__init__(message, **kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WrappedOpenAIError(PromptyException):
|
|
65
|
+
"""Exception raised when an OpenAI error is encountered."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, *, message: Optional[str] = None, error: Optional[OpenAIError] = None, **kwargs):
|
|
68
|
+
kwargs.setdefault("category", ErrorCategory.FAILED_EXECUTION)
|
|
69
|
+
kwargs.setdefault("target", ErrorTarget.EVAL_RUN)
|
|
70
|
+
kwargs.setdefault("blame", ErrorBlame.USER_ERROR)
|
|
71
|
+
|
|
72
|
+
message = (
|
|
73
|
+
message or self.to_openai_error_message(error)
|
|
74
|
+
if error
|
|
75
|
+
else "An error occurred while executing the OpenAI API."
|
|
76
|
+
)
|
|
77
|
+
super().__init__(message, **kwargs)
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def to_openai_error_message(e: OpenAIError) -> str:
|
|
81
|
+
# TODO ralphe: Error handling that relies on string matching is fragile and should be replaced
|
|
82
|
+
# with a more robust solution that examines the actual error type since that provies
|
|
83
|
+
# more than enough information to handle errors.
|
|
84
|
+
ex_type = type(e).__name__
|
|
85
|
+
error_message = str(e)
|
|
86
|
+
# https://learn.microsoft.com/en-gb/azure/ai-services/openai/reference
|
|
87
|
+
if error_message == "<empty message>":
|
|
88
|
+
msg = "The api key is invalid or revoked. " "You can correct or regenerate the api key of your connection."
|
|
89
|
+
return f"OpenAI API hits {ex_type}: {msg}"
|
|
90
|
+
# for models that do not support the `functions` parameter.
|
|
91
|
+
elif "Unrecognized request argument supplied: functions" in error_message:
|
|
92
|
+
msg = (
|
|
93
|
+
"Current model does not support the `functions` parameter. If you are using openai connection, then "
|
|
94
|
+
"please use gpt-3.5-turbo, gpt-4, gpt-4-32k, gpt-3.5-turbo-0613 or gpt-4-0613. You can refer to "
|
|
95
|
+
"https://platform.openai.com/docs/guides/gpt/function-calling. If you are using azure openai "
|
|
96
|
+
"connection, then please first go to your Azure OpenAI resource, deploy model 'gpt-35-turbo' or "
|
|
97
|
+
"'gpt-4' with version 0613. You can refer to "
|
|
98
|
+
"https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/function-calling."
|
|
99
|
+
)
|
|
100
|
+
return f"OpenAI API hits {ex_type}: {msg}"
|
|
101
|
+
elif "Invalid content type. image_url is only supported by certain models" in error_message:
|
|
102
|
+
msg = (
|
|
103
|
+
"Current model does not support the image input. If you are using openai connection, then please use "
|
|
104
|
+
"gpt-4-vision-preview. You can refer to https://platform.openai.com/docs/guides/vision."
|
|
105
|
+
"If you are using azure openai connection, then please first go to your Azure OpenAI resource, "
|
|
106
|
+
'create a GPT-4 Turbo with Vision deployment by selecting model name: "gpt-4" and '
|
|
107
|
+
'model version "vision-preview". You can refer to '
|
|
108
|
+
"https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/gpt-with-vision"
|
|
109
|
+
)
|
|
110
|
+
return f"OpenAI API hits {ex_type}: {msg}"
|
|
111
|
+
elif (
|
|
112
|
+
"'response_format' of type" in error_message and "is not supported with this model." in error_message
|
|
113
|
+
) or (
|
|
114
|
+
"Additional properties are not allowed" in error_message
|
|
115
|
+
and "unexpected) - 'response_format'" in error_message
|
|
116
|
+
):
|
|
117
|
+
msg = (
|
|
118
|
+
'The response_format parameter needs to be a dictionary such as {"type": "text"}. '
|
|
119
|
+
"The value associated with the type key should be either 'text' or 'json_object' "
|
|
120
|
+
'If you are using openai connection, you can only set response_format to { "type": "json_object" } '
|
|
121
|
+
"when calling gpt-3.5-turbo-1106 or gpt-4-1106-preview to enable JSON mode. You can refer to "
|
|
122
|
+
"https://platform.openai.com/docs/guides/text-generation/json-mode. If you are using azure openai "
|
|
123
|
+
"connection, then please first go to your Azure OpenAI resource, compatible with GPT-4 Turbo and "
|
|
124
|
+
"all GPT-3.5 Turbo models newer than gpt-35-turbo-1106. You can refer to "
|
|
125
|
+
"https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/json-mode?tabs=python."
|
|
126
|
+
)
|
|
127
|
+
return f"OpenAI API hits {ex_type}: {msg}"
|
|
128
|
+
elif "Principal does not have access to API/Operation" in error_message:
|
|
129
|
+
msg = (
|
|
130
|
+
"Principal does not have access to API/Operation. If you are using azure openai connection, "
|
|
131
|
+
"please make sure you have proper role assignment on your azure openai resource. You can refer to "
|
|
132
|
+
"https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/role-based-access-control"
|
|
133
|
+
)
|
|
134
|
+
return f"OpenAI API hits {ex_type}: {msg}"
|
|
135
|
+
else:
|
|
136
|
+
return (
|
|
137
|
+
f"OpenAI API hits {ex_type}: {error_message} [Error reference: "
|
|
138
|
+
"https://platform.openai.com/docs/guides/error-codes/api-errors]"
|
|
139
|
+
)
|