Skip to content

Commit 17fd10c

Browse files
authored
FIX: Bug in RolePlayAttack (microsoft#1211)
1 parent afda988 commit 17fd10c

7 files changed

Lines changed: 555 additions & 249 deletions

File tree

doc/code/executor/attack/context_compliance_attack.ipynb

Lines changed: 116 additions & 59 deletions
Large diffs are not rendered by default.

doc/code/executor/attack/context_compliance_attack.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
# extension: .py
66
# format_name: percent
77
# format_version: '1.3'
8-
# jupytext_version: 1.17.3
8+
# jupytext_version: 1.18.1
9+
# kernelspec:
10+
# display_name: pyrit-dev
11+
# language: python
12+
# name: python3
913
# ---
1014

1115
# %% [markdown]
@@ -23,12 +27,12 @@
2327
#
2428
# This conversation is sent to the `objective_target`.
2529
#
26-
# Before you begin, ensure you have the correct version of PyRIT installed and have secrets configured as described [here](../../../index.md#Installation-Guide.
30+
# Before you begin, ensure you have the correct version of PyRIT installed and have secrets configured as described [here](../../../setup/1a_install_conda.md).
2731
#
2832
# The results and intermediate interactions will be saved to memory according to the environment settings. For details, see the [Memory Configuration Guide](../../memory/0_memory.md).
2933

3034
# %%
31-
35+
import os
3236

3337
from pyrit.executor.attack import (
3438
AttackAdversarialConfig,
@@ -38,26 +42,34 @@
3842
ConsoleAttackResultPrinter,
3943
ContextComplianceAttack,
4044
)
41-
from pyrit.prompt_converter import RandomCapitalLettersConverter
45+
from pyrit.prompt_converter import TranslationConverter
4246
from pyrit.prompt_normalizer import PromptConverterConfiguration
4347
from pyrit.prompt_target import OpenAIChatTarget
44-
from pyrit.score import AzureContentFilterScorer
48+
from pyrit.score import AzureContentFilterScorer, FloatScaleThresholdScorer
4549
from pyrit.setup import IN_MEMORY, initialize_pyrit
4650

4751
initialize_pyrit(memory_db_type=IN_MEMORY)
4852

4953
objective_target = OpenAIChatTarget()
50-
adversarial_chat = OpenAIChatTarget()
54+
adversarial_chat = OpenAIChatTarget(
55+
endpoint=os.environ["AZURE_OPENAI_GPT4O_UNSAFE_ENDPOINT"],
56+
api_key=os.environ["AZURE_OPENAI_GPT4O_UNSAFE_CHAT_KEY"],
57+
)
58+
59+
scoring_config = AttackScoringConfig(
60+
objective_scorer=FloatScaleThresholdScorer(scorer=AzureContentFilterScorer(), threshold=0.2),
61+
)
62+
63+
language_converter = TranslationConverter(converter_target=adversarial_chat, language="German")
5164

5265
attack = ContextComplianceAttack(
5366
objective_target=objective_target,
5467
attack_adversarial_config=AttackAdversarialConfig(target=adversarial_chat),
5568
attack_converter_config=AttackConverterConfig(
56-
request_converters=PromptConverterConfiguration.from_converters(
57-
converters=[RandomCapitalLettersConverter(percentage=50)]
58-
)
69+
request_converters=PromptConverterConfiguration.from_converters(converters=[language_converter]),
70+
response_converters=PromptConverterConfiguration.from_converters(converters=[language_converter]),
5971
),
60-
attack_scoring_config=AttackScoringConfig(auxiliary_scorers=[AzureContentFilterScorer()]),
72+
attack_scoring_config=scoring_config,
6173
)
6274

6375
objectives = [

doc/code/executor/attack/role_play_attack.ipynb

Lines changed: 205 additions & 160 deletions
Large diffs are not rendered by default.

doc/code/executor/attack/role_play_attack.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
# extension: .py
66
# format_name: percent
77
# format_version: '1.3'
8-
# jupytext_version: 1.17.3
8+
# jupytext_version: 1.18.1
9+
# kernelspec:
10+
# display_name: pyrit-dev
11+
# language: python
12+
# name: python3
913
# ---
1014

1115
# %% [markdown]
@@ -14,7 +18,7 @@
1418
# This attack prepends some prompts defined in `role_play_definition`, along with an `adversarial_chat` target LLM to generate the first turns to send. Typically these prompts describe a fictional scenario to attempt and elicit harmful responses.
1519
# Any converters that you provide will be applied to the prompt that has already been converted by the role play definition (using the provided `adversarial_chat` target). You may see better success if you provide a LLM that has no content moderation or other safety mechanisms. Otherwise, it may refuse to convert the prompt as expected.
1620
#
17-
# Before you begin, ensure you have the correct version of PyRIT installed and have secrets configured as described [here](../../../index.md#Installation-Guide.
21+
# Before you begin, ensure you have the correct version of PyRIT installed and have secrets configured as described [here](../../../setup/1a_install_conda.md).
1822
#
1923
# The results and intermediate interactions will be saved to memory according to the environment settings. For details, see the [Memory Configuration Guide](../../memory/0_memory.md).
2024

@@ -32,7 +36,7 @@
3236
from pyrit.prompt_converter import CharSwapConverter
3337
from pyrit.prompt_normalizer import PromptConverterConfiguration
3438
from pyrit.prompt_target import OpenAIChatTarget
35-
from pyrit.score import AzureContentFilterScorer
39+
from pyrit.score import AzureContentFilterScorer, FloatScaleThresholdScorer
3640
from pyrit.setup import IN_MEMORY, initialize_pyrit
3741

3842
initialize_pyrit(memory_db_type=IN_MEMORY)
@@ -47,7 +51,7 @@
4751
converter_config = AttackConverterConfig(request_converters=converters)
4852

4953
scoring_config = AttackScoringConfig(
50-
auxiliary_scorers=[AzureContentFilterScorer()],
54+
objective_scorer=FloatScaleThresholdScorer(scorer=AzureContentFilterScorer(), threshold=0.2),
5155
)
5256

5357
attack = RolePlayAttack(
@@ -67,4 +71,4 @@
6771
)
6872

6973
for result in results:
70-
await ConsoleAttackResultPrinter().print_conversation_async(result=result, include_auxiliary_scores=True) # type: ignore
74+
await ConsoleAttackResultPrinter().print_result_async(result=result, include_auxiliary_scores=True) # type: ignore

doc/code/scenarios/0_scenarios.ipynb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@
264264
}
265265
],
266266
"source": [
267-
"\n",
268267
"from pyrit.cli.frontend_core import FrontendCore, print_scenarios_list\n",
269268
"\n",
270269
"print_scenarios_list(context=FrontendCore())"

pyrit/executor/attack/single_turn/role_play.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from pyrit.models import (
1717
Message,
1818
SeedDataset,
19+
SeedGroup,
20+
SeedPrompt,
1921
)
2022
from pyrit.prompt_converter import LLMGenericTextConverter
2123
from pyrit.prompt_normalizer import PromptConverterConfiguration, PromptNormalizer
@@ -101,7 +103,7 @@ def __init__(
101103
self._parse_role_play_definition(role_play_definition)
102104

103105
# Create the rephrase converter configuration
104-
rephrase_converter = PromptConverterConfiguration.from_converters(
106+
self._rephrase_converter = PromptConverterConfiguration.from_converters(
105107
converters=[
106108
LLMGenericTextConverter(
107109
converter_target=self._adversarial_chat,
@@ -110,19 +112,25 @@ def __init__(
110112
]
111113
)
112114

113-
# Prepend the rephrase converter to existing request converters
114-
self._request_converters = rephrase_converter + self._request_converters
115-
116115
async def _setup_async(self, *, context: SingleTurnAttackContext) -> None:
117116
"""
118-
Sets up the attack by preparing conversation context with role-play start.
117+
Sets up the attack by preparing conversation context with role-play start
118+
and converting the objective to role-play format.
119119
120120
Args:
121121
context (SingleTurnAttackContext): The attack context containing attack parameters.
122122
"""
123-
# Get role-play conversation start
123+
# Get role-play conversation start (turns 0 and 1)
124124
context.prepended_conversation = await self._get_conversation_start() or []
125125

126+
# Rephrase the objective using the LLM converter
127+
# This converts the user's objective into a role-play scenario
128+
rephrased_objective = await self._rephrase_objective_async(objective=context.objective)
129+
130+
# Set the rephrased objective as the seed_group
131+
# This will be used by _get_prompt_group() to send the rephrased content to the target
132+
context.seed_group = SeedGroup(seeds=[SeedPrompt(value=rephrased_objective, data_type="text")])
133+
126134
# Call parent setup which handles conversation ID generation, memory labels, etc.
127135
await super()._setup_async(context=context)
128136

@@ -134,12 +142,35 @@ def _validate_context(self, *, context: SingleTurnAttackContext) -> None:
134142
context (SingleTurnAttackContext): The attack context containing parameters and objective.
135143
136144
Raises:
137-
ValueError: If the context is invalid.
145+
ValueError: If seed_group or prepended_conversation are provided by the user.
138146
"""
147+
if context.seed_group is not None:
148+
raise ValueError(
149+
"RolePlayAttack does not accept a seed_group parameter. "
150+
"The seed group is generated internally by rephrasing the objective."
151+
)
139152
if context.prepended_conversation:
140-
raise ValueError("RolePlayAttack does not support prepended conversations.")
153+
raise ValueError(
154+
"RolePlayAttack does not accept prepended_conversation parameter. "
155+
"The conversation start is generated internally from the role-play definition."
156+
)
141157
super()._validate_context(context=context)
142158

159+
async def _rephrase_objective_async(self, *, objective: str) -> str:
160+
"""
161+
Rephrase the objective into a role-play scenario using the adversarial chat.
162+
163+
Args:
164+
objective (str): The original objective to rephrase.
165+
166+
Returns:
167+
str: The rephrased objective in role-play format.
168+
"""
169+
# Use the LLMGenericTextConverter to rephrase the objective
170+
converter = self._rephrase_converter[0].converters[0]
171+
result = await converter.convert_async(prompt=objective, input_type="text")
172+
return result.output_text
173+
143174
async def _get_conversation_start(self) -> Optional[list[Message]]:
144175
"""
145176
Get the role-play conversation start messages.

0 commit comments

Comments
 (0)