FIX: threshold scorer attributes result to first score, not the aggregate - #2451
Merged
Richard Lundeen (rlundeen2) merged 2 commits intoAug 22, 2026
Conversation
…gate
FloatScaleThresholdScorer computes its true/false decision from the aggregate of
the wrapped scorer's outputs, but when that scorer returned more than one score it
reused scores[0] verbatim for everything except the value. The category, rationale
and metadata therefore described the first constituent score rather than the one
that actually crossed the threshold.
AzureContentFilterScorer returns one float_scale score per harm category, ordered
by category name, so this is reachable in normal use. For Hate 0.0, SelfHarm 0.0,
Sexual 0.0, Violence 0.857 at threshold 0.5 the scorer produced:
score_value True
score_category ['Hate']
score_metadata {'azure_severity': 0, 'original_float_value': 0.857}
rationale ... Rationale for scale score: Hate rationale, severity 0
A content-safety flag that fires on Violence, reported as Hate at severity 0, with
an original_float_value from a different category sitting beside it. The value is
right and everything explaining it is wrong, so the score reads as a false positive
on a category that scored zero.
The else branch of the same method, taken when every piece is filtered out, already
builds the score from aggregate_score.category and aggregate_score.metadata. This
makes the populated branch consistent with it: category, metadata and rationale now
come from the aggregate. For a single wrapped score the aggregator returns that
score's own category, metadata and rationale, so that path is unchanged.
Adds test_float_scale_threshold_scorer_attributes_result_to_aggregate_not_first_score,
which fails on main with 'Violence' not in ['Hate'], and
test_float_scale_threshold_scorer_single_score_attribution_unchanged to pin the
single-score behaviour. The existing multi-category test asserted only the value and
the number of scores returned, which is why this went unnoticed.
All 1628 tests in tests/unit/score pass.
Signed-off-by: WatchTree-19 <119982314+WatchTree-19@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bdc32b5f-3091-4f95-a8cc-6a4ff10479a1
Richard Lundeen (rlundeen2)
approved these changes
Aug 22, 2026
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.
Description
FloatScaleThresholdScorercomputes its true/false decision from the aggregate of the wrapped scorer's outputs, but when that scorer returns more than one score it reusesscores[0]verbatim for everything except the value. The category, rationale and metadata therefore describe the first constituent score rather than the one that actually crossed the threshold.AzureContentFilterScorerreturns onefloat_scalescore per harm category, ordered by category name, so this is reachable in normal use.Reproduction
Wrapped scorer returns Hate 0.0, SelfHarm 0.0, Sexual 0.0, Violence 0.857. Threshold 0.5, default MAX aggregator. On
main:A content-safety flag that fired on Violence, reported as Hate at severity 0, with an
original_float_valuefrom a different category sitting next to it. The value is correct and everything explaining it is wrong, so the score reads as a false positive on a category that scored zero. Anyone filtering or reporting byscore_categorygets the wrong attribution silently.Fix
The
elsebranch of the same method, taken when every piece is filtered out, already builds the score fromaggregate_score.categoryandaggregate_score.metadata. This makes the populated branch consistent with it, so category, metadata and rationale all come from the aggregate.For a single wrapped score the aggregator returns that score's own category, metadata and rationale, so that path is unchanged.
Tests
test_float_scale_threshold_scorer_attributes_result_to_aggregate_not_first_scorefails onmainwithassert 'Violence' in (['Hate'])and passes with the fix.test_float_scale_threshold_scorer_single_score_attribution_unchangedpins the single-score path so this doesn't quietly change common usage.The existing
test_float_scale_threshold_scorer_returns_single_score_with_multi_category_scorercovers this exact code path but only asserts the value and the number of scores returned, which is why it went unnoticed. Its own fixture data (Hate 0.2, Violence 0.0, Sexual 0.8) would have been mis-attributed to Hate.All 1628 tests in
tests/unit/scorepass.ruff format --checkandruff checkclean.Note for reviewers
combine_metadata_and_categoriesmerges constituent metadata withdict.update, so where several scores share a metadata key (azure_severity) the last one wins by iteration order rather than by which score drove the aggregate. That is pre-existing aggregator behaviour and I have not changed it here. If you would prefer the threshold score to attribute to the argmax constituent specifically, rather than to the aggregate as a whole, that is a slightly larger design change and I am happy to do it that way instead.