Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
0acd48a
new .env_example with adversrial target models.
May 20, 2026
6881d19
.env_example formatting
May 20, 2026
1f1a07b
More .env_example formatting
May 20, 2026
81d529d
Reorganized unit test directory
May 20, 2026
6290bbe
Renaming unit tests for consistency
May 20, 2026
42887b9
FIX: TargetInitializer propagates config.tags to registry entries
May 21, 2026
b7af0de
FEAT: Add TargetRegistry.get_by_tag_query for TagQuery-based lookup
May 21, 2026
190ee4a
FEAT: Register ADVERSARIAL_CHAT singleturn/multiturn/reasoning variants
May 21, 2026
c1476bf
FEAT: Add BenchmarkInitializer for adversarial-target fan-out
May 21, 2026
9ec8234
Merge remote-tracking branch 'origin/main' into adversarial_benchmark…
May 21, 2026
d652a56
REFACTOR: Collapse AdversarialBenchmark to RapidResponse pattern
May 21, 2026
b54c47f
FEAT: Add skip_cached cross-run caching to AdversarialBenchmark
May 21, 2026
8b5bcc9
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 21, 2026
091a501
FEAT: Widen AdversarialBenchmark objective_scorer to Scorer (stage 1)
May 21, 2026
40ff08b
TEST: e2e per-scenario initializer override for benchmark.adversarial
May 21, 2026
4323383
DOCS: Rewrite benchmark scanner notebook for registry-driven flow
May 21, 2026
7e450b1
STYLE: Move TYPE_CHECKING-only imports in target_registry
May 21, 2026
34b86f0
FIX: TargetInitializer initialized adversarial chats as OpenAIChatTar…
May 21, 2026
2f2c209
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 21, 2026
a3e1ffa
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 22, 2026
37a22c7
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 26, 2026
c7e7b93
Removed _cached_strategy_class and scorer type validation.
May 26, 2026
5840b25
REFACTOR: Remove BenchmarkInitializer and source adversarial targets …
May 26, 2026
4697a1a
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 26, 2026
53bd312
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 26, 2026
2ec2a49
DOCS: Trim adversarial benchmark scanner doc to basic-usage shape
May 26, 2026
cfdf5bf
DOCS: Add executed benchmark scanner notebook outputs
May 26, 2026
7a664c5
Add mcp Python SDK dependency
May 26, 2026
d68d75c
Merge branch 'main' into MCP
ValbuenaVC May 26, 2026
c7d65d3
Add tools/ package with tool_loop decorator and CallableToolBackend
May 26, 2026
39752ac
Addition of pyrit/tools package and unit tests. Introduces base model…
May 26, 2026
e8ab8ff
Addition of MCP components including the MCP client and tool backend.
May 26, 2026
c61bcfe
Add supports_tool_use capability + ToolEventPolicy and wire tool_loop…
May 27, 2026
f566914
Merge branch 'main' into MCP
ValbuenaVC May 27, 2026
3a3c26b
Drop C5 (Chat target tool calling): defer to follow-up, redesign arou…
May 28, 2026
eb84ed5
Migrate OpenAIResponseTarget onto @tool_loop and LocalToolBackend
May 28, 2026
e57ff8f
DOCS: Migrate reST roles, drop dead spec-selector and stale scorer-fl…
May 28, 2026
0e622c7
Add integration tests for RedTeamingAttack with real MCP tool dispatch
May 28, 2026
fe0f2d3
REFACTOR: Delete unused `TargetInitializerTags.ADVERSARIAL`
May 28, 2026
610ed05
DOCS: Compare singleturn vs multiturn adversarial targets in benchmar…
May 28, 2026
4c10a4d
Merge branch 'main' into adversarial_benchmark_refactor
May 28, 2026
8164e7c
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 28, 2026
f43a303
FEAT: Add ObjectiveTargetEvaluationIdentifier + analytics cache lookup
May 28, 2026
cf62a83
Merge branch 'vvalbuena-microsoft/mcp-target-decorator-design' into a…
May 28, 2026
e7ee9c0
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 28, 2026
94f1f9f
FEAT: Wire adversarial benchmark cache to analytics primitives
May 28, 2026
6827aa9
TEST: Add real-memory coverage for adversarial cache wiring
May 28, 2026
bbe5121
Merge remote-tracking branch 'origin/main' into vvalbuena-microsoft/f…
May 28, 2026
0af14e7
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 29, 2026
ccebf2f
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 29, 2026
31ed2fb
Removing tool components from this branch. Tool-calling is a feature …
May 29, 2026
5dbd1e4
Remove remaining tool-calling leakage from PR #1811
May 29, 2026
80e1d4a
Merge resolution.
May 29, 2026
1f5d061
Fix merge conflicts with main (PR #1785/#1784): adopt factory registr…
May 29, 2026
eae30ed
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 29, 2026
db03b0c
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC May 30, 2026
c39d7cc
Merge branch 'main' into adversarial_benchmark_refactor
Jun 1, 2026
6fc708a
FEAT: inject cached attack results into AdversarialBenchmark Scenario…
Jun 1, 2026
7672e1e
feat: add dataset-level scoping to _collect_cached_completion_pairs
Jun 1, 2026
68cff20
Merge remote-tracking branch 'origin/main' into adversarial_benchmark…
Jun 1, 2026
b0dc4b0
fix: use uuid4 for conversation_id in _persist_attack_result to preve…
Jun 1, 2026
617572b
Merge branch 'main' into adversarial_benchmark_refactor
ValbuenaVC Jun 1, 2026
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
14 changes: 14 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,24 @@ AZURE_OPENAI_GPT4O_UNSAFE_CHAT_MODEL2="deployment-name"
AZURE_OPENAI_GPT4O_UNSAFE_CHAT_UNDERLYING_MODEL2=""

# Adversarial chat target (used by scenario attack techniques, e.g. role-play, TAP)
# Default endpoint goes here; specialized ones below
ADVERSARIAL_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/v1"
ADVERSARIAL_CHAT_KEY="xxxxx"
ADVERSARIAL_CHAT_MODEL="deployment-name"

ADVERSARIAL_CHAT_SINGLETURN_ENDPOINT="https://xxxxxx.westus3.inference.ml.azure.com/score"
ADVERSARIAL_CHAT_SINGLETURN_KEY="xxxxx"
ADVERSARIAL_CHAT_SINGLETURN_MODEL="deployment-name"

ADVERSARIAL_CHAT_MULTITURN_ENDPOINT="https://xxxxxx.westus3.inference.ml.azure.com/score"
ADVERSARIAL_CHAT_MULTITURN_KEY="xxxxx"
ADVERSARIAL_CHAT_MULTITURN_MODEL="deployment-name"

ADVERSARIAL_CHAT_REASONING_ENDPOINT="https://xxxxxx.westus3.inference.ml.azure.com/score"
Comment thread
ValbuenaVC marked this conversation as resolved.
ADVERSARIAL_CHAT_REASONING_KEY="xxxxx"
ADVERSARIAL_CHAT_REASONING_MODEL="deployment-name"


# Objective Scorer chat target (used in scorers in scenarios)
OBJECTIVE_SCORER_CHAT_ENDPOINT="https://xxxxx.openai.azure.com/openai/v1"
OBJECTIVE_SCORER_CHAT_KEY="xxxxx"
Expand Down
321 changes: 210 additions & 111 deletions doc/scanner/benchmark.ipynb

Large diffs are not rendered by default.

179 changes: 164 additions & 15 deletions doc/scanner/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,186 @@
# %% [markdown]
# # Benchmark Scenarios
#
# Benchmark scenarios are a subset of scenarios that compare the effectiveness of attacks across an axis that varies within the scenario itself. The axis can be many things; currently, the only benchmark variant is the adversarial benchmark, whose axis of change is the adversarial model used in attacks.
# Benchmark scenarios compare the effectiveness of attacks across an axis that varies within the
# scenario itself. The axis can be many things; currently, the only benchmark variant is the
# adversarial benchmark, whose axis of change is the **adversarial chat helper model** used in
# attacks.

# %% [markdown]
# ## Adversarial Benchmark
# The adversarial benchmarking scenario (`AdversarialBenchmark`) compares the effectiveness of different adversarial models in successfully executing attacks against a target model.
#
# `AdversarialBenchmark` holds the objective target and dataset constant and varies the adversarial
# chat model used to drive multi-turn attacks (and crescendo-style simulated conversations). Useful
# for evaluating which adversarial helper models produce stronger or weaker attack success rates
# against the same target.
#
# Model fan-out is owned by `BenchmarkInitializer`, which discovers every ADVERSARIAL-tagged target
# in `TargetRegistry` (populated by `TargetInitializer` from `ADVERSARIAL_CHAT_*` env vars) and
# registers one fanned variant per `(adversarial-capable technique, target)` pair into
# `AttackTechniqueRegistry`. The scenario then reads those variants when it builds its strategy
# enum.
#
# ### Prerequisites
#
# Set at least one `ADVERSARIAL_CHAT_*` group of env vars (see `.env_example`):
#
# ```bash
# # Default adversarial target (always available when set)
# ADVERSARIAL_CHAT_ENDPOINT="https://your-endpoint.openai.azure.com/openai/v1"
# ADVERSARIAL_CHAT_KEY="your-key"
# ADVERSARIAL_CHAT_MODEL="deployment-name"
#
# # Optional turn-style variants — auto-discovered by TargetInitializer when set
# ADVERSARIAL_CHAT_SINGLETURN_ENDPOINT="..."
# ADVERSARIAL_CHAT_SINGLETURN_KEY="..."
# ADVERSARIAL_CHAT_SINGLETURN_MODEL="..."
# # ADVERSARIAL_CHAT_MULTITURN_* and ADVERSARIAL_CHAT_REASONING_* follow the same pattern
# ```
#
# If no adversarial-tagged target is registered, `BenchmarkInitializer.initialize_async` raises
# `ValueError` naming the env vars to set.
#
# ### CLI quickstart
#
# ```bash
# pyrit_scan benchmark.adversarial \
Comment thread
ValbuenaVC marked this conversation as resolved.
# --initializers target load_default_datasets benchmark \
# --target openai_chat \
# --max-dataset-size 4
# ```
#
# **Available strategies (depend on registered adversarial targets):** `all`, `light`, `single_turn`,
# `multi_turn`, plus one concrete member per fanned variant named
# `f"{source_technique}__{target_name}"` (e.g. `red_teaming__adversarial_chat`,
# `tap__adversarial_chat_singleturn`).

# %% [markdown]
# ## Setup
#
# `BenchmarkInitializer` runs after `TargetInitializer` so adversarial-tagged targets are present
# in the registry before the fan-out logic queries it. The initializer chain is executed in list
# order.

# %%
from pyrit.output import output_scenario_async
from pyrit.prompt_target import OpenAIChatTarget
from pyrit.scenario import DatasetConfiguration
from pyrit.scenario.scenarios.benchmark import AdversarialBenchmark
from pyrit.setup import IN_MEMORY, initialize_pyrit_async
from pyrit.setup.initializers import LoadDefaultDatasets
from pyrit.setup.initializers import (
BenchmarkInitializer,
LoadDefaultDatasets,
ScorerInitializer,
TargetInitializer,
)

await initialize_pyrit_async( # type: ignore
memory_db_type=IN_MEMORY,
initializers=[
TargetInitializer(),
ScorerInitializer(),
LoadDefaultDatasets(),
BenchmarkInitializer(),
],
)

await initialize_pyrit_async(memory_db_type=IN_MEMORY, initializers=[LoadDefaultDatasets()]) # type: ignore
objective_target = OpenAIChatTarget()

# Pass any number of adversarial PromptTarget instances (with chat-target
# capabilities — multi-turn and editable history) as a list; AdversarialBenchmark
# infers a label for each from its identifier and runs every benchmark-friendly
# attack technique against the objective target with each adversarial model.
adversarial_model = OpenAIChatTarget()
# %% [markdown]
# ## Run the benchmark
#
# Instantiate with no model arguments — adversarial models come from the registry via
# `BenchmarkInitializer`. The default strategy (`light`) runs the benchmark-friendly subset of
# fanned variants for a quick comparison.

benchmark_scenario = AdversarialBenchmark(adversarial_models=[adversarial_model])
# %%
dataset_config = DatasetConfiguration(dataset_names=["harmbench"], max_dataset_size=4)

await benchmark_scenario.initialize_async( # type: ignore
objective_target=OpenAIChatTarget(), max_concurrency=2
scenario = AdversarialBenchmark()
await scenario.initialize_async( # type: ignore
Comment thread
ValbuenaVC marked this conversation as resolved.
objective_target=objective_target,
dataset_config=dataset_config,
)

baseline_result = await benchmark_scenario.run_async() # type: ignore
baseline_result = await scenario.run_async() # type: ignore

# Resume handle: re-run with `AdversarialBenchmark(..., scenario_result_id=<this id>)` to pick
# up where this run left off (constructor args must match the original run).
# Save this id to resume the run later via AdversarialBenchmark(scenario_result_id=...).
print(f"Scenario result id: {baseline_result.id}")

await output_scenario_async(baseline_result)

# %% [markdown]
# ## Cross-run caching
Comment thread
ValbuenaVC marked this conversation as resolved.
Outdated
#
# Re-run the benchmark with `skip_cached=True` and atomic attacks that completed (`SUCCESS` or
# `FAILURE` outcome) in any prior `COMPLETED` run of the same scenario name + version are skipped.
# `ERROR` and `UNDETERMINED` outcomes always retry, so transient failures don't poison the cache.
#
# The cache key is `(atomic_attack_name, technique_eval_hash)` — two atomic attacks that share a
# name but use different technique configurations (e.g. different scorer) don't cross-pollinate.
#
# Useful for:
# * Resuming a long-running benchmark after a crash or `Ctrl-C`.
# * Incrementally adding new adversarial targets without re-running the existing ones — the new
# fanned variants have new names, so they don't match any cached entry and execute fresh.

# %%
scenario_cached = AdversarialBenchmark(skip_cached=True)
await scenario_cached.initialize_async( # type: ignore
objective_target=objective_target,
dataset_config=dataset_config,
)

cached_result = await scenario_cached.run_async() # type: ignore

await output_scenario_async(cached_result)

# %% [markdown]
# ## Narrowing the fan-out
#
# Limit the benchmark to a specific subset of registered adversarial targets via
# `BenchmarkInitializer`'s `target_names` parameter. Settable from `.pyrit_conf` (see next section),
# or programmatically:
#
# ```python
# narrow_init = BenchmarkInitializer()
# narrow_init.set_params_from_args(args={"target_names": ["adversarial_chat_singleturn"]})
# await narrow_init.initialize_async()
# ```
#
# Unknown names raise `ValueError` listing both the unknowns and the discovered set, so typos fail
# loudly rather than silently expanding the run.

# %% [markdown]
# ## Bootstrapping from `.pyrit_conf`
#
# For production / repeated runs, declare the full initializer chain in `.pyrit_conf`. Initializers
# run in list order; `target` must precede `benchmark` so adversarial-tagged targets are present
# when `BenchmarkInitializer` queries the registry.
#
# ```yaml
# memory_db_type: duckdb
# initializers:
# - name: target
# - name: scorer
# - name: load_default_datasets
# - name: benchmark
# args:
# target_names:
# - adversarial_chat_singleturn
# - adversarial_chat_reasoning
# scenario:
# name: benchmark.adversarial
# ```
#
# Then run `pyrit_scan --config-file .pyrit_conf` — the scenario picks up the initializer-registered
# fan-out automatically.

# %% [markdown]
# ## Scorer flexibility (forward-looking)
#
# `AdversarialBenchmark.__init__`'s `objective_scorer` parameter is typed as the broad `Scorer`
# base class. The current runtime contract is still `TrueFalseScorer` — passing a non-true/false
# scorer raises `TypeError` with a pointer to the planned scoring follow-up. The annotation is
# widened ahead of runtime support so callers coding against the signature today see the eventual
# contract, and the follow-up that drops the guard won't require a signature change.
29 changes: 29 additions & 0 deletions pyrit/registry/object_registries/target_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

if TYPE_CHECKING:
from pyrit.prompt_target import PromptTarget
from pyrit.registry.object_registries.base_instance_registry import RegistryEntry
from pyrit.registry.tag_query import TagQuery

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -74,3 +76,30 @@ def get_instance_by_name(self, name: str) -> Optional[PromptTarget]:
The target instance, or None if not found.
"""
return self.get(name)

def get_by_tag_query(self, *, query: TagQuery) -> list[RegistryEntry[PromptTarget]]:
Comment thread
ValbuenaVC marked this conversation as resolved.
Outdated
"""
Get all entries whose tag keys satisfy ``query``.

``TagQuery`` operates on a tag set, so this method matches against
``entry.tags.keys()`` and ignores tag values. For value-aware
single-tag lookups use ``get_by_tag(*, tag, value)`` on the base
class.

Composite queries compose with ``&`` and ``|`` operators, e.g.
``TagQuery.all("adversarial") & TagQuery.any_of("singleturn", "multiturn")``.

Args:
query: The tag predicate to evaluate against each entry.

Returns:
List of matching ``RegistryEntry`` objects sorted by registry name.
"""
results: list[RegistryEntry[PromptTarget]] = []
# Note: this erases insertion order, but respects the base_instance_registry pattern
# (get_by_tag).
for name in sorted(self._registry_items.keys()):
entry = self._registry_items[name]
if query.matches(set(entry.tags.keys())):
results.append(entry)
return results
Loading
Loading