Skip to content

Commit 5aeda85

Browse files
richlundeenCopilot
andcommitted
Simplify TAP node expectation handling
Require nodes to use the resolved execution expectation and remove the per-turn fallback. Update direct node fixtures and cover default and explicit expectations through branching. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3089d12 commit 5aeda85

6 files changed

Lines changed: 25 additions & 13 deletions

File tree

‎pyrit/executor/attack/multi_turn/tree_of_attacks.py‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,8 @@ def __init__(
380380
attack_strategy_name: str,
381381
modality_router: _ModalityFeedbackRouter,
382382
record_objective_conversation: Callable[..., None],
383+
expectation: ScoringExpectation,
383384
use_score_as_feedback: bool = True,
384-
expectation: ScoringExpectation | None = None,
385385
memory_labels: dict[str, str] | None = None,
386386
parent_id: str | None = None,
387387
prompt_normalizer: PromptNormalizer | None = None,
@@ -411,9 +411,9 @@ def __init__(
411411
messages. Typically shared across all nodes of the same attack.
412412
record_objective_conversation (Callable[..., None]): Records an objective-target
413413
conversation ID for cleanup before each objective send.
414+
expectation (ScoringExpectation): The execution's resolved scoring question.
414415
use_score_as_feedback (bool): Whether subsequent adversarial prompts include
415416
the objective score. Defaults to True.
416-
expectation (ScoringExpectation | None): The execution's scoring question.
417417
memory_labels (dict[str, str] | None): Labels for memory storage.
418418
parent_id (str | None): ID of the parent node, if this is a child node
419419
prompt_normalizer (PromptNormalizer | None): Normalizer for handling prompts and responses.
@@ -818,7 +818,7 @@ async def _score_response_async(self, *, response: Message, objective: str) -> N
818818
response (Message): The response from the objective target to evaluate.
819819
This contains the target's reply to the adversarial prompt.
820820
objective (str): The attack objective describing what the attacker wants to achieve.
821-
This is passed to scorers as context for evaluation.
821+
Used for execution diagnostics; the resolved expectation supplies scoring criteria.
822822
823823
Raises:
824824
RuntimeError: If the scoring process returns no objective score.
@@ -843,9 +843,7 @@ async def _score_response_async(self, *, response: Message, objective: str) -> N
843843
response=response,
844844
objective_scorer=self._objective_scorer,
845845
auxiliary_scorers=self._auxiliary_scorers,
846-
expectation=self._expectation
847-
if self._expectation is not None
848-
else ScoringExpectation(objective=objective),
846+
expectation=self._expectation,
849847
)
850848

851849
# Extract objective score

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
TreeOfAttacksWithPruningAttack,
2727
_TreeOfAttacksNode,
2828
)
29-
from pyrit.models import Message, MessagePiece
29+
from pyrit.models import Message, MessagePiece, ScoringExpectation
3030
from pyrit.prompt_normalizer import PromptNormalizer
3131

3232
# Text the JsonSchemaNormalizer appends when the target cannot enforce a schema
@@ -116,6 +116,7 @@ async def test_tap_forwards_schema_to_adversarial_target(patch_central_database)
116116
attack_strategy_name="TreeOfAttacksWithPruningAttack",
117117
modality_router=_ModalityFeedbackRouter(adversarial_chat=adversarial, objective_target=objective),
118118
record_objective_conversation=lambda *, conversation_id: None,
119+
expectation=ScoringExpectation(objective="Test objective"),
119120
)
120121

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

‎tests/unit/executor/attack/multi_turn/test_prepended_history_normalization.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Message,
3232
MessagePiece,
3333
PromptDataType,
34+
ScoringExpectation,
3435
)
3536
from pyrit.prompt_normalizer import ConverterConfiguration, PromptNormalizer
3637
from pyrit.prompt_target import (
@@ -297,6 +298,7 @@ def _make_tap_node(*, target: PromptTarget) -> _TreeOfAttacksNode:
297298
objective_target=target,
298299
),
299300
record_objective_conversation=lambda *, conversation_id: None,
301+
expectation=ScoringExpectation(objective="Test objective"),
300302
)
301303

302304

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
MultiTurnAttackContext,
1717
)
1818
from pyrit.memory import CentralMemory
19-
from pyrit.models import ConversationType, Message, MessagePiece
19+
from pyrit.models import ConversationType, Message, MessagePiece, ScoringExpectation
2020
from pyrit.prompt_target import PromptTarget
2121
from pyrit.prompt_target.common.target_capabilities import TargetCapabilities
2222
from pyrit.prompt_target.common.target_configuration import TargetConfiguration
@@ -492,6 +492,7 @@ def _make_tap_node(self, *, supports_multi_turn: bool):
492492
objective_target=target,
493493
),
494494
record_objective_conversation=lambda *, conversation_id: None,
495+
expectation=ScoringExpectation(objective="Test objective"),
495496
)
496497

