Skip to content

Commit 8b3826b

Browse files
richlundeenCopilothannahwestra25
authored
FEAT: Policy Scorer Compatibility (phase 2.5) (#2518)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: hannahwestra25 <hannahwestra@microsoft.com> Copilot-Session: 9cd5233d-054f-4496-8237-4dd41b2ed544 Copilot-Session: 5e2a5ec1-82af-4b4b-a69c-3306c1507164
1 parent 151cac9 commit 8b3826b

38 files changed

Lines changed: 1267 additions & 850 deletions

doc/code/framework.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ If you are contributing to PyRIT, that work will most likely land in one of the
258258
- Any decision an attack makes should be based on a scorer result
259259
- A scorer is not limited to a message, it could be anything (e.g. was this tool called or was this file written). It receives a `Scorable`, which identifies that evidence, and an optional `ScoringExpectation`.
260260
- `TrueFalseScorer` and `FloatScaleScorer` define result families. `MessageScorer` adds message resolution and message-only policy on top of them.
261-
- `Score.status` marks a verdict complete or undetermined, and the attack decides how to branch on it.
261+
- A scorer declares which evidence it reads, rather than the caller filtering evidence for it. A `MessageScorer` states the conversation roles and data types it reads on its `ScorerPromptValidator`.
262262
- **Does not own**: acting on its own result. A scorer evaluates a response and returns a score; branching on that score is the attack's job, and aggregating scores across runs is analytics'. It may call a target to evaluate, but it doesn't send the attack's objective prompt or manage the conversation.
263263

264264
**Framework Plans**:

doc/code/scoring/0_scoring.ipynb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,24 @@
219219
"storage and stores its SHA-256 digest. The score remains resolvable after the source file is\n",
220220
"removed.\n",
221221
"\n",
222-
"A complete score has `status=\"complete\"` and a typed value. An undetermined score has\n",
223-
"`status=\"undetermined\"` and no value. A fully blocked response is a complete negative result\n",
224-
"by default: `False` for message true/false scorers and `0.0` for message float-scale scorers.\n",
225-
"`SelfAskRefusalScorer` is the intentional exception because a content-filter block is a\n",
226-
"refusal, so it returns `True`. Other response errors remain undetermined."
222+
"Scoring APIs return `list[Score]`. An empty list means that the scorer does not apply to the\n",
223+
"evidence, such as a message with no supported role or data type. A non-empty list contains\n",
224+
"completed or undetermined scores.\n",
225+
"\n",
226+
"A complete score has `status=\"complete\"` and a typed domain verdict. An undetermined score\n",
227+
"has `status=\"undetermined\"` and no value because supported evidence failed to load. A fully\n",
228+
"blocked response is a complete negative result by default: `False` for message true/false\n",
229+
"scorers and `0.0` for message float-scale scorers. `SelfAskRefusalScorer` is the intentional\n",
230+
"exception because a content-filter block is a refusal, so it returns `True`.\n",
231+
"\n",
232+
"A scorer declares which evidence it reads; the caller does not filter evidence on its behalf.\n",
233+
"A message scorer names the conversation roles it reads with `supported_roles` on its\n",
234+
"`ScorerPromptValidator`. Prepended (`simulated_assistant`) turns are fabricated history, so a\n",
235+
"scorer must opt in to read them. Every scorer still receives a failed response, because a\n",
236+
"scorer whose evidence never came from the response must run even when the response failed.\n",
237+
"Explicit `role_filter` and `skip_on_error_result` values remain supported until removal.\n",
238+
"Callers that rely on their historical defaults must now pass them explicitly. New code\n",
239+
"should use `supported_roles` and the scorer's unreadable-evidence fallback instead."
227240
]
228241
},
229242
{

doc/code/scoring/0_scoring.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,24 @@
121121
# storage and stores its SHA-256 digest. The score remains resolvable after the source file is
122122
# removed.
123123
#
124-
# A complete score has `status="complete"` and a typed value. An undetermined score has
125-
# `status="undetermined"` and no value. A fully blocked response is a complete negative result
126-
# by default: `False` for message true/false scorers and `0.0` for message float-scale scorers.
127-
# `SelfAskRefusalScorer` is the intentional exception because a content-filter block is a
128-
# refusal, so it returns `True`. Other response errors remain undetermined.
124+
# Scoring APIs return `list[Score]`. An empty list means that the scorer does not apply to the
125+
# evidence, such as a message with no supported role or data type. A non-empty list contains
126+
# completed or undetermined scores.
127+
#
128+
# A complete score has `status="complete"` and a typed domain verdict. An undetermined score
129+
# has `status="undetermined"` and no value because supported evidence failed to load. A fully
130+
# blocked response is a complete negative result by default: `False` for message true/false
131+
# scorers and `0.0` for message float-scale scorers. `SelfAskRefusalScorer` is the intentional
132+
# exception because a content-filter block is a refusal, so it returns `True`.
133+
#
134+
# A scorer declares which evidence it reads; the caller does not filter evidence on its behalf.
135+
# A message scorer names the conversation roles it reads with `supported_roles` on its
136+
# `ScorerPromptValidator`. Prepended (`simulated_assistant`) turns are fabricated history, so a
137+
# scorer must opt in to read them. Every scorer still receives a failed response, because a
138+
# scorer whose evidence never came from the response must run even when the response failed.
139+
# Explicit `role_filter` and `skip_on_error_result` values remain supported until removal.
140+
# Callers that rely on their historical defaults must now pass them explicitly. New code
141+
# should use `supported_roles` and the scorer's unreadable-evidence fallback instead.
129142
# %% [markdown]
130143
# ## Scoring directly
131144
#

doc/code/scoring/3_combining_scorers.ipynb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@
100100
"text `ContentScorable` evidence. It returns a dynamic wrapper that remains the same scorer\n",
101101
"kind as its input.\n",
102102
"\n",
103+
"An empty child result means that the scorer did not apply. A composite scorer ignores empty\n",
104+
"child results and aggregates the remaining results. It returns an empty list if every child\n",
105+
"result is empty. Inverter and threshold wrappers pass an empty result through unchanged.\n",
106+
"A conversation wrapper returns an empty result when it finds no applicable conversation\n",
107+
"evidence or its child returns no score. Any outer wrapper then applies the rules above.\n",
108+
"\n",
103109
"Deprecated message-shaped calls remain on `MessageScorer`, but generic wrappers do not\n",
104110
"project those APIs from their children. Score wrappers through the canonical `Scorable` API.\n",
105111
"\n",

doc/code/scoring/3_combining_scorers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
# text `ContentScorable` evidence. It returns a dynamic wrapper that remains the same scorer
8484
# kind as its input.
8585
#
86+
# An empty child result means that the scorer did not apply. A composite scorer ignores empty
87+
# child results and aggregates the remaining results. It returns an empty list if every child
88+
# result is empty. Inverter and threshold wrappers pass an empty result through unchanged.
89+
# A conversation wrapper returns an empty result when it finds no applicable conversation
90+
# evidence or its child returns no score. Any outer wrapper then applies the rules above.
91+
#
8692
# Deprecated message-shaped calls remain on `MessageScorer`, but generic wrappers do not
8793
# project those APIs from their children. Score wrappers through the canonical `Scorable` API.
8894
#

pyrit/executor/attack/multi_turn/crescendo.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,9 +713,7 @@ async def _score_response_async(self, *, context: CrescendoAttackContext) -> Sco
713713
response=context.last_response,
714714
objective_scorer=self._objective_scorer,
715715
auxiliary_scorers=self._auxiliary_scorers,
716-
role_filter="assistant",
717716
objective=context.objective,
718-
skip_on_error_result=False,
719717
)
720718

