azure-ai-evaluation 0.0.0b0__py3-none-any.whl → 1.0.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.
- azure/ai/evaluation/__init__.py +82 -0
- azure/ai/evaluation/_common/__init__.py +16 -0
- azure/ai/evaluation/_common/_experimental.py +172 -0
- azure/ai/evaluation/_common/constants.py +72 -0
- azure/ai/evaluation/_common/math.py +89 -0
- azure/ai/evaluation/_common/rai_service.py +632 -0
- azure/ai/evaluation/_common/utils.py +445 -0
- azure/ai/evaluation/_constants.py +72 -0
- azure/ai/evaluation/_evaluate/__init__.py +3 -0
- azure/ai/evaluation/_evaluate/_batch_run/__init__.py +9 -0
- azure/ai/evaluation/_evaluate/_batch_run/code_client.py +188 -0
- azure/ai/evaluation/_evaluate/_batch_run/eval_run_context.py +89 -0
- azure/ai/evaluation/_evaluate/_batch_run/proxy_client.py +99 -0
- azure/ai/evaluation/_evaluate/_batch_run/target_run_context.py +46 -0
- azure/ai/evaluation/_evaluate/_eval_run.py +571 -0
- azure/ai/evaluation/_evaluate/_evaluate.py +850 -0
- azure/ai/evaluation/_evaluate/_telemetry/__init__.py +179 -0
- azure/ai/evaluation/_evaluate/_utils.py +298 -0
- azure/ai/evaluation/_evaluators/__init__.py +3 -0
- azure/ai/evaluation/_evaluators/_bleu/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_bleu/_bleu.py +72 -0
- azure/ai/evaluation/_evaluators/_coherence/__init__.py +7 -0
- azure/ai/evaluation/_evaluators/_coherence/_coherence.py +107 -0
- azure/ai/evaluation/_evaluators/_coherence/coherence.prompty +99 -0
- azure/ai/evaluation/_evaluators/_common/__init__.py +13 -0
- azure/ai/evaluation/_evaluators/_common/_base_eval.py +344 -0
- azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +88 -0
- azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +133 -0
- azure/ai/evaluation/_evaluators/_content_safety/__init__.py +17 -0
- azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +144 -0
- azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +129 -0
- azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +123 -0
- azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +125 -0
- azure/ai/evaluation/_evaluators/_content_safety/_violence.py +126 -0
- azure/ai/evaluation/_evaluators/_eci/__init__.py +0 -0
- azure/ai/evaluation/_evaluators/_eci/_eci.py +89 -0
- azure/ai/evaluation/_evaluators/_f1_score/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +157 -0
- azure/ai/evaluation/_evaluators/_fluency/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_fluency/_fluency.py +104 -0
- azure/ai/evaluation/_evaluators/_fluency/fluency.prompty +86 -0
- azure/ai/evaluation/_evaluators/_gleu/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_gleu/_gleu.py +69 -0
- azure/ai/evaluation/_evaluators/_groundedness/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +144 -0
- azure/ai/evaluation/_evaluators/_groundedness/groundedness_with_query.prompty +113 -0
- azure/ai/evaluation/_evaluators/_groundedness/groundedness_without_query.prompty +99 -0
- azure/ai/evaluation/_evaluators/_meteor/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_meteor/_meteor.py +90 -0
- azure/ai/evaluation/_evaluators/_multimodal/__init__.py +20 -0
- azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal.py +132 -0
- azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal_base.py +55 -0
- azure/ai/evaluation/_evaluators/_multimodal/_hate_unfairness.py +100 -0
- azure/ai/evaluation/_evaluators/_multimodal/_protected_material.py +124 -0
- azure/ai/evaluation/_evaluators/_multimodal/_self_harm.py +100 -0
- azure/ai/evaluation/_evaluators/_multimodal/_sexual.py +100 -0
- azure/ai/evaluation/_evaluators/_multimodal/_violence.py +100 -0
- azure/ai/evaluation/_evaluators/_protected_material/__init__.py +5 -0
- azure/ai/evaluation/_evaluators/_protected_material/_protected_material.py +113 -0
- azure/ai/evaluation/_evaluators/_qa/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_qa/_qa.py +93 -0
- azure/ai/evaluation/_evaluators/_relevance/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_relevance/_relevance.py +114 -0
- azure/ai/evaluation/_evaluators/_relevance/relevance.prompty +100 -0
- azure/ai/evaluation/_evaluators/_retrieval/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_retrieval/_retrieval.py +112 -0
- azure/ai/evaluation/_evaluators/_retrieval/retrieval.prompty +93 -0
- azure/ai/evaluation/_evaluators/_rouge/__init__.py +10 -0
- azure/ai/evaluation/_evaluators/_rouge/_rouge.py +98 -0
- azure/ai/evaluation/_evaluators/_service_groundedness/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_service_groundedness/_service_groundedness.py +148 -0
- azure/ai/evaluation/_evaluators/_similarity/__init__.py +9 -0
- azure/ai/evaluation/_evaluators/_similarity/_similarity.py +140 -0
- azure/ai/evaluation/_evaluators/_similarity/similarity.prompty +66 -0
- azure/ai/evaluation/_evaluators/_xpia/__init__.py +5 -0
- azure/ai/evaluation/_evaluators/_xpia/xpia.py +125 -0
- azure/ai/evaluation/_exceptions.py +128 -0
- azure/ai/evaluation/_http_utils.py +466 -0
- azure/ai/evaluation/_model_configurations.py +123 -0
- azure/ai/evaluation/_user_agent.py +6 -0
- azure/ai/evaluation/_vendor/__init__.py +3 -0
- azure/ai/evaluation/_vendor/rouge_score/__init__.py +14 -0
- azure/ai/evaluation/_vendor/rouge_score/rouge_scorer.py +328 -0
- azure/ai/evaluation/_vendor/rouge_score/scoring.py +63 -0
- azure/ai/evaluation/_vendor/rouge_score/tokenize.py +63 -0
- azure/ai/evaluation/_vendor/rouge_score/tokenizers.py +53 -0
- azure/ai/evaluation/_version.py +5 -0
- azure/ai/evaluation/py.typed +0 -0
- azure/ai/evaluation/simulator/__init__.py +16 -0
- azure/ai/evaluation/simulator/_adversarial_scenario.py +46 -0
- azure/ai/evaluation/simulator/_adversarial_simulator.py +471 -0
- azure/ai/evaluation/simulator/_constants.py +27 -0
- azure/ai/evaluation/simulator/_conversation/__init__.py +316 -0
- azure/ai/evaluation/simulator/_conversation/_conversation.py +178 -0
- azure/ai/evaluation/simulator/_conversation/constants.py +30 -0
- azure/ai/evaluation/simulator/_data_sources/__init__.py +3 -0
- azure/ai/evaluation/simulator/_data_sources/grounding.json +1150 -0
- azure/ai/evaluation/simulator/_direct_attack_simulator.py +218 -0
- azure/ai/evaluation/simulator/_helpers/__init__.py +4 -0
- azure/ai/evaluation/simulator/_helpers/_language_suffix_mapping.py +17 -0
- azure/ai/evaluation/simulator/_helpers/_simulator_data_classes.py +96 -0
- azure/ai/evaluation/simulator/_indirect_attack_simulator.py +220 -0
- azure/ai/evaluation/simulator/_model_tools/__init__.py +23 -0
- azure/ai/evaluation/simulator/_model_tools/_identity_manager.py +195 -0
- azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +244 -0
- azure/ai/evaluation/simulator/_model_tools/_rai_client.py +168 -0
- azure/ai/evaluation/simulator/_model_tools/_template_handler.py +201 -0
- azure/ai/evaluation/simulator/_model_tools/models.py +614 -0
- azure/ai/evaluation/simulator/_prompty/__init__.py +0 -0
- azure/ai/evaluation/simulator/_prompty/task_query_response.prompty +65 -0
- azure/ai/evaluation/simulator/_prompty/task_simulate.prompty +37 -0
- azure/ai/evaluation/simulator/_simulator.py +716 -0
- azure/ai/evaluation/simulator/_tracing.py +89 -0
- azure/ai/evaluation/simulator/_utils.py +132 -0
- azure_ai_evaluation-1.0.0.dist-info/METADATA +595 -0
- azure_ai_evaluation-1.0.0.dist-info/NOTICE.txt +70 -0
- azure_ai_evaluation-1.0.0.dist-info/RECORD +119 -0
- {azure_ai_evaluation-0.0.0b0.dist-info → azure_ai_evaluation-1.0.0.dist-info}/WHEEL +1 -1
- azure_ai_evaluation-1.0.0.dist-info/top_level.txt +1 -0
- azure_ai_evaluation-0.0.0b0.dist-info/METADATA +0 -7
- azure_ai_evaluation-0.0.0b0.dist-info/RECORD +0 -4
- azure_ai_evaluation-0.0.0b0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# ---------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import math
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, List, Literal, Mapping, Type, TypeVar, Tuple, Union, cast, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
import nltk
|
|
11
|
+
from typing_extensions import NotRequired, Required, TypeGuard
|
|
12
|
+
from promptflow.core._errors import MissingRequiredPackage
|
|
13
|
+
from azure.ai.evaluation._constants import AZURE_OPENAI_TYPE, OPENAI_TYPE
|
|
14
|
+
from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
|
|
15
|
+
from azure.ai.evaluation._model_configurations import (
|
|
16
|
+
AzureAIProject,
|
|
17
|
+
AzureOpenAIModelConfiguration,
|
|
18
|
+
OpenAIModelConfiguration,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from . import constants
|
|
22
|
+
|
|
23
|
+
_nltk_data_download_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
T_TypedDict = TypeVar("T_TypedDict", bound=Mapping[Any, Any])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_harm_severity_level(harm_score: Union[float, int]) -> Union[str, float]:
|
|
29
|
+
"""Generate harm severity level based on harm score.
|
|
30
|
+
|
|
31
|
+
:param harm_score: The harm score to be evaluated.
|
|
32
|
+
:type harm_score: int
|
|
33
|
+
:return: The harm severity level. If harm score is None or numpy.nan, returns numpy.nan.
|
|
34
|
+
:rtype: str
|
|
35
|
+
"""
|
|
36
|
+
HARM_SEVERITY_LEVEL_MAPPING = {
|
|
37
|
+
constants.HarmSeverityLevel.VeryLow: [0, 1],
|
|
38
|
+
constants.HarmSeverityLevel.Low: [2, 3],
|
|
39
|
+
constants.HarmSeverityLevel.Medium: [4, 5],
|
|
40
|
+
constants.HarmSeverityLevel.High: [6, 7],
|
|
41
|
+
}
|
|
42
|
+
if math.isnan(harm_score) or harm_score is None:
|
|
43
|
+
return math.nan
|
|
44
|
+
for harm_level, harm_score_range in HARM_SEVERITY_LEVEL_MAPPING.items():
|
|
45
|
+
if harm_score_range[0] <= harm_score <= harm_score_range[1]:
|
|
46
|
+
return harm_level.value
|
|
47
|
+
return math.nan
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_nltk_data_downloaded():
|
|
51
|
+
"""Download NLTK data packages if not already downloaded."""
|
|
52
|
+
nltk_data = [
|
|
53
|
+
("wordnet", "corpora/wordnet.zip"),
|
|
54
|
+
("perluniprops", "misc/perluniprops.zip"),
|
|
55
|
+
("punkt", "tokenizers/punkt.zip"),
|
|
56
|
+
("punkt_tab", "tokenizers/punkt_tab.zip"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
with _nltk_data_download_lock:
|
|
60
|
+
for _id, resource_name in nltk_data:
|
|
61
|
+
try:
|
|
62
|
+
nltk.find(resource_name)
|
|
63
|
+
except LookupError:
|
|
64
|
+
nltk.download(_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def nltk_tokenize(text: str) -> List[str]:
|
|
68
|
+
"""Tokenize the input text using the NLTK tokenizer.
|
|
69
|
+
|
|
70
|
+
:param text: The text to tokenize
|
|
71
|
+
:type text: str
|
|
72
|
+
:return: A list of tokens
|
|
73
|
+
:rtype: list[str]
|
|
74
|
+
"""
|
|
75
|
+
ensure_nltk_data_downloaded()
|
|
76
|
+
|
|
77
|
+
if not text.isascii():
|
|
78
|
+
# Use NISTTokenizer for international tokenization
|
|
79
|
+
from nltk.tokenize.nist import NISTTokenizer
|
|
80
|
+
|
|
81
|
+
tokens = NISTTokenizer().international_tokenize(text)
|
|
82
|
+
else:
|
|
83
|
+
# By default, use NLTK word tokenizer
|
|
84
|
+
tokens = nltk.word_tokenize(text)
|
|
85
|
+
|
|
86
|
+
return list(tokens)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_aoi_model_config(val: object) -> TypeGuard[AzureOpenAIModelConfiguration]:
|
|
90
|
+
return isinstance(val, dict) and all(isinstance(val.get(k), str) for k in ("azure_endpoint", "azure_deployment"))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_openai_model_config(val: object) -> TypeGuard[OpenAIModelConfiguration]:
|
|
94
|
+
return isinstance(val, dict) and all(isinstance(val.get(k), str) for k in ("model"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_model_config_type(
|
|
98
|
+
model_config: Union[AzureOpenAIModelConfiguration, OpenAIModelConfiguration],
|
|
99
|
+
) -> None:
|
|
100
|
+
if _is_aoi_model_config(model_config):
|
|
101
|
+
model_config["type"] = AZURE_OPENAI_TYPE
|
|
102
|
+
elif _is_openai_model_config(model_config):
|
|
103
|
+
model_config["type"] = OPENAI_TYPE
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def construct_prompty_model_config(
|
|
107
|
+
model_config: Union[AzureOpenAIModelConfiguration, OpenAIModelConfiguration],
|
|
108
|
+
default_api_version: str,
|
|
109
|
+
user_agent: str,
|
|
110
|
+
) -> dict:
|
|
111
|
+
parse_model_config_type(model_config)
|
|
112
|
+
|
|
113
|
+
if _is_aoi_model_config(model_config):
|
|
114
|
+
model_config["api_version"] = model_config.get("api_version", default_api_version)
|
|
115
|
+
|
|
116
|
+
prompty_model_config: dict = {"configuration": model_config, "parameters": {"extra_headers": {}}}
|
|
117
|
+
|
|
118
|
+
# Handle "RuntimeError: Event loop is closed" from httpx AsyncClient
|
|
119
|
+
# https://github.com/encode/httpx/discussions/2959
|
|
120
|
+
prompty_model_config["parameters"]["extra_headers"].update({"Connection": "close"})
|
|
121
|
+
|
|
122
|
+
if _is_aoi_model_config(model_config) and user_agent:
|
|
123
|
+
prompty_model_config["parameters"]["extra_headers"].update({"x-ms-useragent": user_agent})
|
|
124
|
+
|
|
125
|
+
return prompty_model_config
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate_azure_ai_project(o: object) -> AzureAIProject:
|
|
129
|
+
fields = {"subscription_id": str, "resource_group_name": str, "project_name": str}
|
|
130
|
+
|
|
131
|
+
if not isinstance(o, dict):
|
|
132
|
+
msg = "The 'azure_ai_project' parameter must be a dictionary."
|
|
133
|
+
raise EvaluationException(
|
|
134
|
+
message=msg,
|
|
135
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
136
|
+
blame=ErrorBlame.USER_ERROR,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
missing_fields = set(fields.keys()) - o.keys()
|
|
140
|
+
|
|
141
|
+
if missing_fields:
|
|
142
|
+
msg = (
|
|
143
|
+
"The 'azure_ai_project' dictionary is missing the following required "
|
|
144
|
+
f"field(s): {', '.join(f'{field}' for field in missing_fields)}."
|
|
145
|
+
)
|
|
146
|
+
raise EvaluationException(
|
|
147
|
+
message=msg,
|
|
148
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
149
|
+
blame=ErrorBlame.USER_ERROR,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
for field_name, expected_type in fields.items():
|
|
153
|
+
if isinstance(o[field_name], expected_type):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
msg = f"Invalid type for field '{field_name}'. Expected {expected_type}, but got {type(o[field_name])}."
|
|
157
|
+
raise EvaluationException(
|
|
158
|
+
message=msg,
|
|
159
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
160
|
+
blame=ErrorBlame.USER_ERROR,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return cast(AzureAIProject, o)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_model_config(config: dict) -> Union[AzureOpenAIModelConfiguration, OpenAIModelConfiguration]:
|
|
167
|
+
try:
|
|
168
|
+
return _validate_typed_dict(config, AzureOpenAIModelConfiguration)
|
|
169
|
+
except TypeError:
|
|
170
|
+
try:
|
|
171
|
+
return _validate_typed_dict(config, OpenAIModelConfiguration)
|
|
172
|
+
except TypeError as e:
|
|
173
|
+
msg = "Model config validation failed."
|
|
174
|
+
raise EvaluationException(
|
|
175
|
+
message=msg, internal_message=msg, category=ErrorCategory.MISSING_FIELD, blame=ErrorBlame.USER_ERROR
|
|
176
|
+
) from e
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _validate_typed_dict(o: object, t: Type[T_TypedDict]) -> T_TypedDict:
|
|
180
|
+
"""Do very basic runtime validation that an object is a typed dict
|
|
181
|
+
|
|
182
|
+
.. warning::
|
|
183
|
+
|
|
184
|
+
This validation is very basic, robust enough to cover some very simple TypedDicts.
|
|
185
|
+
Ideally, validation of this kind should be delegated to something more robust.
|
|
186
|
+
|
|
187
|
+
You will very quickly run into limitations trying to apply this function more broadly:
|
|
188
|
+
* Doesn't support stringized annotations at all
|
|
189
|
+
* Very limited support for generics, and "special form" (NoReturn, NotRequired, Required, etc...) types.
|
|
190
|
+
* Error messages are poor, especially if there is any nesting.
|
|
191
|
+
|
|
192
|
+
:param object o: The object to check
|
|
193
|
+
:param Type[T_TypedDict] t: The TypedDict to validate against
|
|
194
|
+
:raises NotImplementedError: Several forms of validation are unsupported
|
|
195
|
+
* Checking against stringized annotations
|
|
196
|
+
* Checking a generic that is not one of a few basic forms
|
|
197
|
+
:raises TypeError: If a value does not match the specified annotation
|
|
198
|
+
:raises ValueError: If t's annotation is not a string, type of a special form (e.g. NotRequired, Required, etc...)
|
|
199
|
+
:returns: The object passed in
|
|
200
|
+
:rtype: T_TypedDict
|
|
201
|
+
"""
|
|
202
|
+
if not isinstance(o, dict):
|
|
203
|
+
raise TypeError(f"Expected type 'dict', got type '{type(object)}'.")
|
|
204
|
+
|
|
205
|
+
annotations = t.__annotations__
|
|
206
|
+
is_total = getattr(t, "__total__", False)
|
|
207
|
+
unknown_keys = set(o.keys()) - annotations.keys()
|
|
208
|
+
|
|
209
|
+
if unknown_keys:
|
|
210
|
+
raise TypeError(f"dict contains unknown keys: {list(unknown_keys)!r}")
|
|
211
|
+
|
|
212
|
+
required_keys = {
|
|
213
|
+
k
|
|
214
|
+
for k in annotations
|
|
215
|
+
if (is_total and get_origin(annotations[k]) is not NotRequired)
|
|
216
|
+
or (not is_total and get_origin(annotations[k]) is Required)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
missing_keys = required_keys - o.keys()
|
|
220
|
+
|
|
221
|
+
if missing_keys:
|
|
222
|
+
raise TypeError(f"Missing required keys: {list(missing_keys)!r}.")
|
|
223
|
+
|
|
224
|
+
def validate_annotation(v: object, annotation: Union[str, type, object]) -> bool:
|
|
225
|
+
if isinstance(annotation, str):
|
|
226
|
+
raise NotImplementedError("Missing support for validating against stringized annotations.")
|
|
227
|
+
|
|
228
|
+
if (origin := get_origin(annotation)) is not None:
|
|
229
|
+
if origin is tuple:
|
|
230
|
+
validate_annotation(v, tuple)
|
|
231
|
+
tuple_args = get_args(annotation)
|
|
232
|
+
if len(cast(tuple, v)) != len(tuple_args):
|
|
233
|
+
raise TypeError(f"Expected a {len(tuple_args)}-tuple, got a {len(cast(tuple, v))}-tuple.")
|
|
234
|
+
for tuple_val, tuple_args in zip(cast(tuple, v), tuple_args):
|
|
235
|
+
validate_annotation(tuple_val, tuple_args)
|
|
236
|
+
elif origin is dict:
|
|
237
|
+
validate_annotation(v, dict)
|
|
238
|
+
dict_key_ann, dict_val_ann = get_args(annotation)
|
|
239
|
+
for dict_key, dict_val in cast(dict, v).items():
|
|
240
|
+
validate_annotation(dict_val, dict_val_ann)
|
|
241
|
+
validate_annotation(dict_key, dict_key_ann)
|
|
242
|
+
elif origin is list:
|
|
243
|
+
validate_annotation(v, list)
|
|
244
|
+
list_val_ann = get_args(annotation)[0]
|
|
245
|
+
for list_val in cast(list, v):
|
|
246
|
+
validate_annotation(list_val, list_val_ann)
|
|
247
|
+
elif origin is Union:
|
|
248
|
+
for generic_arg in get_args(annotation):
|
|
249
|
+
try:
|
|
250
|
+
validate_annotation(v, generic_arg)
|
|
251
|
+
return True
|
|
252
|
+
except TypeError:
|
|
253
|
+
pass
|
|
254
|
+
raise TypeError(f"Expected value to have type {annotation}. Received type {type(v)}")
|
|
255
|
+
elif origin is Literal:
|
|
256
|
+
literal_args = get_args(annotation)
|
|
257
|
+
if not any(type(literal) is type(v) and literal == v for literal in literal_args):
|
|
258
|
+
raise TypeError(f"Expected value to be one of {list(literal_args)!r}. Received type {type(v)}")
|
|
259
|
+
elif any(origin is g for g in (NotRequired, Required)):
|
|
260
|
+
validate_annotation(v, get_args(annotation)[0])
|
|
261
|
+
else:
|
|
262
|
+
raise NotImplementedError(f"Validation not implemented for generic {origin}.")
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
if isinstance(annotation, type):
|
|
266
|
+
if not isinstance(v, annotation):
|
|
267
|
+
raise TypeError(f"Expected value to have type {annotation}. Received type {type(v)}.")
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
raise ValueError("Annotation to validate against should be a str, type, or generic.")
|
|
271
|
+
|
|
272
|
+
for k, v in o.items():
|
|
273
|
+
validate_annotation(v, annotations[k])
|
|
274
|
+
|
|
275
|
+
return cast(T_TypedDict, o)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def parse_quality_evaluator_reason_score(llm_output: str) -> Tuple[float, str]:
|
|
279
|
+
"""Parse the output of prompt-based quality evaluators that return a score and reason.
|
|
280
|
+
|
|
281
|
+
Current supported evaluators:
|
|
282
|
+
- Fluency
|
|
283
|
+
- Relevance
|
|
284
|
+
- Retrieval
|
|
285
|
+
- Groundedness
|
|
286
|
+
- Coherence
|
|
287
|
+
|
|
288
|
+
:param llm_output: The output of the prompt-based quality evaluator.
|
|
289
|
+
:type llm_output: str
|
|
290
|
+
:return: The score and reason.
|
|
291
|
+
:rtype: Tuple[float, str]
|
|
292
|
+
"""
|
|
293
|
+
score = math.nan
|
|
294
|
+
reason = ""
|
|
295
|
+
if llm_output:
|
|
296
|
+
try:
|
|
297
|
+
score_pattern = r"<S2>\D*?([1-5]).*?</S2>"
|
|
298
|
+
reason_pattern = r"<S1>(.*?)</S1>"
|
|
299
|
+
score_match = re.findall(score_pattern, llm_output, re.DOTALL)
|
|
300
|
+
reason_match = re.findall(reason_pattern, llm_output, re.DOTALL)
|
|
301
|
+
if score_match:
|
|
302
|
+
score = float(score_match[0].strip())
|
|
303
|
+
if reason_match:
|
|
304
|
+
reason = reason_match[0].strip()
|
|
305
|
+
except ValueError as exc:
|
|
306
|
+
raise EvaluationException(
|
|
307
|
+
message=f"Failed to parse model output: \n{llm_output}",
|
|
308
|
+
internal_message="Failed to parse model output.",
|
|
309
|
+
category=ErrorCategory.FAILED_EXECUTION,
|
|
310
|
+
blame=ErrorBlame.SYSTEM_ERROR,
|
|
311
|
+
) from exc
|
|
312
|
+
|
|
313
|
+
return score, reason
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def remove_optional_singletons(eval_class, singletons):
|
|
317
|
+
required_singletons = singletons.copy()
|
|
318
|
+
if hasattr(eval_class, "_OPTIONAL_PARAMS"): # pylint: disable=protected-access
|
|
319
|
+
for param in eval_class._OPTIONAL_PARAMS: # pylint: disable=protected-access
|
|
320
|
+
if param in singletons:
|
|
321
|
+
del required_singletons[param]
|
|
322
|
+
return required_singletons
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def retrieve_content_type(assistant_messages: List, metric: str) -> str:
|
|
326
|
+
"""Get the content type for service payload.
|
|
327
|
+
|
|
328
|
+
:param assistant_messages: The list of messages to be annotated by evaluation service
|
|
329
|
+
:type assistant_messages: list
|
|
330
|
+
:param metric: A string representing the metric type
|
|
331
|
+
:type metric: str
|
|
332
|
+
:return: A text representing the content type. Example: 'text', or 'image'
|
|
333
|
+
:rtype: str
|
|
334
|
+
"""
|
|
335
|
+
# Check if metric is "protected_material"
|
|
336
|
+
if metric == "protected_material":
|
|
337
|
+
return "image"
|
|
338
|
+
|
|
339
|
+
# Iterate through each message
|
|
340
|
+
for message in assistant_messages:
|
|
341
|
+
# Ensure "content" exists in the message and is iterable
|
|
342
|
+
if isinstance(message.get("content", []), list):
|
|
343
|
+
for content in message.get("content", []):
|
|
344
|
+
if content.get("type") == "image_url":
|
|
345
|
+
return "image"
|
|
346
|
+
# Default return if no image was found
|
|
347
|
+
return "text"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def validate_conversation(conversation):
|
|
351
|
+
def raise_exception(msg, target):
|
|
352
|
+
raise EvaluationException(
|
|
353
|
+
message=msg,
|
|
354
|
+
internal_message=msg,
|
|
355
|
+
target=target,
|
|
356
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
357
|
+
blame=ErrorBlame.USER_ERROR,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if not conversation or "messages" not in conversation:
|
|
361
|
+
raise_exception(
|
|
362
|
+
"Attribute 'messages' is missing in the request",
|
|
363
|
+
ErrorTarget.CONTENT_SAFETY_CHAT_EVALUATOR,
|
|
364
|
+
)
|
|
365
|
+
messages = conversation["messages"]
|
|
366
|
+
if not isinstance(messages, list):
|
|
367
|
+
raise_exception(
|
|
368
|
+
"'messages' parameter must be a JSON-compatible list of chat messages",
|
|
369
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
370
|
+
)
|
|
371
|
+
expected_roles = {"user", "assistant", "system"}
|
|
372
|
+
image_found = False
|
|
373
|
+
assistant_message_count = 0
|
|
374
|
+
user_message_count = 0
|
|
375
|
+
for num, message in enumerate(messages, 1):
|
|
376
|
+
if not isinstance(message, dict):
|
|
377
|
+
try:
|
|
378
|
+
from azure.ai.inference.models import (
|
|
379
|
+
ChatRequestMessage,
|
|
380
|
+
UserMessage,
|
|
381
|
+
AssistantMessage,
|
|
382
|
+
SystemMessage,
|
|
383
|
+
ImageContentItem,
|
|
384
|
+
)
|
|
385
|
+
except ImportError as ex:
|
|
386
|
+
raise MissingRequiredPackage(
|
|
387
|
+
message="Please install 'azure-ai-inference' package to use SystemMessage, "
|
|
388
|
+
"UserMessage or AssistantMessage."
|
|
389
|
+
) from ex
|
|
390
|
+
|
|
391
|
+
if isinstance(message, ChatRequestMessage) and not isinstance(
|
|
392
|
+
message, (UserMessage, AssistantMessage, SystemMessage)
|
|
393
|
+
):
|
|
394
|
+
raise_exception(
|
|
395
|
+
f"Messages must be a strongly typed class of ChatRequestMessage. Message number: {num}",
|
|
396
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
397
|
+
)
|
|
398
|
+
if isinstance(message, AssistantMessage):
|
|
399
|
+
assistant_message_count += 1
|
|
400
|
+
if isinstance(message, UserMessage):
|
|
401
|
+
user_message_count += 1
|
|
402
|
+
if isinstance(message.content, list) and any(
|
|
403
|
+
isinstance(item, ImageContentItem) for item in message.content
|
|
404
|
+
):
|
|
405
|
+
image_found = True
|
|
406
|
+
continue
|
|
407
|
+
if message.get("role") not in expected_roles:
|
|
408
|
+
raise_exception(
|
|
409
|
+
f"Invalid role provided: {message.get('role')}. Message number: {num}",
|
|
410
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
411
|
+
)
|
|
412
|
+
if message.get("role") == "assistant":
|
|
413
|
+
assistant_message_count += 1
|
|
414
|
+
if message.get("role") == "user":
|
|
415
|
+
user_message_count += 1
|
|
416
|
+
content = message.get("content")
|
|
417
|
+
if not isinstance(content, (str, list)):
|
|
418
|
+
raise_exception(
|
|
419
|
+
f"Content in each turn must be a string or array. Message number: {num}",
|
|
420
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
421
|
+
)
|
|
422
|
+
if isinstance(content, list):
|
|
423
|
+
if any(item.get("type") == "image_url" and "url" in item.get("image_url", {}) for item in content):
|
|
424
|
+
image_found = True
|
|
425
|
+
if not image_found:
|
|
426
|
+
raise_exception(
|
|
427
|
+
"Message needs to have multi-modal input like images.",
|
|
428
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
429
|
+
)
|
|
430
|
+
if assistant_message_count == 0:
|
|
431
|
+
raise_exception(
|
|
432
|
+
"Assistant role required in one of the messages.",
|
|
433
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
434
|
+
)
|
|
435
|
+
if user_message_count == 0:
|
|
436
|
+
raise_exception(
|
|
437
|
+
"User role required in one of the messages.",
|
|
438
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
439
|
+
)
|
|
440
|
+
if assistant_message_count > 1:
|
|
441
|
+
raise_exception(
|
|
442
|
+
"Evaluators for multimodal conversations only support single turn. "
|
|
443
|
+
"User and assistant role expected as the only role in each message.",
|
|
444
|
+
ErrorTarget.CONTENT_SAFETY_MULTIMODAL_EVALUATOR,
|
|
445
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ---------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EvaluationMetrics:
|
|
8
|
+
"""Metrics for model evaluation."""
|
|
9
|
+
|
|
10
|
+
GROUNDEDNESS = "groundedness"
|
|
11
|
+
RELEVANCE = "relevance"
|
|
12
|
+
COHERENCE = "coherence"
|
|
13
|
+
FLUENCY = "fluency"
|
|
14
|
+
SIMILARITY = "similarity"
|
|
15
|
+
F1_SCORE = "f1_score"
|
|
16
|
+
RETRIEVAL_SCORE = "retrieval_score"
|
|
17
|
+
HATE_FAIRNESS = "hate_fairness"
|
|
18
|
+
HATE_UNFAIRNESS = "hate_unfairness"
|
|
19
|
+
VIOLENCE = "violence"
|
|
20
|
+
SELF_HARM = "self_harm"
|
|
21
|
+
SEXUAL = "sexual"
|
|
22
|
+
PROTECTED_MATERIAL = "protected_material"
|
|
23
|
+
XPIA = "xpia"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _InternalEvaluationMetrics:
|
|
27
|
+
"""Evaluation metrics that are not publicly supported.
|
|
28
|
+
These metrics are experimental and subject to potential change or migration to the main
|
|
29
|
+
enum over time.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
ECI = "eci"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Prefixes:
|
|
36
|
+
"""Column prefixes for inputs and outputs."""
|
|
37
|
+
|
|
38
|
+
INPUTS = "inputs."
|
|
39
|
+
OUTPUTS = "outputs."
|
|
40
|
+
TSG_OUTPUTS = "__outputs."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DefaultOpenEncoding:
|
|
44
|
+
"""Enum that captures SDK's default values for the encoding param of open(...)"""
|
|
45
|
+
|
|
46
|
+
READ = "utf-8-sig"
|
|
47
|
+
"""SDK Default Encoding when reading a file"""
|
|
48
|
+
WRITE = "utf-8"
|
|
49
|
+
"""SDK Default Encoding when writing a file"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EvaluationRunProperties:
|
|
53
|
+
"""Defines properties used to identify an evaluation run by UI"""
|
|
54
|
+
|
|
55
|
+
RUN_TYPE = "runType"
|
|
56
|
+
EVALUATION_RUN = "_azureml.evaluation_run"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
DEFAULT_EVALUATION_RESULTS_FILE_NAME = "evaluation_results.json"
|
|
60
|
+
|
|
61
|
+
CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT = 4
|
|
62
|
+
|
|
63
|
+
PF_BATCH_TIMEOUT_SEC_DEFAULT = 3600
|
|
64
|
+
PF_BATCH_TIMEOUT_SEC = "PF_BATCH_TIMEOUT_SEC"
|
|
65
|
+
PF_DISABLE_TRACING = "PF_DISABLE_TRACING"
|
|
66
|
+
|
|
67
|
+
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"
|
|
68
|
+
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT_DEFAULT = 60
|
|
69
|
+
|
|
70
|
+
AZURE_OPENAI_TYPE: Literal["azure_openai"] = "azure_openai"
|
|
71
|
+
|
|
72
|
+
OPENAI_TYPE: Literal["openai"] = "openai"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# ---------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------
|
|
4
|
+
from .eval_run_context import EvalRunContext
|
|
5
|
+
from .code_client import CodeClient
|
|
6
|
+
from .proxy_client import ProxyClient
|
|
7
|
+
from .target_run_context import TargetRunContext
|
|
8
|
+
|
|
9
|
+
__all__ = ["CodeClient", "ProxyClient", "EvalRunContext", "TargetRunContext"]
|