|
4 | 4 | from __future__ import annotations |
5 | 5 |
|
6 | 6 | import importlib.util |
| 7 | +import json |
7 | 8 | import sys |
8 | 9 | from collections.abc import Iterator |
9 | 10 | from contextlib import contextmanager |
@@ -934,8 +935,16 @@ def test_benchmark_case_can_run_optional_evaluation( |
934 | 935 | tmp_path: Path, |
935 | 936 | ) -> None: |
936 | 937 | tool = load_tool("measurement_benchmark_tool_evaluate", REPO_ROOT / "tools/measurement/run_benchmarks.py") |
| 938 | + from anonymizer.interface.results import AnonymizerResult |
| 939 | + |
937 | 940 | calls: list[Any] = [] |
938 | | - run_result = object() |
| 941 | + run_result = AnonymizerResult( |
| 942 | + dataframe=pd.DataFrame({"text": ["Alice works at Acme"]}), |
| 943 | + trace_dataframe=pd.DataFrame({"text": ["Alice works at Acme"]}), |
| 944 | + resolved_text_column="text", |
| 945 | + failed_records=[], |
| 946 | + replace_method=None, |
| 947 | + ) |
939 | 948 |
|
940 | 949 | @contextmanager |
941 | 950 | def fake_measurement_session(_config: Any) -> Iterator[None]: |
@@ -974,3 +983,143 @@ def evaluate(self, result: object) -> object: |
974 | 983 | ) |
975 | 984 |
|
976 | 985 | assert calls == [("run", tool.Redact(), "text"), ("evaluate", run_result)] |
| 986 | + |
| 987 | + |
| 988 | +def test_benchmark_optional_evaluation_records_sanitized_judge_metrics(tmp_path: Path) -> None: |
| 989 | + tool = load_tool("measurement_benchmark_tool_evaluate_metrics", REPO_ROOT / "tools/measurement/run_benchmarks.py") |
| 990 | + from anonymizer.interface.results import AnonymizerResult |
| 991 | + |
| 992 | + dangerous_values = [ |
| 993 | + "alice@example.com", |
| 994 | + "bob@example.com", |
| 995 | + "sk-secret-123", |
| 996 | + "replacement-output-secret", |
| 997 | + "nested-malformed-secret", |
| 998 | + "raw judge prompt", |
| 999 | + "raw judge response", |
| 1000 | + ] |
| 1001 | + run_result = AnonymizerResult( |
| 1002 | + dataframe=pd.DataFrame({"text": ["Alice has sk-secret-123"]}), |
| 1003 | + trace_dataframe=pd.DataFrame({"text": ["Alice has sk-secret-123"]}), |
| 1004 | + resolved_text_column="text", |
| 1005 | + failed_records=[], |
| 1006 | + replace_method=None, |
| 1007 | + ) |
| 1008 | + evaluated_public_columns = { |
| 1009 | + "text": ["Alice has sk-secret-123"], |
| 1010 | + "text_replaced": ["Avery has replacement-output-secret"], |
| 1011 | + "final_entities": [[{"value": "alice@example.com", "label": "email"}]], |
| 1012 | + "detection_valid": [False], |
| 1013 | + "detection_invalid_entities": [{"invalid_entities": [{"value": "alice@example.com", "label": "email"}]}], |
| 1014 | + "type_fidelity_valid": [False], |
| 1015 | + "type_fidelity_invalid_replacements": [ |
| 1016 | + {"invalid_replacements": [{"original": "alice@example.com", "synthetic": "bob@example.com"}]} |
| 1017 | + ], |
| 1018 | + "relational_consistency_valid": [False], |
| 1019 | + "relational_consistency_invalid_relations": [{"invalid_relations": [{"reasoning": "raw judge response"}]}], |
| 1020 | + "attribute_fidelity_valid": [False], |
| 1021 | + "attribute_fidelity_invalid_entities": ['[{"entity": "nested-malformed-secret"}'], |
| 1022 | + } |
| 1023 | + evaluated_result = AnonymizerResult( |
| 1024 | + dataframe=pd.DataFrame(evaluated_public_columns), |
| 1025 | + trace_dataframe=pd.DataFrame( |
| 1026 | + { |
| 1027 | + **evaluated_public_columns, |
| 1028 | + "_detection_judge": [ |
| 1029 | + { |
| 1030 | + "prompt": "raw judge prompt", |
| 1031 | + "response": "raw judge response", |
| 1032 | + "invalid_entities": [{"value": "alice@example.com"}], |
| 1033 | + } |
| 1034 | + ], |
| 1035 | + "_type_fidelity_judge": [ |
| 1036 | + {"invalid_replacements": [{"original": "alice@example.com", "synthetic": "bob@example.com"}]} |
| 1037 | + ], |
| 1038 | + } |
| 1039 | + ), |
| 1040 | + resolved_text_column="text", |
| 1041 | + failed_records=[], |
| 1042 | + replace_method=None, |
| 1043 | + ) |
| 1044 | + |
| 1045 | + class FakeAnonymizer: |
| 1046 | + def run(self, *, config: Any, data: Any) -> AnonymizerResult: |
| 1047 | + return run_result |
| 1048 | + |
| 1049 | + def evaluate(self, result: AnonymizerResult) -> AnonymizerResult: |
| 1050 | + assert result is run_result |
| 1051 | + return evaluated_result |
| 1052 | + |
| 1053 | + spec = _minimal_benchmark_spec( |
| 1054 | + tool, |
| 1055 | + suite_id="evaluate-suite", |
| 1056 | + configs=[ |
| 1057 | + tool.ConfigSpec( |
| 1058 | + id="substitute", |
| 1059 | + replace=tool.ReplaceSpec(strategy=tool.ReplaceKind.substitute), |
| 1060 | + evaluate=True, |
| 1061 | + ) |
| 1062 | + ], |
| 1063 | + ) |
| 1064 | + _write_text_input(tmp_path, "Alice has sk-secret-123") |
| 1065 | + case = _minimal_benchmark_case(tool, suite_id="evaluate-suite", config_id="substitute") |
| 1066 | + measurement_path = tmp_path / "raw" / "input__substitute__r000.jsonl" |
| 1067 | + |
| 1068 | + tool._execute_case( |
| 1069 | + FakeAnonymizer(), |
| 1070 | + spec.workloads[0], |
| 1071 | + spec.configs[0], |
| 1072 | + raw_path=measurement_path, |
| 1073 | + trace_path=None, |
| 1074 | + task_trace_path=None, |
| 1075 | + case=case, |
| 1076 | + spec=spec, |
| 1077 | + base_dir=tmp_path, |
| 1078 | + dd_trace=tool.DDTraceMode.none, |
| 1079 | + ) |
| 1080 | + |
| 1081 | + serialized = measurement_path.read_text(encoding="utf-8") |
| 1082 | + rows = [json.loads(line) for line in serialized.splitlines()] |
| 1083 | + evaluation_rows = [row for row in rows if row["record_type"] == "evaluation_record"] |
| 1084 | + |
| 1085 | + assert len(evaluation_rows) == 1 |
| 1086 | + assert { |
| 1087 | + "record_type": "evaluation_record", |
| 1088 | + "mode": "replace", |
| 1089 | + "strategy": "Substitute", |
| 1090 | + "row_index": 0, |
| 1091 | + "detection_valid": False, |
| 1092 | + "detection_invalid_entity_count": 1, |
| 1093 | + "type_fidelity_valid": False, |
| 1094 | + "type_fidelity_invalid_replacement_count": 1, |
| 1095 | + "relational_consistency_valid": False, |
| 1096 | + "relational_consistency_invalid_relation_count": 1, |
| 1097 | + "attribute_fidelity_valid": False, |
| 1098 | + "attribute_fidelity_invalid_entity_count": 0, |
| 1099 | + }.items() <= evaluation_rows[0].items() |
| 1100 | + forbidden_fields = { |
| 1101 | + "text", |
| 1102 | + "text_replaced", |
| 1103 | + "text_with_spans", |
| 1104 | + "final_entities", |
| 1105 | + "detection_invalid_entities", |
| 1106 | + "type_fidelity_invalid_replacements", |
| 1107 | + "relational_consistency_invalid_relations", |
| 1108 | + "attribute_fidelity_invalid_entities", |
| 1109 | + "_detection_judge", |
| 1110 | + "_type_fidelity_judge", |
| 1111 | + "_relational_consistency_judge", |
| 1112 | + "_attribute_fidelity_judge", |
| 1113 | + } |
| 1114 | + assert forbidden_fields.isdisjoint(evaluation_rows[0]) |
| 1115 | + for raw_value in dangerous_values: |
| 1116 | + assert raw_value not in serialized |
| 1117 | + |
| 1118 | + table_dir = tmp_path / "tables" |
| 1119 | + tool.export_measurement_tables(measurement_path, table_dir) |
| 1120 | + exported = pd.read_parquet(table_dir / "evaluation_record.parquet") |
| 1121 | + exported_text = str(exported.to_json(orient="records")) |
| 1122 | + |
| 1123 | + assert forbidden_fields.isdisjoint(exported.columns) |
| 1124 | + for raw_value in dangerous_values: |
| 1125 | + assert raw_value not in exported_text |
0 commit comments