721719
objective_score = scoring_results["objective_scores"]

pyrit/executor/attack/multi_turn/multi_prompt_sending.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,7 @@ async def _evaluate_response_async(self, *, response: Message, objective: str) -
408408
response=response,
409409
auxiliary_scorers=self._auxiliary_scorers,
410410
objective_scorer=self._objective_scorer if self._objective_scorer else None,
411-
role_filter="assistant",
412411
objective=objective,
413-
skip_on_error_result=True,
414412
)
415413

416414
objective_scores = scoring_results["objective_scores"]

pyrit/executor/attack/multi_turn/tree_of_attacks.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -804,12 +804,11 @@ async def _score_response_async(self, *, response: Message, objective: str) -> N
804804
and any auxiliary scorers (which provide additional metrics). The scoring results are
805805
used by the TAP algorithm to decide which branches to explore further.
806806
807-
Blocked or errored responses are scored via the scorer's unified default behavior:
808-
``TrueFalseScorer`` returns
809-
``Score(False)`` and ``FloatScaleScorer``
810-
returns ``Score(0.0)`` whenever no supported pieces remain after validator filtering
811-
(the normal outcome for a blocked piece). This keeps blocked branches at the bottom
812-
of the priority queue without needing attack-level error mapping.
807+
Scorers apply their own unreadable-response policy. A fully blocked response uses the
808+
scorer family's neutral fallback unless the scorer overrides it. An unreadable transport
809+
or protocol response produces an undetermined score. A response with no supported role
810+
or data type makes the scorer return ``[]``, so this method raises ``RuntimeError``. Tree
811+
of Attacks does not map these outcomes to ``False`` or ``0.0``.
813812
814813
Args:
815814
response (Message): The response from the objective target to evaluate.
@@ -841,9 +840,7 @@ async def _score_response_async(self, *, response: Message, objective: str) -> N
841840
response=response,
842841
objective_scorer=self._objective_scorer,
843842
auxiliary_scorers=self._auxiliary_scorers,
844-
role_filter="assistant",
845843
objective=objective,
846-
skip_on_error_result=False,
847844
)
848845

