Skip to content

Commit 8d52198

Browse files
committed
keep original_float_value on fallback scores
1 parent f916d8e commit 8d52198

2 files changed

Lines changed: 24 additions & 0 deletions

File tree

‎pyrit/score/fallback_scorer.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from pyrit.models import ComponentIdentifier, MessageScorable, Scorable, Score, ScoringExpectation
88
from pyrit.score.observation.execution import _merge_observation_ids
9+
from pyrit.score.score_utils import ORIGINAL_FLOAT_VALUE_KEY
910
from pyrit.score.scorer import Scorer
1011

1112
if TYPE_CHECKING:
@@ -139,6 +140,9 @@ def _build_result(
139140
f"primary.{key}": value for key, value in (primary.score_metadata or {}).items()
140141
}
141142
metadata["resolved_by"] = "fallback" if fallback is not None else "primary"
143+
# Attacks rank true/false scores by this top-level key, so keep the selected child's value.
144+
if selected.score_metadata and ORIGINAL_FLOAT_VALUE_KEY in selected.score_metadata:
145+
metadata[ORIGINAL_FLOAT_VALUE_KEY] = selected.score_metadata[ORIGINAL_FLOAT_VALUE_KEY]
142146
rationale = primary.score_rationale
143147
if fallback_attempted:
144148
metadata["primary_rationale"] = primary.score_rationale or ""

‎tests/unit/score/test_fallback_scorer.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
TrueFalseScorer,
4141
)
4242
from pyrit.score.fallback_scorer import _FallbackScorer
43+
from pyrit.score.score_utils import ORIGINAL_FLOAT_VALUE_KEY, normalize_score_to_float
4344

4445
pytestmark = pytest.mark.usefixtures("patch_central_database")
4546

@@ -156,6 +157,25 @@ async def test_complete_primary_skips_fallback_async(
156157
assert isinstance(wrapper, FloatScaleScorer if family == "float_scale" else TrueFalseScorer)
157158

158159

160+
@pytest.mark.parametrize("primary_resolves", [False, True])
161+
async def test_selected_original_float_value_stays_readable_async(*, primary_resolves: bool) -> None:
162+
primary, fallback = _TrueFalseScorer(), _TrueFalseScorer()
163+
wrapper = TrueFalseFallbackScorer(scorer=primary, fallback_scorer=fallback)
164+
primary_score = _score(
165+
family="true_false",
166+
value="True" if primary_resolves else None,
167+
metadata={ORIGINAL_FLOAT_VALUE_KEY: 0.3},
168+
)
169+
fallback_score = _score(family="true_false", value="True", metadata={ORIGINAL_FLOAT_VALUE_KEY: 0.6})
170+
with (
171+
patch.object(primary, "_score_scorable_async", new_callable=AsyncMock, return_value=[primary_score]),
172+
patch.object(fallback, "_score_scorable_async", new_callable=AsyncMock, return_value=[fallback_score]),
173+
):
174+
result = (await wrapper.score_async(scorable=ContentScorable(value="evidence")))[0]
175+
176+
assert normalize_score_to_float(result) == (0.3 if primary_resolves else 0.6)
177+
178+
159179
@pytest.mark.parametrize("fallback_abstains", [False, True])
160180
async def test_fallback_preserves_both_attempts_without_mutating_children_async(
161181
*, pair: tuple[Scorer, Scorer, Scorer], family: ScoreType, fallback_abstains: bool, sqlite_instance: MemoryInterface

0 commit comments

Comments
 (0)