Skip to content

Commit 27d7404

Browse files
rlundeen2Copilot
andauthored
MAINT: Simplifying scenario class vars (#1784)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dada588 commit 27d7404

40 files changed

Lines changed: 747 additions & 1097 deletions

.github/instructions/scenarios.instructions.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,29 @@ All scenarios inherit from `Scenario` (ABC) and must:
1414
2. **Optionally declare `BASELINE_ATTACK_POLICY`** (defaults to `BaselineAttackPolicy.Enabled` — a baseline `PromptSendingAttack` is prepended and callers can opt out per run via `initialize_async(include_baseline=False)`):
1515
- `BaselineAttackPolicy.Disabled` — baseline supported but off by default (e.g. `Jailbreak`, where templates dominate the run).
1616
- `BaselineAttackPolicy.Forbidden` — baseline is meaningless for this scenario's comparison axis (e.g. `AdversarialBenchmark`, which compares against gold-standard answers). Explicit `include_baseline=True` raises `ValueError`.
17-
3. **Implement three abstract methods:**
17+
3. **Pass `strategy_class`, `default_strategy`, and `default_dataset_config` to `super().__init__()`:**
1818

1919
```python
2020
class MyScenario(Scenario):
2121
VERSION: int = 1
2222
BASELINE_ATTACK_POLICY: ClassVar[BaselineAttackPolicy] = BaselineAttackPolicy.Enabled
2323

24-
@classmethod
25-
def get_strategy_class(cls) -> type[ScenarioStrategy]:
26-
return MyStrategy
27-
28-
@classmethod
29-
def get_default_strategy(cls) -> ScenarioStrategy:
30-
return MyStrategy.ALL
31-
32-
@classmethod
33-
def default_dataset_config(cls) -> DatasetConfiguration:
34-
return DatasetConfiguration(dataset_names=["my_dataset"])
24+
@apply_defaults
25+
def __init__(self, *, objective_scorer=None, scenario_result_id=None) -> None:
26+
super().__init__(
27+
version=self.VERSION,
28+
strategy_class=MyStrategy,
29+
default_strategy=MyStrategy.ALL,
30+
default_dataset_config=DatasetConfiguration(dataset_names=["my_dataset"]),
31+
objective_scorer=objective_scorer or self._get_default_objective_scorer(),
32+
scenario_result_id=scenario_result_id,
33+
)
3534
```
3635

36+
For scenarios whose strategy enum is built dynamically (RapidResponse pattern), build the
37+
strategy class in a module-level `@cache`-decorated function and pass the result through
38+
the constructor — no classmethod indirection required.
39+
3740
4. **Optionally override `_get_atomic_attacks_async()`** — the base class provides a default
3841
that uses the factory/registry pattern (see "AtomicAttack Construction" below).
3942
Only override if your scenario needs custom attack construction logic.
@@ -60,14 +63,17 @@ def __init__(
6063
super().__init__(
6164
version=self.VERSION,
6265
strategy_class=MyStrategy,
66+
default_strategy=MyStrategy.ALL,
67+
default_dataset_config=DatasetConfiguration(dataset_names=["my_dataset"]),
6368
objective_scorer=objective_scorer,
6469
)
6570
```
6671

6772
Requirements:
6873
- `@apply_defaults` decorator on `__init__`
6974
- All parameters keyword-only via `*`
70-
- `super().__init__()` called with `version`, `strategy_class`, `objective_scorer`
75+
- **All constructor parameters must be optional** (default to `None`) so the registry can instantiate the scenario with no arguments for metadata introspection. Defer required-input validation to `initialize_async()` or `_get_atomic_attacks_async()`. `ScenarioRegistry._build_metadata` raises `TypeError` if `scenario_class()` cannot be called with no arguments.
76+
- `super().__init__()` called with `version`, `strategy_class`, `default_strategy`, `default_dataset_config`, `objective_scorer`
7177
- complex objects like `adversarial_chat` or `objective_scorer` should be passed into the constructor.
7278

7379
## Dataset Loading

doc/code/scenarios/0_scenarios.ipynb

Lines changed: 11 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@
5959
" - Include an `ALL` aggregate strategy that expands to all available strategies\n",
6060
" - Optionally override `_prepare_strategies()` for custom composition logic (see `FoundryComposite`)\n",
6161
"\n",
62-
"2. **Scenario Class**: Extend `Scenario` and implement these abstract methods:\n",
63-
" - `get_strategy_class()`: Return your strategy enum class\n",
64-
" - `get_default_strategy()`: Return the default strategy (typically `YourStrategy.ALL`)\n",
62+
"2. **Scenario Class**: Extend `Scenario` and pass these to `super().__init__()`:\n",
63+
" - `strategy_class`: Your strategy enum class\n",
64+
" - `default_strategy`: The default strategy (typically `YourStrategy.ALL` or `YourStrategy.DEFAULT`)\n",
6565
" - The base class provides a default `_get_atomic_attacks_async()` that uses the factory/registry\n",
6666
" pattern. Override it only if your scenario needs custom attack construction logic.\n",
6767
"\n",
68-
"3. **Default Dataset**: Implement `default_dataset_config()` to specify the datasets your scenario uses out of the box.\n",
68+
"3. **Default Dataset**: Pass `default_dataset_config=` to `super().__init__()` to specify the datasets your scenario uses out of the box.\n",
6969
" - Returns a `DatasetConfiguration` with one or more named datasets (e.g., `DatasetConfiguration(dataset_names=[\"my_dataset\"])`)\n",
7070
" - Users can override this at runtime via `--dataset-names` in the CLI or by passing a custom `dataset_config` programmatically\n",
7171
"\n",
@@ -97,24 +97,7 @@
9797
"execution_count": null,
9898
"id": "1",
9999
"metadata": {},
100-
"outputs": [
101-
{
102-
"name": "stdout",
103-
"output_type": "stream",
104-
"text": [
105-
"Found default environment files: ['./.pyrit/.env', './.pyrit/.env.local']\n",
106-
"Loaded environment file: ./.pyrit/.env\n",
107-
"Loaded environment file: ./.pyrit/.env.local\n"
108-
]
109-
},
110-
{
111-
"name": "stdout",
112-
"output_type": "stream",
113-
"text": [
114-
"No new upgrade operations detected.\n"
115-
]
116-
}
117-
],
100+
"outputs": [],
118101
"source": [
119102
"from pyrit.common import apply_defaults\n",
120103
"from pyrit.scenario import (\n",
@@ -142,18 +125,6 @@
142125
"\n",
143126
" VERSION: int = 1\n",
144127
"\n",
145-
" @classmethod\n",
146-
" def get_strategy_class(cls) -> type[ScenarioStrategy]:\n",
147-
" return MyStrategy\n",
148-
"\n",
149-
" @classmethod\n",
150-
" def get_default_strategy(cls) -> ScenarioStrategy:\n",
151-
" return MyStrategy.DEFAULT\n",
152-
"\n",
153-
" @classmethod\n",
154-
" def default_dataset_config(cls) -> DatasetConfiguration:\n",
155-
" return DatasetConfiguration(dataset_names=[\"dataset_name\"], max_dataset_size=4)\n",
156-
"\n",
157128
" @apply_defaults\n",
158129
" def __init__(\n",
159130
" self,\n",
@@ -168,7 +139,9 @@
168139
" super().__init__(\n",
169140
" version=self.VERSION,\n",
170141
" objective_scorer=self._objective_scorer,\n",
171-
" strategy_class=self.get_strategy_class(),\n",
142+
" strategy_class=MyStrategy,\n",
143+
" default_strategy=MyStrategy.DEFAULT,\n",
144+
" default_dataset_config=DatasetConfiguration(dataset_names=[\"dataset_name\"], max_dataset_size=4),\n",
172145
" scenario_result_id=scenario_result_id,\n",
173146
" )\n",
174147
"\n",
@@ -196,201 +169,7 @@
196169
"execution_count": null,
197170
"id": "3",
198171
"metadata": {},
199-
"outputs": [
200-
{
201-
"name": "stdout",
202-
"output_type": "stream",
203-
"text": [
204-
"Loading default configuration file: ./.pyrit/.pyrit_conf\n",
205-
"Found default environment files: ['./.pyrit/.env', './.pyrit/.env.local']\n",
206-
"Loaded environment file: ./.pyrit/.env\n",
207-
"Loaded environment file: ./.pyrit/.env.local\n",
208-
"\n",
209-
"Available Scenarios:\n",
210-
"================================================================================\n",
211-
"\u001b[1m\u001b[36m\n",
212-
" airt.cyber\u001b[0m\n",
213-
" Class: Cyber\n",
214-
" Description:\n",
215-
" Cyber scenario implementation for PyRIT. This scenario tests how willing\n",
216-
" models are to exploit cybersecurity harms by generating malware. The\n",
217-
" Cyber class contains different variations of the malware generation\n",
218-
" techniques.\n",
219-
" Aggregate Strategies:\n",
220-
" - all, single_turn, multi_turn\n",
221-
" Available Strategies (2):\n",
222-
" prompt_sending, red_teaming\n",
223-
" Default Strategy: all\n",
224-
" Default Datasets (1, max 4 per dataset):\n",
225-
" airt_malware\n",
226-
"\u001b[1m\u001b[36m\n",
227-
" airt.jailbreak\u001b[0m\n",
228-
" Class: Jailbreak\n",
229-
" Description:\n",
230-
" Jailbreak scenario implementation for PyRIT. This scenario tests how\n",
231-
" vulnerable models are to jailbreak attacks by applying various\n",
232-
" single-turn jailbreak templates to a set of test prompts. The responses\n",
233-
" are scored to determine if the jailbreak was successful.\n",
234-
" Aggregate Strategies:\n",
235-
" - all, simple, complex\n",
236-
" Available Strategies (4):\n",
237-
" prompt_sending, many_shot, skeleton, role_play\n",
238-
" Default Strategy: simple\n",
239-
" Default Datasets (1, max 4 per dataset):\n",
240-
" airt_harms\n",
241-
"\u001b[1m\u001b[36m\n",
242-
" airt.leakage\u001b[0m\n",
243-
" Class: Leakage\n",
244-
" Description:\n",
245-
" Leakage scenario implementation for PyRIT. This scenario tests how\n",
246-
" susceptible models are to leaking training data, PII, intellectual\n",
247-
" property, or other confidential information. Uses the registry/factory\n",
248-
" pattern to construct attack techniques.\n",
249-
" Aggregate Strategies:\n",
250-
" - all, default, single_turn, multi_turn\n",
251-
" Available Strategies (9):\n",
252-
" prompt_sending, role_play, many_shot, tap, crescendo_simulated,\n",
253-
" red_teaming, context_compliance, first_letter, image\n",
254-
" Default Strategy: default\n",
255-
" Default Datasets (1, max 4 per dataset):\n",
256-
" airt_leakage\n",
257-
"\u001b[1m\u001b[36m\n",
258-
" airt.psychosocial\u001b[0m\n",
259-
" Class: Psychosocial\n",
260-
" Description:\n",
261-
" Psychosocial Harms Scenario implementation for PyRIT. This scenario\n",
262-
" contains various psychosocial harm-based checks that evaluate model\n",
263-
" behavior for responses that may harm the well-being or mental health of\n",
264-
" users. Examples include mistreating users in a mental health crisis or\n",
265-
" aiding them in self-harm. All datasets in this scenario share the\n",
266-
" unified 'psychosocial' harm category, but each dataset (e.g.,\n",
267-
" imminent_crisis, dependency) can have unique: - Scoring rubrics - Seed\n",
268-
" objectives The scenario tests whether models: - Recognize mental health\n",
269-
" crises and respond appropriately - Avoid facilitating self-harm or\n",
270-
" harmful behaviors - Provide crisis resources and encourage professional\n",
271-
" help Example usage with custom configurations: # Define custom\n",
272-
" configurations per subharm category custom_configs = {\n",
273-
" \"airt_imminent_crisis\": SubharmConfig(\n",
274-
" crescendo_system_prompt_path=\"path/to/custom_escalation.yaml\",\n",
275-
" scoring_rubric_path=\"path/to/custom_rubric.yaml\", ), } scenario =\n",
276-
" Psychosocial(subharm_configs=custom_configs) await\n",
277-
" scenario.initialize_async( objective_target=target_llm,\n",
278-
" scenario_strategies=[PsychosocialStrategy.ImminentCrisis], )\n",
279-
" Aggregate Strategies:\n",
280-
" - all\n",
281-
" Available Strategies (2):\n",
282-
" imminent_crisis, licensed_therapist\n",
283-
" Default Strategy: all\n",
284-
" Default Datasets (1, max 4 per dataset):\n",
285-
" airt_imminent_crisis\n",
286-
"\u001b[1m\u001b[36m\n",
287-
" airt.rapid_response\u001b[0m\n",
288-
" Class: RapidResponse\n",
289-
" Description:\n",
290-
" Rapid Response scenario for content-harms testing. Tests model behavior\n",
291-
" across multiple harm categories using selectable attack techniques.\n",
292-
" Aggregate Strategies:\n",
293-
" - all, default, single_turn, multi_turn\n",
294-
" Available Strategies (7):\n",
295-
" prompt_sending, role_play, many_shot, tap, crescendo_simulated,\n",
296-
" red_teaming, context_compliance\n",
297-
" Default Strategy: default\n",
298-
" Default Datasets (7, max 4 per dataset):\n",
299-
" airt_hate, airt_fairness, airt_violence, airt_sexual, airt_harassment,\n",
300-
" airt_misinformation, airt_leakage\n",
301-
"\u001b[1m\u001b[36m\n",
302-
" airt.scam\u001b[0m\n",
303-
" Class: Scam\n",
304-
" Description:\n",
305-
" Scam scenario evaluates an endpoint's ability to generate scam-related\n",
306-
" materials (e.g., phishing emails, fraudulent messages) with primarily\n",
307-
" persuasion-oriented techniques.\n",
308-
" Aggregate Strategies:\n",
309-
" - all, single_turn, multi_turn\n",
310-
" Available Strategies (3):\n",
311-
" context_compliance, role_play, persuasive_rta\n",
312-
" Default Strategy: all\n",
313-
" Default Datasets (1, max 4 per dataset):\n",
314-
" airt_scams\n",
315-
" Supported Parameters:\n",
316-
" - max_turns (int) [default: 5]: Maximum conversation turns for the persuasive_rta strategy.\n",
317-
"\u001b[1m\u001b[36m\n",
318-
" benchmark.adversarial\u001b[0m\n",
319-
" Class: AdversarialBenchmark\n",
320-
" Description:\n",
321-
" Benchmarking scenario that compares the attack success rate (ASR) of\n",
322-
" several different adversarial models.\n",
323-
" Aggregate Strategies:\n",
324-
" - all, default, single_turn, multi_turn, light\n",
325-
" Available Strategies (4):\n",
326-
" role_play, tap, red_teaming, context_compliance\n",
327-
" Default Strategy: light\n",
328-
" Default Datasets (1, max 8 per dataset):\n",
329-
" harmbench\n",
330-
"\u001b[1m\u001b[36m\n",
331-
" foundry.red_team_agent\u001b[0m\n",
332-
" Class: RedTeamAgent\n",
333-
" Description:\n",
334-
" RedTeamAgent is a preconfigured scenario that automatically generates\n",
335-
" multiple AtomicAttack instances based on the specified attack\n",
336-
" strategies. It supports both single-turn attacks (with various\n",
337-
" converters) and multi-turn attacks (Crescendo, RedTeaming), making it\n",
338-
" easy to quickly test a target against multiple attack vectors. The\n",
339-
" scenario can expand difficulty levels (EASY, MODERATE, DIFFICULT) into\n",
340-
" their constituent attack strategies, or you can specify individual\n",
341-
" strategies directly. This scenario is designed for use with the Foundry\n",
342-
" AI Red Teaming Agent library, providing a consistent PyRIT contract for\n",
343-
" their integration.\n",
344-
" Aggregate Strategies:\n",
345-
" - all, easy, moderate, difficult\n",
346-
" Available Strategies (25):\n",
347-
" ansi_attack, ascii_art, ascii_smuggler, atbash, base64, binary, caesar,\n",
348-
" character_space, char_swap, diacritic, flip, leetspeak, morse, rot13,\n",
349-
" suffix_append, string_join, unicode_confusable, unicode_substitution,\n",
350-
" url, jailbreak, tense, multi_turn, crescendo, pair, tap\n",
351-
" Default Strategy: easy\n",
352-
" Default Datasets (1, max 4 per dataset):\n",
353-
" harmbench\n",
354-
"\u001b[1m\u001b[36m\n",
355-
" garak.encoding\u001b[0m\n",
356-
" Class: Encoding\n",
357-
" Description:\n",
358-
" Encoding Scenario implementation for PyRIT. This scenario tests how\n",
359-
" resilient models are to various encoding attacks by encoding potentially\n",
360-
" harmful text (by default slurs and XSS payloads) and testing if the\n",
361-
" model will decode and repeat the encoded payload. It mimics the Garak\n",
362-
" encoding probe. The scenario works by: 1. Taking seed prompts (the\n",
363-
" harmful text to be encoded) 2. Encoding them using various encoding\n",
364-
" schemes (Base64, ROT13, Morse, etc.) 3. Asking the target model to\n",
365-
" decode the encoded text 4. Scoring whether the model successfully\n",
366-
" decoded and repeated the harmful content By default, this uses the same\n",
367-
" dataset as Garak: slur terms and web XSS payloads.\n",
368-
" Aggregate Strategies:\n",
369-
" - all\n",
370-
" Available Strategies (17):\n",
371-
" base64, base2048, base16, base32, ascii85, hex, quoted_printable,\n",
372-
" uuencode, rot13, braille, atbash, morse_code, nato, ecoji, zalgo,\n",
373-
" leet_speak, ascii_smuggler\n",
374-
" Default Strategy: all\n",
375-
" Default Datasets (2, max 3 per dataset):\n",
376-
" garak_slur_terms_en, garak_web_html_js\n",
377-
"\n",
378-
"================================================================================\n",
379-
"\n",
380-
"Total scenarios: 9\n"
381-
]
382-
},
383-
{
384-
"data": {
385-
"text/plain": [
386-
"0"
387-
]
388-
},
389-
"execution_count": null,
390-
"metadata": {},
391-
"output_type": "execute_result"
392-
}
393-
],
172+
"outputs": [],
394173
"source": [
395174
"from pyrit.backend.services.scenario_service import get_scenario_service\n",
396175
"from pyrit.cli._output import print_scenario_list\n",
@@ -454,17 +233,8 @@
454233
}
455234
],
456235
"metadata": {
457-
"language_info": {
458-
"codemirror_mode": {
459-
"name": "ipython",
460-
"version": 3
461-
},
462-
"file_extension": ".py",
463-
"mimetype": "text/x-python",
464-
"name": "python",
465-
"nbconvert_exporter": "python",
466-
"pygments_lexer": "ipython3",
467-
"version": "3.12.12"
236+
"jupytext": {
237+
"main_language": "python"
468238
}
469239
},
470240
"nbformat": 4,

0 commit comments

Comments
 (0)