Skip to content

Commit aeb2e1a

Browse files
slister1001scbedd
andauthored
[RedTeam] Add new CI stage for running unit tests with pyrit installed (#40311)
* small fixes * typo * undo dev requirements change * dev requirements change * version bump for azure-core and azure-storage-blob in setup * update unit tests * pin to newer pyrit version * fix unit tests * fix min deps * updates * new CI stage for pyrit * add pyrit to cspell * Update sdk/evaluation/platform-matrix.json Co-authored-by: Scott Beddall <[email protected]> * Update sdk/evaluation/platform-matrix.json Co-authored-by: Scott Beddall <[email protected]> --------- Co-authored-by: Scott Beddall <[email protected]>
1 parent 7e6ad3d commit aeb2e1a

File tree

6 files changed

+226
-171
lines changed

6 files changed

+226
-171
lines changed

.vscode/cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@
388388
"pyright",
389389
"pyrightconfig",
390390
"parameterizing",
391+
"pyrit",
391392
"pytyped",
392393
"pytz",
393394
"pywin",

sdk/evaluation/azure-ai-evaluation/azure/ai/evaluation/red_team/_red_team.py

+25-25
Original file line numberDiff line numberDiff line change
@@ -283,14 +283,14 @@ def _start_redteam_mlflow_run(
283283

284284
async def _log_redteam_results_to_mlflow(
285285
self,
286-
redteam_output: RedTeamResult,
286+
redteam_result: RedTeamResult,
287287
eval_run: EvalRun,
288288
data_only: bool = False,
289289
) -> Optional[str]:
290290
"""Log the Red Team Agent results to MLFlow.
291291
292-
:param redteam_output: The output from the red team agent evaluation
293-
:type redteam_output: ~azure.ai.evaluation.RedTeamOutput
292+
:param redteam_result: The output from the red team agent evaluation
293+
:type redteam_result: ~azure.ai.evaluation.RedTeamResult
294294
:param eval_run: The MLFlow run object
295295
:type eval_run: ~azure.ai.evaluation._evaluate._eval_run.EvalRun
296296
:param data_only: Whether to log only data without evaluation results
@@ -308,21 +308,21 @@ async def _log_redteam_results_to_mlflow(
308308
with open(artifact_path, "w", encoding=DefaultOpenEncoding.WRITE) as f:
309309
if data_only:
310310
# In data_only mode, we write the conversations in conversation/messages format
311-
f.write(json.dumps({"conversations": redteam_output.attack_details or []}))
312-
elif redteam_output.scan_result:
311+
f.write(json.dumps({"conversations": redteam_result.attack_details or []}))
312+
elif redteam_result.scan_result:
313313
# Create a copy to avoid modifying the original scan result
314-
result_with_conversations = redteam_output.scan_result.copy() if isinstance(redteam_output.scan_result, dict) else {}
314+
result_with_conversations = redteam_result.scan_result.copy() if isinstance(redteam_result.scan_result, dict) else {}
315315

316316
# Preserve all original fields needed for scorecard generation
317317
result_with_conversations["scorecard"] = result_with_conversations.get("scorecard", {})
318318
result_with_conversations["parameters"] = result_with_conversations.get("parameters", {})
319319

320320
# Add conversations field with all conversation data including user messages
321-
result_with_conversations["conversations"] = redteam_output.attack_details or []
321+
result_with_conversations["conversations"] = redteam_result.attack_details or []
322322

323323
# Keep original attack_details field to preserve compatibility with existing code
324-
if "attack_details" not in result_with_conversations and redteam_output.attack_details is not None:
325-
result_with_conversations["attack_details"] = redteam_output.attack_details
324+
if "attack_details" not in result_with_conversations and redteam_result.attack_details is not None:
325+
result_with_conversations["attack_details"] = redteam_result.attack_details
326326

327327
json.dump(result_with_conversations, f)
328328

@@ -340,10 +340,10 @@ async def _log_redteam_results_to_mlflow(
340340
f.write(json.dumps(red_team_info_logged))
341341

342342
# Also save a human-readable scorecard if available
343-
if not data_only and redteam_output.scan_result:
343+
if not data_only and redteam_result.scan_result:
344344
scorecard_path = os.path.join(self.scan_output_dir, "scorecard.txt")
345345
with open(scorecard_path, "w", encoding=DefaultOpenEncoding.WRITE) as f:
346-
f.write(self._to_scorecard(redteam_output.scan_result))
346+
f.write(self._to_scorecard(redteam_result.scan_result))
347347
self.logger.debug(f"Saved scorecard to: {scorecard_path}")
348348

349349
# Create a dedicated artifacts directory with proper structure for MLFlow
@@ -354,13 +354,13 @@ async def _log_redteam_results_to_mlflow(
354354
# First, create the main artifact file that MLFlow expects
355355
with open(os.path.join(tmpdir, artifact_name), "w", encoding=DefaultOpenEncoding.WRITE) as f:
356356
if data_only:
357-
f.write(json.dumps({"conversations": redteam_output.attack_details or []}))
358-
elif redteam_output.scan_result:
359-
redteam_output.scan_result["redteaming_scorecard"] = redteam_output.scan_result.get("scorecard", None)
360-
redteam_output.scan_result["redteaming_parameters"] = redteam_output.scan_result.get("parameters", None)
361-
redteam_output.scan_result["redteaming_data"] = redteam_output.scan_result.get("attack_details", None)
357+
f.write(json.dumps({"conversations": redteam_result.attack_details or []}))
358+
elif redteam_result.scan_result:
359+
redteam_result.scan_result["redteaming_scorecard"] = redteam_result.scan_result.get("scorecard", None)
360+
redteam_result.scan_result["redteaming_parameters"] = redteam_result.scan_result.get("parameters", None)
361+
redteam_result.scan_result["redteaming_data"] = redteam_result.scan_result.get("attack_details", None)
362362

363-
json.dump(redteam_output.scan_result, f)
363+
json.dump(redteam_result.scan_result, f)
364364

365365
# Copy all relevant files to the temp directory
366366
import shutil
@@ -401,9 +401,9 @@ async def _log_redteam_results_to_mlflow(
401401
artifact_file = Path(tmpdir) / artifact_name
402402
with open(artifact_file, "w", encoding=DefaultOpenEncoding.WRITE) as f:
403403
if data_only:
404-
f.write(json.dumps({"conversations": redteam_output.attack_details or []}))
405-
elif redteam_output.scan_result:
406-
json.dump(redteam_output.scan_result, f)
404+
f.write(json.dumps({"conversations": redteam_result.attack_details or []}))
405+
elif redteam_result.scan_result:
406+
json.dump(redteam_result.scan_result, f)
407407
eval_run.log_artifact(tmpdir, artifact_name)
408408
self.logger.debug(f"Logged artifact: {artifact_name}")
409409

@@ -414,8 +414,8 @@ async def _log_redteam_results_to_mlflow(
414414
"_azureml.evaluate_artifacts": json.dumps([{"path": artifact_name, "type": "table"}]),
415415
})
416416

417-
if redteam_output.scan_result:
418-
scorecard = redteam_output.scan_result["scorecard"]
417+
if redteam_result.scan_result:
418+
scorecard = redteam_result.scan_result["scorecard"]
419419
joint_attack_summary = scorecard["joint_risk_attack_summary"]
420420

421421
if joint_attack_summary:
@@ -1641,7 +1641,7 @@ async def scan(
16411641
:param timeout: The timeout in seconds for API calls (default: 120)
16421642
:type timeout: int
16431643
:return: The output from the red team scan
1644-
:rtype: RedTeamOutput
1644+
:rtype: RedTeamResult
16451645
"""
16461646
# Start timing for performance tracking
16471647
self.start_time = time.time()
@@ -1673,7 +1673,7 @@ def filter(self, record):
16731673
return False
16741674
if 'The path to the artifact is either not a directory or does not exist' in record.getMessage():
16751675
return False
1676-
if 'RedTeamOutput object at' in record.getMessage():
1676+
if 'RedTeamResult object at' in record.getMessage():
16771677
return False
16781678
if 'timeout won\'t take effect' in record.getMessage():
16791679
return False
@@ -2006,7 +2006,7 @@ def filter(self, record):
20062006
# Log results to MLFlow
20072007
self.logger.info("Logging results to MLFlow")
20082008
await self._log_redteam_results_to_mlflow(
2009-
redteam_output=output,
2009+
redteam_result=output,
20102010
eval_run=eval_run,
20112011
data_only=data_only
20122012
)

sdk/evaluation/azure-ai-evaluation/tests/unittests/test_redteam/test_formatting_utils.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,21 @@ class TestScorecardFormatting:
140140

141141
def test_format_scorecard_empty(self):
142142
"""Test scorecard formatting with empty data."""
143-
redteam_result = {
144-
"redteaming_scorecard": {
143+
scan_result = {
144+
"scorecard": {
145145
"risk_category_summary": [],
146146
"joint_risk_attack_summary": []
147147
}
148148
}
149149

150-
result = format_scorecard(redteam_result)
150+
result = format_scorecard(scan_result)
151151

152152
assert "Overall ASR: 0%" in result
153153

154154
def test_format_scorecard_with_data(self):
155155
"""Test scorecard formatting with actual data."""
156-
redteam_result = {
157-
"redteaming_scorecard": {
156+
scan_result = {
157+
"scorecard": {
158158
"risk_category_summary": [{
159159
"overall_asr": 25.5
160160
}],
@@ -171,7 +171,7 @@ def test_format_scorecard_with_data(self):
171171
"studio_url": "https://example.com/studio"
172172
}
173173

174-
result = format_scorecard(redteam_result)
174+
result = format_scorecard(scan_result)
175175

176176
assert "Overall ASR: 25.5%" in result
177177
assert "Violence" in result # Should show capitalized risk category
@@ -182,8 +182,8 @@ def test_format_scorecard_with_data(self):
182182

183183
def test_format_scorecard_partial_data(self):
184184
"""Test scorecard formatting with partial data."""
185-
redteam_result = {
186-
"redteaming_scorecard": {
185+
scan_result = {
186+
"scorecard": {
187187
"risk_category_summary": [{
188188
"overall_asr": 15.0
189189
}],
@@ -198,7 +198,7 @@ def test_format_scorecard_partial_data(self):
198198
}
199199
}
200200

201-
result = format_scorecard(redteam_result)
201+
result = format_scorecard(scan_result)
202202

203203
assert "Overall ASR: 15.0%" in result
204204
assert "Hate-unfairness" in result # Should show formatted risk category

0 commit comments

Comments
 (0)