azure-ai-evaluation 1.5.0__py3-none-any.whl → 1.6.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 (123) hide show
  1. azure/ai/evaluation/__init__.py +9 -0
  2. azure/ai/evaluation/_aoai/__init__.py +10 -0
  3. azure/ai/evaluation/_aoai/aoai_grader.py +89 -0
  4. azure/ai/evaluation/_aoai/label_grader.py +66 -0
  5. azure/ai/evaluation/_aoai/string_check_grader.py +65 -0
  6. azure/ai/evaluation/_aoai/text_similarity_grader.py +88 -0
  7. azure/ai/evaluation/_azure/_clients.py +4 -4
  8. azure/ai/evaluation/_azure/_envs.py +208 -0
  9. azure/ai/evaluation/_azure/_token_manager.py +12 -7
  10. azure/ai/evaluation/_common/__init__.py +5 -0
  11. azure/ai/evaluation/_common/evaluation_onedp_client.py +118 -0
  12. azure/ai/evaluation/_common/onedp/__init__.py +32 -0
  13. azure/ai/evaluation/_common/onedp/_client.py +139 -0
  14. azure/ai/evaluation/_common/onedp/_configuration.py +73 -0
  15. azure/ai/evaluation/_common/onedp/_model_base.py +1232 -0
  16. azure/ai/evaluation/_common/onedp/_patch.py +21 -0
  17. azure/ai/evaluation/_common/onedp/_serialization.py +2032 -0
  18. azure/ai/evaluation/_common/onedp/_types.py +21 -0
  19. azure/ai/evaluation/_common/onedp/_validation.py +50 -0
  20. azure/ai/evaluation/_common/onedp/_vendor.py +50 -0
  21. azure/ai/evaluation/_common/onedp/_version.py +9 -0
  22. azure/ai/evaluation/_common/onedp/aio/__init__.py +29 -0
  23. azure/ai/evaluation/_common/onedp/aio/_client.py +143 -0
  24. azure/ai/evaluation/_common/onedp/aio/_configuration.py +75 -0
  25. azure/ai/evaluation/_common/onedp/aio/_patch.py +21 -0
  26. azure/ai/evaluation/_common/onedp/aio/_vendor.py +40 -0
  27. azure/ai/evaluation/_common/onedp/aio/operations/__init__.py +39 -0
  28. azure/ai/evaluation/_common/onedp/aio/operations/_operations.py +4494 -0
  29. azure/ai/evaluation/_common/onedp/aio/operations/_patch.py +21 -0
  30. azure/ai/evaluation/_common/onedp/models/__init__.py +142 -0
  31. azure/ai/evaluation/_common/onedp/models/_enums.py +162 -0
  32. azure/ai/evaluation/_common/onedp/models/_models.py +2228 -0
  33. azure/ai/evaluation/_common/onedp/models/_patch.py +21 -0
  34. azure/ai/evaluation/_common/onedp/operations/__init__.py +39 -0
  35. azure/ai/evaluation/_common/onedp/operations/_operations.py +5655 -0
  36. azure/ai/evaluation/_common/onedp/operations/_patch.py +21 -0
  37. azure/ai/evaluation/_common/onedp/py.typed +1 -0
  38. azure/ai/evaluation/_common/onedp/servicepatterns/__init__.py +1 -0
  39. azure/ai/evaluation/_common/onedp/servicepatterns/aio/__init__.py +1 -0
  40. azure/ai/evaluation/_common/onedp/servicepatterns/aio/operations/__init__.py +25 -0
  41. azure/ai/evaluation/_common/onedp/servicepatterns/aio/operations/_operations.py +34 -0
  42. azure/ai/evaluation/_common/onedp/servicepatterns/aio/operations/_patch.py +20 -0
  43. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/__init__.py +1 -0
  44. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/__init__.py +1 -0
  45. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/operations/__init__.py +22 -0
  46. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/operations/_operations.py +29 -0
  47. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/aio/operations/_patch.py +20 -0
  48. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/operations/__init__.py +22 -0
  49. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/operations/_operations.py +29 -0
  50. azure/ai/evaluation/_common/onedp/servicepatterns/buildingblocks/operations/_patch.py +20 -0
  51. azure/ai/evaluation/_common/onedp/servicepatterns/operations/__init__.py +25 -0
  52. azure/ai/evaluation/_common/onedp/servicepatterns/operations/_operations.py +34 -0
  53. azure/ai/evaluation/_common/onedp/servicepatterns/operations/_patch.py +20 -0
  54. azure/ai/evaluation/_common/rai_service.py +158 -28
  55. azure/ai/evaluation/_common/raiclient/_version.py +1 -1
  56. azure/ai/evaluation/_common/utils.py +79 -1
  57. azure/ai/evaluation/_constants.py +16 -0
  58. azure/ai/evaluation/_eval_mapping.py +71 -0
  59. azure/ai/evaluation/_evaluate/_batch_run/_run_submitter_client.py +30 -16
  60. azure/ai/evaluation/_evaluate/_batch_run/eval_run_context.py +8 -0
  61. azure/ai/evaluation/_evaluate/_batch_run/proxy_client.py +5 -0
  62. azure/ai/evaluation/_evaluate/_batch_run/target_run_context.py +17 -1
  63. azure/ai/evaluation/_evaluate/_eval_run.py +1 -1
  64. azure/ai/evaluation/_evaluate/_evaluate.py +325 -74
  65. azure/ai/evaluation/_evaluate/_evaluate_aoai.py +534 -0
  66. azure/ai/evaluation/_evaluate/_utils.py +117 -4
  67. azure/ai/evaluation/_evaluators/_common/_base_eval.py +8 -3
  68. azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +12 -3
  69. azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +2 -2
  70. azure/ai/evaluation/_evaluators/_document_retrieval/__init__.py +11 -0
  71. azure/ai/evaluation/_evaluators/_document_retrieval/_document_retrieval.py +467 -0
  72. azure/ai/evaluation/_evaluators/_fluency/_fluency.py +1 -1
  73. azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +1 -1
  74. azure/ai/evaluation/_evaluators/_intent_resolution/_intent_resolution.py +6 -2
  75. azure/ai/evaluation/_evaluators/_relevance/_relevance.py +1 -1
  76. azure/ai/evaluation/_evaluators/_response_completeness/_response_completeness.py +7 -2
  77. azure/ai/evaluation/_evaluators/_response_completeness/response_completeness.prompty +31 -46
  78. azure/ai/evaluation/_evaluators/_similarity/_similarity.py +1 -1
  79. azure/ai/evaluation/_evaluators/_task_adherence/_task_adherence.py +5 -2
  80. azure/ai/evaluation/_evaluators/_tool_call_accuracy/_tool_call_accuracy.py +6 -2
  81. azure/ai/evaluation/_exceptions.py +2 -0
  82. azure/ai/evaluation/_legacy/_adapters/__init__.py +0 -14
  83. azure/ai/evaluation/_legacy/_adapters/_check.py +17 -0
  84. azure/ai/evaluation/_legacy/_adapters/_flows.py +1 -1
  85. azure/ai/evaluation/_legacy/_batch_engine/_engine.py +51 -32
  86. azure/ai/evaluation/_legacy/_batch_engine/_openai_injector.py +114 -8
  87. azure/ai/evaluation/_legacy/_batch_engine/_result.py +6 -0
  88. azure/ai/evaluation/_legacy/_batch_engine/_run.py +6 -0
  89. azure/ai/evaluation/_legacy/_batch_engine/_run_submitter.py +69 -29
  90. azure/ai/evaluation/_legacy/_batch_engine/_trace.py +54 -62
  91. azure/ai/evaluation/_legacy/_batch_engine/_utils.py +19 -1
  92. azure/ai/evaluation/_legacy/_common/__init__.py +3 -0
  93. azure/ai/evaluation/_legacy/_common/_async_token_provider.py +124 -0
  94. azure/ai/evaluation/_legacy/_common/_thread_pool_executor_with_context.py +15 -0
  95. azure/ai/evaluation/_legacy/prompty/_connection.py +11 -74
  96. azure/ai/evaluation/_legacy/prompty/_exceptions.py +80 -0
  97. azure/ai/evaluation/_legacy/prompty/_prompty.py +119 -9
  98. azure/ai/evaluation/_legacy/prompty/_utils.py +72 -2
  99. azure/ai/evaluation/_safety_evaluation/_safety_evaluation.py +90 -17
  100. azure/ai/evaluation/_version.py +1 -1
  101. azure/ai/evaluation/red_team/_attack_strategy.py +1 -1
  102. azure/ai/evaluation/red_team/_red_team.py +825 -450
  103. azure/ai/evaluation/red_team/_utils/metric_mapping.py +23 -0
  104. azure/ai/evaluation/red_team/_utils/strategy_utils.py +1 -1
  105. azure/ai/evaluation/simulator/_adversarial_simulator.py +63 -39
  106. azure/ai/evaluation/simulator/_constants.py +1 -0
  107. azure/ai/evaluation/simulator/_conversation/__init__.py +13 -6
  108. azure/ai/evaluation/simulator/_conversation/_conversation.py +2 -1
  109. azure/ai/evaluation/simulator/_direct_attack_simulator.py +35 -22
  110. azure/ai/evaluation/simulator/_helpers/_language_suffix_mapping.py +1 -0
  111. azure/ai/evaluation/simulator/_indirect_attack_simulator.py +40 -25
  112. azure/ai/evaluation/simulator/_model_tools/__init__.py +2 -1
  113. azure/ai/evaluation/simulator/_model_tools/_generated_rai_client.py +24 -18
  114. azure/ai/evaluation/simulator/_model_tools/_identity_manager.py +5 -10
  115. azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +65 -41
  116. azure/ai/evaluation/simulator/_model_tools/_template_handler.py +9 -5
  117. azure/ai/evaluation/simulator/_model_tools/models.py +20 -17
  118. {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.6.0.dist-info}/METADATA +25 -2
  119. {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.6.0.dist-info}/RECORD +123 -65
  120. /azure/ai/evaluation/_legacy/{_batch_engine → _common}/_logging.py +0 -0
  121. {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.6.0.dist-info}/NOTICE.txt +0 -0
  122. {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.6.0.dist-info}/WHEEL +0 -0
  123. {azure_ai_evaluation-1.5.0.dist-info → azure_ai_evaluation-1.6.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
- """Promptflow instrumentation.
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
- logging.debug("injecting OpenAI API...")
29
- inject_openai_api()
30
- logging.debug("OpenAI API injected.")
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
- collection = kwargs.get("_collection", _get_collection_from_cwd())
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
- # TODO ralphe: This has OpenTelemetry dependency. That is a future task to resolve.
68
- # return not getattr(trace.get_tracer_provider(), TRACER_PROVIDER_PROTECTED_COLLECTION_ATTR, False)
69
- return True
70
-
71
-
72
- def _get_collection_from_cwd() -> str:
73
- """Try to use cwd folder name as collection name; will fall back to default value if run into exception."""
74
- cur_folder_name = ""
75
- try:
76
- cwd = os.getcwd()
77
- cur_folder_name = os.path.basename(cwd)
78
- except Exception: # pylint: disable=broad-except
79
- # possible exception: PermissionError, FileNotFoundError, OSError, etc.
80
- pass
81
- collection = cur_folder_name or "default"
82
- return collection
83
-
84
-
85
- def _set_tracer_provider(res_attrs: Dict[str, str], protected_collection: bool) -> None:
86
- # TODO ralphe: OpenTelemetry dependency. This is a future task to resolve.
87
- pass
88
- # res = Resource(attributes=res_attrs)
89
- # tracer_provider = TracerProvider(resource=res)
90
-
91
- # cur_tracer_provider = trace.get_tracer_provider()
92
- # if isinstance(cur_tracer_provider, TracerProvider):
93
- # logging.info("tracer provider is already set, will merge the resource attributes...")
94
- # cur_res = cur_tracer_provider.resource
95
- # logging.debug("current resource: %s", cur_res.attributes)
96
- # new_res = cur_res.merge(res)
97
- # cur_tracer_provider._resource = new_res
98
- # logging.info("tracer provider is updated with resource attributes: %s", new_res.attributes)
99
- # else:
100
- # trace.set_tracer_provider(tracer_provider)
101
- # logging.info("tracer provider is set with resource attributes: %s", res.attributes)
102
-
103
- # if protected_collection:
104
- # logging.info("user specifies collection, will add a flag on tracer provider to avoid override...")
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,3 @@
1
+ # ---------------------------------------------------------
2
+ # Copyright (c) Microsoft Corporation. All rights reserved.
3
+ # ---------------------------------------------------------
@@ -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, Set, Union
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
- ignored_fields = set(["azure_deployment", "model"])
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
- connection: Connection
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
- api_key: Optional[str] = None # TODO ralphe: Replace this TokenCredential to allow for more flexible authentication
147
- azure_deployment: Optional[str] = None
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
+ )