Skip to content

Commit f916d8e

Browse files
richlundeenCopilot
andauthored
[BREAKING] FEAT: Make scenario dataset sources and limits explicit (#2956)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 480fc7f commit f916d8e

96 files changed

Lines changed: 2498 additions & 839 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/instructions/scenarios.instructions.md‎

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class MyScenario(Scenario):
2828
super().__init__(
2929
version=self.VERSION,
3030
technique_class=MyTechnique,
31-
default_dataset_config=DatasetConfiguration(dataset_names=["my_dataset"]),
31+
default_dataset_config=DatasetAttackConfiguration(sources=[DatasetSource(name="my_dataset")]),
3232
objective_scorer=objective_scorer or self._get_default_objective_scorer(),
3333
scenario_result_id=scenario_result_id,
3434
)
@@ -68,7 +68,7 @@ def __init__(
6868
super().__init__(
6969
version=self.VERSION,
7070
technique_class=MyTechnique,
71-
default_dataset_config=DatasetConfiguration(dataset_names=["my_dataset"]),
71+
default_dataset_config=DatasetAttackConfiguration(sources=[DatasetSource(name="my_dataset")]),
7272
objective_scorer=objective_scorer,
7373
)
7474
```
@@ -113,30 +113,64 @@ Dropping a common input is not silent: `set_params_from_args` rejects any value
113113

114114
## Dataset Loading
115115

116-
Datasets are read from `CentralMemory`.
116+
Datasets are read from `CentralMemory`. New runs call `prepare_async()` before
117+
reading. Reads, discovery, estimates, and resume must never fetch or store datasets.
118+
Resolve parameter-dependent source names before preparation, not during seed reads.
117119

118120
### Basic — named datasets:
119121
```python
120-
DatasetConfiguration(
121-
dataset_names=["airt_hate", "airt_violence"],
122-
max_dataset_size=10, # optional: sample up to N per dataset
122+
DatasetAttackConfiguration(
123+
sources=[
124+
DatasetSource(name="harmbench"),
125+
DatasetSource(name="airt_hate", max_size=10),
126+
DatasetSource(name="team_objectives", max_size="all"),
127+
],
128+
max_per_dataset=5,
129+
max_total=12,
123130
)
124131
```
125132

133+
With filtered populations of 100, 40, and 7 groups, selection has two steps:
134+
135+
| Dataset | Full population | After source cap |
136+
| --- | --- | --- |
137+
| `harmbench` | 100 | 5 (inherits `max_per_dataset`) |
138+
| `airt_hate` | 40 | 10 (explicit `max_size`) |
139+
| `team_objectives` | 7 | 7 (`"all"` removes the source cap) |
140+
141+
The total cap then samples 12 groups from these 22 groups. The split can be 3, 6,
142+
and 3, for example; it is not balanced or guaranteed to cover every dataset.
143+
A dataset with no selected groups keeps its key with an empty list.
144+
126145
### Advanced — custom subclass for filtering:
127146
```python
128-
class MyDatasetConfiguration(DatasetConfiguration):
129-
def get_seed_groups(self) -> dict[str, list[SeedGroup]]:
130-
result = super().get_seed_groups()
131-
# Filter by selected techniques via self._scenario_techniques
132-
return filtered_result
147+
class MyDatasetConfiguration(DatasetAttackConfiguration):
148+
def _build_attack_groups(self, seeds: list[Seed]) -> list[AttackSeedGroup]:
149+
return build_custom_groups(seeds)
133150
```
134151

135152
Options:
136-
- `dataset_names` — load by name from memory
137-
- `seed_groups` — pass explicit groups (mutually exclusive with `dataset_names`)
138-
- `max_dataset_size` — cap per dataset
139-
- Override `_load_seed_groups_for_dataset()` for custom loading
153+
- `sources` selects datasets by name. Source `max_size` overrides `max_per_dataset`;
154+
omitted, `None`, empty, and `"default"` mean inherit; `"all"` means no cap.
155+
- All dataset limits use the same rule: omitted, `None`, empty, and `"default"` use the
156+
default; `"all"` removes that limit; a positive integer sets a cap.
157+
- `max_per_dataset=5` is the named-objective default. `max_total="all"` leaves the
158+
combined selection uncapped. Apply source limits before the total limit.
159+
- `fetch=DatasetFetchPolicy.IF_MISSING` prepares absent registered datasets.
160+
`NEVER` requires stored data. A filter miss must never fetch.
161+
- `seed_groups` and `seeds` are inline alternatives. They never use memory or providers.
162+
- Validators run on full filtered populations before any sampling.
163+
- Ingredients must remain complete. Use `max_per_dataset="all"` and no finite source
164+
`max_size`; cap the assembled attack groups with `max_total`, not ingredient rows.
165+
- Use `with_overrides()` instead of reconstructing a subclass or mutating its defaults.
166+
- Keep custom shaping in `_build_attack_groups()` or `_build_groups_by_dataset_async()`.
167+
Reads use `_collect_seeds_for_dataset_async()` and must not fetch.
168+
- `dataset_names`, `max_dataset_size`, `auto_fetch`, and `per_dataset()` are deprecated.
169+
170+
`max_dataset_size` is an exact alias for `max_total`; it does not disable source caps.
171+
To keep the old total-only selection of up to 10 groups, use
172+
`max_per_dataset="all", max_total=10`. To remove both caps, set both to `"all"`;
173+
`None` now uses the default, not unlimited selection.
140174

141175
## Technique Enum
142176

‎doc/code/datasets/6_generated_datasets.ipynb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,11 @@
240240
],
241241
"source": [
242242
"from pyrit.output import output_scenario_async, output_scenario_attacks_async\n",
243-
"from pyrit.scenario import DatasetAttackConfiguration\n",
243+
"from pyrit.scenario import DatasetAttackConfiguration, DatasetFetchPolicy, DatasetSource\n",
244244
"from pyrit.scenario.airt import RapidResponse, RapidResponseTechnique\n",
245245
"from pyrit.score import SelfAskTrueFalseScorer\n",
246246
"\n",
247-
"dataset_config = DatasetAttackConfiguration(dataset_names=[dataset_name], auto_fetch=False)\n",
247+
"dataset_config = DatasetAttackConfiguration(sources=[DatasetSource(name=dataset_name)], fetch=DatasetFetchPolicy.NEVER)\n",
248248
"scenario = RapidResponse(objective_scorer=SelfAskTrueFalseScorer(chat_target=OpenAIChatTarget()))\n",
249249
"scenario.set_params_from_args(\n",
250250
" args={\n",

‎doc/code/datasets/6_generated_datasets.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@
7676

7777
# %%
7878
from pyrit.output import output_scenario_async, output_scenario_attacks_async
79-
from pyrit.scenario import DatasetAttackConfiguration
79+
from pyrit.scenario import DatasetAttackConfiguration, DatasetFetchPolicy, DatasetSource
8080
from pyrit.scenario.airt import RapidResponse, RapidResponseTechnique
8181
from pyrit.score import SelfAskTrueFalseScorer
8282

83-
dataset_config = DatasetAttackConfiguration(dataset_names=[dataset_name], auto_fetch=False)
83+
dataset_config = DatasetAttackConfiguration(sources=[DatasetSource(name=dataset_name)], fetch=DatasetFetchPolicy.NEVER)
8484
scenario = RapidResponse(objective_scorer=SelfAskTrueFalseScorer(chat_target=OpenAIChatTarget()))
8585
scenario.set_params_from_args(
8686
args={

‎doc/code/scenarios/0_scenarios.ipynb‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@
6565
" Matrix-shaped scenarios delegate to `build_matrix_atomic_attacks(context=...)` in one line.\n",
6666
"\n",
6767
"3. **Default Dataset**: Pass `default_dataset_config=` to `super().__init__()` to specify the datasets your scenario uses out of the box.\n",
68-
" - Returns a `DatasetConfiguration` with one or more named datasets (e.g., `DatasetConfiguration(dataset_names=[\"my_dataset\"])`)\n",
68+
" - Returns a `DatasetAttackConfiguration` with named sources (e.g., `sources=[DatasetSource(name=\"my_dataset\")]`)\n",
6969
" - Users can override this at runtime via `--dataset-names` in the CLI or by passing a custom `dataset_config` programmatically\n",
70-
" - `DatasetAttackConfiguration` selects at most 5 attack groups unless you set `max_dataset_size`; `max_dataset_size=None` uses all groups\n",
70+
" - Named sources select at most 5 attack groups per dataset by default; `max_total` caps the union. For any limit, omitted/`None`/empty/`\"default\"` uses the default; `\"all\"` removes that limit.\n",
71+
" - The deprecated `max_dataset_size` is an exact alias for `max_total`, not a way to disable source caps. To keep the old total-only selection, use `max_per_dataset=\"all\", max_total=10`; to remove both caps, set both to `\"all\"`. `None` now uses the default, not unlimited selection.\n",
72+
" - New runs prepare missing registered datasets once; reads, estimates, and resume never fetch\n",
7173
"\n",
7274
"4. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:\n",
7375
" - `name`: Descriptive name for your scenario\n",
@@ -113,7 +115,8 @@
113115
"source": [
114116
"from pyrit.common import apply_defaults\n",
115117
"from pyrit.scenario import (\n",
116-
" DatasetConfiguration,\n",
118+
" DatasetAttackConfiguration,\n",
119+
" DatasetSource,\n",
117120
" Scenario,\n",
118121
" ScenarioTechnique,\n",
119122
")\n",
@@ -159,7 +162,9 @@
159162
" version=self.VERSION,\n",
160163
" objective_scorer=self._objective_scorer,\n",
161164
" technique_class=MyTechnique,\n",
162-
" default_dataset_config=DatasetConfiguration(dataset_names=[\"dataset_name\"], max_dataset_size=4),\n",
165+
" default_dataset_config=DatasetAttackConfiguration(\n",
166+
" sources=[DatasetSource(name=\"dataset_name\")], max_total=4\n",
167+
" ),\n",
163168
" scenario_result_id=scenario_result_id,\n",
164169
" )\n",
165170
"\n",

‎doc/code/scenarios/0_scenarios.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@
6767
# Matrix-shaped scenarios delegate to `build_matrix_atomic_attacks(context=...)` in one line.
6868
#
6969
# 3. **Default Dataset**: Pass `default_dataset_config=` to `super().__init__()` to specify the datasets your scenario uses out of the box.
70-
# - Returns a `DatasetConfiguration` with one or more named datasets (e.g., `DatasetConfiguration(dataset_names=["my_dataset"])`)
70+
# - Returns a `DatasetAttackConfiguration` with named sources (e.g., `sources=[DatasetSource(name="my_dataset")]`)
7171
# - Users can override this at runtime via `--dataset-names` in the CLI or by passing a custom `dataset_config` programmatically
72-
# - `DatasetAttackConfiguration` selects at most 5 attack groups unless you set `max_dataset_size`; `max_dataset_size=None` uses all groups
72+
# - Named sources select at most 5 attack groups per dataset by default; `max_total` caps the union. For any limit, omitted/`None`/empty/`"default"` uses the default; `"all"` removes that limit.
73+
# - The deprecated `max_dataset_size` is an exact alias for `max_total`, not a way to disable source caps. To keep the old total-only selection, use `max_per_dataset="all", max_total=10`; to remove both caps, set both to `"all"`. `None` now uses the default, not unlimited selection.
74+
# - New runs prepare missing registered datasets once; reads, estimates, and resume never fetch
7375
#
7476
# 4. **Constructor**: Use `@apply_defaults` decorator and call `super().__init__()` with scenario metadata:
7577
# - `name`: Descriptive name for your scenario
@@ -97,7 +99,8 @@
9799
# %%
98100
from pyrit.common import apply_defaults
99101
from pyrit.scenario import (
100-
DatasetConfiguration,
102+
DatasetAttackConfiguration,
103+
DatasetSource,
101104
Scenario,
102105
ScenarioTechnique,
103106
)
@@ -143,7 +146,9 @@ def __init__(
143146
version=self.VERSION,
144147
objective_scorer=self._objective_scorer,
145148
technique_class=MyTechnique,
146-
default_dataset_config=DatasetConfiguration(dataset_names=["dataset_name"], max_dataset_size=4),
149+
default_dataset_config=DatasetAttackConfiguration(
150+
sources=[DatasetSource(name="dataset_name")], max_total=4
151+
),
147152
scenario_result_id=scenario_result_id,
148153
)
149154

‎doc/code/scenarios/1_common_scenario_parameters.ipynb‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"## Dataset Configuration\n",
7676
"\n",
7777
"`DatasetAttackConfiguration` controls which prompts (objectives) are sent to the target.\n",
78-
"The simplest approach uses `dataset_names` to load datasets by name from memory.\n",
78+
"Use `sources` to select named datasets from memory.\n",
7979
"By default, `RedTeamAgent` loads four random objectives from HarmBench [@mazeika2024harmbench]."
8080
]
8181
},
@@ -86,9 +86,9 @@
8686
"metadata": {},
8787
"outputs": [],
8888
"source": [
89-
"from pyrit.scenario import DatasetAttackConfiguration\n",
89+
"from pyrit.scenario import DatasetAttackConfiguration, DatasetSource\n",
9090
"\n",
91-
"dataset_config = DatasetAttackConfiguration(dataset_names=[\"harmbench\"], max_dataset_size=2)"
91+
"dataset_config = DatasetAttackConfiguration(sources=[DatasetSource(name=\"harmbench\")], max_total=2)"
9292
]
9393
},
9494
{
@@ -129,8 +129,8 @@
129129
" if all(seed.value.isascii() and seed.value.isprintable() for seed in group.seeds)\n",
130130
"]\n",
131131
"\n",
132-
"# Pass explicit seed_groups instead of dataset_names\n",
133-
"dataset_config = DatasetAttackConfiguration(seed_groups=seed_groups, max_dataset_size=2)"
132+
"# Pass explicit seed_groups instead of named sources\n",
133+
"dataset_config = DatasetAttackConfiguration(seed_groups=seed_groups, max_total=2)"
134134
]
135135
},
136136
{

‎doc/code/scenarios/1_common_scenario_parameters.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@
4242
# ## Dataset Configuration
4343
#
4444
# `DatasetAttackConfiguration` controls which prompts (objectives) are sent to the target.
45-
# The simplest approach uses `dataset_names` to load datasets by name from memory.
45+
# Use `sources` to select named datasets from memory.
4646
# By default, `RedTeamAgent` loads four random objectives from HarmBench [@mazeika2024harmbench].
4747

4848
# %%
49-
from pyrit.scenario import DatasetAttackConfiguration
49+
from pyrit.scenario import DatasetAttackConfiguration, DatasetSource
5050

51-
dataset_config = DatasetAttackConfiguration(dataset_names=["harmbench"], max_dataset_size=2)
51+
dataset_config = DatasetAttackConfiguration(sources=[DatasetSource(name="harmbench")], max_total=2)
5252

5353
# %% [markdown]
5454
# For more control, use `SeedDatasetProvider` to fetch datasets and pass explicit `seed_groups`.
@@ -69,8 +69,8 @@
6969
if all(seed.value.isascii() and seed.value.isprintable() for seed in group.seeds)
7070
]
7171

72-
# Pass explicit seed_groups instead of dataset_names
73-
dataset_config = DatasetAttackConfiguration(seed_groups=seed_groups, max_dataset_size=2)
72+
# Pass explicit seed_groups instead of named sources
73+
dataset_config = DatasetAttackConfiguration(seed_groups=seed_groups, max_total=2)
7474

7575
# %% [markdown]
7676
# ## Technique Selection and Composition

‎doc/code/scenarios/3_adaptive_scenarios.ipynb‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
"\n",
114114
"from pyrit.output.scenario_result.pretty import PrettyScenarioResultMemoryPrinter\n",
115115
"from pyrit.registry import TargetRegistry\n",
116-
"from pyrit.scenario import DatasetAttackConfiguration\n",
116+
"from pyrit.scenario import DatasetAttackConfiguration, DatasetSource\n",
117117
"from pyrit.scenario.scenarios.adaptive import TextAdaptive\n",
118118
"from pyrit.setup import initialize_from_config_async\n",
119119
"\n",
@@ -452,8 +452,8 @@
452452
" \"objective_target\": objective_target,\n",
453453
" \"scenario_techniques\": [technique_class(\"single_turn\")],\n",
454454
" \"dataset_config\": DatasetAttackConfiguration(\n",
455-
" dataset_names=[\"airt_hate\", \"airt_violence\"],\n",
456-
" max_dataset_size=4,\n",
455+
" sources=[DatasetSource(name=\"airt_hate\"), DatasetSource(name=\"airt_violence\")],\n",
456+
" max_total=4,\n",
457457
" ),\n",
458458
" }\n",
459459
")\n",
@@ -577,8 +577,8 @@
577577
" \"objective_target\": objective_target,\n",
578578
" \"scenario_techniques\": [technique_class(\"single_turn\")],\n",
579579
" \"dataset_config\": DatasetAttackConfiguration(\n",
580-
" dataset_names=[\"airt_hate\", \"airt_violence\"],\n",
581-
" max_dataset_size=4,\n",
580+
" sources=[DatasetSource(name=\"airt_hate\"), DatasetSource(name=\"airt_violence\")],\n",
581+
" max_total=4,\n",
582582
" ),\n",
583583
" }\n",
584584
")\n",

‎doc/code/scenarios/3_adaptive_scenarios.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848

4949
from pyrit.output.scenario_result.pretty import PrettyScenarioResultMemoryPrinter
5050
from pyrit.registry import TargetRegistry
51-
from pyrit.scenario import DatasetAttackConfiguration
51+
from pyrit.scenario import DatasetAttackConfiguration, DatasetSource
5252
from pyrit.scenario.scenarios.adaptive import TextAdaptive
5353
from pyrit.setup import initialize_from_config_async
5454

@@ -102,8 +102,8 @@
102102
"objective_target": objective_target,
103103
"scenario_techniques": [technique_class("single_turn")],
104104
"dataset_config": DatasetAttackConfiguration(
105-
dataset_names=["airt_hate", "airt_violence"],
106-
max_dataset_size=4,
105+
sources=[DatasetSource(name="airt_hate"), DatasetSource(name="airt_violence")],
106+
max_total=4,
107107
),
108108
}
109109
)
@@ -132,8 +132,8 @@
132132
"objective_target": objective_target,
133133
"scenario_techniques": [technique_class("single_turn")],
134134
"dataset_config": DatasetAttackConfiguration(
135-
dataset_names=["airt_hate", "airt_violence"],
136-
max_dataset_size=4,
135+
sources=[DatasetSource(name="airt_hate"), DatasetSource(name="airt_violence")],
136+
max_total=4,
137137
),
138138
}
139139
)

