Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
7242256
pyrit foundry integration spec
slister1001 Jan 5, 2026
72338ec
init implementation still need to figure out xpia / context and fix t…
slister1001 Jan 8, 2026
3fe0168
fix tests and imports
slister1001 Jan 9, 2026
2c8ca66
updates
slister1001 Jan 9, 2026
660b75a
updates
slister1001 Jan 9, 2026
82602b6
Fix red team tests for new PyRIT API compatibility
slister1001 Jan 21, 2026
7e0e881
Add e2e tests for RedTeam Foundry integration
slister1001 Jan 21, 2026
b947996
Fix PyRIT API compatibility for Foundry integration
slister1001 Jan 22, 2026
22ab661
Handle baseline-only execution limitation in PyRIT Foundry
slister1001 Jan 22, 2026
12406ad
Use PyRIT branch with baseline-only support and simplify orchestrator
slister1001 Jan 22, 2026
32a7ec3
Remove spec file from PR (documentation only)
slister1001 Jan 22, 2026
f95c748
Revert to Azure/PyRIT@main until PR #1321 is merged
slister1001 Jan 22, 2026
db79bc1
Split dev requirements to avoid pillow version conflict
slister1001 Jan 22, 2026
42b2075
Add script for running red team tests locally
slister1001 Jan 22, 2026
906c86f
Add dedicated CI job for red team tests
slister1001 Jan 22, 2026
9b88df3
Add unit tests for DatasetConfigurationBuilder binary_path support
slister1001 Jan 22, 2026
8c175fa
Fix CI issues: add cspell words and handle sphinx autodoc gracefully
slister1001 Jan 22, 2026
6747772
Add @pyrit_target_retry support to CallbackChatTarget
slister1001 Jan 22, 2026
1c82f24
Fix tests to use updated PyRIT API parameter names
slister1001 Jan 30, 2026
da4ea1b
Apply black formatting to red team module
slister1001 Jan 30, 2026
79e9ec1
fix: Accept both message and prompt_request keywords in _CallbackChat…
slister1001 Jan 30, 2026
bffcfed
fix: Store context_items in metadata for standard attacks
slister1001 Jan 30, 2026
8b5e067
fix: Enable baseline-only execution in ScenarioOrchestrator
slister1001 Jan 30, 2026
1e40a74
fix: Standardize FoundryStrategy imports and remove unused variable
slister1001 Jan 30, 2026
c8cd29a
chore: Update pyrit dependency to released v0.11.0
slister1001 Feb 5, 2026
32252bd
Merge upstream/main and resolve dev_requirements.txt conflict
slister1001 Feb 5, 2026
de72431
fix: Remove duplicate package install in CI
slister1001 Feb 5, 2026
52e03d0
fix: Replace class-level mutable state with per-instance TemporaryDir…
slister1001 Feb 6, 2026
51b4c81
Fix ASR artificially lowered by scoring errors
slister1001 Feb 9, 2026
8d1c50e
Fix review issues: null guards, response validation, cleanup safety, …
slister1001 Feb 9, 2026
339cf30
Fix scoring: switch RAIServiceScorer to sync_evals endpoint with pass…
slister1001 Feb 9, 2026
99dcaec
Add regression tests for council review findings (H4, M1-M6)
slister1001 Feb 9, 2026
5ff25bb
Address review comments: add logging, deduplicate code, extract redte…
slister1001 Feb 9, 2026
446844b
Revert redteam CI to await eng sys InjectedPackages support
slister1001 Feb 10, 2026
8cd6bf2
Fix build: remove _comment_todo from platform-matrix.json and apply b…
slister1001 Feb 10, 2026
3fc9ad5
Apply black formatting and align setup.py httpx bound with upstream main
slister1001 Feb 11, 2026
72767e0
Fix red team e2e test failures and apply SDK black formatting (24.4.0)
slister1001 Feb 11, 2026
32d01de
Update red team test recording assets to tag _5922d0e1e4
slister1001 Feb 11, 2026
784a59b
Remove eval_sim_test_image_understanding.jsonl and spec_pyrit_foundry.md
slister1001 Feb 11, 2026
8f06383
Enable red team tests in CI via InjectedPackages matrix entry
slister1001 Feb 11, 2026
aafbd7a
Fix pillow conflict: remove [redteam] from dev_requirements.txt
slister1001 Feb 11, 2026
c3f0e9b
fix: keep whl check enabled for redteam CI job
slister1001 Feb 12, 2026
8bf6839
Merge branch 'main' into spec/pyrit-foundry
slister1001 Feb 12, 2026
46bffe8
Fix redteam CI: inject pyrit directly to avoid conflicting URLs
slister1001 Feb 12, 2026
120d7df
Fix PROXY_URL usage: call as function after upstream change
slister1001 Feb 12, 2026
7b5a598
Fix pillow version conflict between pyrit and promptflow-devkit in re…
slister1001 Feb 12, 2026
a355659
fix build issues
slister1001 Feb 13, 2026
3168970
Fix test failures: skip promptflow-dependent tests when promptflow is…
slister1001 Feb 13, 2026
e53ea52
Remove unused dev_requirements_redteam.txt and run_redteam_tests.py
slister1001 Feb 13, 2026
7ab038c
Guard Configuration.set_config against opentelemetry import errors in…
slister1001 Feb 13, 2026
73eaf07
fix tests and promptflow-less env
slister1001 Feb 13, 2026
a2cf3ec
Fix CI: promptflow Configuration compat + test recording sanitizer
slister1001 Feb 17, 2026
79bd544
Merge branch 'main' into spec/pyrit-foundry
slister1001 Feb 17, 2026
9498fac
Fix SK build (promptflow 1.18.1 compat) and redteam recording mismatch
slister1001 Feb 17, 2026
c5c5678
Merge branch 'main' into spec/pyrit-foundry
slister1001 Feb 17, 2026
910dae2
Fix strategy-name mismatch in Foundry red team path
slister1001 Feb 17, 2026
7b8afe9
Fix CI: multi-turn assertion, IndirectJailbreak strategy key
slister1001 Feb 17, 2026
3233e52
Defensive fix for attack_identifier type mismatch (Bug 1)
slister1001 Feb 17, 2026
13b4be7
Merge branch 'main' into spec/pyrit-foundry
slister1001 Feb 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions eng/scripts/dispatch_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ def _inject_custom_reqs(req_file: str, injected_packages: str, package_dir: str)
if not injected_list:
return

