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.
Files changed (48) hide show
  1. azure/ai/evaluation/__init__.py +1 -15
  2. azure/ai/evaluation/_azure/_clients.py +24 -8
  3. azure/ai/evaluation/_azure/_models.py +2 -2
  4. azure/ai/evaluation/_common/utils.py +8 -8
  5. azure/ai/evaluation/_constants.py +21 -0
  6. azure/ai/evaluation/_evaluate/_batch_run/__init__.py +2 -1
  7. azure/ai/evaluation/_evaluate/_eval_run.py +3 -1
  8. azure/ai/evaluation/_evaluate/_evaluate.py +74 -14
  9. azure/ai/evaluation/_evaluate/_utils.py +27 -0
  10. azure/ai/evaluation/_evaluators/_bleu/_bleu.py +46 -25
  11. azure/ai/evaluation/_evaluators/_common/__init__.py +2 -0
  12. azure/ai/evaluation/_evaluators/_common/_base_eval.py +69 -4
  13. azure/ai/evaluation/_evaluators/_common/_base_multi_eval.py +61 -0
  14. azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +7 -1
  15. azure/ai/evaluation/_evaluators/_common/_conversation_aggregators.py +49 -0
  16. azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +5 -42
  17. azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +2 -0
  18. azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +2 -0
  19. azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +2 -0
  20. azure/ai/evaluation/_evaluators/_content_safety/_violence.py +2 -0
  21. azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +61 -68
  22. azure/ai/evaluation/_evaluators/_gleu/_gleu.py +45 -23
  23. azure/ai/evaluation/_evaluators/_meteor/_meteor.py +55 -34
  24. azure/ai/evaluation/_evaluators/_qa/_qa.py +32 -27
  25. azure/ai/evaluation/_evaluators/_rouge/_rouge.py +44 -23
  26. azure/ai/evaluation/_evaluators/_similarity/_similarity.py +41 -81
  27. azure/ai/evaluation/_exceptions.py +0 -1
  28. azure/ai/evaluation/_safety_evaluation/__init__.py +3 -0
  29. azure/ai/evaluation/_safety_evaluation/_safety_evaluation.py +640 -0
  30. azure/ai/evaluation/_version.py +2 -1
  31. azure/ai/evaluation/simulator/_adversarial_simulator.py +10 -3
  32. azure/ai/evaluation/simulator/_conversation/__init__.py +4 -5
  33. azure/ai/evaluation/simulator/_conversation/_conversation.py +4 -0
  34. azure/ai/evaluation/simulator/_model_tools/_proxy_completion_model.py +2 -0
  35. azure/ai/evaluation/simulator/_simulator.py +21 -13
  36. {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/METADATA +77 -7
  37. {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/RECORD +40 -44
  38. azure/ai/evaluation/_evaluators/_multimodal/__init__.py +0 -20
  39. azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal.py +0 -132
  40. azure/ai/evaluation/_evaluators/_multimodal/_content_safety_multimodal_base.py +0 -55
  41. azure/ai/evaluation/_evaluators/_multimodal/_hate_unfairness.py +0 -100
  42. azure/ai/evaluation/_evaluators/_multimodal/_protected_material.py +0 -124
  43. azure/ai/evaluation/_evaluators/_multimodal/_self_harm.py +0 -100
  44. azure/ai/evaluation/_evaluators/_multimodal/_sexual.py +0 -100
  45. azure/ai/evaluation/_evaluators/_multimodal/_violence.py +0 -100
  46. {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/NOTICE.txt +0 -0
  47. {azure_ai_evaluation-1.1.0.dist-info → azure_ai_evaluation-1.3.0.dist-info}/WHEEL +0 -0
  48. {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
+ )
@@ -1,5 +1,6 @@
1
1
  # ---------------------------------------------------------
2
2
  # Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  # ---------------------------------------------------------
4
+ # represents upcoming version
4
5
 
5
- VERSION = "1.1.0"
6
+ VERSION = "1.3.0"