pyrit/score/score_aggregator.py (48 lines of code) (raw):
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import functools
import operator
from dataclasses import dataclass
from typing import Callable, Iterable, TypeAlias
from pyrit.models import Score
@dataclass(frozen=True, slots=True)
class ScoreAggregatorResult:
value: bool
rationale: str
BinaryBoolOp: TypeAlias = Callable[[bool, bool], bool]
ScoreAggregator: TypeAlias = Callable[[Iterable[Score]], ScoreAggregatorResult]
def _lift_binary(
name: str,
op: BinaryBoolOp,
true_msg: str,
false_msg: str,
) -> ScoreAggregator:
"""
Turn a binary Boolean operator (e.g. operator.and_) into an aggregation function
"""
sep = "-"
def aggregator(scores: Iterable[Score]) -> ScoreAggregatorResult:
scores = list(scores)
result = functools.reduce(op, (s.get_value() for s in scores))
headline = true_msg if result else false_msg
details = "\n".join(f" {sep} {s.score_category}: {s.score_rationale or ''}" for s in scores)
return ScoreAggregatorResult(value=result, rationale=f"{headline}\n{details}")
aggregator.__name__ = f"{name}_"
return aggregator
AND_ = _lift_binary(
"AND",
operator.and_,
"All constituent scorers returned True in an AND composite scorer.",
"At least one constituent scorer returned False in an AND composite scorer.",
)
OR_ = _lift_binary(
"OR",
operator.or_,
"At least one constituent scorer returned True in an OR composite scorer.",
"All constituent scorers returned False in an OR composite scorer.",
)
def MAJORITY_(scores: Iterable[Score]) -> ScoreAggregatorResult:
scores = list(scores)
result = sum(s.get_value() for s in scores) > len(scores) / 2
headline = (
"A strict majority of constituent scorers returned True."
if result
else "A strict majority of constituent scorers did not return True."
)
details = "\n".join(f" - {s.score_category}: {s.score_rationale or ''}" for s in scores)
return ScoreAggregatorResult(value=result, rationale=f"{headline}\n{details}")