Skip to content

Commit c8fb10c

Browse files
romanlutzCopilot
andauthored
FEAT Overlay harm-category standardization across dataset loaders (microsoft#2158)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3b7af75 commit c8fb10c

88 files changed

Lines changed: 3669 additions & 414 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.

pyrit/datasets/seed_datasets/remote/aegis_ai_content_safety_dataset.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_RemoteDatasetLoader,
1111
)
1212
from pyrit.models import Modality, SeedDataset, SeedPrompt, SeedUnion
13+
from pyrit.models.harm_category import HarmCategory
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -142,6 +143,49 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
142143
cache=cache,
143144
)
144145

146+
# Map AEGIS-specific categories to PyRIT harm categories
147+
alias_overrides: dict[str, list[HarmCategory]] = {
148+
"Controlled/Regulated Substances": [HarmCategory.DRUG_USE, HarmCategory.REGULATED_GOODS],
149+
"Copyright/Trademark/Plagiarism": [
150+
HarmCategory.COPYRIGHT,
151+
HarmCategory.TRADEMARK,
152+
HarmCategory.PLAGIARISM,
153+
],
154+
"Criminal Planning/Confessions": [HarmCategory.COORDINATION_HARM],
155+
"Fraud/Deception": [HarmCategory.SCAMS, HarmCategory.DECEPTION],
156+
"Guns and Illegal Weapons": [
157+
HarmCategory.REGULATED_GOODS,
158+
HarmCategory.COORDINATION_HARM,
159+
HarmCategory.VIOLENT_CONTENT,
160+
],
161+
"Hate/Identity Hate": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
162+
"High Risk Gov Decision Making": [HarmCategory.HIGH_RISK_GOVERNMENT],
163+
"Illegal Activity": [HarmCategory.COORDINATION_HARM],
164+
"Immoral/Unethical": [HarmCategory.OTHER],
165+
"Manipulation": [HarmCategory.DECEPTION],
166+
"Needs Caution": [HarmCategory.OTHER],
167+
"PII/Privacy": [HarmCategory.PPI],
168+
"Political/Misinformation/Conspiracy": [
169+
HarmCategory.INFO_INTEGRITY,
170+
HarmCategory.CURRENT_EVENTS_MISINFO,
171+
HarmCategory.CAMPAIGNING,
172+
],
173+
"Sexual": [HarmCategory.SEXUAL_CONTENT],
174+
"Sexual (minor)": [HarmCategory.SEXUALIZATION, HarmCategory.CHILD_LEAKAGE, HarmCategory.SEXUAL_CONTENT],
175+
"Suicide and Self Harm": [HarmCategory.SUICIDE, HarmCategory.SELF_HARM],
176+
"Threat": [HarmCategory.VIOLENT_THREATS],
177+
"Unauthorized Advice": [
178+
HarmCategory.FINANCIAL_ADVICE,
179+
HarmCategory.HEALTH_DIAGNOSIS,
180+
HarmCategory.LEGAL_ADVICE,
181+
],
182+
"Violence": [
183+
HarmCategory.VIOLENT_CONTENT,
184+
HarmCategory.VIOLENT_THREATS,
185+
HarmCategory.COORDINATION_HARM,
186+
],
187+
}
188+
145189
seed_prompts: list[SeedUnion] = []
146190

147191
for split_name in hf_dataset:
@@ -162,6 +206,10 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
162206
if violated_categories
163207
else []
164208
)
209+
standardized_categories = self._standardize_harm_categories(
210+
prompt_harm_categories,
211+
alias_overrides=alias_overrides,
212+
)
165213

166214
# Filter by harm_categories if specified
167215
if self._selected_category_values is not None and not any(
@@ -174,7 +222,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
174222
value=prompt_value,
175223
data_type="text",
176224
dataset_name=self.dataset_name,
177-
harm_categories=prompt_harm_categories if prompt_harm_categories else None,
225+
harm_categories=standardized_categories if standardized_categories else None,
178226
source=self.source,
179227
authors=self._AUTHORS,
180228
groups=self._GROUPS,
@@ -184,6 +232,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
184232
"response_label": example.get("response_label"),
185233
"prompt_label_source": example.get("prompt_label_source"),
186234
"response_label_source": example.get("response_label_source"),
235+
"aegis_violated_categories": ", ".join(prompt_harm_categories),
187236
},
188237
)
189238
)

