azure-ai-evaluation 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of azure-ai-evaluation might be problematic. Click here for more details.

Files changed (123) hide show
  1. azure/ai/evaluation/__init__.py +43 -1
  2. azure/ai/evaluation/_azure/_models.py +6 -6
  3. azure/ai/evaluation/_common/constants.py +6 -2
  4. azure/ai/evaluation/_common/rai_service.py +38 -4
  5. azure/ai/evaluation/_common/raiclient/__init__.py +34 -0
  6. azure/ai/evaluation/_common/raiclient/_client.py +128 -0
  7. azure/ai/evaluation/_common/raiclient/_configuration.py +87 -0
  8. azure/ai/evaluation/_common/raiclient/_model_base.py +1235 -0
  9. azure/ai/evaluation/_common/raiclient/_patch.py +20 -0
  10. azure/ai/evaluation/_common/raiclient/_serialization.py +2050 -0
  11. azure/ai/evaluation/_common/raiclient/_version.py +9 -0
  12. azure/ai/evaluation/_common/raiclient/aio/__init__.py +29 -0
  13. azure/ai/evaluation/_common/raiclient/aio/_client.py +130 -0
  14. azure/ai/evaluation/_common/raiclient/aio/_configuration.py +87 -0
  15. azure/ai/evaluation/_common/raiclient/aio/_patch.py +20 -0
  16. azure/ai/evaluation/_common/raiclient/aio/operations/__init__.py +25 -0
  17. azure/ai/evaluation/_common/raiclient/aio/operations/_operations.py +981 -0
  18. azure/ai/evaluation/_common/raiclient/aio/operations/_patch.py +20 -0
  19. azure/ai/evaluation/_common/raiclient/models/__init__.py +60 -0
  20. azure/ai/evaluation/_common/raiclient/models/_enums.py +18 -0
  21. azure/ai/evaluation/_common/raiclient/models/_models.py +651 -0
  22. azure/ai/evaluation/_common/raiclient/models/_patch.py +20 -0
  23. azure/ai/evaluation/_common/raiclient/operations/__init__.py +25 -0
  24. azure/ai/evaluation/_common/raiclient/operations/_operations.py +1225 -0
  25. azure/ai/evaluation/_common/raiclient/operations/_patch.py +20 -0
  26. azure/ai/evaluation/_common/raiclient/py.typed +1 -0
  27. azure/ai/evaluation/_common/utils.py +22 -2
  28. azure/ai/evaluation/_constants.py +7 -0
  29. azure/ai/evaluation/_converters/__init__.py +3 -0
  30. azure/ai/evaluation/_converters/_ai_services.py +804 -0
  31. azure/ai/evaluation/_converters/_models.py +302 -0
  32. azure/ai/evaluation/_evaluate/_batch_run/__init__.py +10 -3
  33. azure/ai/evaluation/_evaluate/_batch_run/_run_submitter_client.py +104 -0
  34. azure/ai/evaluation/_evaluate/_batch_run/batch_clients.py +82 -0
  35. azure/ai/evaluation/_evaluate/_eval_run.py +1 -1
  36. azure/ai/evaluation/_evaluate/_evaluate.py +31 -2
  37. azure/ai/evaluation/_evaluators/_bleu/_bleu.py +23 -3
  38. azure/ai/evaluation/_evaluators/_code_vulnerability/__init__.py +5 -0
  39. azure/ai/evaluation/_evaluators/_code_vulnerability/_code_vulnerability.py +120 -0
  40. azure/ai/evaluation/_evaluators/_coherence/_coherence.py +21 -2
  41. azure/ai/evaluation/_evaluators/_common/_base_eval.py +43 -3
  42. azure/ai/evaluation/_evaluators/_common/_base_multi_eval.py +3 -1
  43. azure/ai/evaluation/_evaluators/_common/_base_prompty_eval.py +43 -4
  44. azure/ai/evaluation/_evaluators/_common/_base_rai_svc_eval.py +16 -4
  45. azure/ai/evaluation/_evaluators/_content_safety/_content_safety.py +42 -5
  46. azure/ai/evaluation/_evaluators/_content_safety/_hate_unfairness.py +15 -0
  47. azure/ai/evaluation/_evaluators/_content_safety/_self_harm.py +15 -0
  48. azure/ai/evaluation/_evaluators/_content_safety/_sexual.py +15 -0
  49. azure/ai/evaluation/_evaluators/_content_safety/_violence.py +15 -0
  50. azure/ai/evaluation/_evaluators/_f1_score/_f1_score.py +28 -4
  51. azure/ai/evaluation/_evaluators/_fluency/_fluency.py +21 -2
  52. azure/ai/evaluation/_evaluators/_gleu/_gleu.py +26 -3
  53. azure/ai/evaluation/_evaluators/_groundedness/_groundedness.py +21 -3
  54. azure/ai/evaluation/_evaluators/_intent_resolution/__init__.py +7 -0
  55. azure/ai/evaluation/_evaluators/_intent_resolution/_intent_resolution.py +152 -0
  56. azure/ai/evaluation/_evaluators/_intent_resolution/intent_resolution.prompty +161 -0
  57. azure/ai/evaluation/_evaluators/_meteor/_meteor.py +26 -3
  58. azure/ai/evaluation/_evaluators/_qa/_qa.py +51 -7
  59. azure/ai/evaluation/_evaluators/_relevance/_relevance.py +26 -2
  60. azure/ai/evaluation/_evaluators/_response_completeness/__init__.py +7 -0
  61. azure/ai/evaluation/_evaluators/_response_completeness/_response_completeness.py +157 -0
  62. azure/ai/evaluation/_evaluators/_response_completeness/response_completeness.prompty +99 -0
  63. azure/ai/evaluation/_evaluators/_retrieval/_retrieval.py +21 -2
  64. azure/ai/evaluation/_evaluators/_rouge/_rouge.py +113 -4
  65. azure/ai/evaluation/_evaluators/_service_groundedness/_service_groundedness.py +23 -3
  66. azure/ai/evaluation/_evaluators/_similarity/_similarity.py +24 -5
  67. azure/ai/evaluation/_evaluators/_task_adherence/__init__.py +7 -0
  68. azure/ai/evaluation/_evaluators/_task_adherence/_task_adherence.py +148 -0
  69. azure/ai/evaluation/_evaluators/_task_adherence/task_adherence.prompty +117 -0
  70. azure/ai/evaluation/_evaluators/_tool_call_accuracy/__init__.py +9 -0
  71. azure/ai/evaluation/_evaluators/_tool_call_accuracy/_tool_call_accuracy.py +292 -0
  72. azure/ai/evaluation/_evaluators/_tool_call_accuracy/tool_call_accuracy.prompty +71 -0
  73. azure/ai/evaluation/_evaluators/_ungrounded_attributes/__init__.py +5 -0
  74. azure/ai/evaluation/_evaluators/_ungrounded_attributes/_ungrounded_attributes.py +103 -0
  75. azure/ai/evaluation/_evaluators/_xpia/xpia.py +2 -0
  76. azure/ai/evaluation/_exceptions.py +5 -0
  77. azure/ai/evaluation/_legacy/__init__.py +3 -0
  78. azure/ai/evaluation/_legacy/_batch_engine/__init__.py +9 -0
  79. azure/ai/evaluation/_legacy/_batch_engine/_config.py +45 -0
  80. azure/ai/evaluation/_legacy/_batch_engine/_engine.py +368 -0
  81. azure/ai/evaluation/_legacy/_batch_engine/_exceptions.py +88 -0
  82. azure/ai/evaluation/_legacy/_batch_engine/_logging.py +292 -0
  83. azure/ai/evaluation/_legacy/_batch_engine/_openai_injector.py +23 -0
  84. azure/ai/evaluation/_legacy/_batch_engine/_result.py +99 -0
  85. azure/ai/evaluation/_legacy/_batch_engine/_run.py +121 -0
  86. azure/ai/evaluation/_legacy/_batch_engine/_run_storage.py +128 -0
  87. azure/ai/evaluation/_legacy/_batch_engine/_run_submitter.py +217 -0
  88. azure/ai/evaluation/_legacy/_batch_engine/_status.py +25 -0
  89. azure/ai/evaluation/_legacy/_batch_engine/_trace.py +105 -0
  90. azure/ai/evaluation/_legacy/_batch_engine/_utils.py +82 -0
  91. azure/ai/evaluation/_legacy/_batch_engine/_utils_deprecated.py +131 -0
  92. azure/ai/evaluation/_legacy/prompty/__init__.py +36 -0
  93. azure/ai/evaluation/_legacy/prompty/_connection.py +182 -0
  94. azure/ai/evaluation/_legacy/prompty/_exceptions.py +59 -0
  95. azure/ai/evaluation/_legacy/prompty/_prompty.py +313 -0
  96. azure/ai/evaluation/_legacy/prompty/_utils.py +545 -0
  97. azure/ai/evaluation/_legacy/prompty/_yaml_utils.py +99 -0
  98. azure/ai/evaluation/_red_team/__init__.py +3 -0
  99. azure/ai/evaluation/_red_team/_attack_objective_generator.py +192 -0
  100. azure/ai/evaluation/_red_team/_attack_strategy.py +42 -0
  101. azure/ai/evaluation/_red_team/_callback_chat_target.py +74 -0
  102. azure/ai/evaluation/_red_team/_default_converter.py +21 -0
  103. azure/ai/evaluation/_red_team/_red_team.py +1858 -0
  104. azure/ai/evaluation/_red_team/_red_team_result.py +246 -0
  105. azure/ai/evaluation/_red_team/_utils/__init__.py +3 -0
  106. azure/ai/evaluation/_red_team/_utils/constants.py +64 -0
  107. azure/ai/evaluation/_red_team/_utils/formatting_utils.py +164 -0
  108. azure/ai/evaluation/_red_team/_utils/logging_utils.py +139 -0
  109. azure/ai/evaluation/_red_team/_utils/strategy_utils.py +188 -0
  110. azure/ai/evaluation/_safety_evaluation/__init__.py +1 -1
  111. azure/ai/evaluation/_safety_evaluation/_generated_rai_client.py +0 -0
  112. azure/ai/evaluation/_safety_evaluation/_safety_evaluation.py +251 -150
  113. azure/ai/evaluation/_version.py +1 -1
  114. azure/ai/evaluation/simulator/_adversarial_scenario.py +3 -1
  115. azure/ai/evaluation/simulator/_adversarial_simulator.py +54 -27
  116. azure/ai/evaluation/simulator/_model_tools/_generated_rai_client.py +145 -0
  117. azure/ai/evaluation/simulator/_model_tools/_rai_client.py +71 -1
  118. {azure_ai_evaluation-1.3.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/METADATA +69 -15
  119. azure_ai_evaluation-1.4.0.dist-info/RECORD +197 -0
  120. {azure_ai_evaluation-1.3.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/WHEEL +1 -1
  121. azure_ai_evaluation-1.3.0.dist-info/RECORD +0 -119
  122. {azure_ai_evaluation-1.3.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/NOTICE.txt +0 -0
  123. {azure_ai_evaluation-1.3.0.dist-info → azure_ai_evaluation-1.4.0.dist-info}/top_level.txt +0 -0
@@ -8,14 +8,32 @@ import inspect
8
8
  import logging
9
9
  from datetime import datetime
10
10
  from azure.ai.evaluation._common._experimental import experimental
11
- from typing import Any, Callable, Dict, List, Optional, Tuple, Union
11
+ from typing import Any, Callable, Dict, List, Optional, Union, cast
12
12
  from azure.ai.evaluation._common.math import list_mean_nan_safe
13
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
14
+ from azure.ai.evaluation._evaluators import (
15
+ _content_safety,
16
+ _protected_material,
17
+ _groundedness,
18
+ _relevance,
19
+ _similarity,
20
+ _fluency,
21
+ _xpia,
22
+ _coherence,
23
+ )
24
+ from azure.ai.evaluation._evaluators._eci._eci import ECIEvaluator
15
25
  from azure.ai.evaluation._evaluate import _evaluate
16
26
  from azure.ai.evaluation._exceptions import ErrorBlame, ErrorCategory, ErrorTarget, EvaluationException
17
27
  from azure.ai.evaluation._model_configurations import AzureAIProject, EvaluationResult
18
- from azure.ai.evaluation.simulator import Simulator, AdversarialSimulator, AdversarialScenario, AdversarialScenarioJailbreak, IndirectAttackSimulator, DirectAttackSimulator
28
+ from azure.ai.evaluation.simulator import (
29
+ Simulator,
30
+ AdversarialSimulator,
31
+ AdversarialScenario,
32
+ AdversarialScenarioJailbreak,
33
+ IndirectAttackSimulator,
34
+ DirectAttackSimulator ,
35
+ )
36
+ from azure.ai.evaluation.simulator._adversarial_scenario import _UnstableAdversarialScenario
19
37
  from azure.ai.evaluation.simulator._utils import JsonLineList
20
38
  from azure.ai.evaluation._common.utils import validate_azure_ai_project
21
39
  from azure.ai.evaluation._model_configurations import AzureOpenAIModelConfiguration, OpenAIModelConfiguration
@@ -24,6 +42,9 @@ import json
24
42
  from pathlib import Path
25
43
 
26
44
  logger = logging.getLogger(__name__)
45
+ JAILBREAK_EXT = "_Jailbreak"
46
+ DATA_EXT = "_Data.jsonl"
47
+ RESULTS_EXT = "_Results.jsonl"
27
48
 
28
49
  def _setup_logger():
29
50
  """Configure and return a logger instance for the CustomAdversarialSimulator.
@@ -33,20 +54,21 @@ def _setup_logger():
33
54
  """
34
55
  log_filename = datetime.now().strftime("%Y_%m_%d__%H_%M.log")
35
56
  logger = logging.getLogger("CustomAdversarialSimulatorLogger")
36
- logger.setLevel(logging.DEBUG)
57
+ logger.setLevel(logging.DEBUG)
37
58
  file_handler = logging.FileHandler(log_filename)
38
59
  file_handler.setLevel(logging.DEBUG)
39
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
60
+ formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
40
61
  file_handler.setFormatter(formatter)
41
62
  logger.addHandler(file_handler)
42
63
 
43
64
  return logger
44
65
 
66
+
45
67
  @experimental
46
68
  class _SafetyEvaluator(Enum):
47
- '''
69
+ """
48
70
  Evaluator types for Safety evaluation.
49
- '''
71
+ """
50
72
 
51
73
  CONTENT_SAFETY = "content_safety"
52
74
  GROUNDEDNESS = "groundedness"
@@ -57,6 +79,8 @@ class _SafetyEvaluator(Enum):
57
79
  COHERENCE = "coherence"
58
80
  INDIRECT_ATTACK = "indirect_attack"
59
81
  DIRECT_ATTACK = "direct_attack"
82
+ ECI = "eci"
83
+
60
84
 
61
85
  @experimental
62
86
  class _SafetyEvaluation:
@@ -66,7 +90,7 @@ class _SafetyEvaluation:
66
90
  credential: TokenCredential,
67
91
  model_config: Optional[Union[AzureOpenAIModelConfiguration, OpenAIModelConfiguration]] = None,
68
92
  ):
69
- '''
93
+ """
70
94
  Initializes a SafetyEvaluation object.
71
95
 
72
96
  :param azure_ai_project: A dictionary defining the Azure AI project. Required keys are 'subscription_id', 'resource_group_name', and 'project_name'.
@@ -76,7 +100,7 @@ class _SafetyEvaluation:
76
100
  :param model_config: A dictionary defining the configuration for the model. Acceptable types are AzureOpenAIModelConfiguration and OpenAIModelConfiguration.
77
101
  :type model_config: Union[~azure.ai.evaluation.AzureOpenAIModelConfiguration, ~azure.ai.evaluation.OpenAIModelConfiguration]
78
102
  :raises ValueError: If the model_config does not contain the required keys or any value is None.
79
- '''
103
+ """
80
104
  if model_config:
81
105
  self._validate_model_config(model_config)
82
106
  self.model_config = model_config
@@ -84,9 +108,10 @@ class _SafetyEvaluation:
84
108
  self.model_config = None
85
109
  validate_azure_ai_project(azure_ai_project)
86
110
  self.azure_ai_project = AzureAIProject(**azure_ai_project)
87
- self.credential=credential
111
+ self.credential = credential
88
112
  self.logger = _setup_logger()
89
113
 
114
+
90
115
  @staticmethod
91
116
  def _validate_model_config(model_config: Any):
92
117
  """
@@ -123,17 +148,17 @@ class _SafetyEvaluation:
123
148
  raise ValueError(f"The following keys in model_config must not be None: {', '.join(none_keys)}")
124
149
 
125
150
  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,
151
+ self,
152
+ target: Callable,
153
+ max_conversation_turns: int = 1,
154
+ max_simulation_results: int = 3,
155
+ conversation_turns: List[List[Union[str, Dict[str, Any]]]] = [],
156
+ tasks: List[str] = [],
157
+ adversarial_scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak, _UnstableAdversarialScenario]] = None,
158
+ source_text: Optional[str] = None,
159
+ direct_attack: bool = False,
135
160
  ) -> Dict[str, str]:
136
- '''
161
+ """
137
162
  Generates synthetic conversations based on provided parameters.
138
163
 
139
164
  :param target: The target function to call during the simulation.
@@ -152,15 +177,16 @@ class _SafetyEvaluation:
152
177
  :type source_text: Optional[str]
153
178
  :param direct_attack: If True, the DirectAttackSimulator will be run.
154
179
  :type direct_attack: bool
155
- '''
180
+ """
181
+
156
182
  ## Define callback
157
183
  async def callback(
158
184
  messages: List[Dict],
159
185
  stream: bool = False,
160
186
  session_state: Optional[str] = None,
161
- context: Optional[Dict] = None
187
+ context: Optional[Dict] = None,
162
188
  ) -> dict:
163
- messages_list = messages["messages"] # type: ignore
189
+ messages_list = messages["messages"] # type: ignore
164
190
  latest_message = messages_list[-1]
165
191
  application_input = latest_message["content"]
166
192
  context = latest_message.get("context", None)
@@ -180,18 +206,25 @@ class _SafetyEvaluation:
180
206
  "context": latest_context if latest_context else context,
181
207
  }
182
208
  ## 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
-
209
+ messages["messages"].append(formatted_response) # type: ignore
210
+ return {
211
+ "messages": messages_list,
212
+ "stream": stream,
213
+ "session_state": session_state,
214
+ "context": latest_context if latest_context else context,
215
+ }
216
+
186
217
  ## Run simulator
187
- data_path = "simulator_outputs.jsonl"
218
+ simulator = None
188
219
  simulator_outputs = None
189
220
  jailbreak_outputs = None
190
221
  simulator_data_paths = {}
191
222
 
192
223
  # if IndirectAttack, run IndirectAttackSimulator
193
224
  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}")
225
+ self.logger.info(
226
+ 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}"
227
+ )
195
228
  simulator = IndirectAttackSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
