Skip to content

Commit b4b743d

Browse files
committed
Privatize modality feedback router API
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c6a6ba7 commit b4b743d

10 files changed

Lines changed: 46 additions & 50 deletions

File tree

pyrit/executor/attack/component/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111
get_prepended_turn_count,
1212
mark_messages_as_simulated,
1313
)
14-
from pyrit.executor.attack.component.modality_router import (
15-
ModalityFeedbackRouter,
16-
)
1714
from pyrit.executor.attack.component.prepended_conversation_config import (
1815
PrependedConversationConfig,
1916
)
@@ -25,6 +22,5 @@
2522
"get_adversarial_chat_messages",
2623
"get_prepended_turn_count",
2724
"mark_messages_as_simulated",
28-
"ModalityFeedbackRouter",
2925
"PrependedConversationConfig",
3026
]

pyrit/executor/attack/component/modality_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
logger = logging.getLogger(__name__)
2626

2727

28-
class ModalityFeedbackRouter:
28+
class _ModalityFeedbackRouter:
2929
"""
3030
Capability-aware utility for building ``Message`` objects between an
3131
adversarial chat and an objective target in multi-turn attacks.

pyrit/executor/attack/multi_turn/crescendo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
)
2222
from pyrit.executor.attack.component import (
2323
ConversationManager,
24-
ModalityFeedbackRouter,
2524
PrependedConversationConfig,
2625
)
26+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
2727
from pyrit.executor.attack.core import (
2828
AttackAdversarialConfig,
2929
AttackConverterConfig,
@@ -226,7 +226,7 @@ def __init__(
226226
# whether prior media should travel back to the adversarial chat or
227227
# forward to the objective target, and that fills in adversarial
228228
# placeholders when ``next_message`` carries seed media.
229-
self._modality_router = ModalityFeedbackRouter(
229+
self._modality_router = _ModalityFeedbackRouter(
230230
adversarial_chat=self._adversarial_chat,
231231
objective_target=objective_target,
232232
)

pyrit/executor/attack/multi_turn/red_teaming.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
from pyrit.exceptions import ComponentRole, execution_context
1515
from pyrit.executor.attack.component import (
1616
ConversationManager,
17-
ModalityFeedbackRouter,
1817
get_adversarial_chat_messages,
1918
)
19+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
2020
from pyrit.executor.attack.core.attack_config import (
2121
DEFAULT_ADVERSARIAL_SEED_PROMPT,
2222
AttackAdversarialConfig,
@@ -161,7 +161,7 @@ def __init__(
161161
# whether prior media should travel back to the adversarial chat or
162162
# forward to the objective target, and that fills in adversarial
163163
# placeholders when ``next_message`` carries seed media.
164-
self._modality_router = ModalityFeedbackRouter(
164+
self._modality_router = _ModalityFeedbackRouter(
165165
adversarial_chat=self._adversarial_chat,
166166
objective_target=objective_target,
167167
)

pyrit/executor/attack/multi_turn/tree_of_attacks.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525
)
2626
from pyrit.executor.attack.component import (
2727
ConversationManager,
28-
ModalityFeedbackRouter,
2928
PrependedConversationConfig,
3029
get_prepended_turn_count,
3130
)
3231
from pyrit.executor.attack.component.conversation_manager import (
3332
build_conversation_context_string_async,
3433
)
34+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
3535
from pyrit.executor.attack.core.attack_config import (
3636
AttackAdversarialConfig,
3737
AttackConverterConfig,
@@ -295,7 +295,7 @@ def __init__(
295295
auxiliary_scorers: list[Scorer] | None,
296296
attack_id: ComponentIdentifier,
297297
attack_strategy_name: str,
298-
modality_router: ModalityFeedbackRouter,
298+
modality_router: _ModalityFeedbackRouter,
299299
memory_labels: dict[str, str] | None = None,
300300
parent_id: str | None = None,
301301
prompt_normalizer: PromptNormalizer | None = None,
@@ -318,7 +318,7 @@ def __init__(
318318
auxiliary_scorers (list[Scorer] | None): Additional scorers for the response
319319
attack_id (ComponentIdentifier): Unique identifier for the attack.
320320
attack_strategy_name (str): Name of the attack strategy for execution context.
321-
modality_router (ModalityFeedbackRouter): Capability-aware router that decides
321+
modality_router (_ModalityFeedbackRouter): Capability-aware router that decides
322322
whether prior media should travel back to the adversarial chat or forward to
323323
the objective target, and fills adversarial-placeholder pieces in seed
324324
messages. Typically shared across all nodes of the same attack.
@@ -1430,7 +1430,7 @@ def __init__(
14301430
# forward to the objective target, and that fills in adversarial
14311431
# placeholders when ``next_message`` carries seed media. Shared across
14321432
# all nodes of the tree.
1433-
self._modality_router = ModalityFeedbackRouter(
1433+
self._modality_router = _ModalityFeedbackRouter(
14341434
adversarial_chat=self._adversarial_chat,
14351435
objective_target=objective_target,
14361436
)

tests/unit/executor/attack/component/test_modality_router.py

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

4-
"""Unit tests for :class:`ModalityFeedbackRouter`."""
4+
"""Unit tests for :class:`_ModalityFeedbackRouter`."""
55

66
from __future__ import annotations
77

@@ -10,7 +10,7 @@
1010

1111
import pytest
1212

13-
from pyrit.executor.attack.component import ModalityFeedbackRouter
13+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
1414
from pyrit.models import Message, MessagePiece
1515
from pyrit.prompt_target.common.target_capabilities import TargetCapabilities
1616
from pyrit.prompt_target.common.target_configuration import TargetConfiguration
@@ -53,23 +53,23 @@ def _make_response(*, data_type: PromptDataType, value: str = "some-value") -> M
5353
# ---------------------------------------------------------------------------
5454
class TestValidateFirstTurnSeed:
5555
def test_text_only_target_no_seed_required(self):
56-
router = ModalityFeedbackRouter(
56+
router = _ModalityFeedbackRouter(
5757
adversarial_chat=_build_target(input_modalities=[{"text"}]),
5858
objective_target=_build_target(input_modalities=[{"text"}]),
5959
)
6060

6161
router.validate_first_turn_seed(next_message=None)
6262

6363
def test_multimodal_target_with_text_allowed_no_seed_required(self):
64-
router = ModalityFeedbackRouter(
64+
router = _ModalityFeedbackRouter(
6565
adversarial_chat=_build_target(input_modalities=[{"text"}]),
6666
objective_target=_build_target(input_modalities=[{"text"}, {"text", "image_path"}]),
6767
)
6868

6969
router.validate_first_turn_seed(next_message=None)
7070

7171
def test_edit_only_target_without_next_message_raises(self):
72-
router = ModalityFeedbackRouter(
72+
router = _ModalityFeedbackRouter(
7373
adversarial_chat=_build_target(input_modalities=[{"text"}]),
7474
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
7575
)
@@ -78,7 +78,7 @@ def test_edit_only_target_without_next_message_raises(self):
7878
router.validate_first_turn_seed(next_message=None)
7979

8080
def test_edit_only_target_with_text_only_next_message_raises(self):
81-
router = ModalityFeedbackRouter(
81+
router = _ModalityFeedbackRouter(
8282
adversarial_chat=_build_target(input_modalities=[{"text"}]),
8383
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
8484
)
@@ -88,7 +88,7 @@ def test_edit_only_target_with_text_only_next_message_raises(self):
8888
router.validate_first_turn_seed(next_message=next_message)
8989

9090
def test_edit_only_target_with_media_seed_passes(self):
91-
router = ModalityFeedbackRouter(
91+
router = _ModalityFeedbackRouter(
9292
adversarial_chat=_build_target(input_modalities=[{"text"}]),
9393
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
9494
)
@@ -120,7 +120,7 @@ def test_edit_only_target_with_media_seed_passes(self):
120120
class TestBuildAdversarialInputMessage:
121121
@pytest.mark.parametrize("media_type", ["image_path", "audio_path", "video_path"])
122122
def test_text_only_adversarial_falls_back_to_text(self, media_type):
123-
router = ModalityFeedbackRouter(
123+
router = _ModalityFeedbackRouter(
124124
adversarial_chat=_build_target(input_modalities=[{"text"}]),
125125
objective_target=_build_target(input_modalities=[{"text"}, {"text", media_type}]),
126126
)
@@ -137,7 +137,7 @@ def test_text_only_adversarial_falls_back_to_text(self, media_type):
137137

138138
@pytest.mark.parametrize("media_type", ["image_path", "audio_path", "video_path"])
139139
def test_multimodal_adversarial_attaches_media(self, media_type):
140-
router = ModalityFeedbackRouter(
140+
router = _ModalityFeedbackRouter(
141141
adversarial_chat=_build_target(input_modalities=[{"text"}, {"text", media_type}]),
142142
objective_target=_build_target(input_modalities=[{"text"}]),
143143
)
@@ -157,7 +157,7 @@ def test_multimodal_adversarial_attaches_media(self, media_type):
157157
assert msg.message_pieces[0].conversation_id == msg.message_pieces[1].conversation_id
158158

159159
def test_no_last_response_returns_text_only(self):
160-
router = ModalityFeedbackRouter(
160+
router = _ModalityFeedbackRouter(
161161
adversarial_chat=_build_target(input_modalities=[{"text"}, {"text", "image_path"}]),
162162
objective_target=_build_target(input_modalities=[{"text"}]),
163163
)
@@ -172,7 +172,7 @@ def test_no_last_response_returns_text_only(self):
172172

173173
def test_response_media_type_not_supported_falls_back(self):
174174
# Adversarial accepts {text, image_path} but the response is audio_path.
175-
router = ModalityFeedbackRouter(
175+
router = _ModalityFeedbackRouter(
176176
adversarial_chat=_build_target(input_modalities=[{"text"}, {"text", "image_path"}]),
177177
objective_target=_build_target(input_modalities=[{"text"}, {"text", "audio_path"}]),
178178
)
@@ -213,7 +213,7 @@ def _make_placeholder_message() -> Message:
213213
)
214214

215215
def test_replaces_placeholder_only(self):
216-
router = ModalityFeedbackRouter(
216+
router = _ModalityFeedbackRouter(
217217
adversarial_chat=_build_target(input_modalities=[{"text"}]),
218218
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
219219
)
@@ -232,7 +232,7 @@ def test_replaces_placeholder_only(self):
232232
assert filled.message_pieces[1].original_value_data_type == "image_path"
233233

234234
def test_does_not_mutate_input_message(self):
235-
router = ModalityFeedbackRouter(
235+
router = _ModalityFeedbackRouter(
236236
adversarial_chat=_build_target(input_modalities=[{"text"}]),
237237
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
238238
)
@@ -248,7 +248,7 @@ def test_does_not_mutate_input_message(self):
248248
assert message.message_pieces[0].original_value == ""
249249

250250
def test_filled_pieces_share_conversation_id(self):
251-
router = ModalityFeedbackRouter(
251+
router = _ModalityFeedbackRouter(
252252
adversarial_chat=_build_target(input_modalities=[{"text"}]),
253253
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
254254
)
@@ -268,7 +268,7 @@ def test_filled_pieces_share_conversation_id(self):
268268
# ---------------------------------------------------------------------------
269269
class TestBuildObjectiveInputMessage:
270270
def test_turn_zero_text_only_target(self):
271-
router = ModalityFeedbackRouter(
271+
router = _ModalityFeedbackRouter(
272272
adversarial_chat=_build_target(input_modalities=[{"text"}]),
273273
objective_target=_build_target(input_modalities=[{"text"}]),
274274
)
@@ -283,7 +283,7 @@ def test_turn_zero_text_only_target(self):
283283
assert msg.get_value() == "adversarial prompt"
284284

285285
def test_turn_zero_edit_only_target_raises_defensive(self):
286-
router = ModalityFeedbackRouter(
286+
router = _ModalityFeedbackRouter(
287287
adversarial_chat=_build_target(input_modalities=[{"text"}]),
288288
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
289289
)
@@ -296,7 +296,7 @@ def test_turn_zero_edit_only_target_raises_defensive(self):
296296
)
297297

298298
def test_turn_n_attaches_prev_media_when_supported(self):
299-
router = ModalityFeedbackRouter(
299+
router = _ModalityFeedbackRouter(
300300
adversarial_chat=_build_target(input_modalities=[{"text"}]),
301301
objective_target=_build_target(input_modalities=[{"text"}, {"text", "image_path"}]),
302302
)
@@ -314,7 +314,7 @@ def test_turn_n_attaches_prev_media_when_supported(self):
314314
assert msg.message_pieces[1].original_value == "/tmp/prev.png"
315315

316316
def test_turn_n_text_only_target_drops_media(self):
317-
router = ModalityFeedbackRouter(
317+
router = _ModalityFeedbackRouter(
318318
adversarial_chat=_build_target(input_modalities=[{"text"}]),
319319
objective_target=_build_target(input_modalities=[{"text"}]),
320320
)
@@ -330,7 +330,7 @@ def test_turn_n_text_only_target_drops_media(self):
330330
assert msg.get_value() == "refine this further"
331331

332332
def test_turn_n_no_prev_response_returns_text_only(self):
333-
router = ModalityFeedbackRouter(
333+
router = _ModalityFeedbackRouter(
334334
adversarial_chat=_build_target(input_modalities=[{"text"}]),
335335
objective_target=_build_target(input_modalities=[{"text"}, {"text", "image_path"}]),
336336
)
@@ -346,7 +346,7 @@ def test_turn_n_no_prev_response_returns_text_only(self):
346346

347347
@pytest.mark.parametrize("media_type", ["image_path", "audio_path", "video_path"])
348348
def test_turn_n_generic_media_types(self, media_type):
349-
router = ModalityFeedbackRouter(
349+
router = _ModalityFeedbackRouter(
350350
adversarial_chat=_build_target(input_modalities=[{"text"}]),
351351
objective_target=_build_target(input_modalities=[{"text"}, {"text", media_type}]),
352352
)
@@ -367,15 +367,15 @@ def test_turn_n_generic_media_types(self, media_type):
367367
# ---------------------------------------------------------------------------
368368
class TestProperties:
369369
def test_objective_target_requires_media_on_first_turn_when_text_excluded(self):
370-
router = ModalityFeedbackRouter(
370+
router = _ModalityFeedbackRouter(
371371
adversarial_chat=_build_target(input_modalities=[{"text"}]),
372372
objective_target=_build_target(input_modalities=[{"text", "image_path"}]),
373373
)
374374

375375
assert router.objective_target_requires_media_on_first_turn is True
376376

377377
def test_objective_does_not_require_media_when_text_allowed(self):
378-
router = ModalityFeedbackRouter(
378+
router = _ModalityFeedbackRouter(
379379
adversarial_chat=_build_target(input_modalities=[{"text"}]),
380380
objective_target=_build_target(input_modalities=[{"text"}, {"text", "image_path"}]),
381381
)

tests/unit/executor/attack/multi_turn/test_adversarial_chat_schema_forwarding.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from unit.mocks import MockPromptTarget
1515

1616
from pyrit.executor.attack import AttackAdversarialConfig, AttackParameters
17-
from pyrit.executor.attack.component import ModalityFeedbackRouter
17+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
1818
from pyrit.executor.attack.multi_turn.crescendo import (
1919
ConversationSession,
2020
CrescendoAttack,
@@ -84,7 +84,7 @@ async def test_tap_forwards_schema_to_adversarial_target(patch_central_database)
8484
auxiliary_scorers=[],
8585
attack_id=attack.get_identifier(),
8686
attack_strategy_name="TreeOfAttacksWithPruningAttack",
87-
modality_router=ModalityFeedbackRouter(adversarial_chat=adversarial, objective_target=objective),
87+
modality_router=_ModalityFeedbackRouter(adversarial_chat=adversarial, objective_target=objective),
8888
)
8989

9090
await node._send_to_adversarial_chat_async(prompt_text="hello")

tests/unit/executor/attack/multi_turn/test_crescendo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def create_mock_chat_target(*, name: str = "MockChatTarget") -> MagicMock:
6767
target.send_prompt_async = AsyncMock()
6868
target.set_system_prompt = MagicMock()
6969
target.get_identifier.return_value = _mock_target_id(name)
70-
# Sensible default for the ModalityFeedbackRouter: text-only target. Tests
70+
# Sensible default for the _ModalityFeedbackRouter: text-only target. Tests
7171
# that need multimodal behavior override input_modalities on their own copy.
7272
target.configuration.capabilities.input_modalities = frozenset({frozenset({"text"})})
7373
target.configuration.capabilities.output_modalities = frozenset({frozenset({"text"})})

tests/unit/executor/attack/multi_turn/test_supports_multi_turn_attacks.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ class TestTAPNodeDuplicateSystemMessages:
373373

374374
def _make_tap_node(self, *, supports_multi_turn: bool):
375375
"""Create a minimal _TreeOfAttacksNode for testing."""
376-
from pyrit.executor.attack.component import ModalityFeedbackRouter
376+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
377377
from pyrit.executor.attack.multi_turn.tree_of_attacks import _TreeOfAttacksNode
378378

379379
target = MagicMock()
@@ -408,7 +408,7 @@ def _make_tap_node(self, *, supports_multi_turn: bool):
408408
auxiliary_scorers=None,
409409
attack_id=MagicMock(),
410410
attack_strategy_name="TAP",
411-
modality_router=ModalityFeedbackRouter(
411+
modality_router=_ModalityFeedbackRouter(
412412
adversarial_chat=adversarial_chat,
413413
objective_target=target,
414414
),
@@ -756,7 +756,7 @@ class TestTAPBranchingPreservesSystemPrompts:
756756

757757
def _make_tap_node(self, *, supports_multi_turn: bool):
758758
"""Create a _TreeOfAttacksNode with real memory."""
759-
from pyrit.executor.attack.component import ModalityFeedbackRouter
759+
from pyrit.executor.attack.component.modality_router import _ModalityFeedbackRouter
760760
from pyrit.executor.attack.multi_turn.tree_of_attacks import _TreeOfAttacksNode
761761

762762
target = MagicMock()
@@ -791,7 +791,7 @@ def _make_tap_node(self, *, supports_multi_turn: bool):
791791
auxiliary_scorers=None,
792792
attack_id=MagicMock(),
793793
attack_strategy_name="TAP",
794-
modality_router=ModalityFeedbackRouter(
794+
modality_router=_ModalityFeedbackRouter(
795795
adversarial_chat=adversarial_chat,
796796
objective_target=target,
797797
),

0 commit comments

Comments
 (0)