Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions pyrit/executor/attack/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
AttackAdversarialConfig,
AttackConverterConfig,
AttackScoringConfig,
resolve_adversarial_system_prompt,
)
from pyrit.executor.attack.core.attack_executor import AttackExecutor, AttackExecutorResult
from pyrit.executor.attack.core.attack_parameters import (
Expand All @@ -32,4 +33,5 @@
"AttackStrategyResultT",
"AttackExecutor",
"AttackExecutorResult",
"resolve_adversarial_system_prompt",
]
92 changes: 89 additions & 3 deletions pyrit/executor/attack/core/attack_config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import logging
from dataclasses import dataclass, field
from pathlib import Path

from pyrit.common.deprecation import print_deprecation_message
from pyrit.executor.core import StrategyConverterConfig
from pyrit.models import SeedPrompt
from pyrit.prompt_target import PromptTarget
from pyrit.score import Scorer, TrueFalseScorer

logger = logging.getLogger(__name__)

# Default first-message seed prompt for adversarial chat targets.
DEFAULT_ADVERSARIAL_SEED_PROMPT = "Generate your first message to achieve: {{ objective }}"


@dataclass
class AttackAdversarialConfig:
Expand All @@ -24,11 +31,90 @@ class AttackAdversarialConfig:
# Adversarial chat target for the attack
target: PromptTarget

# Path to the YAML file containing the system prompt for the adversarial chat target
# Path to the YAML file containing the system prompt for the adversarial chat target.
# Deprecated: use ``system_prompt`` (an inline string or SeedPrompt) instead.
system_prompt_path: str | Path | None = None

# Seed prompt for the adversarial chat target (supports {{ objective }} template variable)
seed_prompt: str | SeedPrompt = "Generate your first message to achieve: {{ objective }}"
# Seed prompt for the adversarial chat target (supports {{ objective }} template variable).
# May be None for strategies that do not use a first-message seed prompt.
seed_prompt: str | SeedPrompt | None = DEFAULT_ADVERSARIAL_SEED_PROMPT

# System prompt for the adversarial chat target, as an inline Jinja template string or a
# SeedPrompt. Takes precedence over ``system_prompt_path`` when both are provided.
system_prompt: str | SeedPrompt | None = None

def __post_init__(self) -> None:
"""Emit a deprecation warning when the legacy ``system_prompt_path`` is used."""
if self.system_prompt_path is not None:
print_deprecation_message(
old_item="AttackAdversarialConfig.system_prompt_path",
new_item="AttackAdversarialConfig.system_prompt",
removed_in="0.17.0",
)
if self.system_prompt is not None:
logger.warning(
"Both 'system_prompt' and 'system_prompt_path' are set on AttackAdversarialConfig; "
"'system_prompt' takes precedence and 'system_prompt_path' is ignored."
)


def resolve_adversarial_system_prompt(
*,
config: AttackAdversarialConfig,
default_system_prompt_path: str | Path,
required_parameters: list[str],
error_message: str | None = None,
) -> SeedPrompt:
"""
Resolve the effective adversarial system-prompt ``SeedPrompt`` for a strategy.

Resolution order:

1. ``config.system_prompt`` (inline string or SeedPrompt), if provided.
2. ``config.system_prompt_path`` (deprecated), if provided.
3. ``default_system_prompt_path``.

Inline strings are trusted: they are wrapped in a Jinja ``SeedPrompt`` whose declared
parameters are set to ``required_parameters``. Explicitly provided ``SeedPrompt`` objects
and YAML files are validated against ``required_parameters``.

Args:
config: The adversarial configuration to resolve the system prompt from.
default_system_prompt_path: Fallback YAML path when neither inline nor path is set.
required_parameters: Parameter names the resolved template must support.
error_message: Optional custom error message for validation failures.

Returns:
The resolved adversarial system-prompt SeedPrompt.

Raises:
ValueError: If an explicitly provided SeedPrompt is missing required parameters.
"""
system_prompt = config.system_prompt
if system_prompt is not None:
if isinstance(system_prompt, SeedPrompt):
# Validate only explicitly provided SeedPrompts against the required parameters.
declared = system_prompt.parameters or []
missing = [param for param in required_parameters if param not in declared]
if missing:
raise ValueError(
error_message or f"Adversarial system prompt is missing required parameters: {missing}"
)
return system_prompt

# Inline strings are trusted — declare all required params so Jinja rendering works.
return SeedPrompt(
value=system_prompt,
is_jinja_template=True,
parameters=list(required_parameters),
)

template_path = config.system_prompt_path or default_system_prompt_path
return SeedPrompt.from_yaml_with_required_parameters(
template_path=template_path,
required_parameters=required_parameters,
error_message=error_message,
)


@dataclass
Expand Down
57 changes: 55 additions & 2 deletions pyrit/executor/attack/core/attack_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@
ConversationReference,
Identifiable,
Message,
SeedPrompt,
)
from pyrit.prompt_target.common.target_requirements import TargetRequirements