196
229
  simulator_outputs = await simulator(
197
230
  scenario=adversarial_scenario,
@@ -202,22 +235,27 @@ class _SafetyEvaluation:
202
235
  text=source_text,
203
236
  target=callback,
204
237
  )
205
-
238
+
206
239
  # 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}")
240
+ elif direct_attack and isinstance(adversarial_scenario, AdversarialScenario):
241
+ self.logger.info(
242
+ f"Running DirectAttackSimulator with inputs: adversarial_scenario={adversarial_scenario}, max_conversation_turns={max_conversation_turns}, max_simulation_results={max_simulation_results}"
243
+ )
209
244
  simulator = DirectAttackSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
210
245
  simulator_outputs = await simulator(
211
246
  scenario=adversarial_scenario if adversarial_scenario else AdversarialScenario.ADVERSARIAL_REWRITE,
212
247
  max_conversation_turns=max_conversation_turns,
213
248
  max_simulation_results=max_simulation_results,
214
- target=callback)
249
+ target=callback,
250
+ )
215
251
  jailbreak_outputs = simulator_outputs["jailbreak"]
216
252
  simulator_outputs = simulator_outputs["regular"]
217
-
253
+
218
254
  ## If adversarial_scenario is not provided, run Simulator
219
255
  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}")
256
+ self.logger.info(
257
+ 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}"
258
+ )
221
259
  simulator = Simulator(self.model_config)
