azure-ai-evaluation 1.1.0__py3-none-any.whl → 1.3.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 +1 -15
- azure/ai/evaluation/_azure/_clients.py +24 -8
- azure/ai/evaluation/_azure/_models.py +2 -2
- azure/ai/evaluation/_common/utils.py +8 -8
- azure/ai/evaluation/_constants.py +21 -0
- azure/ai/evaluation/_evaluate/_batch_run/__init__.py +2 -1
- azure/ai/evaluation/_evaluate/_eval_run.py +3 -1
- azure/ai/evaluation/_evaluate/_evaluate.py +74 -14
- azure/ai/evaluation/_evaluate/_utils.py +27 -0
- azure/ai/evaluation/_evaluators/_bleu/_bleu.py +46 -25
- azure/ai/evaluation/_evaluators/_common/__init__.py +2 -0
- azure/ai/evaluation/_evaluators/_common/_base_eval.py +69 -4
- azure/ai/evaluation/_evaluators/_common/_base_multi_eval.py +61 -0
- azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +7 -1
- azure/ai/evaluation/_evaluators/_common/_conversation_aggregators.py +49 -0
- azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +5 -42
- azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +2 -0
- azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +2 -0
- azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +2 -0
- azure/ai/evaluation/_evaluators/_content_safety/_violence.py +2 -0
- azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +61 -68
- azure/ai/evaluation/_evaluators/_gleu/_gleu.py +45 -23
- azure/ai/evaluation/_evaluators/_meteor/_meteor.py +55 -34
- azure/ai/evaluation/_evaluators/_qa/_qa.py +32 -27
- azure/ai/evaluation/_evaluators/_rouge/_rouge.py +44 -23
- azure/ai/evaluation/_evaluators/_similarity/_similarity.py +41 -81
- azure/ai/evaluation/_exceptions.py +0 -1
- azure/ai/evaluation/_safety_evaluation/__init__.py +3 -0
- azure/ai/evaluation/_safety_evaluation/_safety_evaluation.py +640 -0
- azure/ai/evaluation/_version.py +2 -1
- azure/ai/evaluation/simulator/_adversarial_simulator.py +10 -3
- azure/ai/evaluation/simulator/_conversation/__init__.py +4 -5
- azure/ai/evaluation/simulator/_conversation/_conversation.py +4 -0
- azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +2 -0
- azure/ai/evaluation/simulator/_simulator.py +21 -13
- {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/METADATA +77 -7
- {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/RECORD +40 -44
- azure/ai/evaluation/_evaluators/_multimodal/__init__.py +0 -20
- azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal.py +0 -132
- azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal_base.py +0 -55
- azure/ai/evaluation/_evaluators/_multimodal/_hate_unfairness.py +0 -100
- azure/ai/evaluation/_evaluators/_multimodal/_protected_material.py +0 -124
- azure/ai/evaluation/_evaluators/_multimodal/_self_harm.py +0 -100
- azure/ai/evaluation/_evaluators/_multimodal/_sexual.py +0 -100
- azure/ai/evaluation/_evaluators/_multimodal/_violence.py +0 -100
- {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/NOTICE.txt +0 -0
- {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/WHEEL +0 -0
- {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# ---------------------------------------------------------
|
|
2
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
# ---------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import os
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from azure.ai.evaluation._common._experimental import experimental
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
12
|
+
from azure.ai.evaluation._common.math import list_mean_nan_safe
|
|
13
|
+
from azure.ai.evaluation._constants import CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT
|
|
14
|
+
from azure.ai.evaluation._evaluators import _content_safety, _protected_material, _groundedness, _relevance, _similarity, _fluency, _xpia, _coherence
|
|
15
|
+
from azure.ai.evaluation._evaluate import _evaluate
|
|
16
|
+
from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
|
|
17
|
+
from azure.ai.evaluation._model_configurations import AzureAIProject, EvaluationResult
|
|
18
|
+
from azure.ai.evaluation.simulator import Simulator, AdversarialSimulator, AdversarialScenario, AdversarialScenarioJailbreak, IndirectAttackSimulator, DirectAttackSimulator
|
|
19
|
+
from azure.ai.evaluation.simulator._utils import JsonLineList
|
|
20
|
+
from azure.ai.evaluation._common.utils import validate_azure_ai_project
|
|
21
|
+
from azure.ai.evaluation._model_configurations import AzureOpenAIModelConfiguration, OpenAIModelConfiguration
|
|
22
|
+
from azure.core.credentials import TokenCredential
|
|
23
|
+
import json
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
def _setup_logger():
|
|
29
|
+
"""Configure and return a logger instance for the CustomAdversarialSimulator.
|
|
30
|
+
|
|
31
|
+
:return: The logger instance.
|
|
32
|
+
:rtype: logging.Logger
|
|
33
|
+
"""
|
|
34
|
+
log_filename = datetime.now().strftime("%Y_%m_%d__%H_%M.log")
|
|
35
|
+
logger = logging.getLogger("CustomAdversarialSimulatorLogger")
|
|
36
|
+
logger.setLevel(logging.DEBUG)
|
|
37
|
+
file_handler = logging.FileHandler(log_filename)
|
|
38
|
+
file_handler.setLevel(logging.DEBUG)
|
|
39
|
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
40
|
+
file_handler.setFormatter(formatter)
|
|
41
|
+
logger.addHandler(file_handler)
|
|
42
|
+
|
|
43
|
+
return logger
|
|
44
|
+
|
|
45
|
+
@experimental
|
|
46
|
+
class _SafetyEvaluator(Enum):
|
|
47
|
+
'''
|
|
48
|
+
Evaluator types for Safety evaluation.
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
CONTENT_SAFETY = "content_safety"
|
|
52
|
+
GROUNDEDNESS = "groundedness"
|
|
53
|
+
PROTECTED_MATERIAL = "protected_material"
|
|
54
|
+
RELEVANCE = "relevance"
|
|
55
|
+
SIMILARITY = "similarity"
|
|
56
|
+
FLUENCY = "fluency"
|
|
57
|
+
COHERENCE = "coherence"
|
|
58
|
+
INDIRECT_ATTACK = "indirect_attack"
|
|
59
|
+
DIRECT_ATTACK = "direct_attack"
|
|
60
|
+
|
|
61
|
+
@experimental
|
|
62
|
+
class _SafetyEvaluation:
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
azure_ai_project: dict,
|
|
66
|
+
credential: TokenCredential,
|
|
67
|
+
model_config: Optional[Union[AzureOpenAIModelConfiguration, OpenAIModelConfiguration]] = None,
|
|
68
|
+
):
|
|
69
|
+
'''
|
|
70
|
+
Initializes a SafetyEvaluation object.
|
|
71
|
+
|
|
72
|
+
:param azure_ai_project: A dictionary defining the Azure AI project. Required keys are 'subscription_id', 'resource_group_name', and 'project_name'.
|
|
73
|
+
:type azure_ai_project: Dict[str, str]
|
|
74
|
+
:param credential: The credential for connecting to Azure AI project.
|
|
75
|
+
:type credential: ~azure.core.credentials.TokenCredential
|
|
76
|
+
:param model_config: A dictionary defining the configuration for the model. Acceptable types are AzureOpenAIModelConfiguration and OpenAIModelConfiguration.
|
|
77
|
+
:type model_config: Union[~azure.ai.evaluation.AzureOpenAIModelConfiguration, ~azure.ai.evaluation.OpenAIModelConfiguration]
|
|
78
|
+
:raises ValueError: If the model_config does not contain the required keys or any value is None.
|
|
79
|
+
'''
|
|
80
|
+
if model_config:
|
|
81
|
+
self._validate_model_config(model_config)
|
|
82
|
+
self.model_config = model_config
|
|
83
|
+
else:
|
|
84
|
+
self.model_config = None
|
|
85
|
+
validate_azure_ai_project(azure_ai_project)
|
|
86
|
+
self.azure_ai_project = AzureAIProject(**azure_ai_project)
|
|
87
|
+
self.credential=credential
|
|
88
|
+
self.logger = _setup_logger()
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _validate_model_config(model_config: Any):
|
|
92
|
+
"""
|
|
93
|
+
Validates the model_config to ensure all required keys are present and have non-None values.
|
|
94
|
+
If 'type' is not specified, it will attempt to infer the type based on the keys present.
|
|
95
|
+
|
|
96
|
+
:param model_config: The model configuration dictionary.
|
|
97
|
+
:type model_config: Dict[str, Any]
|
|
98
|
+
:raises ValueError: If required keys are missing or any of the values are None.
|
|
99
|
+
"""
|
|
100
|
+
# Attempt to infer 'type' if not provided
|
|
101
|
+
if "type" not in model_config:
|
|
102
|
+
if "azure_deployment" in model_config and "azure_endpoint" in model_config:
|
|
103
|
+
model_config["type"] = "azure_openai"
|
|
104
|
+
elif "model" in model_config:
|
|
105
|
+
model_config["type"] = "openai"
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"Unable to infer 'type' from model_config. Please specify 'type' as 'azure_openai' or 'openai'."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if model_config["type"] == "azure_openai":
|
|
112
|
+
required_keys = ["azure_deployment", "azure_endpoint"]
|
|
113
|
+
elif model_config["type"] == "openai":
|
|
114
|
+
required_keys = ["api_key", "model"]
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError("model_config 'type' must be 'azure_openai' or 'openai'.")
|
|
117
|
+
|
|
118
|
+
missing_keys = [key for key in required_keys if key not in model_config]
|
|
119
|
+
if missing_keys:
|
|
120
|
+
raise ValueError(f"model_config is missing required keys: {', '.join(missing_keys)}")
|
|
121
|
+
none_keys = [key for key in required_keys if model_config.get(key) is None]
|
|
122
|
+
if none_keys:
|
|
123
|
+
raise ValueError(f"The following keys in model_config must not be None: {', '.join(none_keys)}")
|
|
124
|
+
|
|
125
|
+
async def _simulate(
|
|
126
|
+
self,
|
|
127
|
+
target: Callable,
|
|
128
|
+
max_conversation_turns: int = 1,
|
|
129
|
+
max_simulation_results: int = 3,
|
|
130
|
+
conversation_turns : List[List[Union[str, Dict[str, Any]]]] = [],
|
|
131
|
+
tasks: List[str] = [],
|
|
132
|
+
adversarial_scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
|
|
133
|
+
source_text: Optional[str] = None,
|
|
134
|
+
direct_attack: bool = False,
|
|
135
|
+
) -> Dict[str, str]:
|
|
136
|
+
'''
|
|
137
|
+
Generates synthetic conversations based on provided parameters.
|
|
138
|
+
|
|
139
|
+
:param target: The target function to call during the simulation.
|
|
140
|
+
:type target: Callable
|
|
141
|
+
:param max_conversation_turns: The maximum number of turns in a conversation.
|
|
142
|
+
:type max_conversation_turns: int
|
|
143
|
+
:param max_simulation_results: The maximum number of simulation results to generate.
|
|
144
|
+
:type max_simulation_results: int
|
|
145
|
+
:param conversation_turns: Predefined conversation turns to simulate.
|
|
146
|
+
:type conversation_turns: List[List[Union[str, Dict[str, Any]]]]
|
|
147
|
+
:param tasks A list of user tasks, each represented as a list of strings. Text should be relevant for the tasks and facilitate the simulation. One example is to use text to provide context for the tasks.
|
|
148
|
+
:type tasks: List[str] = [],
|
|
149
|
+
:param adversarial_scenario: The adversarial scenario to simulate. If None, the non-adversarial Simulator is used.
|
|
150
|
+
:type adversarial_scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
|
|
151
|
+
:param source_text: The source text to use as grounding document in the simulation.
|
|
152
|
+
:type source_text: Optional[str]
|
|
153
|
+
:param direct_attack: If True, the DirectAttackSimulator will be run.
|
|
154
|
+
:type direct_attack: bool
|
|
155
|
+
'''
|
|
156
|
+
## Define callback
|
|
157
|
+
async def callback(
|
|
158
|
+
messages: List[Dict],
|
|
159
|
+
stream: bool = False,
|
|
160
|
+
session_state: Optional[str] = None,
|
|
161
|
+
context: Optional[Dict] = None
|
|
162
|
+
) -> dict:
|
|
163
|
+
messages_list = messages["messages"] # type: ignore
|
|
164
|
+
latest_message = messages_list[-1]
|
|
165
|
+
application_input = latest_message["content"]
|
|
166
|
+
context = latest_message.get("context", None)
|
|
167
|
+
latest_context = None
|
|
168
|
+
try:
|
|
169
|
+
if self._check_target_returns_context(target):
|
|
170
|
+
response, latest_context = target(query=application_input)
|
|
171
|
+
else:
|
|
172
|
+
response = target(query=application_input)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
response = f"Something went wrong {e!s}"
|
|
175
|
+
|
|
176
|
+
## We format the response to follow the openAI chat protocol format
|
|
177
|
+
formatted_response = {
|
|
178
|
+
"content": response,
|
|
179
|
+
"role": "assistant",
|
|
180
|
+
"context": latest_context if latest_context else context,
|
|
181
|
+
}
|
|
182
|
+
## NOTE: In the future, instead of appending to messages we should just return `formatted_response`
|
|
183
|
+
messages["messages"].append(formatted_response) # type: ignore
|
|
184
|
+
return {"messages": messages_list, "stream": stream, "session_state": session_state, "context": latest_context if latest_context else context}
|
|
185
|
+
|
|
186
|
+
## Run simulator
|
|
187
|
+
data_path = "simulator_outputs.jsonl"
|
|
188
|
+
simulator_outputs = None
|
|
189
|
+
jailbreak_outputs = None
|
|
190
|
+
simulator_data_paths = {}
|
|
191
|
+
|
|
192
|
+
# if IndirectAttack, run IndirectAttackSimulator
|
|
193
|
+
if adversarial_scenario == AdversarialScenarioJailbreak.ADVERSARIAL_INDIRECT_JAILBREAK:
|
|
194
|
+
self.logger.info(f"Running IndirectAttackSimulator with inputs: adversarial_scenario={adversarial_scenario}, max_conversation_turns={max_conversation_turns}, max_simulation_results={max_simulation_results}, conversation_turns={conversation_turns}, text={source_text}")
|
|
195
|
+
simulator = IndirectAttackSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
|
|
196
|
+
simulator_outputs = await simulator(
|
|
197
|
+
scenario=adversarial_scenario,
|
|
198
|
+
max_conversation_turns=max_conversation_turns,
|
|
199
|
+
max_simulation_results=max_simulation_results,
|
|
200
|
+
tasks=tasks,
|
|
201
|
+
conversation_turns=conversation_turns,
|
|
202
|
+
text=source_text,
|
|
203
|
+
target=callback,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# if DirectAttack, run DirectAttackSimulator
|
|
207
|
+
elif direct_attack:
|
|
208
|
+
self.logger.info(f"Running DirectAttackSimulator with inputs: adversarial_scenario={adversarial_scenario}, max_conversation_turns={max_conversation_turns}, max_simulation_results={max_simulation_results}")
|
|
209
|
+
simulator = DirectAttackSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
|
|
210
|
+
simulator_outputs = await simulator(
|
|
211
|
+
scenario=adversarial_scenario if adversarial_scenario else AdversarialScenario.ADVERSARIAL_REWRITE,
|
|
212
|
+
max_conversation_turns=max_conversation_turns,
|
|
213
|
+
max_simulation_results=max_simulation_results,
|
|
214
|
+
target=callback)
|
|
215
|
+
jailbreak_outputs = simulator_outputs["jailbreak"]
|
|
216
|
+
simulator_outputs = simulator_outputs["regular"]
|
|
217
|
+
|
|
218
|
+
## If adversarial_scenario is not provided, run Simulator
|
|
219
|
+
elif adversarial_scenario is None and self.model_config:
|
|
220
|
+
self.logger.info(f"Running Simulator with inputs: adversarial_scenario={adversarial_scenario}, max_conversation_turns={max_conversation_turns}, max_simulation_results={max_simulation_results}, conversation_turns={conversation_turns}, source_text={source_text}")
|
|
221
|
+
simulator = Simulator(self.model_config)
|
|
222
|
+
simulator_outputs = await simulator(
|
|
223
|
+
max_conversation_turns=max_conversation_turns,
|
|
224
|
+
max_simulation_results=max_simulation_results,
|
|
225
|
+
conversation_turns=conversation_turns,
|
|
226
|
+
num_queries=max_simulation_results,
|
|
227
|
+
target=callback,
|
|
228
|
+
text=source_text if source_text else "",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
## Run AdversarialSimulator
|
|
232
|
+
elif adversarial_scenario:
|
|
233
|
+
self.logger.info(f"Running AdversarialSimulator with inputs: adversarial_scenario={adversarial_scenario}, max_conversation_turns={max_conversation_turns}, max_simulation_results={max_simulation_results}, conversation_turns={conversation_turns}, source_text={source_text}")
|
|
234
|
+
simulator = AdversarialSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
|
|
235
|
+
simulator_outputs = await simulator(
|
|
236
|
+
scenario=adversarial_scenario,
|
|
237
|
+
max_conversation_turns=max_conversation_turns,
|
|
238
|
+
max_simulation_results=max_simulation_results,
|
|
239
|
+
conversation_turns=conversation_turns,
|
|
240
|
+
target=callback,
|
|
241
|
+
text=source_text,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
## If no outputs are generated, raise an exception
|
|
245
|
+
if not simulator_outputs:
|
|
246
|
+
self.logger.error("No outputs generated by the simulator")
|
|
247
|
+
msg = "No outputs generated by the simulator"
|
|
248
|
+
raise EvaluationException(
|
|
249
|
+
message=msg,
|
|
250
|
+
internal_message=msg,
|
|
251
|
+
target=ErrorTarget.ADVERSARIAL_SIMULATOR,
|
|
252
|
+
category=ErrorCategory.UNKNOWN,
|
|
253
|
+
blame=ErrorBlame.USER_ERROR,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
## Write outputs to file according to scenario
|
|
257
|
+
if direct_attack and jailbreak_outputs:
|
|
258
|
+
jailbreak_data_path = "jailbreak_simulator_outputs.jsonl"
|
|
259
|
+
with Path(jailbreak_data_path).open("w") as f:
|
|
260
|
+
f.writelines(jailbreak_outputs.to_eval_qr_json_lines())
|
|
261
|
+
simulator_data_paths["jailbreak"] = jailbreak_data_path
|
|
262
|
+
with Path(data_path).open("w") as f:
|
|
263
|
+
if not adversarial_scenario or adversarial_scenario != AdversarialScenario.ADVERSARIAL_CONVERSATION:
|
|
264
|
+
if source_text or self._check_target_returns_context(target):
|
|
265
|
+
eval_input_data_json_lines = ""
|
|
266
|
+
for output in simulator_outputs:
|
|
267
|
+
query = None
|
|
268
|
+
response = None
|
|
269
|
+
context = source_text
|
|
270
|
+
ground_truth = source_text
|
|
271
|
+
for message in output["messages"]:
|
|
272
|
+
if message["role"] == "user":
|
|
273
|
+
query = message["content"]
|
|
274
|
+
if message["role"] == "assistant":
|
|
275
|
+
response = message["content"]
|
|
276
|
+
if query and response:
|
|
277
|
+
eval_input_data_json_lines += (
|
|
278
|
+
json.dumps(
|
|
279
|
+
{
|
|
280
|
+
"query": query,
|
|
281
|
+
"response": response,
|
|
282
|
+
"context": context,
|
|
283
|
+
"ground_truth": ground_truth,
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
+ "\n"
|
|
287
|
+
)
|
|
288
|
+
f.write(eval_input_data_json_lines)
|
|
289
|
+
elif isinstance(simulator_outputs,JsonLineList):
|
|
290
|
+
f.writelines(simulator_outputs.to_eval_qr_json_lines())
|
|
291
|
+
else:
|
|
292
|
+
f.writelines(output.to_eval_qr_json_lines() for output in simulator_outputs)
|
|
293
|
+
else:
|
|
294
|
+
f.writelines(
|
|
295
|
+
[json.dumps({"conversation": {"messages": conversation["messages"]}}) + "\n" for conversation in simulator_outputs]
|
|
296
|
+
)
|
|
297
|
+
simulator_data_paths["regular"] = data_path
|
|
298
|
+
|
|
299
|
+
return simulator_data_paths
|
|
300
|
+
|
|
301
|
+
def _get_scenario(
|
|
302
|
+
self,
|
|
303
|
+
evaluators: List[_SafetyEvaluator],
|
|
304
|
+
num_turns: int = 3,
|
|
305
|
+
scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
|
|
306
|
+
) -> Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]:
|
|
307
|
+
'''
|
|
308
|
+
Returns the Simulation scenario based on the provided list of SafetyEvaluator.
|
|
309
|
+
|
|
310
|
+
:param evaluators: A list of SafetyEvaluator.
|
|
311
|
+
:type evaluators: List[SafetyEvaluator]
|
|
312
|
+
:param num_turns: The number of turns in a conversation.
|
|
313
|
+
:type num_turns: int
|
|
314
|
+
:param scenario: The adversarial scenario to simulate.
|
|
315
|
+
:type scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
|
|
316
|
+
'''
|
|
317
|
+
if len(evaluators) == 0: return AdversarialScenario.ADVERSARIAL_QA
|
|
318
|
+
for evaluator in evaluators:
|
|
319
|
+
if evaluator in [_SafetyEvaluator.CONTENT_SAFETY, _SafetyEvaluator.DIRECT_ATTACK]:
|
|
320
|
+
if num_turns == 1 and scenario: return scenario
|
|
321
|
+
return (
|
|
322
|
+
AdversarialScenario.ADVERSARIAL_CONVERSATION
|
|
323
|
+
if num_turns > 1
|
|
324
|
+
else AdversarialScenario.ADVERSARIAL_QA
|
|
325
|
+
)
|
|
326
|
+
if evaluator in [
|
|
327
|
+
_SafetyEvaluator.GROUNDEDNESS,
|
|
328
|
+
_SafetyEvaluator.RELEVANCE,
|
|
329
|
+
_SafetyEvaluator.SIMILARITY,
|
|
330
|
+
_SafetyEvaluator.FLUENCY,
|
|
331
|
+
_SafetyEvaluator.COHERENCE,
|
|
332
|
+
]:
|
|
333
|
+
return None
|
|
334
|
+
if evaluator == _SafetyEvaluator.PROTECTED_MATERIAL:
|
|
335
|
+
return AdversarialScenario.ADVERSARIAL_CONTENT_PROTECTED_MATERIAL
|
|
336
|
+
if evaluator == _SafetyEvaluator.INDIRECT_ATTACK:
|
|
337
|
+
return AdversarialScenarioJailbreak.ADVERSARIAL_INDIRECT_JAILBREAK
|
|
338
|
+
|
|
339
|
+
msg = f"Invalid evaluator: {evaluator}. Supported evaluators: {_SafetyEvaluator.__members__.values()}"
|
|
340
|
+
raise EvaluationException(
|
|
341
|
+
message=msg,
|
|
342
|
+
internal_message=msg,
|
|
343
|
+
target=ErrorTarget.UNKNOWN,
|
|
344
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
345
|
+
blame=ErrorBlame.USER_ERROR,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _get_evaluators(
|
|
349
|
+
self,
|
|
350
|
+
evaluators: List[_SafetyEvaluator],
|
|
351
|
+
) -> Dict[str, Callable]:
|
|
352
|
+
'''
|
|
353
|
+
Returns a dictionary of evaluators based on the provided list of SafetyEvaluator.
|
|
354
|
+
|
|
355
|
+
:param evaluators: A list of SafetyEvaluator.
|
|
356
|
+
:type evaluators: List[SafetyEvaluator]
|
|
357
|
+
'''
|
|
358
|
+
evaluators_dict = {}
|
|
359
|
+
# Default to content safety when no evaluators are specified
|
|
360
|
+
if len(evaluators) == 0:
|
|
361
|
+
evaluators_dict["content_safety"] = _content_safety.ContentSafetyEvaluator(
|
|
362
|
+
azure_ai_project=self.azure_ai_project, credential=self.credential
|
|
363
|
+
)
|
|
364
|
+
return evaluators_dict
|
|
365
|
+
|
|
366
|
+
for evaluator in evaluators:
|
|
367
|
+
if evaluator == _SafetyEvaluator.CONTENT_SAFETY:
|
|
368
|
+
evaluators_dict["content_safety"] = _content_safety.ContentSafetyEvaluator(
|
|
369
|
+
azure_ai_project=self.azure_ai_project, credential=self.credential
|
|
370
|
+
)
|
|
371
|
+
elif evaluator == _SafetyEvaluator.GROUNDEDNESS:
|
|
372
|
+
evaluators_dict["groundedness"] = _groundedness.GroundednessEvaluator(
|
|
373
|
+
model_config=self.model_config,
|
|
374
|
+
)
|
|
375
|
+
elif evaluator == _SafetyEvaluator.PROTECTED_MATERIAL:
|
|
376
|
+
evaluators_dict["protected_material"] = _protected_material.ProtectedMaterialEvaluator(
|
|
377
|
+
azure_ai_project=self.azure_ai_project, credential=self.credential
|
|
378
|
+
)
|
|
379
|
+
elif evaluator == _SafetyEvaluator.RELEVANCE:
|
|
380
|
+
evaluators_dict["relevance"] = _relevance.RelevanceEvaluator(
|
|
381
|
+
model_config=self.model_config,
|
|
382
|
+
)
|
|
383
|
+
elif evaluator == _SafetyEvaluator.SIMILARITY:
|
|
384
|
+
evaluators_dict["similarity"] = _similarity.SimilarityEvaluator(
|
|
385
|
+
model_config=self.model_config,
|
|
386
|
+
)
|
|
387
|
+
elif evaluator == _SafetyEvaluator.FLUENCY:
|
|
388
|
+
evaluators_dict["fluency"] = _fluency.FluencyEvaluator(
|
|
389
|
+
model_config=self.model_config,
|
|
390
|
+
)
|
|
391
|
+
elif evaluator == _SafetyEvaluator.COHERENCE:
|
|
392
|
+
evaluators_dict["coherence"] = _coherence.CoherenceEvaluator(
|
|
393
|
+
model_config=self.model_config,
|
|
394
|
+
)
|
|
395
|
+
elif evaluator == _SafetyEvaluator.INDIRECT_ATTACK:
|
|
396
|
+
evaluators_dict["indirect_attack"] = _xpia.IndirectAttackEvaluator(
|
|
397
|
+
azure_ai_project=self.azure_ai_project, credential=self.credential
|
|
398
|
+
)
|
|
399
|
+
elif evaluator == _SafetyEvaluator.DIRECT_ATTACK:
|
|
400
|
+
evaluators_dict["content_safety"] = _content_safety.ContentSafetyEvaluator(
|
|
401
|
+
azure_ai_project=self.azure_ai_project, credential=self.credential
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
msg = f"Invalid evaluator: {evaluator}. Supported evaluators are: {_SafetyEvaluator.__members__.values()}"
|
|
405
|
+
raise EvaluationException(
|
|
406
|
+
message=msg,
|
|
407
|
+
internal_message=msg,
|
|
408
|
+
target=ErrorTarget.UNKNOWN, ## NOTE: We should add a target for this potentially
|
|
409
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
410
|
+
blame=ErrorBlame.USER_ERROR,
|
|
411
|
+
)
|
|
412
|
+
return evaluators_dict
|
|
413
|
+
|
|
414
|
+
@staticmethod
|
|
415
|
+
def _check_target_returns_context(target: Callable) -> bool:
|
|
416
|
+
'''
|
|
417
|
+
Checks if the target function returns a tuple. We assume the second value in the tuple is the "context".
|
|
418
|
+
|
|
419
|
+
:param target: The target function to check.
|
|
420
|
+
:type target: Callable
|
|
421
|
+
'''
|
|
422
|
+
sig = inspect.signature(target)
|
|
423
|
+
ret_type = sig.return_annotation
|
|
424
|
+
if ret_type == inspect.Signature.empty:
|
|
425
|
+
return False
|
|
426
|
+
if ret_type is tuple:
|
|
427
|
+
return True
|
|
428
|
+
return False
|
|
429
|
+
|
|
430
|
+
def _validate_inputs(
|
|
431
|
+
self,
|
|
432
|
+
evaluators: List[_SafetyEvaluator],
|
|
433
|
+
target: Callable,
|
|
434
|
+
num_turns: int = 1,
|
|
435
|
+
scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
|
|
436
|
+
source_text: Optional[str] = None,
|
|
437
|
+
):
|
|
438
|
+
'''
|
|
439
|
+
Validates the inputs provided to the __call__ function of the SafetyEvaluation object.
|
|
440
|
+
:param evaluators: A list of SafetyEvaluator.
|
|
441
|
+
:type evaluators: List[SafetyEvaluator]
|
|
442
|
+
:param target: The target function to call during the evaluation.
|
|
443
|
+
:type target: Callable
|
|
444
|
+
:param num_turns: The number of turns in a between the target application and the caller.
|
|
445
|
+
:type num_turns: int
|
|
446
|
+
:param scenario: The adversarial scenario to simulate.
|
|
447
|
+
:type scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
|
|
448
|
+
:param source_text: The source text to use as grounding document in the evaluation.
|
|
449
|
+
:type source_text: Optional[str]
|
|
450
|
+
'''
|
|
451
|
+
if _SafetyEvaluator.GROUNDEDNESS in evaluators and not (self._check_target_returns_context(target) or source_text):
|
|
452
|
+
self.logger.error(f"GroundednessEvaluator requires either source_text or a target function that returns context. Source text: {source_text}, _check_target_returns_context: {self._check_target_returns_context(target)}")
|
|
453
|
+
msg = "GroundednessEvaluator requires either source_text or a target function that returns context"
|
|
454
|
+
raise EvaluationException(
|
|
455
|
+
message=msg,
|
|
456
|
+
internal_message=msg,
|
|
457
|
+
target=ErrorTarget.GROUNDEDNESS_EVALUATOR,
|
|
458
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
459
|
+
blame=ErrorBlame.USER_ERROR,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if scenario and not _SafetyEvaluator.CONTENT_SAFETY in evaluators:
|
|
463
|
+
self.logger.error(f"Adversarial scenario {scenario} is not supported without content safety evaluation.")
|
|
464
|
+
msg = f"Adversarial scenario {scenario} is not supported without content safety evaluation."
|
|
465
|
+
raise EvaluationException(
|
|
466
|
+
message=msg,
|
|
467
|
+
internal_message=msg,
|
|
468
|
+
target=ErrorTarget.UNKNOWN,
|
|
469
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
470
|
+
blame=ErrorBlame.USER_ERROR,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if _SafetyEvaluator.CONTENT_SAFETY in evaluators and scenario and num_turns > 1:
|
|
474
|
+
self.logger.error(f"Adversarial scenario {scenario} is not supported for content safety evaluation with more than 1 turn.")
|
|
475
|
+
msg = f"Adversarial scenario {scenario} is not supported for content safety evaluation with more than 1 turn."
|
|
476
|
+
raise EvaluationException(
|
|
477
|
+
message=msg,
|
|
478
|
+
internal_message=msg,
|
|
479
|
+
target=ErrorTarget.UNKNOWN,
|
|
480
|
+
category=ErrorCategory.INVALID_VALUE,
|
|
481
|
+
blame=ErrorBlame.USER_ERROR,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if any(e in evaluators for e in [
|
|
485
|
+
_SafetyEvaluator.GROUNDEDNESS,
|
|
486
|
+
_SafetyEvaluator.RELEVANCE,
|
|
487
|
+
_SafetyEvaluator.SIMILARITY,
|
|
488
|
+
_SafetyEvaluator.FLUENCY,
|
|
489
|
+
_SafetyEvaluator.COHERENCE,
|
|
490
|
+
]) and not self.model_config:
|
|
491
|
+
self.logger.error(f"Model configuration is required for evaluators: {evaluators}")
|
|
492
|
+
msg = f"Model configuration is required for evaluators: {evaluators}"
|
|
493
|
+
raise EvaluationException(
|
|
494
|
+
message=msg,
|
|
495
|
+
internal_message=msg,
|
|
496
|
+
target=ErrorTarget.UNKNOWN,
|
|
497
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
498
|
+
blame=ErrorBlame.USER_ERROR,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _calculate_defect_rate(self, evaluation_result_dict) -> EvaluationResult:
|
|
503
|
+
hate_unfairness_defects = []
|
|
504
|
+
violence_defects = []
|
|
505
|
+
sexual_defects = []
|
|
506
|
+
self_harm_defects = []
|
|
507
|
+
for i, row in enumerate(zip(evaluation_result_dict['jailbreak']['rows'], evaluation_result_dict['regular']['rows'])):
|
|
508
|
+
hate_unfairness_defects.append(1 if (row[0]['outputs.content_safety.hate_unfairness_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT and not row[1]['outputs.content_safety.hate_unfairness_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT) else 0)
|
|
509
|
+
violence_defects.append(1 if (row[0]['outputs.content_safety.violence_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT and not row[1]['outputs.content_safety.violence_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT) else 0)
|
|
510
|
+
sexual_defects.append(1 if (row[0]['outputs.content_safety.sexual_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT and not row[1]['outputs.content_safety.sexual_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT) else 0)
|
|
511
|
+
self_harm_defects.append(1 if (row[0]['outputs.content_safety.self_harm_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT and not row[1]['outputs.content_safety.self_harm_score'] > CONTENT_SAFETY_DEFECT_RATE_THRESHOLD_DEFAULT) else 0)
|
|
512
|
+
hate_unfairness_defect_rate = list_mean_nan_safe(hate_unfairness_defects)
|
|
513
|
+
violence_defect_rate = list_mean_nan_safe(violence_defects)
|
|
514
|
+
sexual_defect_rate = list_mean_nan_safe(sexual_defects)
|
|
515
|
+
self_harm_defect_rate = list_mean_nan_safe(self_harm_defects)
|
|
516
|
+
|
|
517
|
+
evaluation_result: EvaluationResult = {'metrics': {}, 'rows': [], 'studio_url': ''}
|
|
518
|
+
evaluation_result['rows'] = evaluation_result_dict['jailbreak']['rows'] + evaluation_result_dict['regular']['rows']
|
|
519
|
+
evaluation_result['metrics'] = {
|
|
520
|
+
'content_safety.violence_defect_rate': hate_unfairness_defect_rate,
|
|
521
|
+
'content_safety.sexual_defect_rate': violence_defect_rate,
|
|
522
|
+
'content_safety.hate_unfairness_defect_rate': sexual_defect_rate,
|
|
523
|
+
'content_safety.self_harm_defect_rate': self_harm_defect_rate,
|
|
524
|
+
}
|
|
525
|
+
evaluation_result['studio_url'] = evaluation_result_dict['jailbreak']['studio_url'] + '\t' + evaluation_result_dict['regular']['studio_url']
|
|
526
|
+
return evaluation_result
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def __call__(
|
|
530
|
+
self,
|
|
531
|
+
target: Callable,
|
|
532
|
+
evaluators: List[_SafetyEvaluator] = [],
|
|
533
|
+
evaluation_name: Optional[str] = None,
|
|
534
|
+
num_turns : int = 1,
|
|
535
|
+
num_rows: int = 5,
|
|
536
|
+
scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
|
|
537
|
+
conversation_turns : List[List[Union[str, Dict[str, Any]]]] = [],
|
|
538
|
+
tasks: List[str] = [],
|
|
539
|
+
source_text: Optional[str] = None,
|
|
540
|
+
data_path: Optional[Union[str, os.PathLike]] = None,
|
|
541
|
+
jailbreak_data_path: Optional[Union[str, os.PathLike]] = None,
|
|
542
|
+
output_path: Optional[Union[str, os.PathLike]] = None
|
|
543
|
+
) -> Union[EvaluationResult, Dict[str, EvaluationResult]]:
|
|
544
|
+
'''
|
|
545
|
+
Evaluates the target function based on the provided parameters.
|
|
546
|
+
|
|
547
|
+
:param target: The target function to call during the evaluation.
|
|
548
|
+
:type target: Callable
|
|
549
|
+
:param evaluators: A list of SafetyEvaluator.
|
|
550
|
+
:type evaluators: List[_SafetyEvaluator]
|
|
551
|
+
:param evaluation_name: The display name name of the evaluation.
|
|
552
|
+
:type evaluation_name: Optional[str]
|
|
553
|
+
:param num_turns: The number of turns in a between the target application and the caller.
|
|
554
|
+
:type num_turns: int
|
|
555
|
+
:param num_rows: The (maximum) number of rows to generate for evaluation.
|
|
556
|
+
:type num_rows: int
|
|
557
|
+
:param scenario: The adversarial scenario to simulate.
|
|
558
|
+
:type scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
|
|
559
|
+
:param conversation_turns: Predefined conversation turns to simulate.
|
|
560
|
+
:type conversation_turns: List[List[Union[str, Dict[str, Any]]]]
|
|
561
|
+
:param tasks A list of user tasks, each represented as a list of strings. Text should be relevant for the tasks and facilitate the simulation. One example is to use text to provide context for the tasks.
|
|
562
|
+
:type tasks: List[str] = [],
|
|
563
|
+
:param source_text: The source text to use as grounding document in the evaluation.
|
|
564
|
+
:type source_text: Optional[str]
|
|
565
|
+
:param data_path: The path to the data file generated by the Simulator. If None, the Simulator will be run.
|
|
566
|
+
:type data_path: Optional[Union[str, os.PathLike]]
|
|
567
|
+
:param jailbreak_data_path: The path to the data file generated by the Simulator for jailbreak scenario. If None, the DirectAttackSimulator will be run.
|
|
568
|
+
:type jailbreak_data_path: Optional[Union[str, os.PathLike]]
|
|
569
|
+
:param output_path: The path to write the evaluation results to if set.
|
|
570
|
+
:type output_path: Optional[Union[str, os.PathLike]]
|
|
571
|
+
'''
|
|
572
|
+
## Log inputs
|
|
573
|
+
self.logger.info(f"User inputs: evaluators{evaluators}, evaluation_name={evaluation_name}, num_turns={num_turns}, num_rows={num_rows}, conversation_turns={conversation_turns}, tasks={tasks}, source_text={source_text}, data_path={data_path}, jailbreak_data_path={jailbreak_data_path}, output_path={output_path}")
|
|
574
|
+
|
|
575
|
+
## Validate arguments
|
|
576
|
+
self._validate_inputs(
|
|
577
|
+
evaluators=evaluators,
|
|
578
|
+
target=target,
|
|
579
|
+
num_turns=num_turns,
|
|
580
|
+
scenario=scenario,
|
|
581
|
+
source_text=source_text,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Get scenario
|
|
585
|
+
adversarial_scenario = self._get_scenario(evaluators, num_turns=num_turns, scenario=scenario)
|
|
586
|
+
|
|
587
|
+
## Get evaluators
|
|
588
|
+
evaluators_dict = self._get_evaluators(evaluators)
|
|
589
|
+
|
|
590
|
+
## If `data_path` is not provided, run simulator
|
|
591
|
+
if data_path is None and jailbreak_data_path is None:
|
|
592
|
+
self.logger.info(f"No data_path provided. Running simulator.")
|
|
593
|
+
data_paths = await self._simulate(
|
|
594
|
+
target=target,
|
|
595
|
+
adversarial_scenario=adversarial_scenario,
|
|
596
|
+
max_conversation_turns=num_turns,
|
|
597
|
+
max_simulation_results=num_rows,
|
|
598
|
+
conversation_turns=conversation_turns,
|
|
599
|
+
tasks=tasks,
|
|
600
|
+
source_text=source_text,
|
|
601
|
+
direct_attack=_SafetyEvaluator.DIRECT_ATTACK in evaluators
|
|
602
|
+
)
|
|
603
|
+
data_path = data_paths.get("regular", None)
|
|
604
|
+
jailbreak_data_path = data_paths.get("jailbreak", None)
|
|
605
|
+
|
|
606
|
+
## Run evaluation
|
|
607
|
+
evaluation_results = {}
|
|
608
|
+
if _SafetyEvaluator.DIRECT_ATTACK in evaluators and jailbreak_data_path:
|
|
609
|
+
self.logger.info(f"Running evaluation for jailbreak data with inputs jailbreak_data_path={jailbreak_data_path}, evaluators={evaluators_dict}, azure_ai_project={self.azure_ai_project}, output_path=jailbreak_{output_path}, credential={self.credential}")
|
|
610
|
+
evaluate_outputs_jailbreak = _evaluate.evaluate(
|
|
611
|
+
data=jailbreak_data_path,
|
|
612
|
+
evaluators=evaluators_dict,
|
|
613
|
+
azure_ai_project=self.azure_ai_project,
|
|
614
|
+
output_path=Path("jailbreak_" + str(output_path)),
|
|
615
|
+
evaluation_name=evaluation_name,
|
|
616
|
+
)
|
|
617
|
+
evaluation_results["jailbreak"] = evaluate_outputs_jailbreak
|
|
618
|
+
|
|
619
|
+
if data_path:
|
|
620
|
+
self.logger.info(f"Running evaluation for data with inputs data_path={data_path}, evaluators={evaluators_dict}, azure_ai_project={self.azure_ai_project}, output_path={output_path}")
|
|
621
|
+
evaluate_outputs = _evaluate.evaluate(
|
|
622
|
+
data=data_path,
|
|
623
|
+
evaluators=evaluators_dict,
|
|
624
|
+
azure_ai_project=self.azure_ai_project,
|
|
625
|
+
evaluation_name=evaluation_name,
|
|
626
|
+
output_path=output_path,
|
|
627
|
+
)
|
|
628
|
+
if _SafetyEvaluator.DIRECT_ATTACK in evaluators:
|
|
629
|
+
evaluation_results["regular"] = evaluate_outputs
|
|
630
|
+
return self._calculate_defect_rate(evaluation_results)
|
|
631
|
+
|
|
632
|
+
return evaluate_outputs
|
|
633
|
+
else:
|
|
634
|
+
raise EvaluationException(
|
|
635
|
+
message="No data path found after simulation",
|
|
636
|
+
internal_message="No data path found after simulation",
|
|
637
|
+
target=ErrorTarget.UNKNOWN,
|
|
638
|
+
category=ErrorCategory.MISSING_FIELD,
|
|
639
|
+
blame=ErrorBlame.USER_ERROR,
|
|
640
|
+
)
|
azure/ai/evaluation/_version.py
CHANGED