if TYPE_CHECKING:
from pyrit.executor.attack.core.attack_config import AttackScoringConfig
from pyrit.executor.attack.core.attack_config import (
AttackAdversarialConfig,
AttackScoringConfig,
)
from pyrit.executor.attack.core.attack_result_attribution import AttackResultAttribution
from pyrit.prompt_target import PromptTarget

Expand Down Expand Up @@ -432,11 +436,28 @@ def _create_identifier(
"objective_target": self.get_objective_target().get_identifier(),
}

merged_params: dict[str, Any] = dict(params) if params else {}

# Add scorer if present
scoring_config = self.get_attack_scoring_config()
if scoring_config and scoring_config.objective_scorer:
all_children["objective_scorer"] = scoring_config.objective_scorer.get_identifier()

# Add adversarial chat target and its effective prompts if present. The adversarial
# target becomes a child (filtered to model params by the eval rule), while the
# effective system/seed prompts land on the attack-strategy node so they are included
# in both the full component hash and the eval hash. None-valued params are dropped by
# ComponentIdentifier.of, so strategies that do not use a given prompt simply omit it.
adversarial_config = self.get_attack_adversarial_config()
if adversarial_config is not None and getattr(adversarial_config, "target", None) is not None:
all_children["adversarial_chat"] = adversarial_config.target.get_identifier()
merged_params["adversarial_system_prompt"] = self._extract_adversarial_prompt_text(
adversarial_config.system_prompt
)
merged_params["adversarial_seed_prompt"] = self._extract_adversarial_prompt_text(
adversarial_config.seed_prompt
)