222
260
  simulator_outputs = await simulator(
223
261
  max_conversation_turns=max_conversation_turns,
@@ -230,10 +268,12 @@ class _SafetyEvaluation:
230
268
 
231
269
  ## Run AdversarialSimulator
232
270
  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}")
271
+ self.logger.info(
272
+ 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}"
273
+ )
234
274
  simulator = AdversarialSimulator(azure_ai_project=self.azure_ai_project, credential=self.credential)
235
275
  simulator_outputs = await simulator(
236
- scenario=adversarial_scenario,
276
+ scenario=adversarial_scenario, #type: ignore
237
277
  max_conversation_turns=max_conversation_turns,
238
278
  max_simulation_results=max_simulation_results,
239
279
  conversation_turns=conversation_turns,
@@ -253,13 +293,15 @@ class _SafetyEvaluation:
253
293
  blame=ErrorBlame.USER_ERROR,
254
294
  )
255
295
 
296
+ data_path_base = simulator.__class__.__name__
297
+
256
298
  ## Write outputs to file according to scenario
257
299
  if direct_attack and jailbreak_outputs:
258
- jailbreak_data_path = "jailbreak_simulator_outputs.jsonl"
259
- with Path(jailbreak_data_path).open("w") as f:
300
+ jailbreak_data_path = data_path_base + JAILBREAK_EXT
301
+ with Path(jailbreak_data_path + DATA_EXT).open("w") as f:
260
302
  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:
303
+ simulator_data_paths[jailbreak_data_path] = jailbreak_data_path + DATA_EXT
304
+ with Path(data_path_base + DATA_EXT).open("w") as f:
263
305
  if not adversarial_scenario or adversarial_scenario != AdversarialScenario.ADVERSARIAL_CONVERSATION:
264
306
  if source_text or self._check_target_returns_context(target):
265
307
  eval_input_data_json_lines = ""
@@ -286,25 +328,28 @@ class _SafetyEvaluation:
286
328
  + "\n"
287
329
  )
288
330
  f.write(eval_input_data_json_lines)
289
- elif isinstance(simulator_outputs,JsonLineList):
331
+ elif isinstance(simulator_outputs, JsonLineList):
290
332
  f.writelines(simulator_outputs.to_eval_qr_json_lines())
291
333
  else:
292
334
  f.writelines(output.to_eval_qr_json_lines() for output in simulator_outputs)
293
335
  else:
294
336
  f.writelines(
295
- [json.dumps({"conversation": {"messages": conversation["messages"]}}) + "\n" for conversation in simulator_outputs]
337
+ [
338
+ json.dumps({"conversation": {"messages": conversation["messages"]}}) + "\n"
339
+ for conversation in simulator_outputs
340
+ ]
296
341
  )