# Entries prefixed with '!' are exclusion-only: they remove matching packages
# from dev_requirements but are not themselves installed.
excluded = [p[1:] for p in injected_list if p.startswith("!")]
installable = [p for p in injected_list if not p.startswith("!")]
# Build a combined list for filtering (both injected installs and exclusions)
all_filter_names = installable + excluded

logger.info(f"Adding custom packages to requirements for {package_dir}")
with open(req_file, "r") as handle:
for line in handle:
Expand All @@ -95,13 +102,13 @@ def _inject_custom_reqs(req_file: str, injected_packages: str, package_dir: str)
req_lines.append((line, parsed_req))

if req_lines:
all_adjustments = injected_list + [
all_adjustments = installable + [
line_tuple[0].strip()
for line_tuple in req_lines
if line_tuple[0].strip() and not _compare_req_to_injected_reqs(line_tuple[1], injected_list)
if line_tuple[0].strip() and not _compare_req_to_injected_reqs(line_tuple[1], all_filter_names)
]
else:
all_adjustments = injected_list
all_adjustments = installable

logger.info(f"Generated Custom Reqs: {req_lines}")

Expand Down
4 changes: 4 additions & 0 deletions sdk/evaluation/azure-ai-evaluation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Prevent recursive stdout/stderr forwarding when NodeLogManager is nested, avoiding RecursionError in concurrent evaluation runs.

### Other Changes

- The `[redteam]` extra now requires `pyrit==0.11.0`, which depends on `pillow>=12.1.0`. This conflicts with `promptflow-devkit` (`pillow<=11.3.0`). Use separate virtual environments if you need both packages.

## 1.14.0 (2026-01-05)

### Bugs Fixed
Expand Down
2 changes: 1 addition & 1 deletion sdk/evaluation/azure-ai-evaluation/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "python",
"TagPrefix": "python/evaluation/azure-ai-evaluation",
"Tag": "python/evaluation/azure-ai-evaluation_409699f40b"
"Tag": "python/evaluation/azure-ai-evaluation_2ae9b6b8ea"
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
from azure.ai.evaluation._evaluate._batch_run.batch_clients import BatchClientRun, HasAsyncCallable


Configuration.get_instance().set_config("trace.destination", "none")
try:
Configuration.get_instance().set_config("trace.destination", "none")
except Exception:
pass
LOGGER = logging.getLogger(__name__)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
_has_legacy = False
try:
from promptflow._constants import FlowType
from promptflow.client import PFClient

_has_legacy = True
except ImportError:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

try:
from promptflow._sdk._configuration import Configuration as _Configuration
except ImportError:

# Validate that the imported Configuration accepts our expected kwargs.
# Some versions of promptflow expose Configuration but with an incompatible signature.
_Configuration(override_config=None)
except (ImportError, TypeError):
_global_config: Final[Dict[str, Any]] = {}

class _Configuration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,53 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------

_PYRIT_INSTALLED = False

try:
from ._red_team import RedTeam
from ._attack_strategy import AttackStrategy
from ._attack_objective_generator import RiskCategory, SupportedLanguages
from ._red_team_result import RedTeamResult

_PYRIT_INSTALLED = True
except ImportError:
raise ImportError(
"Could not import Pyrit. Please install the dependency with `pip install azure-ai-evaluation[redteam]`."
)
# When pyrit is not installed, provide placeholder classes for documentation
# This allows sphinx autodoc to document the module without the optional dependency
import sys

# Check if we're being imported by sphinx for documentation
_is_sphinx = "sphinx" in sys.modules

if not _is_sphinx:
raise ImportError(
"Could not import Pyrit. Please install the dependency with `pip install azure-ai-evaluation[redteam]`."
)

# Provide placeholder docstrings for sphinx
class RedTeam: # type: ignore[no-redef]
"""Red team testing orchestrator. Requires pyrit: `pip install azure-ai-evaluation[redteam]`."""

pass

class AttackStrategy: # type: ignore[no-redef]
"""Attack strategy enumeration. Requires pyrit: `pip install azure-ai-evaluation[redteam]`."""

pass

class RiskCategory: # type: ignore[no-redef]
"""Risk category enumeration. Requires pyrit: `pip install azure-ai-evaluation[redteam]`."""

pass

class SupportedLanguages: # type: ignore[no-redef]
"""Supported languages enumeration. Requires pyrit: `pip install azure-ai-evaluation[redteam]`."""

pass

class RedTeamResult: # type: ignore[no-redef]
"""Red team result container. Requires pyrit: `pip install azure-ai-evaluation[redteam]`."""

pass


__all__ = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def _get_tool_provider() -> RedTeamToolProvider:


def red_team_fetch_harmful_prompt(
risk_category: str, strategy: str = "baseline", convert_with_strategy: Optional[str] = None
risk_category: str,
strategy: str = "baseline",
convert_with_strategy: Optional[str] = None,
) -> str:
"""
Fetch a harmful prompt for a specific risk category to test content filters.
Expand All @@ -58,7 +60,9 @@ def red_team_fetch_harmful_prompt(
# Run the async method in a new event loop
result = asyncio.run(
provider.fetch_harmful_prompt(
risk_category_text=risk_category, strategy=strategy, convert_with_strategy=convert_with_strategy
risk_category_text=risk_category,
strategy=strategy,
convert_with_strategy=convert_with_strategy,
)
)

Expand Down Expand Up @@ -194,7 +198,13 @@ def red_team_send_to_target(prompt: str) -> str:

return json.dumps({"status": "success", "prompt": prompt, "response": response})
except Exception as e:
return json.dumps({"status": "error", "message": f"Error calling target function: {str(e)}", "prompt": prompt})
return json.dumps(
{
"status": "error",
"message": f"Error calling target function: {str(e)}",
"prompt": prompt,
}
)


# Example User Input for Each Function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from azure.ai.evaluation._common._experimental import experimental
from azure.ai.evaluation.red_team._attack_objective_generator import RiskCategory
from azure.ai.evaluation.simulator._model_tools import ManagedIdentityAPITokenManager
from azure.ai.evaluation.simulator._model_tools._generated_rai_client import GeneratedRAIClient
from azure.ai.evaluation.simulator._model_tools._generated_rai_client import (
GeneratedRAIClient,
)
from ._agent_utils import AgentUtils

# Setup logging
Expand Down Expand Up @@ -59,7 +61,8 @@ def __init__(

# Create the generated RAI client for fetching attack objectives
self.generated_rai_client = GeneratedRAIClient(
azure_ai_project=self.azure_ai_project_endpoint, token_manager=self.token_manager.get_aad_credential()
azure_ai_project=self.azure_ai_project_endpoint,
token_manager=self.token_manager.get_aad_credential(),
)

# Cache for attack objectives to avoid repeated API calls
Expand Down Expand Up @@ -165,11 +168,15 @@ async def _get_attack_objectives(self, risk_category: RiskCategory, strategy: st
# Get strategy-specific dataset for tense strategy
if "tense" in strategy:
objectives_response = await self.generated_rai_client.get_attack_objectives(
risk_category=risk_cat_value, application_scenario=self.application_scenario or "", strategy="tense"
risk_category=risk_cat_value,
application_scenario=self.application_scenario or "",
strategy="tense",
)
else:
objectives_response = await self.generated_rai_client.get_attack_objectives(
risk_category=risk_cat_value, application_scenario=self.application_scenario or "", strategy=None
risk_category=risk_cat_value,
application_scenario=self.application_scenario or "",
strategy=None,
)

# Handle jailbreak strategy - apply jailbreak prefixes to messages
Expand Down Expand Up @@ -199,7 +206,10 @@ async def _get_attack_objectives(self, risk_category: RiskCategory, strategy: st
return []

async def fetch_harmful_prompt(
self, risk_category_text: str, strategy: str = "baseline", convert_with_strategy: Optional[str] = None
self,
risk_category_text: str,
strategy: str = "baseline",
convert_with_strategy: Optional[str] = None,
) -> Dict[str, Any]:
"""Fetch a harmful prompt for a specific risk category.

Expand Down Expand Up @@ -276,7 +286,10 @@ async def fetch_harmful_prompt(
"note": "This prompt was generated and converted for responsible AI testing purposes only.",
}
except Exception as e:
return {"status": "error", "message": f"Error converting prompt: {str(e)}"}
return {
"status": "error",
"message": f"Error converting prompt: {str(e)}",
}

# Return with information about available strategies
return {
Expand Down Expand Up @@ -394,7 +407,10 @@ async def red_team(self, category: str, strategy: Optional[str] = None) -> Dict[
"note": f"This prompt was generated for responsible AI testing purposes only and converted using the {strategy} strategy.",
}
except Exception as e:
return {"status": "error", "message": f"Error converting prompt with strategy {strategy}: {str(e)}"}
return {
"status": "error",
"message": f"Error converting prompt with strategy {strategy}: {str(e)}",
}

except Exception as e:
logger.error(f"Error in red_team: {str(e)}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def __init__(
@kernel_function(description="Fetch a harmful prompt for a specific risk category to test content filters")
async def fetch_harmful_prompt(
self,
risk_category: Annotated[str, "The risk category (e.g., 'violence', 'hate_unfairness', 'sexual', 'self_harm')"],
risk_category: Annotated[
str,
"The risk category (e.g., 'violence', 'hate_unfairness', 'sexual', 'self_harm')",
],
strategy: Annotated[str, "Attack strategy to use (e.g., 'baseline', 'jailbreak')"] = "baseline",
convert_with_strategy: Annotated[str, "Optional strategy to convert the prompt"] = "",
) -> Annotated[str, "A JSON string with the harmful prompt and metadata"]:
Expand All @@ -92,7 +95,9 @@ async def fetch_harmful_prompt(

# Directly await the async method instead of using asyncio.run()
result = await self.tool_provider.fetch_harmful_prompt(
risk_category_text=risk_category, strategy=strategy, convert_with_strategy=convert_with_strategy
risk_category_text=risk_category,
strategy=strategy,
convert_with_strategy=convert_with_strategy,
)

# Store the prompt for later conversion if successful
Expand Down Expand Up @@ -131,7 +136,10 @@ async def convert_prompt(
@kernel_function(description="Get a harmful prompt for a specific risk category and optionally convert it")
async def red_team_unified(
self,
category: Annotated[str, "The risk category (e.g., 'violence', 'hate_unfairness', 'sexual', 'self_harm')"],
category: Annotated[
str,
"The risk category (e.g., 'violence', 'hate_unfairness', 'sexual', 'self_harm')",
],
strategy: Annotated[str, "Optional strategy to convert the prompt"] = "",
) -> Annotated[str, "A JSON string with the harmful prompt and metadata"]:
"""
Expand All @@ -158,7 +166,9 @@ async def red_team_unified(
return json.dumps(result)

@kernel_function(description="Get a list of all available prompt conversion strategies")
async def get_available_strategies(self) -> Annotated[str, "A JSON string with available conversion strategies"]:
async def get_available_strategies(
self,
) -> Annotated[str, "A JSON string with available conversion strategies"]:
"""
Get a list of all available prompt conversion strategies.

Expand All @@ -171,7 +181,9 @@ async def get_available_strategies(self) -> Annotated[str, "A JSON string with a
return json.dumps({"status": "success", "available_strategies": strategies})

@kernel_function(description="Explain the purpose and responsible use of red teaming tools")
async def explain_purpose(self) -> Annotated[str, "A JSON string with information about red teaming tools"]:
async def explain_purpose(
self,
) -> Annotated[str, "A JSON string with information about red teaming tools"]:
"""
Explain the purpose and responsible use of red teaming tools.

Expand Down Expand Up @@ -224,5 +236,9 @@ async def send_to_target(
return json.dumps({"status": "success", "prompt": prompt, "response": response})
except Exception as e:
return json.dumps(
{"status": "error", "message": f"Error calling target function: {str(e)}", "prompt": prompt}
{
"status": "error",
"message": f"Error calling target function: {str(e)}",
"prompt": prompt,
}
)
Loading