draft: text adaptive scenario - #2
Draft
hannahwestra25 wants to merge 53 commits into
Draft
Conversation
…ra/text_adaptive_scenario
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
There was a problem hiding this comment.
Pull request overview
This PR adds a new adaptive text scenario that chooses attack techniques per objective using an epsilon-greedy selector, plus unit tests for the selector, dispatcher, and scenario construction.
Changes:
- Adds
TextAdaptive,AdaptiveDispatchAttack, andAdaptiveTechniqueSelector. - Registers the adaptive scenario virtual package under
pyrit.scenario.adaptive. - Adds unit coverage for adaptive selection, dispatch behavior, and atomic attack generation.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
pyrit/scenario/scenarios/adaptive/text_adaptive.py |
Implements the adaptive scenario and per-objective atomic attack construction. |
pyrit/scenario/scenarios/adaptive/selector.py |
Adds epsilon-greedy technique selection and context helpers. |
pyrit/scenario/scenarios/adaptive/dispatcher.py |
Adds dispatcher attack that selects and runs inner attack arms. |
pyrit/scenario/scenarios/adaptive/__init__.py |
Exports adaptive scenario components. |
pyrit/scenario/__init__.py |
Registers adaptive as a virtual scenario subpackage. |
tests/unit/scenario/scenarios/adaptive/test_text_adaptive.py |
Adds scenario construction and baseline policy tests. |
tests/unit/scenario/scenarios/adaptive/test_selector.py |
Adds selector behavior tests. |
tests/unit/scenario/scenarios/adaptive/test_dispatcher.py |
Adds dispatcher behavior tests. |
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
hannahwestra25
commented
May 18, 2026
…ra/text_adaptive_scenario
Per rlundeen2's review on PR microsoft#1760 (microsoft#1760 (comment)), collapse AdaptiveDispatchAttack's manual per-objective attempt loop into a SequentialAttack(completion_policy=FIRST_SUCCESS) (introduced in PR microsoft#1819). Each objective now produces exactly one AttackResult envelope (SequentialAttackResult, conversation_id=""), and the inner per-technique attempts persist as their own first-class AttackResult rows reachable via child_attack_results / child_attack_result_ids. This removes the prior hack where the winning inner result was rewritten with a fresh uuid and re-persisted alongside its original, producing two rows that shared a conversation_id. What changed in dispatcher.py: - Drop _run_inner_attack_async, the manual for-loop, AttackExecutor usage, and the uuid / datetime / dataclasses.replace / AttackOutcome imports. - Build a SequentialChildAttack per chosen technique (with per-attempt memory labels and per-attempt seed_group.with_technique() merging), then delegate iteration + stop-on-success + envelope construction to SequentialAttack. - Stamp the per-attempt trail at metadata["adaptive_attempts"] on the returned envelope (one entry per attempt that actually ran). Compatibility preserved: same public constructor signature, same filtering of techniques whose seed_technique is incompatible with the current seed_group (still raises ValueError on empty pool so the outer executor drops the seed group rather than silently no-op'ing), same per-attempt memory_labels shape, same seed_technique merging. Tests updated to patch SequentialAttack._run_child_attack_async instead of the deleted _run_inner_attack_async, and assert the envelope is a SequentialAttackResult with conversation_id == "" and the inner result exposed via child_attack_results. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a new 'Rapid Response Scenario' section demonstrating how to use RapidResponse with focused dataset configuration and strategy selection. Update intro text to reference the new section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
hannahwestra25
force-pushed
the
hawestra/text_adaptive_scenario
branch
from
June 1, 2026 23:19
f0db060 to
929d398
Compare
…Pydantic identifiers) Four related fixes needed to unblock CI after the upstream merge that brought in PR microsoft#1881 (Refactoring Identifiers to be Pydantic classes): 1. `pyrit/models/identifiers/evaluation_identifier.py`: The merge hoisted `from pyrit.executor.attack.core.attack_strategy import AttackStrategy` to module level, forming a cycle through `pyrit.executor.attack` -> `pyrit.message_normalizer` -> `pyrit.common.data_url_converter` -> `pyrit.models`. Move it back inside `if TYPE_CHECKING:` (`from __future__ import annotations` is already enabled, so the string annotation `attack: AttackStrategy` still resolves at type-check time). 2. `tests/unit/models/identifiers/test_evaluation_identifier.py`: Add missing blank lines between top-level classes (ruff format) and replace four `from pyrit.identifiers import ...` lines with `from pyrit.models.identifiers import ...` (the former is now a deprecation shim that the static-scan deprecation test forbids internal callers from using). 3. `pyrit/scenario/scenarios/adaptive/adaptive_scenario.py`: Mark the three classmethod stubs as `@abstractmethod` so `inspect.isabstract(AdaptiveScenario)` returns `True` and the scenario registry's auto-discovery skips it (otherwise the registry tries to instantiate the abstract base and raises `NotImplementedError`, breaking `test_load_default_datasets`). 4. `pyrit/scenario/scenarios/adaptive/adaptive_scenario.py` and `pyrit/scenario/scenarios/adaptive/text_adaptive.py`: Move `from pyrit.setup.initializers.components.scenario_techniques import build_scenario_technique_factories` from module level back into the function body. `scenario_techniques` imports `pyrit.scenario.core`, which transitively re-imports the adaptive package during `pyrit.scenario` initialization, so a top-level import forms a cycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
hannahwestra25
force-pushed
the
hawestra/text_adaptive_scenario
branch
2 times, most recently
from
June 2, 2026 16:43
f1b6373 to
77a3704
Compare
…n SequentialAttack
Replaces the previous AdaptiveDispatchAttack (an AttackStrategy subclass that
delegated to an internal SequentialAttack) with a slim factory + subclass:
- AdaptiveTechniqueDispatcher: plain class, not an AttackStrategy. Exposes
compatible_techniques(seed_group=...) and async build_attack_async(seed_group=...).
Selects techniques up-front per objective via TechniqueSelector and returns
a fully-wired AdaptiveSequentialAttack.
- AdaptiveSequentialAttack: ~15-LoC SequentialAttack subclass. Adds a
technique_labels constructor argument and stamps adaptive_attempts
metadata onto the envelope returned by super()._perform_async, then
delegates everything else to the framework.
Atomic-attack shape:
- One AtomicAttack per (dataset, seed_group) instead of one per dataset.
- atomic_attack_name = `{prefix}_{dataset}::{objective_sha[:12]}` so each
objective gets its own deterministic, hash-disambiguated identifier.
- display_group = dataset_name preserves the grouping for reporting.
- All per-dataset dispatchers share the same TechniqueSelector instance so
learning still accumulates globally.
Rationale: eliminates the dispatcher's coupling to SequentialAttack's private
lifecycle internals (no more manual `_perform_async` orchestration, no more
duplicating envelope/metadata wiring) and removes a layer of indirection that
was making the call graph hard to reason about. The dispatcher is now an
`envelope factory'', not an attack.
All 56 adaptive unit tests pass (verified via docker devcontainer pytest run).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ra/text_adaptive_scenario
…ra/text_adaptive_scenario
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add Adaptive Scenario Framework with
TextAdaptiveSummary
Introduces an adaptive scenario framework that picks attack techniques per-objective using an epsilon-greedy bandit informed by observed success rates, rather than running every selected technique against every objective. Concentrates spend on techniques that actually work against the target, and stops early on first success.
Adds:
AdaptiveScenario— modality-agnostic base classTextAdaptive— concrete text-attack subclassAdaptiveTechniqueSelector— epsilon-greedy selector with Laplace-smoothed estimates and pooled cross-context backoffAdaptiveDispatchAttack— per-objective dispatch strategy.pydocMotivation
Static scenarios are
O(techniques × objectives): every technique runs against every objective regardless of whether earlier attempts already succeeded or whether the technique is known to be ineffective against the target. For evaluation runs with many techniques and many objectives, this wastes spend on combinations that aren't informative.Adaptive scenarios reduce this to
O(max_attempts × objectives)by:epsilon) so the table doesn't collapse onto a single technique prematurely,How it works
For each objective the dispatcher loops up to
max_attempts_per_objectivetimes:epsilonpick a random technique, otherwise pick the one with the highest Laplace-smoothed success estimate(s + 1) / (n + 1). Cells with fewer thanpool_thresholdlocal observations fall back to the technique's pooled rate across all contexts (cold-start handling).SeedAttackGroup, merging the technique'sseed_techniqueif it declares one.(context, technique) → (successes, attempts)table and stop early on success.The selector is shared by reference across all per-objective dispatchers in a scenario run, so learning accumulates globally. The per-objective
contextkey is derived by aContextExtractor;global_context(default) shares one table across all objectives,harm_category_contextpartitions by harm category.Public API
Adaptive scenarios are also resumable — pass
scenario_result_id="..."to the constructor and prior dispatch trails are replayed into the selector before the remaining objectives run.notes
BASELINE_POLICY = Forbidden—prompt_sendingparticipates as one of the selector's techniques rather than being prepended as a guaranteed baseline. Mixing a forced baseline with adaptive selection would bias the table.seed_techniqueis incompatible with a given seed group are dropped per-objective rather than globally, so a technique that's incompatible with one objective still participates for others. Objectives with no compatible techniques are skipped with a warning.metadata["adaptive_attempts"]on the outerAttackResult; on resume these trails are replayed viarecord_outcomeso the new selector starts with the prior run's learned state. Already-completed atomics are skipped by the baseScenarioresume path (byatomic_attack_name), so there is no double-counting.AttackResultvia its own post-execute hook; the dispatcher returns areplace-based copy with a freshattack_result_id/timestampand the adaptive trail stamped onto metadata. Both rows shareconversation_id. Documented on the dispatcher's class docstring.AdaptiveTechniqueSelectorguards its counts table with athreading.Lockso individualselect/record_outcomeoperations are atomic. The overall select → execute → record sequence is not serialized.Files
pyrit/scenario/scenarios/adaptive/adaptive_scenario.pypyrit/scenario/scenarios/adaptive/text_adaptive.pypyrit/scenario/scenarios/adaptive/selector.pypyrit/scenario/scenarios/adaptive/dispatcher.pypyrit/scenario/scenarios/adaptive/__init__.py,pyrit/scenario/__init__.pydoc/code/scenarios/3_adaptive_scenarios.py,doc/code/scenarios/3_adaptive_scenarios.ipynbtests/unit/scenario/scenarios/adaptive/test_selector.py,tests/unit/scenario/scenarios/adaptive/test_dispatcher.py,tests/unit/scenario/scenarios/adaptive/test_text_adaptive.pyTesting
pytest tests/unit/scenario/scenarios/adaptive/— 63 tests pass. Coverage includes:record_outcome