297
- simulator_data_paths["regular"] = data_path
342
+ simulator_data_paths[data_path_base] = data_path_base + DATA_EXT
298
343
 
299
344
  return simulator_data_paths
300
-
345
+
301
346
  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
- '''
347
+ self,
348
+ evaluators: List[_SafetyEvaluator],
349
+ num_turns: int = 3,
350
+ scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
351
+ ) -> Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak, _UnstableAdversarialScenario]]:
352
+ """
308
353
  Returns the Simulation scenario based on the provided list of SafetyEvaluator.
309
354
 
310
355
  :param evaluators: A list of SafetyEvaluator.
@@ -313,16 +358,20 @@ class _SafetyEvaluation:
313
358
  :type num_turns: int
314
359
  :param scenario: The adversarial scenario to simulate.
315
360
  :type scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
316
- '''
317
- if len(evaluators) == 0: return AdversarialScenario.ADVERSARIAL_QA
361
+ """
362
+ if len(evaluators) == 0:
363
+ return AdversarialScenario.ADVERSARIAL_QA
318
364
  for evaluator in evaluators:
319
365
  if evaluator in [_SafetyEvaluator.CONTENT_SAFETY, _SafetyEvaluator.DIRECT_ATTACK]:
320
- if num_turns == 1 and scenario: return scenario
366
+ if num_turns == 1 and scenario:
367
+ return scenario
321
368
  return (
322
369
  AdversarialScenario.ADVERSARIAL_CONVERSATION
323
370
  if num_turns > 1
324
371
  else AdversarialScenario.ADVERSARIAL_QA
325
372
  )
373
+ if evaluator == _SafetyEvaluator.ECI:
374
+ return _UnstableAdversarialScenario.ECI
326
375
  if evaluator in [
327
376
  _SafetyEvaluator.GROUNDEDNESS,
328
377
  _SafetyEvaluator.RELEVANCE,
@@ -346,23 +395,23 @@ class _SafetyEvaluation:
346
395
  )
347
396
 
348
397
  def _get_evaluators(
349
- self,
350
- evaluators: List[_SafetyEvaluator],
398
+ self,
399
+ evaluators: List[_SafetyEvaluator],
351
400
  ) -> Dict[str, Callable]:
352
- '''
401
+ """
353
402
  Returns a dictionary of evaluators based on the provided list of SafetyEvaluator.
354
403
 
355
404
  :param evaluators: A list of SafetyEvaluator.
356
405
  :type evaluators: List[SafetyEvaluator]