# Add request converter identifiers if present
if self._request_converters:
all_children["request_converters"] = [
Expand All @@ -452,7 +473,24 @@ def _create_identifier(
if children:
all_children.update(children)

return ComponentIdentifier.of(self, params=params, children=all_children)
return ComponentIdentifier.of(self, params=merged_params or None, children=all_children)

@staticmethod
def _extract_adversarial_prompt_text(value: str | SeedPrompt | None) -> str | None:
"""
Extract a stable text representation of an adversarial prompt for identity.

Args:
value: The adversarial system or seed prompt (string, SeedPrompt, or None).

Returns:
The prompt text, or None when no prompt is set.
"""
if value is None:
return None
if isinstance(value, SeedPrompt):
return value.value
return value

def _build_identifier(self) -> ComponentIdentifier:
"""
Expand Down Expand Up @@ -498,6 +536,21 @@ def get_attack_scoring_config(self) -> AttackScoringConfig | None:
"""
return None

def get_attack_adversarial_config(self) -> AttackAdversarialConfig | None:
"""
Get the attack adversarial configuration used by this strategy.

Returns:
AttackAdversarialConfig | None: The adversarial configuration, or None if not applicable.

Note:
Subclasses that use an adversarial chat target should override this method to return
the effective adversarial configuration (resolved target plus the system/seed prompts
actually used), so the adversarial target and prompts are reflected in the attack
identity. The default implementation returns None.
"""
return None

def get_request_converters(self) -> list[Any]:
"""
Get request converter configurations used by this strategy.
Expand Down
27 changes: 21 additions & 6 deletions pyrit/executor/attack/multi_turn/crescendo.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
AttackAdversarialConfig,
AttackConverterConfig,
AttackScoringConfig,
resolve_adversarial_system_prompt,
)
from pyrit.executor.attack.multi_turn.multi_turn_attack_strategy import (
ConversationSession,
Expand Down Expand Up @@ -219,12 +220,9 @@ def __init__(
except ValueError as exc:
raise ValueError(f"CrescendoAttack {exc}") from exc

system_prompt_template_path = (
attack_adversarial_config.system_prompt_path
or CrescendoAttack.DEFAULT_ADVERSARIAL_CHAT_SYSTEM_PROMPT_TEMPLATE_PATH
)
self._adversarial_chat_system_prompt_template = SeedPrompt.from_yaml_with_required_parameters(
template_path=system_prompt_template_path,
self._adversarial_chat_system_prompt_template = resolve_adversarial_system_prompt(
config=attack_adversarial_config,
default_system_prompt_path=CrescendoAttack.DEFAULT_ADVERSARIAL_CHAT_SYSTEM_PROMPT_TEMPLATE_PATH,
required_parameters=["objective", "max_turns"],
error_message="Crescendo system prompt must have 'objective' and 'max_turns' parameters",
)
Expand Down Expand Up @@ -264,6 +262,23 @@ def get_attack_scoring_config(self) -> AttackScoringConfig | None:
use_score_as_feedback=self._use_score_as_feedback,
)

def get_attack_adversarial_config(self) -> AttackAdversarialConfig | None:
"""
Get the effective adversarial configuration used by this strategy.

Returns:
AttackAdversarialConfig | None: The adversarial target and its resolved system prompt.
Crescendo does not use a configurable first-message seed prompt.
"""
adversarial_chat = getattr(self, "_adversarial_chat", None)
if adversarial_chat is None:
return None
return AttackAdversarialConfig(
target=adversarial_chat,
system_prompt=self._adversarial_chat_system_prompt_template,
seed_prompt=None,
)

def _validate_context(self, *, context: CrescendoAttackContext) -> None:
"""
Validate the Crescendo attack context to ensure it has the necessary configuration.
Expand Down
36 changes: 28 additions & 8 deletions pyrit/executor/attack/multi_turn/red_teaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
get_adversarial_chat_messages,
)
from pyrit.executor.attack.core.attack_config import (
DEFAULT_ADVERSARIAL_SEED_PROMPT,
AttackAdversarialConfig,
AttackConverterConfig,
AttackScoringConfig,
resolve_adversarial_system_prompt,
)
from pyrit.executor.attack.multi_turn.multi_turn_attack_strategy import (
ConversationSession,
Expand Down Expand Up @@ -153,11 +155,9 @@ def __init__(
except ValueError as exc:
raise ValueError(f"RedTeamingAttack {exc}") from exc

system_prompt_template_path = (
attack_adversarial_config.system_prompt_path or RTASystemPromptPaths.TEXT_GENERATION.value
)
self._adversarial_chat_system_prompt_template = SeedPrompt.from_yaml_with_required_parameters(
template_path=system_prompt_template_path,
self._adversarial_chat_system_prompt_template = resolve_adversarial_system_prompt(
config=attack_adversarial_config,
default_system_prompt_path=RTASystemPromptPaths.TEXT_GENERATION.value,
required_parameters=["objective"],
error_message="Adversarial seed prompt must have an objective",
)
Expand Down Expand Up @@ -188,6 +188,23 @@ def get_attack_scoring_config(self) -> AttackScoringConfig | None:
use_score_as_feedback=self._use_score_as_feedback,
)

def get_attack_adversarial_config(self) -> AttackAdversarialConfig | None:
"""
Get the effective adversarial configuration used by this strategy.

Returns:
AttackAdversarialConfig | None: The adversarial target with its resolved system prompt
and first-message seed prompt.
"""
adversarial_chat = getattr(self, "_adversarial_chat", None)
if adversarial_chat is None:
return None
return AttackAdversarialConfig(
target=adversarial_chat,
system_prompt=self._adversarial_chat_system_prompt_template,
seed_prompt=self._adversarial_chat_seed_prompt,
)

def _validate_context(self, *, context: MultiTurnAttackContext[Any]) -> None:
"""
Validate the context before executing the attack.
Expand Down Expand Up @@ -613,16 +630,19 @@ async def _score_response_async(self, *, context: MultiTurnAttackContext[Any]) -
objective_scores = scoring_results
return objective_scores[0] if objective_scores else None

def _set_adversarial_chat_seed_prompt(self, *, seed_prompt: str | SeedPrompt) -> None:
def _set_adversarial_chat_seed_prompt(self, *, seed_prompt: str | SeedPrompt | None) -> None:
"""
Set the seed prompt for the adversarial chat.

Args:
seed_prompt (str | SeedPrompt): The seed prompt to set for the adversarial chat.
seed_prompt (str | SeedPrompt | None): The seed prompt to set for the adversarial chat.
When None, the default seed prompt is used.

Raises:
ValueError: If the seed prompt is not a string or SeedPrompt object.
ValueError: If the seed prompt is not a string, SeedPrompt object, or None.
"""
if seed_prompt is None:
seed_prompt = DEFAULT_ADVERSARIAL_SEED_PROMPT
if isinstance(seed_prompt, str):
self._adversarial_chat_seed_prompt = SeedPrompt(value=seed_prompt, data_type="text", is_jinja_template=True)
elif isinstance(seed_prompt, SeedPrompt):
Expand Down
8 changes: 6 additions & 2 deletions pyrit/executor/attack/multi_turn/simulated_conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,14 @@ async def generate_simulated_conversation_async(
simulated_target_system_prompt_path=simulated_target_system_prompt_path,
)

# Create adversarial config for the simulation
# Create adversarial config for the simulation. Load the optional system prompt path into a
# SeedPrompt so we use the inline ``system_prompt`` field (``system_prompt_path`` is deprecated).
adversarial_system_prompt = (
SeedPrompt.from_yaml_file(adversarial_chat_system_prompt_path) if adversarial_chat_system_prompt_path else None
)
adversarial_config = AttackAdversarialConfig(
target=adversarial_chat,
system_prompt_path=adversarial_chat_system_prompt_path,
system_prompt=adversarial_system_prompt,
)

# Create scoring config
Expand Down
Loading
Loading