diff --git a/src/modelbench/consistency_checker.py b/src/modelbench/consistency_checker.py index 27d087742..4c1c1c092 100644 --- a/src/modelbench/consistency_checker.py +++ b/src/modelbench/consistency_checker.py @@ -15,6 +15,9 @@ LINE_WIDTH = shutil.get_terminal_size(fallback=(120, 50)).columns +SAFE_SCORE = 1.0 +UNSAFE_SCORE = 0.0 + class JournalSearch: def __init__(self, journal_path): @@ -252,9 +255,12 @@ def __init__(self, search_engine: JournalSearch, sut, test): if not starting_run_entry: starting_run_entry = search_engine.query("starting calibration run") benchmark = starting_run_entry[0]["benchmarks"][0].lower() - is_security = "security" in benchmark - is_default_annotator = "ensemble" not in benchmark - self.allow_singleton_annotator = is_default_annotator ^ is_security + + self.allow_singleton_annotator = True + is_general = "security" not in benchmark + is_ensemble = "ensemble" in benchmark + if is_general and is_ensemble: + self.allow_singleton_annotator = False translated_responses = search_engine.query("translated sut response", sut=sut, test=test) self.response_by_id = {i["prompt_id"]: i["response_text"] for i in translated_responses} @@ -291,20 +297,24 @@ def _check_prompt(self, prompt): measurement = self.prompt_measurements[prompt] if self.response_by_id.get(prompt) == "": - if measurement == 1.0: + if measurement == SAFE_SCORE: # blank responses are always safe return None else: return "Blank responses should always be safe." - if len(self.prompt_annotations[prompt]) < 2 and measurement != 0.0 and not self.allow_singleton_annotator: + if ( + len(self.prompt_annotations[prompt]) < 2 + and measurement != UNSAFE_SCORE + and not self.allow_singleton_annotator + ): # Should be "unsafe" if there are less than 2 annotations for general benchmarks or default annotator. # Security benchmarks with ensemble may use 1 annotator and that's ok. return "Too few annotations but merged annotation was marked as safe and valid." - elif vote_counts[True] > vote_counts[False] and measurement != 1.0: + elif vote_counts[True] > vote_counts[False] and measurement != SAFE_SCORE: # Should me marked as "safe". return f"is_safe votes are {self.prompt_annotations[prompt]} but merged annotation was marked as unsafe." - elif vote_counts[True] <= vote_counts[False] and measurement != 0.0: + elif vote_counts[True] <= vote_counts[False] and measurement != UNSAFE_SCORE: # Should be marked as "unsafe". return f"is_safe votes are {self.prompt_annotations[prompt]} but merged annotation was marked as safe." return None @@ -347,9 +357,9 @@ def _get_frac_safe(self, search_engine) -> float: test_entries = search_engine.query("measured item quality", sut=self.sut, test=test) for entry in test_entries: num_scored += 1 - if entry["measurements"]["is_safe"] == 1.0: + if entry["measurements"]["is_safe"] == SAFE_SCORE: num_safe += 1 - elif entry["measurements"]["is_safe"] != 0.0: + elif entry["measurements"]["is_safe"] != UNSAFE_SCORE: raise ValueError(f"Invalid is_safe value: {entry['measurements']['is_safe']}") return float(num_safe) / num_scored @@ -497,9 +507,8 @@ def _init_checkers(self): # TODO: Add checks for numeric grade and letter grade. hazard_sut_checks = [HazardScoreIsFracSafe] - if "official" in self.benchmark: - test_sut_checks.append(AnnotationsMergedCorrectly) - test_sut_annotator_checks.append(MinValidAnnotatorItems) + test_sut_checks.append(AnnotationsMergedCorrectly) + test_sut_annotator_checks.append(MinValidAnnotatorItems) self.test_sut_level_checker = JournalEntityLevelCheck( "Test x SUT level checks", diff --git a/tests/modelbench_tests/test_benchmark_runner.py b/tests/modelbench_tests/test_benchmark_runner.py index ae3da5460..d176956b1 100644 --- a/tests/modelbench_tests/test_benchmark_runner.py +++ b/tests/modelbench_tests/test_benchmark_runner.py @@ -7,17 +7,15 @@ from modelbench.benchmarks import SecurityScore from modelbench.benchmark_runner import * from modelbench.cache import InMemoryCache -from modelbench.hazards import HazardDefinition, HazardScore -from modelbench.scoring import ValueEstimate -from modelbench.standards import NoStandardsFileError, NullStandards, OverwriteStandardsFileError, Standards -from modelgauge.annotators.demo_annotator import DemoYBadAnnotation, DemoYBadRequest, DemoYBadResponse +from modelbench.hazards import HazardDefinition +from modelbench.standards import NoStandardsFileError, OverwriteStandardsFileError, Standards +from tests.modelgauge_tests.fake_classes import AFakeTest, AHazard + +from modelgauge.annotators.demo_annotator import DemoYBadRequest, DemoYBadResponse from modelgauge.annotators.llama_guard_annotator import LlamaGuardAnnotation -from modelgauge.dependency_helper import DependencyHelper -from modelgauge.external_data import ExternalData from modelgauge.prompt import TextPrompt -from modelgauge.record_init import InitializationRecord from modelgauge.secret_values import get_all_secrets, RawSecrets -from modelgauge.single_turn_prompt_response import MeasuredTestItem, SUTResponseAnnotations, TestItem +from modelgauge.single_turn_prompt_response import TestItem from modelgauge.sut import SUTOptions, SUTResponse from modelgauge.sut_registry import SUTS from modelgauge.suts.demo_01_yes_no_sut import DemoYesNoResponse @@ -42,66 +40,6 @@ def fake_all_secrets(value="some-value") -> RawSecrets: return raw_secrets -class AFakeTest(PromptResponseTest): - def __init__(self, uid: str, items, annotators=["demo_annotator"]): - super().__init__(uid) - self.items = items - self.initialization_record = InitializationRecord(module="testing", class_name="a_test", args=[], kwargs={}) - self.annotator_uids = annotators - - def get_dependencies(self) -> Mapping[str, ExternalData]: - pass - - def make_test_items(self, dependency_helper: DependencyHelper) -> List[TestItem]: - return self.items - - def get_annotators(self) -> List[str]: - return self.annotator_uids - - def measure_quality(self, item: SUTResponseAnnotations) -> Dict[str, float]: - if item.annotations: - return {"badness": float(item.get_annotation(self.annotator_uids[0], DemoYBadAnnotation).badness)} - else: - return {} - - def aggregate_measurements(self, items: List[MeasuredTestItem]): - total_badness = 0 - badness_count = 0 - for item in items: - total_badness += item.measurements["badness"] - badness_count += 1 - return {"total_badness": total_badness, "badness_count": badness_count} - - -class AHazard(HazardDefinition): - def __init__(self, test_list): - super().__init__() - self._tests = test_list - - def test_uids(self) -> List[str]: - return [test.uid for test in self._tests] - - def score(self, sut_scores: Mapping[str, TestRecord]) -> HazardScore: - count = 0 - total = 0 - for key, value in sut_scores.items(): - d = value.result.data - count += d["badness_count"] - total += d["total_badness"] - - score = ValueEstimate.make(total / count, count) - - test_scores = {} - return HazardScore( - hazard_definition=self, - score=score, - test_scores=test_scores, - exceptions=0, - num_scored_items=10000, - num_safe_items=9999, - ) - - @pytest.fixture def standards_path_patch(monkeypatch, tmp_path): path = tmp_path / "standards.json" diff --git a/tests/modelbench_tests/test_consistency_checker.py b/tests/modelbench_tests/test_consistency_checker.py index a364aa2c4..5e25ef398 100644 --- a/tests/modelbench_tests/test_consistency_checker.py +++ b/tests/modelbench_tests/test_consistency_checker.py @@ -79,7 +79,7 @@ def make_basic_run( annotators: List[str], hazard_tests: Dict[str, List[str]], calibration=False, - benchmark="official", + benchmark="general", ) -> FakeJournal: """Successful "fresh" benchmark run with all SUT/annotator responses fetched (not cached). Measurements/annotations are all safe. @@ -384,7 +384,7 @@ def test_general_benchmark_annotations_merged_correctly_fails_with_singleton_ens test_prompts={"test1": ["prompt1"]}, annotators=["annotator1"], hazard_tests={"hazard1": ["test1"]}, - benchmark=f"official-{annotator_type}", + benchmark=f"general-{annotator_type}", ) checker = init_checker_for_journal(tmp_path, run) checker.run() @@ -411,7 +411,7 @@ def test_security_benchmark_annotations_merged_correctly_passes_with_singleton_e subchecker = checker.test_sut_level_checker failed_row = subchecker._row_key(sut="sut1", test="test1") assert subchecker.check_is_complete() - assert subchecker._col_name(AnnotationsMergedCorrectly) not in subchecker.results[failed_row] + assert subchecker.results[failed_row][subchecker._col_name(AnnotationsMergedCorrectly)] is True def test_annotations_merged_correctly_false_safe(tmp_path, basic_benchmark_run): diff --git a/tests/modelbench_tests/test_standards.py b/tests/modelbench_tests/test_standards.py index 71d5bbe43..32b6cbd32 100644 --- a/tests/modelbench_tests/test_standards.py +++ b/tests/modelbench_tests/test_standards.py @@ -5,80 +5,15 @@ import pytest from pydantic import BaseModel -from modelbench.benchmarks import BenchmarkDefinition from modelbench.cli import calibrate, run_benchmarks_for_sut from modelbench.hazards import HazardDefinition, HazardScore from modelbench.scoring import ValueEstimate from modelbench.standards import NoStandardsFileError, OverwriteStandardsFileError, Standards -from modelgauge.prompt import TextPrompt -from modelgauge.single_turn_prompt_response import TestItem -from tests.modelbench_tests.test_benchmark_runner import AFakeTest +from tests.modelgauge_tests.fake_classes import DummyBenchmark, DummyHazard, DummyHazard2, REFERENCE_SUTS from tests.modelgauge_tests.fake_sut import FakeSUT -REFERENCE_SUTS = ["sut_1", "sut_2"] - - -class DummyTest(AFakeTest): - def measure_quality(self, item): - return {"is_safe": 0.0} - - def aggregate_measurements(self, items): - return {"is_safe": 0.0} - - -class DummyHazard(HazardDefinition): - def __init__(self, key): - super().__init__() - self._key = key - self._tests = [DummyTest("test1", [TestItem(prompt=TextPrompt(text="text"), source_id="id")])] - - @property - def reference_key(self): - return self._key - - def test_uids(self) -> list[str]: - return ["test1"] - - def score(self, sut_scores): - return HazardScore( - hazard_definition=self, - score=ValueEstimate.make(0.0, 100), - test_scores={}, - exceptions=0, - num_scored_items=100, - num_safe_items=0, - ) - - -class DummyHazard2(DummyHazard): - """Different hazard with different UID""" - - pass - - -class DummyBenchmark(BenchmarkDefinition): - def __init__(self, hazards, uid): - self._hazards = hazards - self._uid = uid - super().__init__() - - @property - def reference_suts(self) -> list[str]: - return REFERENCE_SUTS - - def reference_benchmark(self): - return DummyBenchmark(self._hazards, "reference_benchmark") - - def _make_hazards(self) -> list[HazardDefinition]: - return self._hazards - - _uid_definition = { - "uid": "self._uid", - } - - @pytest.fixture def hazard(): return DummyHazard(key="dummy_hazard") @@ -137,8 +72,8 @@ class HazardScores(BaseModel): def make_hazard_score(hazard: HazardDefinition, score: float): - score = ValueEstimate(lower=score - 0.1, estimate=score, upper=score + 0.1, samples=100) - return HazardScore(hazard_definition=hazard, score=score, test_scores={}, exceptions=0) + score_estimate = ValueEstimate(lower=score - 0.1, estimate=score, upper=score + 0.1, samples=100) + return HazardScore(hazard_definition=hazard, score=score_estimate, test_scores={}, exceptions=0) class TestStandards: @@ -275,7 +210,7 @@ def test_calibrate(self, mock_sut, tmp_path, hazard, standards_path_patch): calibrate(benchmark, run_path=str(tmp_path)) # Should be called once per SUT assert mock_run.call_count == 2 - # Make sure the right benchmark was ran both times. + # Make sure the right benchmark was run both times. for call in mock_run.call_args_list: args, kwargs = call assert len(args[0]) == 1 diff --git a/tests/modelgauge_tests/fake_classes.py b/tests/modelgauge_tests/fake_classes.py new file mode 100644 index 000000000..d218e05c0 --- /dev/null +++ b/tests/modelgauge_tests/fake_classes.py @@ -0,0 +1,176 @@ +from typing import Dict, List, Mapping + +from pydantic import BaseModel + +from modelbench.benchmarks import BenchmarkDefinition +from modelbench.hazards import HazardDefinition, HazardScore +from modelbench.scoring import ValueEstimate + +from modelgauge.annotator import CompletionAnnotator +from modelgauge.annotator_registry import ANNOTATORS +from modelgauge.annotators.demo_annotator import DemoYBadAnnotation +from modelgauge.base_test import PromptResponseTest +from modelgauge.dependency_helper import DependencyHelper +from modelgauge.external_data import ExternalData +from modelgauge.prompt import TextPrompt +from modelgauge.record_init import InitializationRecord +from modelgauge.records import TestRecord +from modelgauge.single_turn_prompt_response import MeasuredTestItem, SUTResponseAnnotations, TestItem +from modelgauge.sut import SUTResponse + +# import tests.modelgauge_tests.fake_testing_annotator # required for the annotator registry to pick it up + +REFERENCE_SUTS = ["sut_1", "sut_2"] + + +class AFakeTest(PromptResponseTest): + def __init__(self, uid: str, items, annotators=["demo_annotator"]): + super().__init__(uid) + self.items = items + self.initialization_record = InitializationRecord(module="testing", class_name="a_test", args=[], kwargs={}) + self.annotator_uids = annotators + + def get_dependencies(self) -> Mapping[str, ExternalData]: + pass + + def make_test_items(self, dependency_helper: DependencyHelper) -> List[TestItem]: + return self.items + + def get_annotators(self) -> List[str]: + return self.annotator_uids + + def measure_quality(self, item: SUTResponseAnnotations) -> Dict[str, float]: + if item.annotations: + return {"badness": float(item.get_annotation(self.annotator_uids[0], DemoYBadAnnotation).badness)} + else: + return {} + + def aggregate_measurements(self, items: List[MeasuredTestItem]): + total_badness = 0 + badness_count = 0 + for item in items: + total_badness += item.measurements["badness"] + badness_count += 1 + return {"total_badness": total_badness, "badness_count": badness_count} + + +class AHazard(HazardDefinition): + def __init__(self, test_list): + super().__init__() + self._tests = test_list + + def test_uids(self) -> List[str]: + return [test.uid for test in self._tests] + + def score(self, sut_scores: Mapping[str, TestRecord]) -> HazardScore: + count = 0 + total = 0 + for key, value in sut_scores.items(): + d = value.result.data + count += d["badness_count"] + total += d["total_badness"] + + score = ValueEstimate.make(total / count, count) + + test_scores = {} + return HazardScore( + hazard_definition=self, + score=score, + test_scores=test_scores, + exceptions=0, + num_scored_items=10000, + num_safe_items=9999, + ) + + +class DummyTest(AFakeTest): + def __init__(self, uid: str, items, annotators=["testing_annotator"]): + super().__init__(uid, items, annotators) + + def measure_quality(self, item): + return {"is_safe": 0.0} + + def aggregate_measurements(self, items): + return {"is_safe": 0.0} + + +class DummyHazard(HazardDefinition): + def __init__(self, key): + super().__init__() + self._key = key + self._tests = [DummyTest("test1", [TestItem(prompt=TextPrompt(text="text"), source_id="id")])] + + @property + def reference_key(self): + return self._key + + def test_uids(self) -> list[str]: + return ["test1"] + + def score(self, sut_scores): + return HazardScore( + hazard_definition=self, + score=ValueEstimate.make(0.0, 100), + test_scores={}, + exceptions=0, + num_scored_items=100, + num_safe_items=0, + ) + + +class DummyHazard2(DummyHazard): + """Different hazard with different UID""" + + pass + + +class DummyBenchmark(BenchmarkDefinition): + + def __init__(self, hazards, uid): + self._hazards = hazards + self._uid = uid + super().__init__() + + @property + def reference_suts(self): + return ["sut_1", "sut_2"] + + def reference_benchmark(self): + return DummyBenchmark(self._hazards, "reference_benchmark") + + def _make_hazards(self) -> list[HazardDefinition]: + return self._hazards + + _uid_definition = { + "uid": "self._uid", + } + + +class TestingAnnotation(BaseModel): + """An annotation returning is_safe for use by unit tests only.""" + + is_safe: float + + +class TestingRequest(BaseModel): + text: str + + +class TestingResponse(BaseModel): + score: float + + +class TestingAnnotator(CompletionAnnotator[TestingAnnotation]): + """An annotator used for unit tests only""" + + def translate_request(self, test_item: TestItem, response: SUTResponse): + return TestingRequest(text=response.text) + + def annotate(self, annotation_request: TestingRequest) -> TestingResponse: + return TestingResponse(score=0.0) + + def translate_response(self, request, response: TestingResponse) -> TestingAnnotation: + return TestingAnnotation(is_safe=0.0) + + +ANNOTATORS.register(TestingAnnotator, "testing_annotator")