357
- '''
406
+ """
358
407
  evaluators_dict = {}
359
408
  # Default to content safety when no evaluators are specified
360
- if len(evaluators) == 0:
409
+ if len(evaluators) == 0:
361
410
  evaluators_dict["content_safety"] = _content_safety.ContentSafetyEvaluator(
362
- azure_ai_project=self.azure_ai_project, credential=self.credential
363
- )
411
+ azure_ai_project=self.azure_ai_project, credential=self.credential
412
+ )
364
413
  return evaluators_dict
365
-
414
+
366
415
  for evaluator in evaluators:
367
416
  if evaluator == _SafetyEvaluator.CONTENT_SAFETY:
368
417
  evaluators_dict["content_safety"] = _content_safety.ContentSafetyEvaluator(
@@ -400,12 +449,18 @@ class _SafetyEvaluation:
400
449
  evaluators_dict["content_safety"] = _content_safety.ContentSafetyEvaluator(
401
450
  azure_ai_project=self.azure_ai_project, credential=self.credential
402
451
  )
452
+ elif evaluator == _SafetyEvaluator.ECI:
453
+ evaluators_dict["eci"] = ECIEvaluator(
454
+ azure_ai_project=self.azure_ai_project, credential=self.credential
455
+ )
403
456
  else:
404
- msg = f"Invalid evaluator: {evaluator}. Supported evaluators are: {_SafetyEvaluator.__members__.values()}"
457
+ msg = (
458
+ f"Invalid evaluator: {evaluator}. Supported evaluators are: {_SafetyEvaluator.__members__.values()}"
459
+ )
405
460
  raise EvaluationException(
406
461
  message=msg,
407
462
  internal_message=msg,
408
- target=ErrorTarget.UNKNOWN, ## NOTE: We should add a target for this potentially
463
+ target=ErrorTarget.UNKNOWN, ## NOTE: We should add a target for this potentially
409
464
  category=ErrorCategory.INVALID_VALUE,
410
465
  blame=ErrorBlame.USER_ERROR,
411
466
  )
@@ -413,9 +468,25 @@ class _SafetyEvaluation:
413
468
 
414
469
  @staticmethod
415
470
  def _check_target_returns_context(target: Callable) -> bool:
416
- '''
471
+ """
417
472
  Checks if the target function returns a tuple. We assume the second value in the tuple is the "context".
418
-
473
+
474
+ :param target: The target function to check.
475
+ :type target: Callable
476
+ """
477
+ sig = inspect.signature(target)
478
+ ret_type = sig.return_annotation
479
+ if ret_type == inspect.Signature.empty:
480
+ return False
481
+ if ret_type is tuple:
482
+ return True
483
+ return False
484
+
485
+ @staticmethod
486
+ def _check_target_returns_str(target: Callable) -> bool:
487
+ '''
488
+ Checks if the target function returns a string.
489
+
419
490
  :param target: The target function to check.
420
491
  :type target: Callable
421
492
  '''
@@ -423,19 +494,26 @@ class _SafetyEvaluation:
423
494
  ret_type = sig.return_annotation
424
495
  if ret_type == inspect.Signature.empty:
425
496
  return False
426
- if ret_type is tuple:
497
+ if ret_type is str:
427
498
  return True
428
499
  return False
500
+
501
+
502
+ @staticmethod
503
+ def _check_target_is_callback(target:Callable) -> bool:
504
+ sig = inspect.signature(target)
505
+ param_names = list(sig.parameters.keys())
506
+ return 'messages' in param_names and 'stream' in param_names and 'session_state' in param_names and 'context' in param_names
429
507
 
430
508
  def _validate_inputs(
431
509
  self,
432
510
  evaluators: List[_SafetyEvaluator],
433
- target: Callable,
511
+ target: Union[Callable, AzureOpenAIModelConfiguration, OpenAIModelConfiguration],
434
512
  num_turns: int = 1,
435
513
  scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
436
514
  source_text: Optional[str] = None,
437
515
  ):