497498
def test_single_turn_target_duplicates_logical_history_without_seed_boundary(self):
@@ -879,6 +880,7 @@ def _make_tap_node(self, *, supports_multi_turn: bool):
879880
objective_target=target,
880881
),
881882
record_objective_conversation=lambda *, conversation_id: None,
883+
expectation=ScoringExpectation(objective="Test objective"),
882884
)
883885

884886
def test_branching_single_turn_target_preserves_system_across_depths(self):

‎tests/unit/executor/attack/multi_turn/test_tree_of_attacks.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
MessagePiece,
4242
Score,
4343
ScoreStatus,
44+
ScoringExpectation,
4445
SeedPrompt,
4546
)
4647
from pyrit.prompt_normalizer import PromptNormalizer
@@ -1197,6 +1198,7 @@ async def test_score_response_delegates_to_scorer_for_blocked(self, attack_build
11971198
objective_target=builder.objective_target,
11981199
),
11991200
record_objective_conversation=lambda *, conversation_id: None,
1201+
expectation=ScoringExpectation(objective="test objective"),
12001202
desired_response_prefix="Sure, here is",
12011203
prompt_normalizer=normalizer,
12021204
)
@@ -1264,6 +1266,7 @@ async def test_score_response_delegates_to_scorer_for_unknown_error(self, attack
12641266
objective_target=builder.objective_target,
12651267
),
12661268
record_objective_conversation=lambda *, conversation_id: None,
1269+
expectation=ScoringExpectation(objective="test objective"),
12671270
desired_response_prefix="Sure, here is",
12681271
prompt_normalizer=normalizer,
12691272
)
@@ -1692,6 +1695,7 @@ def node_components(self, attack_builder):
16921695
"attack_strategy_name": "TreeOfAttacksWithPruningAttack",
16931696
"modality_router": modality_router,
16941697
"record_objective_conversation": lambda *, conversation_id: None,
1698+
"expectation": ScoringExpectation(objective="test objective"),
16951699
"memory_labels": {"test": "label"},
16961700
"parent_id": None,
16971701
"prompt_normalizer": prompt_normalizer,
@@ -3198,6 +3202,7 @@ def node_components(self, attack_builder):
31983202
"attack_strategy_name": "TreeOfAttacksWithPruningAttack",
31993203
"modality_router": modality_router,
32003204
"record_objective_conversation": lambda *, conversation_id: None,
3205+
"expectation": ScoringExpectation(objective="test objective"),
32013206
"memory_labels": {},
32023207
"parent_id": None,
32033208
"prompt_normalizer": prompt_normalizer,

‎tests/unit/executor/attack/test_scoring_expectation_transport.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -571,11 +571,14 @@ async def test_attack_error_names_failing_scorer_async(
571571
assert get_execution_context() is None
572572

573573
@pytest.mark.parametrize("duplicate", [False, True], ids=["initial_node", "duplicated_node"])
574-
async def test_tap_node_preserves_execution_expectation_async(self, duplicate: bool) -> None:
574+
@pytest.mark.parametrize("explicit_expectation", [False, True], ids=["default_objective", "scoring_objective"])
575+
async def test_tap_node_preserves_execution_expectation_async(
576+
self, *, duplicate: bool, explicit_expectation: bool
577+
) -> None:
575578
target = MockPromptTarget()
576579
leaf = _RecordingFloatScorer()
577580
auxiliary = _RecordingScorer()
578-
supplied = _expectation()
581+
supplied = _expectation() if explicit_expectation else None
579582
attack = TreeOfAttacksWithPruningAttack(
580583
objective_target=target,
581584
attack_adversarial_config=AttackAdversarialConfig(target=MockPromptTarget()),
@@ -586,6 +589,7 @@ async def test_tap_node_preserves_execution_expectation_async(self, duplicate: b
586589
on_topic_checking_enabled=False,
587590
)
588591
context = TAPAttackContext(params=AttackParameters(objective="attack objective", expectation=supplied))
592+
expected = context.expectation
589593
with patch.object(attack, "_create_on_topic_scorer", wraps=attack._create_on_topic_scorer) as on_topic:
590594
node = attack._create_attack_node(
591595
context=context, initial_prompt=Message.from_prompt(prompt="seed prompt", role="user")
@@ -600,8 +604,8 @@ async def test_tap_node_preserves_execution_expectation_async(self, duplicate: b
600604
node = child
601605

602606
assert node.objective_score is not None
603-
assert node.objective_score.scored_expectation == supplied
607+
assert node.objective_score.scored_expectation == expected
604608
assert len(leaf.calls) == (2 if duplicate else 1)
605-
assert all(expectation is supplied for _, expectation in leaf.calls)
606-
assert all(expectation is supplied for _, expectation in auxiliary.calls)
609+
assert all(expectation is expected for _, expectation in leaf.calls)
610+
assert all(expectation is expected for _, expectation in auxiliary.calls)
607611
assert target.prompt_sent == ["seed prompt"]

0 commit comments

Comments
 (0)