Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions docs/docs/attacks/pair.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,18 @@ graph TD

### Per-Iteration Cost

Each PAIR iteration performs **3 sequential model calls**:
Each PAIR iteration performs **3 model calls per stream**:

1. **Attacker** call (generate/refine prompt)
2. **Target** call (execute candidate jailbreak)
3. **Judge** call (score response)

So expected wall time is approximately:
With `n_streams` independent conversations, the query budget per goal is
approximately `n_iterations * n_streams`. `batch_size` controls the number of
stream workers: `1` is serial, while a value above `1` executes up to that many
streams concurrently. Expected wall time is approximately:

`iterations * (t_attacker + t_target + t_judge)`
`iterations * ceil(streams / batch_size) * (t_attacker + t_target + t_judge)`

If any provider call is slow, iteration latency increases accordingly.

Expand Down Expand Up @@ -140,7 +143,10 @@ advanced_config = {
},

# Attack parameters
"n_iterations": 20, # Max iterations per goal
"n_iterations": 20, # Max refinement rounds per goal
"n_streams": 5, # Independent attacker conversations per goal
"keep_last_n": 4, # Latest attacker/feedback turn pairs retained per stream
"target_str": "Sure, here is", # Expected prefix of a successful response
"objective": "jailbreak", # Evaluation objective
"jailbreak_threshold": 8, # Stop goal as soon as score >= threshold
"early_stop_on_success": True,
Expand All @@ -165,6 +171,9 @@ advanced_config = {
| Parameter | Description | Default | Range |
|-----------|-------------|---------|-------|
| `n_iterations` | Maximum iterations per goal | 20 | 1-100 |
| `n_streams` | Independent PAIR attacker conversations explored per goal | 5 | 1+ |
| `keep_last_n` | Latest attacker/feedback turn pairs retained in each stream history | 4 | 1+ |
| `target_str` | Expected prefix of a successful target response | `"Sure, here is"` | String |
| `temperature` | Attacker creativity (higher = more varied) | 1.0 | 0.0-2.0 |
| `max_tokens` | Max tokens for attacker response | 500 | 100-2000 |
| `objective` | Evaluation criteria | `"jailbreak"` | See objectives |
Expand Down
15 changes: 11 additions & 4 deletions hackagent/attacks/shared/prompt_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from typing import Dict, Optional


def extract_prompt_and_improvement(content: str) -> Optional[Dict[str, str]]:
def extract_prompt_and_improvement(
content: str, *, allow_plaintext: bool = True
) -> Optional[Dict[str, str]]:
"""
Extract a prompt (+ optional improvement) from attacker output.

Expand Down Expand Up @@ -57,15 +59,20 @@ def extract_prompt_and_improvement(content: str) -> Optional[Dict[str, str]]:
improvement = _unescape_text(improvement_value)
return {"prompt": prompt, "improvement": improvement}

if not raw.startswith("{") and not raw.startswith("[") and len(raw) > 20:
if (
allow_plaintext
and not raw.startswith("{")
and not raw.startswith("[")
and len(raw) > 20
):
return {"prompt": raw, "improvement": ""}

return None


def extract_prompt(content: str) -> Optional[str]:
def extract_prompt(content: str, *, allow_plaintext: bool = True) -> Optional[str]:
"""Extract just the prompt string from attacker output."""
parsed = extract_prompt_and_improvement(content)
parsed = extract_prompt_and_improvement(content, allow_plaintext=allow_plaintext)
if not parsed:
return None
return parsed.get("prompt") or None
Expand Down
Loading