438
- '''
516
+ """
439
517
  Validates the inputs provided to the __call__ function of the SafetyEvaluation object.
440
518
  :param evaluators: A list of SafetyEvaluator.
441
519
  :type evaluators: List[SafetyEvaluator]
@@ -443,14 +521,27 @@ class _SafetyEvaluation:
443
521
  :type target: Callable
444
522
  :param num_turns: The number of turns in a between the target application and the caller.
445
523
  :type num_turns: int
446
- :param scenario: The adversarial scenario to simulate.
524
+ :param scenario: The adversarial scenario to simulate.
447
525
  :type scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
448
526
  :param source_text: The source text to use as grounding document in the evaluation.
449
527
  :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"
528
+ """
529
+ if not callable(target):
530
+ self._validate_model_config(target)
531
+ elif not self._check_target_returns_str(target):
532
+ self.logger.error(f"Target function {target} does not return a string.")
533
+ msg = f"Target function {target} does not return a string."
534
+ raise EvaluationException(
535
+ message=msg,
536
+ internal_message=msg,
537
+ target=ErrorTarget.UNKNOWN,
538
+ category=ErrorCategory.INVALID_VALUE,
539
+ blame=ErrorBlame.USER_ERROR,
540
+ )
541
+
542
+ if _SafetyEvaluator.GROUNDEDNESS in evaluators and not source_text:
543
+ self.logger.error(f"GroundednessEvaluator requires source_text. Source text: {source_text}")
544
+ msg = "GroundednessEvaluator requires source_text"
454
545
  raise EvaluationException(
455
546
  message=msg,
456
547
  internal_message=msg,
@@ -458,8 +549,8 @@ class _SafetyEvaluation:
458
549
  category=ErrorCategory.MISSING_FIELD,
459
550
  blame=ErrorBlame.USER_ERROR,
460
551
  )
461
-
462
- if scenario and not _SafetyEvaluator.CONTENT_SAFETY in evaluators:
552
+
553
+ if scenario and len(evaluators)>0 and not _SafetyEvaluator.CONTENT_SAFETY in evaluators:
463
554
  self.logger.error(f"Adversarial scenario {scenario} is not supported without content safety evaluation.")
464
555
  msg = f"Adversarial scenario {scenario} is not supported without content safety evaluation."
465
556
  raise EvaluationException(
@@ -470,7 +561,7 @@ class _SafetyEvaluation:
470
561
  blame=ErrorBlame.USER_ERROR,
471
562
  )
472
563
 
473
- if _SafetyEvaluator.CONTENT_SAFETY in evaluators and scenario and num_turns > 1:
564
+ if _SafetyEvaluator.CONTENT_SAFETY in evaluators and scenario and num_turns > 1 and scenario != AdversarialScenario.ADVERSARIAL_CONVERSATION:
474
565
  self.logger.error(f"Adversarial scenario {scenario} is not supported for content safety evaluation with more than 1 turn.")
475
566
  msg = f"Adversarial scenario {scenario} is not supported for content safety evaluation with more than 1 turn."
476
567
  raise EvaluationException(
@@ -480,14 +571,20 @@ class _SafetyEvaluation:
480
571
  category=ErrorCategory.INVALID_VALUE,
481
572
  blame=ErrorBlame.USER_ERROR,
482
573
  )
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:
574
+
575
+ if (
576
+ any(
577
+ e in evaluators
578
+ for e in [
579
+ _SafetyEvaluator.GROUNDEDNESS,
580
+ _SafetyEvaluator.RELEVANCE,
581
+ _SafetyEvaluator.SIMILARITY,
582
+ _SafetyEvaluator.FLUENCY,
583
+ _SafetyEvaluator.COHERENCE,
584
+ ]
585
+ )
586
+ and not self.model_config
587
+ ):
491
588
  self.logger.error(f"Model configuration is required for evaluators: {evaluators}")
492
589
  msg = f"Model configuration is required for evaluators: {evaluators}"
493
590
  raise EvaluationException(
@@ -497,38 +594,43 @@ class _SafetyEvaluation:
497
594
  category=ErrorCategory.MISSING_FIELD,
498
595
  blame=ErrorBlame.USER_ERROR,
499
596
  )
500
-
501
597
 
502
598
  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']
599
+ jailbreak_cols = [key for key in evaluation_result_dict.keys() if JAILBREAK_EXT in key]
600
+ regular_cols = [key for key in evaluation_result_dict.keys() if JAILBREAK_EXT not in key]
601
+ evaluation_result: EvaluationResult = {"metrics": {}, "rows": [], "studio_url": ""}
602
+ if len(jailbreak_cols)>0 and len(regular_cols)>0:
603
+ hate_unfairness_defects = []
604
+ violence_defects = []
605
+ sexual_defects = []
606
+ self_harm_defects = []
607
+ for i, row in enumerate(zip(evaluation_result_dict[jailbreak_cols[0]]['rows'], evaluation_result_dict[regular_cols[0]]['rows'])):
608
+ 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)
609
+ 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)
610
+ 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)
611
+ 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)
612
+ hate_unfairness_defect_rate = list_mean_nan_safe(hate_unfairness_defects)
613
+ violence_defect_rate = list_mean_nan_safe(violence_defects)
614
+ sexual_defect_rate = list_mean_nan_safe(sexual_defects)
615
+ self_harm_defect_rate = list_mean_nan_safe(self_harm_defects)
616
+
617
+ evaluation_result["rows"] = (
618
+ evaluation_result_dict[jailbreak_cols[0]]["rows"] + evaluation_result_dict[regular_cols[0]]["rows"]
619
+ )
620
+ evaluation_result["metrics"] = {
621
+ "content_safety.violence_defect_rate": hate_unfairness_defect_rate,
622
+ "content_safety.sexual_defect_rate": violence_defect_rate,
623
+ "content_safety.hate_unfairness_defect_rate": sexual_defect_rate,
624
+ "content_safety.self_harm_defect_rate": self_harm_defect_rate,
625
+ }
626
+ evaluation_result["studio_url"] = (
627
+ evaluation_result_dict[jailbreak_cols[0]]["studio_url"] + "\t" + evaluation_result_dict[regular_cols[0]]["studio_url"]
628
+ )
526
629
  return evaluation_result
527
-
528
-
630
+
529
631
  async def __call__(
530
632
  self,
531
- target: Callable,
633
+ target: Union[Callable, AzureOpenAIModelConfiguration, OpenAIModelConfiguration],
532
634
  evaluators: List[_SafetyEvaluator] = [],
533
635
  evaluation_name: Optional[str] = None,
534
636
  num_turns : int = 1,
@@ -536,14 +638,16 @@ class _SafetyEvaluation:
536
638
  scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]] = None,
537
639
  conversation_turns : List[List[Union[str, Dict[str, Any]]]] = [],
538
640
  tasks: List[str] = [],
641
+ data_only: bool = False,
539
642
  source_text: Optional[str] = None,
540
643
  data_path: Optional[Union[str, os.PathLike]] = None,
541
644
  jailbreak_data_path: Optional[Union[str, os.PathLike]] = None,
542
- output_path: Optional[Union[str, os.PathLike]] = None
543
- ) -> Union[EvaluationResult, Dict[str, EvaluationResult]]:
645
+ output_path: Optional[Union[str, os.PathLike]] = None,
646
+ data_paths: Optional[Union[Dict[str, str], Dict[str, Union[str,os.PathLike]]]] = None
647
+ ) -> Union[Dict[str, EvaluationResult], Dict[str, str], Dict[str, Union[str,os.PathLike]]]:
544
648
  '''