849846
# Extract objective score

pyrit/executor/attack/single_turn/prompt_sending.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,7 @@ async def _evaluate_response_async(
367367
response=response,
368368
objective_scorer=self._objective_scorer,
369369
auxiliary_scorers=self._auxiliary_scorers,
370-
role_filter="assistant",
371370
objective=objective,
372-
skip_on_error_result=True,
373371
)
374372

375373
if not self._objective_scorer:

pyrit/score/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from pyrit.score.float_scale.system_prompt_extraction_scorer import SystemPromptExtractionScorer
4040
from pyrit.score.float_scale.video_float_scale_scorer import VideoFloatScaleScorer
4141
from pyrit.score.message_scorable_resolver import MessageScorableResolver
42-
from pyrit.score.message_scorer import MessageScorer, MessageScoringOptions
42+
from pyrit.score.message_scorer import MessageScorer
4343
from pyrit.score.response_handler import CallableResponseHandler, JsonSchemaResponseHandler, ResponseHandler
4444
from pyrit.score.scorable import ContentScorable, MessageScorable, Scorable
4545
from pyrit.score.scorer import Scorer
@@ -176,7 +176,6 @@
176176
"MessageScorableResolver": "pyrit.score.message_scorable_resolver",
177177
"MessageScorable": "pyrit.score.scorable",
178178
"MessageScorer": "pyrit.score.message_scorer",
179-
"MessageScoringOptions": "pyrit.score.message_scorer",
180179
"MethKeywordScorer": "pyrit.score.true_false.regex.meth_keyword_scorer",
181180
"MetricsType": "pyrit.score.scorer_evaluation.metrics_type",
182181
"NerveAgentKeywordScorer": "pyrit.score.true_false.regex.nerve_agent_keyword_scorer",

0 commit comments

Comments
 (0)