pyrit/datasets/seed_datasets/remote/agent_threat_rules_dataset.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,23 @@ class _AgentThreatRulesDataset(_RemoteDatasetLoader):
111111
upstream metadata fields (``original_rule_id``, ``technique``,
112112
``detection_field``, ``variation_type``) are preserved on
113113
``SeedPrompt.metadata`` so downstream consumers can route, filter, or
114-
score by them. ``harm_categories`` is set to the rule's ATR category
115-
(single-element list).
114+
score by them. ATR categories are agent-security technique labels rather
115+
than content-harm labels, so ``harm_categories`` is intentionally empty.
116116
117117
The optional ``categories``, ``techniques``, ``detection_fields``, and
118118
``variation_types`` arguments narrow the dataset client-side after fetch.
119119
Passing an empty list is rejected — pass ``None`` to disable a filter.
120120
"""
121121

122-
# Class-attribute metadata picked up by SeedDatasetMetadata. Derived from
123-
# _RULE_ID_TO_CATEGORY so the categories the loader claims to cover always
124-
# match the categories it actually produces — same single-source-of-truth
125-
# rationale as the enum-typed dict above.
126-
harm_categories: list[str] = sorted({c.value for c in _RULE_ID_TO_CATEGORY.values()})
122+
# ATR categories are agent-security technique labels, not content-harm
123+
# labels, so this loader intentionally leaves harm categories empty.
124+
harm_categories: list[str] = []
127125
modalities: list[str] = ["text"]
128126
size: str = "large" # 1,054 seeds
127+
# ATR's upstream corpus grows over time, but this loader pins to a specific commit
128+
# by default (see ``source`` below) so a default fetch returns a static, reproducible
129+
# snapshot. It is therefore intentionally NOT tagged as a "feed"; unpin the source
130+
# (pass ``main`` or a newer ref) to track upstream additions.
129131
tags: set[str] = {"safety", "agent_security", "prompt_injection"}
130132

131133
def __init__(
@@ -275,6 +277,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
275277
"detection_field": example["detection_field"],
276278
"variation_type": example["variation_type"],
277279
"atr_id": example["id"],
280+
"atr_category": category_value,
278281
}
279282

280283
# Per-rule description so downstream consumers reading metadata see
@@ -291,7 +294,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
291294
data_type="text",
292295
name=rule_id,
293296
dataset_name=self.dataset_name,
294-
harm_categories=[category_value],
297+
harm_categories=[],
295298
description=description,
296299
authors=authors,
297300
groups=groups,

pyrit/datasets/seed_datasets/remote/aya_redteaming_dataset.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
_RemoteDatasetLoader,
1212
)
1313
from pyrit.models import Modality, SeedDataset, SeedPrompt
14+
from pyrit.models.harm_category import HarmCategory
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -123,10 +124,30 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
123124
cache=cache,
124125
)
125126

126-
seed_prompts = []
127+
harm_category_alias_overrides: dict[str, list[HarmCategory]] = {
128+
"bullying & harassment": [HarmCategory.HARASSMENT],
129+
"discrimination & injustice": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
130+
"graphic material": [HarmCategory.VIOLENT_CONTENT],
131+
"harms of representation allocation and quality of service": [
132+
HarmCategory.REPRESENTATIONAL,
133+
HarmCategory.ALLOCATION,
134+
HarmCategory.QUALITY_OF_SERVICE,
135+
],
136+
"non-consensual sexual content": [HarmCategory.SEXUAL_CONTENT, HarmCategory.NONCONSENSUAL_UPLOAD],
137+
"violence, threats & incitement": [
138+
HarmCategory.VIOLENT_CONTENT,
139+
HarmCategory.VIOLENT_THREATS,
140+
HarmCategory.COORDINATION_HARM,
141+
],
142+
}
143+
seed_prompts: list[SeedPrompt] = []
127144

128145
for example in examples:
129146
categories = ast.literal_eval(example["harm_category"])
147+
standardized_categories = self._standardize_harm_categories(
148+
categories,
149+
alias_overrides=harm_category_alias_overrides,
150+
)
130151

131152
# Apply filters
132153
if self.harm_categories_filter is not None and not any(
@@ -142,7 +163,11 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
142163
value=example["prompt"],
143164
data_type="text",
144165
dataset_name=self.dataset_name,
145-
harm_categories=categories,
166+
harm_categories=standardized_categories if standardized_categories else None,
167+
metadata={
168+
"aya_redteaming_categories": ", ".join(categories),
169+
"aya_redteaming_scope": example["global_or_local"],
170+
},
146171
source="https://huggingface.co/datasets/CohereForAI/aya_redteaming",
147172
authors=self._AUTHORS,
148173
groups=self._GROUPS,

pyrit/datasets/seed_datasets/remote/babelscape_alert_dataset.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_RemoteDatasetLoader,
1111
)
1212
from pyrit.models import Modality, SeedDataset, SeedPrompt
13+
from pyrit.models.harm_category import HarmCategory
1314

1415
if TYPE_CHECKING:
1516
from pyrit.models.seeds.seed_group import SeedUnion
@@ -28,6 +29,41 @@ class _BabelscapeAlertDataset(_RemoteDatasetLoader):
2829
Reference: [@tedeschi2024alert]
2930
"""
3031