545
649
  Evaluates the target function based on the provided parameters.
546
-
650
+
547
651
  :param target: The target function to call during the evaluation.
548
652
  :type target: Callable
549
653
  :param evaluators: A list of SafetyEvaluator.
@@ -554,12 +658,14 @@ class _SafetyEvaluation:
554
658
  :type num_turns: int
555
659
  :param num_rows: The (maximum) number of rows to generate for evaluation.
556
660
  :type num_rows: int
557
- :param scenario: The adversarial scenario to simulate.
661
+ :param scenario: The adversarial scenario to simulate.
558
662
  :type scenario: Optional[Union[AdversarialScenario, AdversarialScenarioJailbreak]]
559
663
  :param conversation_turns: Predefined conversation turns to simulate.
560
664
  :type conversation_turns: List[List[Union[str, Dict[str, Any]]]]
561
665
  :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
666
  :type tasks: List[str] = [],
667
+ :param data_only: If True, the filepath to which simulation results are written will be returned.
668
+ :type data_only: bool
563
669
  :param source_text: The source text to use as grounding document in the evaluation.
564
670
  :type source_text: Optional[str]
565
671
  :param data_path: The path to the data file generated by the Simulator. If None, the Simulator will be run.
@@ -570,12 +676,12 @@ class _SafetyEvaluation:
570
676
  :type output_path: Optional[Union[str, os.PathLike]]
571
677
  '''
572
678
  ## 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}")
679
+ self.logger.info(f"User inputs: evaluators{evaluators}, evaluation_name={evaluation_name}, num_turns={num_turns}, num_rows={num_rows}, scenario={scenario},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
680
 
575
681
  ## Validate arguments
576
682
  self._validate_inputs(
577
- evaluators=evaluators,
578
- target=target,
683
+ evaluators=evaluators,
684
+ target=target,
579
685
  num_turns=num_turns,
580
686
  scenario=scenario,
581
687
  source_text=source_text,
@@ -583,12 +689,13 @@ class _SafetyEvaluation:
583
689
 
584
690
  # Get scenario
585
691
  adversarial_scenario = self._get_scenario(evaluators, num_turns=num_turns, scenario=scenario)
692
+ self.logger.info(f"Using scenario: {adversarial_scenario}")
586
693
 
587
694
  ## Get evaluators
588
695
  evaluators_dict = self._get_evaluators(evaluators)
589
696
 
590
697
  ## If `data_path` is not provided, run simulator
591
- if data_path is None and jailbreak_data_path is None:
698
+ if not data_paths and data_path is None and jailbreak_data_path is None and isinstance(target, Callable):
592
699
  self.logger.info(f"No data_path provided. Running simulator.")
593
700
  data_paths = await self._simulate(
594
701
  target=target,
@@ -598,43 +705,37 @@ class _SafetyEvaluation:
598
705
  conversation_turns=conversation_turns,
599
706
  tasks=tasks,
600
707
  source_text=source_text,
601
- direct_attack=_SafetyEvaluator.DIRECT_ATTACK in evaluators
708
+ direct_attack=_SafetyEvaluator.DIRECT_ATTACK in evaluators,
602
709
  )
603
- data_path = data_paths.get("regular", None)
604
- jailbreak_data_path = data_paths.get("jailbreak", None)
710
+ elif data_path:
711
+ data_paths = {Path(data_path).stem: data_path}
712
+ if jailbreak_data_path:
713
+ data_paths[Path(jailbreak_data_path).stem + JAILBREAK_EXT] = jailbreak_data_path
714
+
715
+ if data_only and data_paths: return data_paths
605
716
 
606
717
  ## Run evaluation
607
718
  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
719
+ if data_paths:
720
+ for strategy, data_path in data_paths.items():
721
+ 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}")
722
+ if evaluation_name: output_prefix = evaluation_name + "_"
723
+ else: output_prefix = ""
724
+ evaluate_outputs = _evaluate.evaluate(
725
+ data=data_path,
726
+ evaluators=evaluators_dict,
727
+ azure_ai_project=self.azure_ai_project,
728
+ evaluation_name=evaluation_name,
729
+ output_path=output_path if output_path else f"{output_prefix}{strategy}{RESULTS_EXT}",
730
+ _use_pf_client=False, #TODO: Remove this once eval logic for red team agent is moved to red team agent
731
+ )
732
+ evaluation_results[strategy] = evaluate_outputs
733
+ return evaluation_results
633
734
  else:
634
735
  raise EvaluationException(
635
- message="No data path found after simulation",
636
- internal_message="No data path found after simulation",
736
+ message="No data found after simulation",
737
+ internal_message="No data found after simulation",
637
738
  target=ErrorTarget.UNKNOWN,
638
739
  category=ErrorCategory.MISSING_FIELD,
639
740
  blame=ErrorBlame.USER_ERROR,
640
- )
741
+ )