‎doc/scanner/1_pyrit_scan.ipynb‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,7 +1287,7 @@
12871287
"\n",
12881288
"from pyrit.common import apply_defaults\n",
12891289
"from pyrit.prompt_target.openai.openai_chat_target import OpenAIChatTarget\n",
1290-
"from pyrit.scenario import DatasetAttackConfiguration, Scenario, ScenarioTechnique\n",
1290+
"from pyrit.scenario import DatasetAttackConfiguration, DatasetSource, Scenario, ScenarioTechnique\n",
12911291
"from pyrit.score import SelfAskRefusalScorer, TrueFalseInverterScorer\n",
12921292
"from pyrit.setup import initialize_pyrit_async\n",
12931293
"\n",
@@ -1311,8 +1311,8 @@
13111311
" version=1,\n",
13121312
" objective_scorer=TrueFalseInverterScorer(scorer=SelfAskRefusalScorer(chat_target=OpenAIChatTarget())),\n",
13131313
" technique_class=MyCustomTechnique,\n",
1314-
" # DatasetAttackConfiguration selects at most 5 attack groups by default; set max_dataset_size to change it.\n",
1315-
" default_dataset_config=DatasetAttackConfiguration(dataset_names=[\"harmbench\"]),\n",
1314+
" # Named sources default to 5 groups each; use max_per_dataset and max_total to set caps.\n",
1315+
" default_dataset_config=DatasetAttackConfiguration(sources=[DatasetSource(name=\"harmbench\")]),\n",
13161316
" scenario_result_id=scenario_result_id,\n",
13171317
" )\n",
13181318
" # ... your scenario-specific initialization code\n",

0 commit comments

Comments
 (0)