Skip to content

Commit d5106c7

Browse files
authored
Merge pull request #409 from AISecurityLab/351-group-intents-by-categories
351 group intents by categories
2 parents fedf9cc + 2e25969 commit d5106c7

17 files changed

Lines changed: 9163 additions & 12 deletions

File tree

docs/docs/datasets/index.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Instead of manually specifying `goals`, use the `dataset` parameter to load goal
1313
- 🎯 **Presets** — 30+ ready-to-use AI safety benchmarks (AgentHarm, JailbreakBench, BeaverTails, etc.)
1414
- 🤗 **HuggingFace Hub** — Any public or private dataset from HuggingFace
1515
- 📁 **Local files** — JSON, JSONL, CSV, or TXT files from your filesystem
16+
- 🧭 **Intent taxonomy selection** — Pick OmniSafeBench categories/subcategories with `intents`
1617

1718
```mermaid
1819
graph LR
@@ -129,6 +130,32 @@ attack_config = {
129130
results = agent.hack(attack_config=attack_config)
130131
```
131132

133+
### 4. Selecting Intent Categories (OmniSafeBench)
134+
135+
When you want category-balanced goals without manually writing prompts, use
136+
`intents` to select categories and subcategories directly from the
137+
OmniSafeBench taxonomy.
138+
139+
```python
140+
attack_config = {
141+
"attack_type": "h4rm3l",
142+
"intents": [
143+
{
144+
"category": "A",
145+
"subcategories": ["A1", "A2"],
146+
"samples_per_subcategory": 2,
147+
}
148+
],
149+
}
150+
```
151+
152+
HackAgent maps this to canonical labels in results/dashboard format:
153+
`A. Ethical and Social Risks` / `A1. Bias and Discrimination`.
154+
155+
Taxonomy source: [OmniSafeBench-MM](https://github.com/jiaxiaojunQAQ/OmniSafeBench-MM/).
156+
157+
[See full guide: Selecting intent categories →](./selecting-intent-categories.md)
158+
132159
---
133160

134161
## Common Dataset Options
@@ -172,6 +199,7 @@ When both `shuffle` and `offset` are used, shuffling happens **first**, then off
172199
## Next Steps
173200

174201
- 📖 [**Datasets Tutorial**](../getting-started/datasets-tutorial.mdx) — Complete walkthrough with examples
202+
- 🧭 [**Selecting intent categories**](./selecting-intent-categories.md) — Use taxonomy categories/subcategories with strings, enums, or label codes
175203
- 🎯 [**Presets**](./presets.md) — All 30+ pre-configured benchmarks
176204
- 🤗 [**HuggingFace Provider**](./huggingface.md) — Load any HuggingFace dataset
177205
- 📁 [**File Provider**](./file.md) — Load from local JSON, CSV, or TXT files

docs/docs/datasets/selecting-intent-categories.md

Lines changed: 201 additions & 0 deletions
Large diffs are not rendered by default.

docs/docs/getting-started/datasets-tutorial.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ sidebar_position: 2
77
This quick-start tutorial covers only the basics you need to start using datasets in HackAgent.
88
Presets are pre-configured benchmark datasets. They are the fastest way to run standardized evaluations.
99

10+
If you want to select goals by risk taxonomy (OmniSafeBench) instead of full datasets,
11+
you can use `intents` with categories/subcategories. See
12+
[Selecting intent categories](../datasets/selecting-intent-categories) for details.
13+
1014
### Basic CLI Example
1115

1216
```bash

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const sidebars: SidebarsConfig = {
7171
id: 'datasets/index',
7272
},
7373
items: [
74+
'datasets/selecting-intent-categories',
7475
'datasets/presets',
7576
'datasets/huggingface',
7677
'datasets/file',

hackagent/attacks/orchestrator.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,19 +195,33 @@ def _prepare_attack_params(self, attack_config: Dict[str, Any]) -> Dict[str, Any
195195
# Check for direct goals first
196196
goals = attack_config.get("goals")
197197
dataset_config = attack_config.get("dataset")
198+
intents_config = attack_config.get("intents")
199+
goal_labels_by_index: Optional[Dict[int, Dict[str, str]]] = None
198200

199201
if goals is not None and dataset_config is not None:
200202
logger.warning(
201203
"Both 'goals' and 'dataset' provided. Using 'goals' directly."
202204
)
203205
dataset_config = None
206+
if goals is not None and intents_config is not None:
207+
logger.warning(
208+
"Both 'goals' and 'intents' provided. Using 'goals' directly."
209+
)
210+
intents_config = None
211+
212+
if intents_config is not None and dataset_config is not None:
213+
logger.warning("Both 'intents' and 'dataset' provided. Using 'intents'.")
214+
dataset_config = None
204215

205-
if dataset_config is not None:
216+
if intents_config is not None:
217+
goals, goal_labels_by_index = self._load_goals_from_intents(intents_config)
218+
elif dataset_config is not None:
206219
# Load goals from dataset source
207220
goals = self._load_goals_from_dataset(dataset_config)
208221
elif goals is None:
209222
raise ValueError(
210-
f"'{self.attack_type}' requires either 'goals' (list) or 'dataset' (config)"
223+
f"'{self.attack_type}' requires either 'goals' (list), "
224+
"'dataset' (config), or 'intents' (config)"
211225
)
212226

213227
if not isinstance(goals, list):
@@ -217,7 +231,10 @@ def _prepare_attack_params(self, attack_config: Dict[str, Any]) -> Dict[str, Any
217231
raise ValueError(f"'goals' list is empty for {self.attack_type}")
218232

219233
logger.info(f"Prepared {len(goals)} goals for {self.attack_type} attack")
220-
return {"goals": goals}
234+
params: Dict[str, Any] = {"goals": goals}
235+
if goal_labels_by_index:
236+
params["_goal_labels_by_index"] = goal_labels_by_index
237+
return params
221238

222239
@staticmethod
223240
def _uses_default_category_classifier(attack_config: Dict[str, Any]) -> bool:
@@ -354,6 +371,26 @@ def _load_goals_from_dataset(self, dataset_config: Dict[str, Any]) -> list:
354371
logger.error(f"Failed to load goals from dataset: {e}", exc_info=True)
355372
raise ValueError(f"Failed to load goals from dataset: {e}") from e
356373

374+
def _load_goals_from_intents(
375+
self, intents_config: Any
376+
) -> Tuple[List[str], Dict[int, Dict[str, str]]]:
377+
"""Load goals from intent taxonomy labels and sample selectors."""
378+
from hackagent.datasets.intents import load_goals_from_intents_config
379+
380+
logger.info("Loading goals from intents taxonomy config")
381+
382+
try:
383+
goals, goal_labels_by_index = load_goals_from_intents_config(intents_config)
384+
logger.info(
385+
"Loaded %s goals from intents across %s labeled entries",
386+
len(goals),
387+
len(goal_labels_by_index),
388+
)
389+
return goals, goal_labels_by_index
390+
except Exception as e:
391+
logger.error(f"Failed to load goals from intents: {e}", exc_info=True)
392+
raise ValueError(f"Failed to load goals from intents: {e}") from e
393+
357394
def _get_attack_impl_kwargs(
358395
self,
359396
attack_config: Dict[str, Any],
@@ -664,9 +701,16 @@ def execute(
664701
"""
665702
# 1. Validate parameters
666703
attack_params = self._prepare_attack_params(attack_config)
704+
goal_labels_by_index = attack_params.pop("_goal_labels_by_index", None)
667705

668706
# Fail-fast preflight before creating Attack/Run DB records.
669-
self._validate_default_category_classifier_requirements(attack_config)
707+
# Skip this when intents already provide explicit category labels.
708+
if goal_labels_by_index:
709+
logger.info(
710+
"Using explicit intents taxonomy labels: category classifier preflight skipped"
711+
)
712+
else:
713+
self._validate_default_category_classifier_requirements(attack_config)
670714

671715
# Enrich run config with expected goal cardinality so downstream views
672716
# can keep RUNNING until all expected goals are fully tracked.
@@ -710,6 +754,13 @@ def execute(
710754
except Exception as e:
711755
logger.warning(f"Failed to update run status to RUNNING: {e}")
712756

757+
if goal_labels_by_index:
758+
attack_config = {
759+
**attack_config,
760+
"_goal_labels_by_index": goal_labels_by_index,
761+
"_disable_goal_category_classifier": True,
762+
}
763+
713764
# Make the event bus available to the technique impl and to the
714765
# tracker via the shared config bag (alongside _run_id / _backend).
715766
if _tui_event_bus is not None:

hackagent/attacks/techniques/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ def _initialize_coordinator(
243243
logger=self.logger,
244244
attack_type=attack_type,
245245
category_classifier_config=self.config.get("category_classifier"),
246+
preclassified_goal_labels_by_index=self.config.get("_goal_labels_by_index"),
247+
disable_goal_category_classifier=bool(
248+
self.config.get("_disable_goal_category_classifier")
249+
),
246250
goals=goals,
247251
initial_metadata=initial_metadata,
248252
goal_index_start=goal_index_start,

hackagent/attacks/techniques/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ class GoalsDatasetConfig(BaseModel):
165165

166166
goals: List[str] = Field(default_factory=list)
167167
dataset: Optional[Union[str, Dict[str, Any]]] = None
168+
intents: Optional[Union[List[Dict[str, Any]], Dict[str, Any]]] = None
168169

169170

170171
class RunConfig(BaseModel):

hackagent/datasets/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
"""
3333

3434
from hackagent.datasets.base import DatasetProvider
35+
from hackagent.datasets.intents import (
36+
IntentCategory,
37+
IntentSubcategory,
38+
load_goals_from_intents_config,
39+
)
3540
from hackagent.datasets.presets import PRESETS, get_preset, list_presets
3641
from hackagent.datasets.registry import (
3742
get_provider,
@@ -42,9 +47,12 @@
4247

4348
__all__ = [
4449
"DatasetProvider",
50+
"IntentCategory",
51+
"IntentSubcategory",
4552
"PRESETS",
4653
"get_preset",
4754
"get_provider",
55+
"load_goals_from_intents_config",
4856
"list_presets",
4957
"load_goals",
5058
"load_goals_from_config",

0 commit comments

Comments
 (0)