32+
HARM_CATEGORY_ALIAS_OVERRIDES: dict[str, list[HarmCategory]] = {
33+
"crime_cyber": [HarmCategory.MALWARE, HarmCategory.COORDINATION_HARM],
34+
"crime_injury": [HarmCategory.VIOLENT_CONTENT],
35+
"crime_kidnapp": [HarmCategory.VIOLENT_THREATS, HarmCategory.COORDINATION_HARM],
36+
"crime_other": [HarmCategory.COORDINATION_HARM],
37+
"crime_privacy": [HarmCategory.PPI],
38+
"crime_propaganda": [HarmCategory.INFO_INTEGRITY],
39+
"crime_tax": [HarmCategory.COORDINATION_HARM],
40+
"crime_theft": [HarmCategory.COORDINATION_HARM],
41+
"hate_body": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
42+
"hate_disabled": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
43+
"hate_ethnic": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
44+
"hate_lgbtq+": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
45+
"hate_other": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
46+
"hate_poor": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
47+
"hate_religion": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
48+
"hate_women": [HarmCategory.HATE_SPEECH, HarmCategory.REPRESENTATIONAL],
49+
"self_harm_other": [HarmCategory.SELF_HARM],
50+
"self_harm_suicide": [HarmCategory.SUICIDE, HarmCategory.SELF_HARM],
51+
"self_harm_thin": [HarmCategory.EATING_DISORDERS, HarmCategory.SELF_HARM],
52+
"sex_harrasment": [HarmCategory.HARASSMENT, HarmCategory.SEXUAL_SOLICITATION],
53+
"sex_other": [HarmCategory.SEXUAL_CONTENT],
54+
"sex_porn": [HarmCategory.SEXUAL_CONTENT],
55+
"substance_alcohol": [HarmCategory.DRUG_USE, HarmCategory.REGULATED_GOODS],
56+
"substance_cannabis": [HarmCategory.DRUG_USE, HarmCategory.REGULATED_GOODS],
57+
"substance_drug": [HarmCategory.DRUG_USE],
58+
"substance_other": [HarmCategory.DRUG_USE],
59+
"substance_tobacco": [HarmCategory.DRUG_USE, HarmCategory.REGULATED_GOODS],
60+
"weapon_biological": [HarmCategory.CBRN],
61+
"weapon_chemical": [HarmCategory.CBRN],
62+
"weapon_firearm": [HarmCategory.REGULATED_GOODS],
63+
"weapon_other": [HarmCategory.REGULATED_GOODS],
64+
"weapon_radioactive": [HarmCategory.CBRN],
65+
}
66+
3167
_AUTHORS = [
3268
"Simone Tedeschi",
3369
"Felix Friedrich",
@@ -112,7 +148,10 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
112148
seed_prompts: list[SeedUnion] = [
113149
SeedPrompt(
114150
value=prompt,
115-
harm_categories=[category],
151+
harm_categories=self._standardize_harm_categories(
152+
category,
153+
alias_overrides=self.HARM_CATEGORY_ALIAS_OVERRIDES,
154+
),
116155
data_type="text",
117156
dataset_name=self.dataset_name,
118157
description=(
@@ -121,6 +160,7 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
121160
"red teaming prompts."
122161
),
123162
source=f"https://huggingface.co/datasets/{self.source}",
163+
metadata={"category": category},
124164
authors=self._AUTHORS,
125165
groups=self._GROUPS,
126166
)

pyrit/datasets/seed_datasets/remote/beaver_tails_dataset.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT license.
33

4+
import json
45
import logging
56

67
from pyrit.datasets.seed_datasets.remote.remote_dataset_loader import (
78
_RemoteDatasetLoader,
89
)
910
from pyrit.models import Modality, SeedDataset, SeedPrompt
11+
from pyrit.models.harm_category import HarmCategory
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -30,6 +32,34 @@ class _BeaverTailsDataset(_RemoteDatasetLoader):
3032
"""
3133

3234
HF_DATASET_NAME: str = "PKU-Alignment/BeaverTails"
35+
HARM_CATEGORY_ALIAS_OVERRIDES: dict[str, list[HarmCategory]] = {
36+
"aiding_and_abetting": [HarmCategory.COORDINATION_HARM],
37+
"animal_abuse": [HarmCategory.OTHER],
38+
"banned_substance": [HarmCategory.DRUG_USE, HarmCategory.REGULATED_GOODS],
39+
"child_abuse": [HarmCategory.GROOMING, HarmCategory.SEXUAL_CONTENT, HarmCategory.CHILD_LEAKAGE],
40+
"controversial_topics": [HarmCategory.OTHER],
41+
"discrimination": [HarmCategory.REPRESENTATIONAL, HarmCategory.HATE_SPEECH],
42+
"drug_abuse": [HarmCategory.DRUG_USE],
43+
"financial_crime": [HarmCategory.SCAMS, HarmCategory.COORDINATION_HARM],
44+
"hate_speech": [HarmCategory.HATE_SPEECH],
45+
"incitement": [HarmCategory.VIOLENT_THREATS],
46+
"injustice": [HarmCategory.REPRESENTATIONAL, HarmCategory.HATE_SPEECH],
47+
"laws_and_safety": [HarmCategory.INFO_INTEGRITY],
48+
"misinformation_regarding_ethics": [HarmCategory.INFO_INTEGRITY],
49+
"non_violent_unethical_behavior": [HarmCategory.OTHER],
50+
"offensive_language": [HarmCategory.HATE_SPEECH],
51+
"organized_crime": [HarmCategory.COORDINATION_HARM],
52+
"politics": [HarmCategory.OTHER],
53+
"privacy_violation": [HarmCategory.PPI],
54+
"property_crime": [HarmCategory.COORDINATION_HARM],
55+
"self_harm": [HarmCategory.SELF_HARM],
56+
"sexually_explicit": [HarmCategory.SEXUAL_CONTENT],
57+
"stereotype": [HarmCategory.REPRESENTATIONAL, HarmCategory.HATE_SPEECH],
58+
"terrorism": [HarmCategory.VIOLENT_EXTREMISM],
59+
"theft": [HarmCategory.COORDINATION_HARM],
60+
"violence": [HarmCategory.VIOLENT_CONTENT, HarmCategory.VIOLENT_THREATS, HarmCategory.COORDINATION_HARM],
61+
"weapons": [HarmCategory.REGULATED_GOODS],
62+
}
3363

3464
# Metadata
3565
modalities: tuple[Modality, ...] = (Modality.TEXT,)
@@ -102,7 +132,13 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
102132
if self.unsafe_only and item["is_safe"]:
103133
continue
104134

105-
harm_categories = [k for k, v in item["category"].items() if v]
135+
raw_harm_categories = [
136+
part.strip() for k, v in item["category"].items() if v for part in k.split(",") if part.strip()
137+
]
138+
harm_categories = self._standardize_harm_categories(
139+
raw_harm_categories,
140+
alias_overrides=self.HARM_CATEGORY_ALIAS_OVERRIDES,
141+
)
106142

107143
seed_prompts.append(
108144
SeedPrompt(
@@ -114,6 +150,10 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
114150
source=source_url,
115151
authors=authors,
116152
groups=groups,
153+
metadata={
154+
"beaver_tails_categories": ",".join(raw_harm_categories),
155+
"beaver_tails_category_flags": json.dumps(item["category"], sort_keys=True),
156+
},
117157
)
118158
)
119159

pyrit/datasets/seed_datasets/remote/categorical_harmful_qa_dataset.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
_RemoteDatasetLoader,
99
)
1010
from pyrit.models import SeedDataset, SeedObjective, SeedUnion
11+
from pyrit.models.harm_category import HarmCategory
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -107,13 +108,55 @@ async def fetch_dataset_async(self, *, cache: bool = True) -> SeedDataset:
107108
)
108109
source_url = f"https://huggingface.co/datasets/{self.HF_DATASET_NAME}"
109110
groups = ["DeCLaRe Lab, Singapore University of Technology and Design"]
111+
harm_category_alias_overrides: dict[str, list[HarmCategory]] = {
112+
"adult content": [HarmCategory.SEXUAL_CONTENT],
113+
"child abuse": [HarmCategory.GROOMING, HarmCategory.SEXUAL_CONTENT, HarmCategory.CHILD_LEAKAGE],
114+
"economic harm": [HarmCategory.SCAMS, HarmCategory.DECEPTION],
115+
"fraud/deception": [HarmCategory.SCAMS, HarmCategory.DECEPTION],
116+
"hate/harass/violence": [
117+
HarmCategory.HATE_SPEECH,
118+
HarmCategory.HARASSMENT,
119+
HarmCategory.VIOLENT_CONTENT,
120+
],
121+
"illegal activity": [HarmCategory.COORDINATION_HARM],
122+
"malware viruses": [HarmCategory.MALWARE],
123+
"physical harm": [HarmCategory.VIOLENT_CONTENT, HarmCategory.COORDINATION_HARM],
124+
"political campaigning": [HarmCategory.CAMPAIGNING],
125+
"privacy violation activity": [HarmCategory.PPI],
126+
"tailored financial advice": [HarmCategory.FINANCIAL_ADVICE],
127+
}
128+
# "Child Abuse" spans both sexual and non-sexual sub-harms, so it is mapped at
129+
# the subcategory level; otherwise non-sexual abuse (physical, emotional,
130+
# neglect) would be incorrectly tagged with CSAM-related categories. Unknown
131+
# subcategories fall back to the category-level mapping above.
132+
child_abuse_subcategory_overrides: dict[str, list[HarmCategory]] = {
133+
"content sexual exploitation": [
134+
HarmCategory.GROOMING,
135+
HarmCategory.SEXUAL_CONTENT,
136+
HarmCategory.CHILD_LEAKAGE,
137+
],
138+
"online predation": [HarmCategory.GROOMING, HarmCategory.SEXUAL_CONTENT, HarmCategory.CHILD_LEAKAGE],
139+
"physical abuse": [HarmCategory.VIOLENT_CONTENT, HarmCategory.COORDINATION_HARM],
140+
"emotional and psychological abuse": [HarmCategory.HARASSMENT],
141+
"neglect": [HarmCategory.DANGEROUS_SITUATIONS],
142+
}
143+
144+
def resolve_harm_categories(category: str | None, subcategory: str | None) -> list[str]:
145+
if (
146+
category is not None
147+
and category.strip().lower() == "child abuse"
148+
and subcategory is not None
149+
and subcategory.strip().lower() in child_abuse_subcategory_overrides
150+
):
151+
return self._standardize_harm_categories(subcategory, alias_overrides=child_abuse_subcategory_overrides)
152+
return self._standardize_harm_categories(category, alias_overrides=harm_category_alias_overrides)
110153

111154
seed_objectives: list[SeedUnion] = [
112155
SeedObjective(
113156
value=item["Question"],
114157
name="CategoricalHarmfulQA",
115158
dataset_name=self.dataset_name,
116-
harm_categories=[item["Category"]] if item.get("Category") else [],
159+
harm_categories=resolve_harm_categories(item.get("Category"), item.get("Subcategory")),
117160
description=description,
118161
source=source_url,
119162
authors=authors,

0 commit comments

Comments
 (0)