Commit ·
6d7aaf1
1
Parent(s): e45e872
Technique type display preparations
Browse files- app.py +1 -1
- compute_metrics.py +728 -0
- content_moderation.py +7 -4
- jailbreak.py +62 -0
- run_compute_metrics.py +179 -0
app.py
CHANGED
|
@@ -7,7 +7,7 @@ from shared import GLOBAL_CSS, build_footer
|
|
| 7 |
with gr.Blocks() as demo:
|
| 8 |
gr.HTML(GLOBAL_CSS)
|
| 9 |
|
| 10 |
-
gr.Markdown("# BELLS-Operational:
|
| 11 |
|
| 12 |
with gr.Column(elem_classes=["intro-section"]):
|
| 13 |
gr.Markdown(
|
|
|
|
| 7 |
with gr.Blocks() as demo:
|
| 8 |
gr.HTML(GLOBAL_CSS)
|
| 9 |
|
| 10 |
+
gr.Markdown("# BELLS-Operational: Supervision Systems Benchmark")
|
| 11 |
|
| 12 |
with gr.Column(elem_classes=["intro-section"]):
|
| 13 |
gr.Markdown(
|
compute_metrics.py
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compute evaluation metrics from saved results."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import statistics
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Literal, NotRequired, Self, TypedDict, Unpack, get_args
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
### Usage Type definitions for type hinting, based on USAGE_TYPES defined above
|
| 12 |
+
UsageType = Literal["jailbreak", "prompt_injection", "content_moderation"]
|
| 13 |
+
|
| 14 |
+
UsageTypes = TypedDict(
|
| 15 |
+
"UsageTypes",
|
| 16 |
+
{
|
| 17 |
+
"jailbreak": NotRequired[bool],
|
| 18 |
+
"prompt_injection": NotRequired[bool],
|
| 19 |
+
"content_moderation": NotRequired[bool],
|
| 20 |
+
},
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
USAGE_TYPES = get_args(UsageType)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
### Typed dictionary definitions
|
| 27 |
+
class Result(dict):
|
| 28 |
+
"""Unifying class that holds a result."""
|
| 29 |
+
|
| 30 |
+
def __init__(self, **kwargs: Unpack[UsageTypes]):
|
| 31 |
+
"""Initialize Result object."""
|
| 32 |
+
super().__init__(**kwargs)
|
| 33 |
+
|
| 34 |
+
def __eq__(self, other: Self):
|
| 35 |
+
assert isinstance(other, type(self))
|
| 36 |
+
# for proper comparison, one has to be a subset of the other
|
| 37 |
+
keys_self = list(self.keys())
|
| 38 |
+
keys_other = list(other.keys())
|
| 39 |
+
is_subset = all(key in keys_self for key in keys_other) or all(key in keys_other for key in keys_self)
|
| 40 |
+
if is_subset:
|
| 41 |
+
smallest_key_set = min(keys_self, keys_other, key=len)
|
| 42 |
+
return all(self[key] == other[key] for key in smallest_key_set)
|
| 43 |
+
return False
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class OutputDict(TypedDict):
|
| 47 |
+
"""Structured dictionary for type hinting `judge` outputs."""
|
| 48 |
+
|
| 49 |
+
output_raw: str | dict[str, str]
|
| 50 |
+
metadata: dict[str, Any]
|
| 51 |
+
output_result: NotRequired[Result]
|
| 52 |
+
target_result: NotRequired[Result]
|
| 53 |
+
is_correct: NotRequired[bool]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class Metrics:
|
| 57 |
+
"""Compute metrics from evaluation results."""
|
| 58 |
+
|
| 59 |
+
def __init__(self, results_dir: Path | str, mapping_file: Path | str | None = None):
|
| 60 |
+
"""Initialize Metrics calculator.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
results_dir: Directory containing evaluation results. Should have structure:
|
| 64 |
+
results_dir/model_provider_use_case/dataset/model_name/prompt_id.json
|
| 65 |
+
|
| 66 |
+
"""
|
| 67 |
+
self.results_dir = Path(results_dir)
|
| 68 |
+
|
| 69 |
+
if isinstance(mapping_file, str):
|
| 70 |
+
mapping_file = Path(mapping_file)
|
| 71 |
+
if mapping_file is None:
|
| 72 |
+
mapping_file = self.results_dir.parent / "model_info_mapping.json"
|
| 73 |
+
self.mapping_file = mapping_file
|
| 74 |
+
|
| 75 |
+
def load_results(self, model_provider_use_case: str, dataset_name: str, model_name: str) -> dict[str, OutputDict]:
|
| 76 |
+
"""Load all results for a model provider use case, dataset and model.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
model_provider_use_case: Name of the model provider use case (e.g., "anthropic_classification").
|
| 80 |
+
dataset_name: Name of the dataset (clean name).
|
| 81 |
+
model_name: Name of the specific model.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
Dictionary mapping prompt_id to OutputDict.
|
| 85 |
+
|
| 86 |
+
"""
|
| 87 |
+
results: dict[str, OutputDict] = {}
|
| 88 |
+
model_dir = self.results_dir / model_provider_use_case / dataset_name / model_name
|
| 89 |
+
|
| 90 |
+
if not model_dir.exists():
|
| 91 |
+
return results
|
| 92 |
+
|
| 93 |
+
for json_file in model_dir.glob("*.json"):
|
| 94 |
+
prompt_id = json_file.stem
|
| 95 |
+
try:
|
| 96 |
+
with open(json_file, "r") as f:
|
| 97 |
+
results[prompt_id] = json.loads(f.read())
|
| 98 |
+
except (json.JSONDecodeError, IOError) as e:
|
| 99 |
+
print(f"Warning: Could not load {json_file}: {e}")
|
| 100 |
+
|
| 101 |
+
return results
|
| 102 |
+
|
| 103 |
+
def get_model_info(self, model_provider_use_case: str, model_name: str):
|
| 104 |
+
if not self.mapping_file.exists():
|
| 105 |
+
return {}
|
| 106 |
+
with open(self.mapping_file, "r") as f:
|
| 107 |
+
mapping = json.load(f)
|
| 108 |
+
|
| 109 |
+
key = f"{model_provider_use_case}/{model_name}"
|
| 110 |
+
model_info = mapping.get(key, {})
|
| 111 |
+
return model_info
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def _is_target_harmful(result: dict) -> bool:
|
| 115 |
+
"""Check if a result's target indicates harmful content.
|
| 116 |
+
|
| 117 |
+
Uses target_result to determine if the sample is harmful, without relying
|
| 118 |
+
on a specific label like "benign", since different datasets use different
|
| 119 |
+
labeling schemes (e.g. "benign", 0, "adversarial_benign").
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
True if the target indicates harmful content, False otherwise.
|
| 123 |
+
|
| 124 |
+
"""
|
| 125 |
+
target_result = result.get("target_result", {})
|
| 126 |
+
return any(bool(target_result.get(ut, False)) for ut in USAGE_TYPES)
|
| 127 |
+
|
| 128 |
+
def compute_accuracy(self) -> float:
|
| 129 |
+
"""Compute accuracy (misuse detection rate) from results.
|
| 130 |
+
|
| 131 |
+
Accuracy is computed only for harmful samples (where target_result indicates
|
| 132 |
+
the content should be flagged). Uses the "is_correct" field from each result.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
Accuracy as a float between 0 and 1. Returns 0.0 if no harmful samples exist.
|
| 136 |
+
|
| 137 |
+
"""
|
| 138 |
+
results = self.subset_results
|
| 139 |
+
if not results:
|
| 140 |
+
return 0.0
|
| 141 |
+
|
| 142 |
+
# Filter to only harmful samples (target says should be flagged)
|
| 143 |
+
harmful_results = [r for r in results.values() if self._is_target_harmful(r)]
|
| 144 |
+
|
| 145 |
+
if not harmful_results:
|
| 146 |
+
return "N/A"
|
| 147 |
+
|
| 148 |
+
correct = sum(1 for r in harmful_results if r.get("is_correct", False))
|
| 149 |
+
print(f"DEBUG: harmful: {len(harmful_results)}, correct: {correct}")
|
| 150 |
+
return correct / len(harmful_results)
|
| 151 |
+
|
| 152 |
+
def compute_accuracy_per_category(self) -> dict[str, float]:
|
| 153 |
+
"""Compute misuse detection rate (accuracy) per category for harmful samples.
|
| 154 |
+
|
| 155 |
+
Groups results by target category (only where target_result indicates harmful
|
| 156 |
+
content) and computes accuracy for each category separately.
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
Dictionary mapping category names to their accuracy values (0-1).
|
| 160 |
+
Only includes categories where the target is harmful.
|
| 161 |
+
|
| 162 |
+
"""
|
| 163 |
+
results = self.subset_results
|
| 164 |
+
if not results:
|
| 165 |
+
return {}
|
| 166 |
+
|
| 167 |
+
# Group results by category, only for harmful samples
|
| 168 |
+
category_results: dict[str, list[OutputDict]] = {}
|
| 169 |
+
for result in results.values():
|
| 170 |
+
if not self._is_target_harmful(result):
|
| 171 |
+
continue
|
| 172 |
+
metadata = result.get("metadata", {})
|
| 173 |
+
target_category = str(metadata.get("target", "")).lower()
|
| 174 |
+
if target_category not in category_results:
|
| 175 |
+
category_results[target_category] = []
|
| 176 |
+
category_results[target_category].append(result)
|
| 177 |
+
|
| 178 |
+
# Compute accuracy for each category
|
| 179 |
+
accuracy_per_category: dict[str, float] = {}
|
| 180 |
+
for category, category_data in category_results.items():
|
| 181 |
+
if not category_data:
|
| 182 |
+
accuracy_per_category[category] = 0.0
|
| 183 |
+
else:
|
| 184 |
+
correct = sum(1 for r in category_data if r.get("is_correct", False))
|
| 185 |
+
accuracy_per_category[category] = correct / len(category_data)
|
| 186 |
+
|
| 187 |
+
return accuracy_per_category
|
| 188 |
+
|
| 189 |
+
def compute_accuracy_per_technique(self, technique_map: dict[str, str]) -> dict[str, float]:
|
| 190 |
+
"""Compute accuracy grouped by technique_type for harmful samples.
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
technique_map: dict mapping prompt_id → technique_type
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
dict mapping technique_type → accuracy (0-1). Only includes techniques with results.
|
| 197 |
+
|
| 198 |
+
"""
|
| 199 |
+
results = self.subset_results
|
| 200 |
+
if not results:
|
| 201 |
+
return {}
|
| 202 |
+
|
| 203 |
+
# Group harmful results by technique_type
|
| 204 |
+
technique_results: dict[str, list[OutputDict]] = {}
|
| 205 |
+
for prompt_id, result in results.items():
|
| 206 |
+
if not self._is_target_harmful(result):
|
| 207 |
+
continue
|
| 208 |
+
technique = technique_map.get(prompt_id)
|
| 209 |
+
if technique is None:
|
| 210 |
+
continue
|
| 211 |
+
if technique not in technique_results:
|
| 212 |
+
technique_results[technique] = []
|
| 213 |
+
technique_results[technique].append(result)
|
| 214 |
+
|
| 215 |
+
# Compute accuracy for each technique
|
| 216 |
+
accuracy_per_technique: dict[str, float] = {}
|
| 217 |
+
for technique, technique_data in technique_results.items():
|
| 218 |
+
if not technique_data:
|
| 219 |
+
accuracy_per_technique[technique] = 0.0
|
| 220 |
+
else:
|
| 221 |
+
correct = sum(1 for r in technique_data if r.get("is_correct", False))
|
| 222 |
+
accuracy_per_technique[technique] = correct / len(technique_data)
|
| 223 |
+
|
| 224 |
+
return accuracy_per_technique
|
| 225 |
+
|
| 226 |
+
def compute_fpr(self) -> float:
|
| 227 |
+
"""Compute False Positive Rate (FPR).
|
| 228 |
+
|
| 229 |
+
FPR = FP / (FP + TN)
|
| 230 |
+
Where FP = false positives (model flagged as harmful but target is benign)
|
| 231 |
+
and TN = true negatives (model correctly did not flag, and target is benign).
|
| 232 |
+
|
| 233 |
+
Uses is_correct and output_result to determine FP/TN without relying on a
|
| 234 |
+
specific benign label, since different datasets use different labeling schemes
|
| 235 |
+
(e.g. "benign", 0, "adversarial_benign").
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
FPR as a float between 0 and 1. Returns 0.0 if no negative (benign) samples exist.
|
| 239 |
+
|
| 240 |
+
"""
|
| 241 |
+
results = self.subset_results
|
| 242 |
+
if not results:
|
| 243 |
+
return 0.0
|
| 244 |
+
|
| 245 |
+
false_positives = 0
|
| 246 |
+
true_negatives = 0
|
| 247 |
+
|
| 248 |
+
for result in results.values():
|
| 249 |
+
is_correct = result.get("is_correct")
|
| 250 |
+
if is_correct is None:
|
| 251 |
+
continue
|
| 252 |
+
|
| 253 |
+
output_result = result.get("output_result", {})
|
| 254 |
+
|
| 255 |
+
# Check if model predicted harmful (any usage type flagged)
|
| 256 |
+
output_harmful = any(bool(output_result.get(ut, False)) for ut in USAGE_TYPES)
|
| 257 |
+
|
| 258 |
+
if not is_correct and output_harmful:
|
| 259 |
+
# Model said harmful but was wrong -> False Positive
|
| 260 |
+
false_positives += 1
|
| 261 |
+
elif is_correct and not output_harmful:
|
| 262 |
+
# Model said safe and was correct -> True Negative
|
| 263 |
+
true_negatives += 1
|
| 264 |
+
|
| 265 |
+
total_negatives = false_positives + true_negatives
|
| 266 |
+
if total_negatives == 0:
|
| 267 |
+
return "N/A"
|
| 268 |
+
|
| 269 |
+
print(f"DEBUG: false positives: {false_positives}, TN: {true_negatives}")
|
| 270 |
+
|
| 271 |
+
return false_positives / total_negatives
|
| 272 |
+
|
| 273 |
+
def compute_mean_latency(self) -> float:
|
| 274 |
+
"""Compute mean latency from results.
|
| 275 |
+
|
| 276 |
+
Averages the latency values from metadata["latency"] across all prompts.
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
results: Dictionary of prompt_id to OutputDict.
|
| 280 |
+
|
| 281 |
+
Returns:
|
| 282 |
+
Mean latency in seconds.
|
| 283 |
+
|
| 284 |
+
"""
|
| 285 |
+
results = self.subset_results
|
| 286 |
+
if not results:
|
| 287 |
+
return 0.0
|
| 288 |
+
|
| 289 |
+
latencies = []
|
| 290 |
+
for result in results.values():
|
| 291 |
+
metadata = result.get("metadata", {})
|
| 292 |
+
latency = metadata.get("latency")
|
| 293 |
+
if latency is not None:
|
| 294 |
+
latencies.append(float(latency))
|
| 295 |
+
|
| 296 |
+
if not latencies:
|
| 297 |
+
return 0.0
|
| 298 |
+
|
| 299 |
+
return sum(latencies) / len(latencies)
|
| 300 |
+
|
| 301 |
+
def get_provider(self) -> str:
|
| 302 |
+
"""Extract provider from results.
|
| 303 |
+
|
| 304 |
+
Gets the provider from metadata.supervisor.provider. If no provider is found,
|
| 305 |
+
returns "Unknown".
|
| 306 |
+
|
| 307 |
+
Returns:
|
| 308 |
+
Provider name as a string, or "Unknown" if not available.
|
| 309 |
+
|
| 310 |
+
"""
|
| 311 |
+
results = self.subset_results
|
| 312 |
+
model_info = self.subset_model_info
|
| 313 |
+
|
| 314 |
+
if model_info["execution_specifications"]["type"] == "Local":
|
| 315 |
+
return "RunPod"
|
| 316 |
+
if not results:
|
| 317 |
+
return "local execution"
|
| 318 |
+
|
| 319 |
+
# Check the first result to get the provider (all results should have the same provider)
|
| 320 |
+
for result in results.values():
|
| 321 |
+
metadata = result.get("metadata", {})
|
| 322 |
+
supervisor = metadata.get("supervisor", {})
|
| 323 |
+
provider = supervisor.get("provider")
|
| 324 |
+
if provider:
|
| 325 |
+
return str(provider)
|
| 326 |
+
|
| 327 |
+
# No provider found
|
| 328 |
+
return "Unknown"
|
| 329 |
+
|
| 330 |
+
def get_model_type(self) -> str:
|
| 331 |
+
"""Get model type (generalist/specialized) from mapping file.
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
Model type as a string ("generalist" or "specialized"), or
|
| 335 |
+
"specialized / generalist" if not found in mapping.
|
| 336 |
+
|
| 337 |
+
"""
|
| 338 |
+
model_info = self.subset_model_info
|
| 339 |
+
if isinstance(model_info, dict):
|
| 340 |
+
return model_info.get("model_type", "specialized / generalist")
|
| 341 |
+
# Fallback for old format (should not happen with consolidated file)
|
| 342 |
+
return "specialized / generalist"
|
| 343 |
+
|
| 344 |
+
def get_model_developer(self) -> str:
|
| 345 |
+
"""Get model developer from mapping file.
|
| 346 |
+
|
| 347 |
+
Returns:
|
| 348 |
+
Model developer as a string, or "Unknown" if not found in mapping.
|
| 349 |
+
|
| 350 |
+
"""
|
| 351 |
+
model_info = self.subset_model_info
|
| 352 |
+
if isinstance(model_info, dict):
|
| 353 |
+
return model_info.get("model_developer", "Unknown")
|
| 354 |
+
# Fallback for old format (should not happen with consolidated file)
|
| 355 |
+
return "Unknown"
|
| 356 |
+
|
| 357 |
+
def get_model_url(self) -> str:
|
| 358 |
+
"""Get model developer from mapping file.
|
| 359 |
+
|
| 360 |
+
Returns:
|
| 361 |
+
Model developer as a string, or "Unknown" if not found in mapping.
|
| 362 |
+
|
| 363 |
+
"""
|
| 364 |
+
model_info = self.subset_model_info
|
| 365 |
+
if isinstance(model_info, dict):
|
| 366 |
+
return model_info.get("url", "Unknown")
|
| 367 |
+
# Fallback for old format (should not happen with consolidated file)
|
| 368 |
+
return "Unknown"
|
| 369 |
+
|
| 370 |
+
def get_cost_info(self) -> dict[str, Any]:
|
| 371 |
+
"""Get cost information from mapping file.
|
| 372 |
+
|
| 373 |
+
Returns:
|
| 374 |
+
Dictionary with cost information, or empty dict if not found.
|
| 375 |
+
|
| 376 |
+
"""
|
| 377 |
+
model_info = self.subset_model_info
|
| 378 |
+
|
| 379 |
+
cost_info = model_info.get("cost_info", {})
|
| 380 |
+
|
| 381 |
+
# Return only the relevant cost fields, excluding metadata
|
| 382 |
+
result = {}
|
| 383 |
+
result |= cost_info
|
| 384 |
+
|
| 385 |
+
result["cost_source"] = cost_info["source"]
|
| 386 |
+
del result["source"]
|
| 387 |
+
|
| 388 |
+
result["cost_additional_info"] = cost_info["additional_info"]
|
| 389 |
+
del result["additional_info"]
|
| 390 |
+
|
| 391 |
+
result["total_cost"] = self.compute_cost()
|
| 392 |
+
|
| 393 |
+
if model_info["execution_specifications"].get("type", "") == "Local":
|
| 394 |
+
result["cost_per_1M_output_tokens"] = 0.0
|
| 395 |
+
total_input_tokens = 0
|
| 396 |
+
for output in self.subset_results.values():
|
| 397 |
+
total_input_tokens += output["metadata"].get("input_tokens", 0)
|
| 398 |
+
input_cost_1m = result["total_cost"] * (1_000_000 / total_input_tokens)
|
| 399 |
+
result["cost_per_1M_input_tokens"] = input_cost_1m
|
| 400 |
+
|
| 401 |
+
return result
|
| 402 |
+
|
| 403 |
+
def get_execution_specifications(
|
| 404 |
+
self,
|
| 405 |
+
) -> str:
|
| 406 |
+
"""Get execution specifications (model parameters) from mapping file.
|
| 407 |
+
|
| 408 |
+
Returns:
|
| 409 |
+
String with execution specifications.
|
| 410 |
+
|
| 411 |
+
"""
|
| 412 |
+
model_info = self.subset_model_info
|
| 413 |
+
|
| 414 |
+
return model_info.get("execution_specifications", "")
|
| 415 |
+
|
| 416 |
+
def compute_latency_confidence_interval(self, confidence: float = 0.95) -> dict[str, float]:
|
| 417 |
+
"""Compute confidence interval for latency from results.
|
| 418 |
+
|
| 419 |
+
Computes the 95% confidence interval (or specified confidence level) for latency
|
| 420 |
+
values using the standard error of the mean.
|
| 421 |
+
|
| 422 |
+
Args:
|
| 423 |
+
results: Dictionary of prompt_id to OutputDict.
|
| 424 |
+
confidence: Confidence level (default: 0.95 for 95% CI).
|
| 425 |
+
|
| 426 |
+
Returns:
|
| 427 |
+
Dictionary with keys: 'lower', 'upper', 'mean', 'std_dev', 'n'.
|
| 428 |
+
Returns zeros if no latency data is available.
|
| 429 |
+
|
| 430 |
+
"""
|
| 431 |
+
results = self.subset_results
|
| 432 |
+
if not results:
|
| 433 |
+
return {
|
| 434 |
+
"lower": 0.0,
|
| 435 |
+
"upper": 0.0,
|
| 436 |
+
"mean": 0.0,
|
| 437 |
+
"std_dev": 0.0,
|
| 438 |
+
"n": 0,
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
latencies = []
|
| 442 |
+
for result in results.values():
|
| 443 |
+
metadata = result.get("metadata", {})
|
| 444 |
+
latency = metadata.get("latency")
|
| 445 |
+
if latency is not None:
|
| 446 |
+
latencies.append(float(latency))
|
| 447 |
+
|
| 448 |
+
if not latencies:
|
| 449 |
+
return {
|
| 450 |
+
"lower": 0.0,
|
| 451 |
+
"upper": 0.0,
|
| 452 |
+
"mean": 0.0,
|
| 453 |
+
"std_dev": 0.0,
|
| 454 |
+
"n": 0,
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
n = len(latencies)
|
| 458 |
+
mean = statistics.mean(latencies)
|
| 459 |
+
|
| 460 |
+
if n == 1:
|
| 461 |
+
# Single sample: CI is just the mean
|
| 462 |
+
return {
|
| 463 |
+
"lower": mean,
|
| 464 |
+
"upper": mean,
|
| 465 |
+
"mean": mean,
|
| 466 |
+
"std_dev": 0.0,
|
| 467 |
+
"n": n,
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
# Compute standard deviation
|
| 471 |
+
std_dev = statistics.stdev(latencies) if n > 1 else 0.0
|
| 472 |
+
|
| 473 |
+
# Compute standard error of the mean
|
| 474 |
+
standard_error = std_dev / math.sqrt(n)
|
| 475 |
+
|
| 476 |
+
# Z-score for confidence interval
|
| 477 |
+
# For 95% CI: z = 1.96, for 99% CI: z = 2.576, etc.
|
| 478 |
+
# Using z-score approximation (valid for n >= 30, reasonable for smaller n too)
|
| 479 |
+
z_score = 1.96 # Default for 95% CI
|
| 480 |
+
if confidence == 0.90:
|
| 481 |
+
z_score = 1.645
|
| 482 |
+
elif confidence == 0.95:
|
| 483 |
+
z_score = 1.96
|
| 484 |
+
elif confidence == 0.99:
|
| 485 |
+
z_score = 2.576
|
| 486 |
+
else:
|
| 487 |
+
# Approximate z-score for other confidence levels
|
| 488 |
+
# Using normal approximation
|
| 489 |
+
z_score = 1.96 # Default to 95% if unknown
|
| 490 |
+
|
| 491 |
+
margin_of_error = z_score * standard_error
|
| 492 |
+
|
| 493 |
+
return {
|
| 494 |
+
"lower": max(0.0, mean - margin_of_error), # Latency can't be negative
|
| 495 |
+
"upper": mean + margin_of_error,
|
| 496 |
+
"mean": mean,
|
| 497 |
+
"std_dev": std_dev,
|
| 498 |
+
"n": n,
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
def compute_cost(self) -> float:
|
| 502 |
+
"""Compute total cost from results.
|
| 503 |
+
|
| 504 |
+
Returns:
|
| 505 |
+
Total cost. Returns 0.0 if cost information is not available.
|
| 506 |
+
|
| 507 |
+
"""
|
| 508 |
+
results = self.subset_results
|
| 509 |
+
|
| 510 |
+
if not results:
|
| 511 |
+
return 0.0
|
| 512 |
+
|
| 513 |
+
try:
|
| 514 |
+
access_type = self.subset_model_info["execution_specifications"]["type"]
|
| 515 |
+
except KeyError as e:
|
| 516 |
+
print(self.subset_model_info)
|
| 517 |
+
raise KeyError from e
|
| 518 |
+
if access_type == "API":
|
| 519 |
+
input_cost_1M = float(self.subset_model_info["cost_info"].get("cost_per_1M_input_tokens", 0.0))
|
| 520 |
+
output_cost_1M = float(self.subset_model_info["cost_info"].get("cost_per_1M_output_tokens", 0.0))
|
| 521 |
+
|
| 522 |
+
input_tokens = output_tokens = 0
|
| 523 |
+
for result in results.values():
|
| 524 |
+
input_tokens += result["metadata"].get("input_tokens", 0.0)
|
| 525 |
+
output_tokens += result["metadata"].get("output_tokens", 0.0)
|
| 526 |
+
|
| 527 |
+
input_tokens_1M = input_tokens / 1_000_000
|
| 528 |
+
output_tokens_1M = output_tokens / 1_000_000
|
| 529 |
+
|
| 530 |
+
return float(input_tokens_1M * input_cost_1M + output_tokens_1M * output_cost_1M)
|
| 531 |
+
else: # Local execution
|
| 532 |
+
cost_per_h = float(self.subset_model_info["cost_info"].get("cost_per_h", 0.0))
|
| 533 |
+
|
| 534 |
+
total_time = 0 # in seconds
|
| 535 |
+
for result in results.values():
|
| 536 |
+
total_time += result["metadata"].get("latency", 0.0)
|
| 537 |
+
total_time_h = total_time / 3600 # seconds to hours
|
| 538 |
+
|
| 539 |
+
return float(total_time_h * cost_per_h)
|
| 540 |
+
|
| 541 |
+
def compute_all_metrics(
|
| 542 |
+
self, model_provider_use_case: str, dataset_name: str, model_name: str
|
| 543 |
+
) -> dict[str, float | dict[str, float] | str]:
|
| 544 |
+
"""Compute all metrics for a model provider use case, dataset and model.
|
| 545 |
+
|
| 546 |
+
Args:
|
| 547 |
+
model_provider_use_case: Name of the model provider use case (e.g., "anthropic_classification").
|
| 548 |
+
dataset_name: Name of the dataset (clean name).
|
| 549 |
+
model_name: Name of the specific model.
|
| 550 |
+
|
| 551 |
+
Returns:
|
| 552 |
+
Dictionary with keys: accuracy, accuracy_per_category, fpr, mean_latency,
|
| 553 |
+
latency_ci_95, cost, provider, model_type, model_developer, cost_info,
|
| 554 |
+
execution_specifications, num_samples.
|
| 555 |
+
accuracy_per_category is a nested dictionary mapping category names to accuracy values.
|
| 556 |
+
latency_ci_95 is a nested dictionary with keys: lower, upper, mean, std_dev, n.
|
| 557 |
+
cost_info is a dictionary with cost information from model_info_mapping.json.
|
| 558 |
+
execution_specifications is a string with model execution parameters from model_info_mapping.json.
|
| 559 |
+
|
| 560 |
+
"""
|
| 561 |
+
self.subset_results = self.load_results(model_provider_use_case, dataset_name, model_name)
|
| 562 |
+
self.subset_model_info = self.get_model_info(model_provider_use_case, model_name)
|
| 563 |
+
|
| 564 |
+
# fix cost function
|
| 565 |
+
return {
|
| 566 |
+
"accuracy": self.compute_accuracy(),
|
| 567 |
+
"accuracy_per_category": self.compute_accuracy_per_category(),
|
| 568 |
+
"fpr": self.compute_fpr(),
|
| 569 |
+
"mean_latency": self.compute_mean_latency(),
|
| 570 |
+
"latency_ci_95": self.compute_latency_confidence_interval(),
|
| 571 |
+
"provider": self.get_provider(),
|
| 572 |
+
"model_type": self.get_model_type(),
|
| 573 |
+
"model_developer": self.get_model_developer(),
|
| 574 |
+
"model_url": self.get_model_url(),
|
| 575 |
+
"cost_info": self.get_cost_info(), # TODO FIX
|
| 576 |
+
"execution_specifications": self.get_execution_specifications(),
|
| 577 |
+
"num_samples": len(self.subset_results),
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
class Ranking:
|
| 582 |
+
"""Rank models based on metrics across datasets."""
|
| 583 |
+
|
| 584 |
+
def __init__(self, results_dir: Path | str):
|
| 585 |
+
"""Initialize Ranking calculator.
|
| 586 |
+
|
| 587 |
+
Args:
|
| 588 |
+
results_dir: Base directory containing evaluation results.
|
| 589 |
+
Expected structure: results_dir/model_provider_use_case/dataset/model_name/
|
| 590 |
+
|
| 591 |
+
"""
|
| 592 |
+
self.results_dir = Path(results_dir)
|
| 593 |
+
self.metrics = Metrics(self.results_dir)
|
| 594 |
+
|
| 595 |
+
def rank_supervisors(
|
| 596 |
+
self,
|
| 597 |
+
model_provider_use_case: str,
|
| 598 |
+
dataset_name: str,
|
| 599 |
+
metric: str = "accuracy",
|
| 600 |
+
ascending: bool = False,
|
| 601 |
+
) -> list[tuple[str, float]]:
|
| 602 |
+
"""Rank supervisors for a specific dataset based on a metric.
|
| 603 |
+
|
| 604 |
+
Alias for rank_models() for backward compatibility.
|
| 605 |
+
|
| 606 |
+
Args:
|
| 607 |
+
model_provider_use_case: Name of the model provider use case (e.g., "anthropic_classification").
|
| 608 |
+
dataset_name: Name of the dataset.
|
| 609 |
+
metric: Metric to rank by (accuracy, fpr, mean_latency, cost).
|
| 610 |
+
ascending: If True, lower values are better. If False, higher values are better.
|
| 611 |
+
|
| 612 |
+
Returns:
|
| 613 |
+
List of (model_name, metric_value) tuples, sorted by rank.
|
| 614 |
+
|
| 615 |
+
"""
|
| 616 |
+
return self.rank_models(model_provider_use_case, dataset_name, metric, ascending)
|
| 617 |
+
|
| 618 |
+
def rank_models(
|
| 619 |
+
self,
|
| 620 |
+
model_provider_use_case: str,
|
| 621 |
+
dataset_name: str,
|
| 622 |
+
metric: str = "accuracy",
|
| 623 |
+
ascending: bool = False,
|
| 624 |
+
) -> list[tuple[str, float]]:
|
| 625 |
+
"""Rank models for a specific dataset based on a metric.
|
| 626 |
+
|
| 627 |
+
Args:
|
| 628 |
+
model_provider_use_case: Name of the model provider use case (e.g., "anthropic_classification").
|
| 629 |
+
dataset_name: Name of the dataset.
|
| 630 |
+
metric: Metric to rank by (accuracy, fpr, mean_latency, cost).
|
| 631 |
+
ascending: If True, lower values are better. If False, higher values are better.
|
| 632 |
+
|
| 633 |
+
Returns:
|
| 634 |
+
List of (model_name, metric_value) tuples, sorted by rank.
|
| 635 |
+
|
| 636 |
+
"""
|
| 637 |
+
all_metrics = self.metrics.compute_metrics_for_all_models(model_provider_use_case, dataset_name)
|
| 638 |
+
|
| 639 |
+
if not all_metrics:
|
| 640 |
+
return []
|
| 641 |
+
|
| 642 |
+
# Extract the specified metric for each model
|
| 643 |
+
# Handle nested dictionaries like accuracy_per_category
|
| 644 |
+
model_scores = []
|
| 645 |
+
for name, metrics in all_metrics.items():
|
| 646 |
+
value = metrics.get(metric)
|
| 647 |
+
if value is not None:
|
| 648 |
+
# If it's a nested dict, we can't use it for ranking directly
|
| 649 |
+
if isinstance(value, dict):
|
| 650 |
+
continue
|
| 651 |
+
model_scores.append((name, float(value)))
|
| 652 |
+
|
| 653 |
+
# Sort based on ascending flag
|
| 654 |
+
# For FPR, latency, and cost, lower is better (ascending=True)
|
| 655 |
+
# For accuracy, higher is better (ascending=False)
|
| 656 |
+
model_scores.sort(key=lambda x: x[1], reverse=not ascending)
|
| 657 |
+
|
| 658 |
+
return model_scores
|
| 659 |
+
|
| 660 |
+
def compute_rankings_table(
|
| 661 |
+
self,
|
| 662 |
+
model_provider_use_case: str,
|
| 663 |
+
dataset_name: str,
|
| 664 |
+
) -> dict[str, dict[str, float | dict[str, float] | str]]:
|
| 665 |
+
"""Compute all metrics for all models and return as a table.
|
| 666 |
+
|
| 667 |
+
Args:
|
| 668 |
+
model_provider_use_case: Name of the model provider use case (e.g., "anthropic_classification").
|
| 669 |
+
dataset_name: Name of the dataset.
|
| 670 |
+
|
| 671 |
+
Returns:
|
| 672 |
+
Dictionary mapping model names to their metrics dictionary.
|
| 673 |
+
|
| 674 |
+
"""
|
| 675 |
+
return self.metrics.compute_metrics_for_all_models(model_provider_use_case, dataset_name)
|
| 676 |
+
|
| 677 |
+
def rank_across_datasets(
|
| 678 |
+
self,
|
| 679 |
+
model_provider_use_case: str,
|
| 680 |
+
dataset_names: list[str],
|
| 681 |
+
metric: str = "accuracy",
|
| 682 |
+
aggregation: str = "mean",
|
| 683 |
+
) -> list[tuple[str, float]]:
|
| 684 |
+
"""Rank models across multiple datasets.
|
| 685 |
+
|
| 686 |
+
Args:
|
| 687 |
+
model_provider_use_case: Name of the model provider use case (e.g., "anthropic_classification").
|
| 688 |
+
dataset_names: List of dataset names to aggregate over.
|
| 689 |
+
metric: Metric to rank by.
|
| 690 |
+
aggregation: How to aggregate across datasets ('mean', 'sum', 'min', 'max').
|
| 691 |
+
|
| 692 |
+
Returns:
|
| 693 |
+
List of (model_name, aggregated_metric_value) tuples, sorted by rank.
|
| 694 |
+
|
| 695 |
+
"""
|
| 696 |
+
all_model_metrics: dict[str, list[float]] = defaultdict(list)
|
| 697 |
+
|
| 698 |
+
for dataset_name in dataset_names:
|
| 699 |
+
metrics_by_model = self.metrics.compute_metrics_for_all_models(model_provider_use_case, dataset_name)
|
| 700 |
+
for model_name, metrics in metrics_by_model.items():
|
| 701 |
+
value = metrics.get(metric)
|
| 702 |
+
# Skip nested dictionaries
|
| 703 |
+
if value is not None and not isinstance(value, dict):
|
| 704 |
+
all_model_metrics[model_name].append(float(value))
|
| 705 |
+
|
| 706 |
+
# Aggregate metrics
|
| 707 |
+
aggregated_scores: dict[str, float] = {}
|
| 708 |
+
for model_name, values in all_model_metrics.items():
|
| 709 |
+
if not values:
|
| 710 |
+
continue
|
| 711 |
+
if aggregation == "mean":
|
| 712 |
+
aggregated_scores[model_name] = sum(values) / len(values)
|
| 713 |
+
elif aggregation == "sum":
|
| 714 |
+
aggregated_scores[model_name] = sum(values)
|
| 715 |
+
elif aggregation == "min":
|
| 716 |
+
aggregated_scores[model_name] = min(values)
|
| 717 |
+
elif aggregation == "max":
|
| 718 |
+
aggregated_scores[model_name] = max(values)
|
| 719 |
+
else:
|
| 720 |
+
raise ValueError(f"Unknown aggregation method: {aggregation}")
|
| 721 |
+
|
| 722 |
+
# Determine if ascending sort is needed
|
| 723 |
+
ascending = metric in ["fpr", "mean_latency", "cost"]
|
| 724 |
+
|
| 725 |
+
# Sort and return
|
| 726 |
+
sorted_scores = sorted(aggregated_scores.items(), key=lambda x: x[1], reverse=not ascending)
|
| 727 |
+
|
| 728 |
+
return sorted_scores
|
content_moderation.py
CHANGED
|
@@ -54,7 +54,7 @@ CATEGORY_LABELS = {
|
|
| 54 |
|
| 55 |
DATASET_DESCRIPTIONS: Dict[str, Dict[str, Any]] = {
|
| 56 |
"input": {
|
| 57 |
-
"url": "https://huggingface.co/datasets/
|
| 58 |
"label": "BELLS-O Content Moderation Input Dataset",
|
| 59 |
"summary": "This dataset contains 1,400 samples across 11 harm categories for evaluating input content moderation. It includes 300 benign prompts (for FPR evaluation) and 1,100 harmful prompts (100 per category). Samples were constructed using three methods: AI-generated (27%), extracted from existing datasets (44%), and handcrafted by experts (29%).",
|
| 60 |
"categories": dict.fromkeys(CATEGORY_LABELS, ""),
|
|
@@ -62,7 +62,7 @@ DATASET_DESCRIPTIONS: Dict[str, Dict[str, Any]] = {
|
|
| 62 |
"fpr_categories": ["benign"],
|
| 63 |
},
|
| 64 |
"output": {
|
| 65 |
-
"url": "https://huggingface.co/datasets/
|
| 66 |
"label": "BELLS-O Content Moderation Output Dataset",
|
| 67 |
"summary": "This dataset contains 1,400 samples across 11 harm categories for evaluating output content moderation. It includes 300 benign outputs (for FPR evaluation) and 1,100 harmful outputs (100 per category). Samples were constructed using three methods: AI-generated (27%), extracted from existing datasets (44%), and handcrafted by experts (29%).",
|
| 68 |
"categories": dict.fromkeys(CATEGORY_LABELS, ""),
|
|
@@ -143,8 +143,11 @@ def create_dataset_info_html(dataset_type: str) -> str:
|
|
| 143 |
|
| 144 |
|
| 145 |
def filter_metrics_by_dataset(dataset_type: str) -> Dict[str, Any]:
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
|
| 150 |
def prepare_leaderboard_data(selected_categories: List[str] | None = None, dataset_type: str = "input") -> pd.DataFrame:
|
|
|
|
| 54 |
|
| 55 |
DATASET_DESCRIPTIONS: Dict[str, Dict[str, Any]] = {
|
| 56 |
"input": {
|
| 57 |
+
"url": "https://huggingface.co/datasets/centrepourlasecuriteia/content-moderation-input-dataset",
|
| 58 |
"label": "BELLS-O Content Moderation Input Dataset",
|
| 59 |
"summary": "This dataset contains 1,400 samples across 11 harm categories for evaluating input content moderation. It includes 300 benign prompts (for FPR evaluation) and 1,100 harmful prompts (100 per category). Samples were constructed using three methods: AI-generated (27%), extracted from existing datasets (44%), and handcrafted by experts (29%).",
|
| 60 |
"categories": dict.fromkeys(CATEGORY_LABELS, ""),
|
|
|
|
| 62 |
"fpr_categories": ["benign"],
|
| 63 |
},
|
| 64 |
"output": {
|
| 65 |
+
"url": "https://huggingface.co/datasets/centrepourlasecuriteia/content-moderation-output-dataset",
|
| 66 |
"label": "BELLS-O Content Moderation Output Dataset",
|
| 67 |
"summary": "This dataset contains 1,400 samples across 11 harm categories for evaluating output content moderation. It includes 300 benign outputs (for FPR evaluation) and 1,100 harmful outputs (100 per category). Samples were constructed using three methods: AI-generated (27%), extracted from existing datasets (44%), and handcrafted by experts (29%).",
|
| 68 |
"categories": dict.fromkeys(CATEGORY_LABELS, ""),
|
|
|
|
| 143 |
|
| 144 |
|
| 145 |
def filter_metrics_by_dataset(dataset_type: str) -> Dict[str, Any]:
|
| 146 |
+
matching_names = {
|
| 147 |
+
f"bells-o-project-content-moderation-{dataset_type}",
|
| 148 |
+
f"centrepourlasecuriteia-content-moderation-{dataset_type}-dataset",
|
| 149 |
+
}
|
| 150 |
+
return {k: v for k, v in METRICS_DATA.items() if v.get("dataset_name") in matching_names}
|
| 151 |
|
| 152 |
|
| 153 |
def prepare_leaderboard_data(selected_categories: List[str] | None = None, dataset_type: str = "input") -> pd.DataFrame:
|
jailbreak.py
CHANGED
|
@@ -82,6 +82,36 @@ DATASET_DESCRIPTIONS: Dict[str, Dict[str, Any]] = {
|
|
| 82 |
"Sexual Content",
|
| 83 |
],
|
| 84 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
"bells-o-project-BET-jailbreak-dataset": {
|
| 86 |
"url": "https://huggingface.co/datasets/centrepourlasecuriteia/BET-jailbreak-dataset",
|
| 87 |
"summary": "This dataset generated adversarial prompts from the companion dataset centrepourlasecuriteia/content-moderation-input using BET (https://github.com/qfeuilla/BehaviorEliciationTool).",
|
|
@@ -112,6 +142,36 @@ DATASET_DESCRIPTIONS: Dict[str, Dict[str, Any]] = {
|
|
| 112 |
"Sexual Content",
|
| 113 |
],
|
| 114 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
"deepset-prompt-injections": {
|
| 116 |
"url": "https://huggingface.co/datasets/deepset/prompt-injections",
|
| 117 |
"summary": "The prompt injections dataset by Deepset.",
|
|
@@ -144,7 +204,9 @@ DATASET_DESCRIPTIONS: Dict[str, Dict[str, Any]] = {
|
|
| 144 |
DATASET_LABELS = {
|
| 145 |
"allenai-wildjailbreak": "WildJailbreak (AllenAI)",
|
| 146 |
"bells-o-project-jailbreak-dataset": "BELLS-O Jailbreak (CeSIA)",
|
|
|
|
| 147 |
"bells-o-project-BET-jailbreak-dataset": "BELLS-O BET Jailbreak (CeSIA)",
|
|
|
|
| 148 |
"deepset-prompt-injections": "Prompt Injections (Deepset)",
|
| 149 |
"local_datasets-malignant": "Prompt Injection Malignant (Mary Camila)",
|
| 150 |
"TrustAIRLab-in-the-wild-jailbreak-prompts": "In-the-Wild Jailbreak Prompts (TrustAIRLab)",
|
|
|
|
| 82 |
"Sexual Content",
|
| 83 |
],
|
| 84 |
},
|
| 85 |
+
"centrepourlasecuriteia-jailbreak-dataset": {
|
| 86 |
+
"url": "https://huggingface.co/datasets/centrepourlasecuriteia/jailbreak-dataset",
|
| 87 |
+
"summary": "This dataset generated adversarial prompts from the companion dataset centrepourlasecuriteia/content-moderation-input utilizing uncensored models.",
|
| 88 |
+
"categories": {
|
| 89 |
+
"Benign": "Adversarial prompts with benign content.",
|
| 90 |
+
"CBRN": "Adversarial prompts with harmful CBRN content.",
|
| 91 |
+
"Cyber": "Adversarial prompts with harmful Cyber content.",
|
| 92 |
+
"Harm to Minors": "Adversarial prompts with harmful content regarding Harm to Minors.",
|
| 93 |
+
"Harmful Manipulation": "Adversarial prompts with harmful content regarding manipulation.",
|
| 94 |
+
"Illegal Activities": "Adversarial prompts with harmful content regarding Illegal Activities.",
|
| 95 |
+
"Integrity & Quality": "Adversarial prompts with harmful content regarding Integrity & Quality.",
|
| 96 |
+
"Physical Harm": "Adversarial prompts with Physical Harm content.",
|
| 97 |
+
"Privacy": "Adversarial prompts with harmful Privacy content.",
|
| 98 |
+
"Self-Harm": "Adversarial prompts with harmful Self-Harm content.",
|
| 99 |
+
"Sexual Content": "Adversarial prompts with harmful sexual content.",
|
| 100 |
+
},
|
| 101 |
+
"fpr_categories": ["Benign"],
|
| 102 |
+
"accuracy_categories": [
|
| 103 |
+
"CBRN",
|
| 104 |
+
"Cyber",
|
| 105 |
+
"Harm to Minors",
|
| 106 |
+
"Harmful Manipulation",
|
| 107 |
+
"Illegal Activities",
|
| 108 |
+
"Integrity & Quality",
|
| 109 |
+
"Physical Harm",
|
| 110 |
+
"Privacy",
|
| 111 |
+
"Self-Harm",
|
| 112 |
+
"Sexual Content",
|
| 113 |
+
],
|
| 114 |
+
},
|
| 115 |
"bells-o-project-BET-jailbreak-dataset": {
|
| 116 |
"url": "https://huggingface.co/datasets/centrepourlasecuriteia/BET-jailbreak-dataset",
|
| 117 |
"summary": "This dataset generated adversarial prompts from the companion dataset centrepourlasecuriteia/content-moderation-input using BET (https://github.com/qfeuilla/BehaviorEliciationTool).",
|
|
|
|
| 142 |
"Sexual Content",
|
| 143 |
],
|
| 144 |
},
|
| 145 |
+
"centrepourlasecuriteia-BET-jailbreak-dataset": {
|
| 146 |
+
"url": "https://huggingface.co/datasets/centrepourlasecuriteia/BET-jailbreak-dataset",
|
| 147 |
+
"summary": "This dataset generated adversarial prompts from the companion dataset centrepourlasecuriteia/content-moderation-input using BET (https://github.com/qfeuilla/BehaviorEliciationTool).",
|
| 148 |
+
"categories": {
|
| 149 |
+
"Benign": "Adversarial prompts with benign content.",
|
| 150 |
+
"CBRN": "Adversarial prompts with harmful CBRN content.",
|
| 151 |
+
"Cyber": "Adversarial prompts with harmful Cyber content.",
|
| 152 |
+
"Harm to Minors": "Adversarial prompts with harmful content regarding Harm to Minors.",
|
| 153 |
+
"Harmful Manipulation": "Adversarial prompts with harmful content regarding manipulation.",
|
| 154 |
+
"Illegal Activities": "Adversarial prompts with harmful content regarding Illegal Activities.",
|
| 155 |
+
"Integrity & Quality": "Adversarial prompts with harmful content regarding Integrity & Quality.",
|
| 156 |
+
"Physical Harm": "Adversarial prompts with Physical Harm content.",
|
| 157 |
+
"Privacy": "Adversarial prompts with harmful Privacy content.",
|
| 158 |
+
"Self-Harm": "Adversarial prompts with harmful Self-Harm content.",
|
| 159 |
+
"Sexual Content": "Adversarial prompts with harmful sexual content.",
|
| 160 |
+
},
|
| 161 |
+
"fpr_categories": ["Benign"],
|
| 162 |
+
"accuracy_categories": [
|
| 163 |
+
"CBRN",
|
| 164 |
+
"Cyber",
|
| 165 |
+
"Harm to Minors",
|
| 166 |
+
"Harmful Manipulation",
|
| 167 |
+
"Illegal Activities",
|
| 168 |
+
"Integrity & Quality",
|
| 169 |
+
"Physical Harm",
|
| 170 |
+
"Privacy",
|
| 171 |
+
"Self-Harm",
|
| 172 |
+
"Sexual Content",
|
| 173 |
+
],
|
| 174 |
+
},
|
| 175 |
"deepset-prompt-injections": {
|
| 176 |
"url": "https://huggingface.co/datasets/deepset/prompt-injections",
|
| 177 |
"summary": "The prompt injections dataset by Deepset.",
|
|
|
|
| 204 |
DATASET_LABELS = {
|
| 205 |
"allenai-wildjailbreak": "WildJailbreak (AllenAI)",
|
| 206 |
"bells-o-project-jailbreak-dataset": "BELLS-O Jailbreak (CeSIA)",
|
| 207 |
+
"centrepourlasecuriteia-jailbreak-dataset": "BELLS-O Jailbreak (CeSIA)",
|
| 208 |
"bells-o-project-BET-jailbreak-dataset": "BELLS-O BET Jailbreak (CeSIA)",
|
| 209 |
+
"centrepourlasecuriteia-BET-jailbreak-dataset": "BELLS-O BET Jailbreak (CeSIA)",
|
| 210 |
"deepset-prompt-injections": "Prompt Injections (Deepset)",
|
| 211 |
"local_datasets-malignant": "Prompt Injection Malignant (Mary Camila)",
|
| 212 |
"TrustAIRLab-in-the-wild-jailbreak-prompts": "In-the-Wild Jailbreak Prompts (TrustAIRLab)",
|
run_compute_metrics.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run compute_metrics over every results folder in the results directory.
|
| 3 |
+
|
| 4 |
+
This script iterates through all model/dataset/run combinations in the results
|
| 5 |
+
directory and computes metrics for each one.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
from hashlib import sha256
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
from compute_metrics import Metrics
|
| 14 |
+
|
| 15 |
+
JAILBREAK_DATASET_CLEAN_NAME = "centrepourlasecuriteia-jailbreak-dataset"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def build_technique_map() -> dict[str, str]:
|
| 19 |
+
"""Build a mapping from prompt_id to technique_type by loading the HF jailbreak dataset.
|
| 20 |
+
|
| 21 |
+
Returns:
|
| 22 |
+
dict mapping prompt_id → technique_type
|
| 23 |
+
|
| 24 |
+
"""
|
| 25 |
+
from datasets import load_dataset
|
| 26 |
+
|
| 27 |
+
ds = load_dataset("centrepourlasecuriteia/jailbreak-dataset")
|
| 28 |
+
technique_map = {}
|
| 29 |
+
for sample in ds["train"]:
|
| 30 |
+
hash_id = sha256(sample["jailbreak_prompt"].encode()).hexdigest()
|
| 31 |
+
prompt_id = f"{JAILBREAK_DATASET_CLEAN_NAME}_{hash_id}"
|
| 32 |
+
technique_map[prompt_id] = sample["technique_type"]
|
| 33 |
+
return technique_map
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def find_all_result_combinations(results_dir: Path) -> list[tuple[str, str, str]]:
|
| 37 |
+
"""Find all model_provider_use_case/dataset/model_name combinations in the results directory.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
results_dir: Base results directory.
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
List of (model_provider_use_case, dataset_name, model_name) tuples.
|
| 44 |
+
|
| 45 |
+
"""
|
| 46 |
+
combinations = []
|
| 47 |
+
|
| 48 |
+
if not results_dir.exists():
|
| 49 |
+
return combinations
|
| 50 |
+
|
| 51 |
+
# Iterate through all model_provider_use_case directories
|
| 52 |
+
for provider_dir in sorted(results_dir.iterdir()):
|
| 53 |
+
if not provider_dir.is_dir():
|
| 54 |
+
continue
|
| 55 |
+
|
| 56 |
+
model_provider_use_case = provider_dir.name
|
| 57 |
+
|
| 58 |
+
# Iterate through all dataset directories within the provider directory
|
| 59 |
+
for dataset_dir in sorted(provider_dir.iterdir()):
|
| 60 |
+
if not dataset_dir.is_dir():
|
| 61 |
+
continue
|
| 62 |
+
|
| 63 |
+
dataset_name = dataset_dir.name
|
| 64 |
+
|
| 65 |
+
# Iterate through all model directories
|
| 66 |
+
for model_dir in sorted(dataset_dir.iterdir()):
|
| 67 |
+
if not model_dir.is_dir():
|
| 68 |
+
continue
|
| 69 |
+
|
| 70 |
+
# Check if this directory contains JSON files
|
| 71 |
+
json_files = list(model_dir.glob("*.json"))
|
| 72 |
+
if json_files:
|
| 73 |
+
model_name = model_dir.name
|
| 74 |
+
combinations.append((model_provider_use_case, dataset_name, model_name))
|
| 75 |
+
|
| 76 |
+
return combinations
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def compute_all_metrics(
|
| 80 |
+
results_dir: Path,
|
| 81 |
+
output_file: Path | None = None,
|
| 82 |
+
eval_type: str = "jailbreak",
|
| 83 |
+
) -> dict[str, Any]:
|
| 84 |
+
"""Compute metrics for all result combinations.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
results_dir: Base results directory.
|
| 88 |
+
output_file: Optional path to save results as JSON.
|
| 89 |
+
eval_type: Type of evaluation ("jailbreak" or "content_moderation").
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
Dictionary mapping (model_provider_use_case, dataset_name, model_name) to metrics.
|
| 93 |
+
|
| 94 |
+
"""
|
| 95 |
+
metrics_calculator = Metrics(results_dir)
|
| 96 |
+
all_combinations = find_all_result_combinations(results_dir)
|
| 97 |
+
|
| 98 |
+
# Build technique map once if processing jailbreak results
|
| 99 |
+
technique_map = None
|
| 100 |
+
if eval_type == "jailbreak":
|
| 101 |
+
has_jailbreak = any(ds == JAILBREAK_DATASET_CLEAN_NAME for _, ds, _ in all_combinations)
|
| 102 |
+
if has_jailbreak:
|
| 103 |
+
print("Loading jailbreak dataset to build technique_type map...")
|
| 104 |
+
technique_map = build_technique_map()
|
| 105 |
+
print(f" Built technique map with {len(technique_map)} entries\n")
|
| 106 |
+
|
| 107 |
+
results = {}
|
| 108 |
+
|
| 109 |
+
print(f"Found {len(all_combinations)} result combinations to process\n")
|
| 110 |
+
|
| 111 |
+
for i, (model_provider_use_case, dataset_name, model_name) in enumerate(all_combinations, 1):
|
| 112 |
+
print(f"[{i}/{len(all_combinations)}] Processing: {model_provider_use_case}/{dataset_name}/{model_name}")
|
| 113 |
+
|
| 114 |
+
metrics = metrics_calculator.compute_all_metrics(
|
| 115 |
+
model_provider_use_case=model_provider_use_case, dataset_name=dataset_name, model_name=model_name
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Add per-technique accuracy for jailbreak dataset
|
| 119 |
+
if technique_map is not None and dataset_name == JAILBREAK_DATASET_CLEAN_NAME:
|
| 120 |
+
metrics["accuracy_per_technique"] = metrics_calculator.compute_accuracy_per_technique(technique_map)
|
| 121 |
+
|
| 122 |
+
key = f"{model_provider_use_case}/{dataset_name}/{model_name}"
|
| 123 |
+
results[key] = {
|
| 124 |
+
"model_provider_use_case": model_provider_use_case,
|
| 125 |
+
"dataset_name": dataset_name,
|
| 126 |
+
"model_name": model_name,
|
| 127 |
+
**metrics,
|
| 128 |
+
}
|
| 129 |
+
if isinstance(metrics["fpr"], float):
|
| 130 |
+
fpr = f"{metrics['fpr']:.4f}"
|
| 131 |
+
else:
|
| 132 |
+
fpr = "N/A"
|
| 133 |
+
print(f" ✓ Accuracy: {metrics['accuracy']:.4f}, FPR: {fpr}, Samples: {metrics['num_samples']}")
|
| 134 |
+
|
| 135 |
+
if output_file:
|
| 136 |
+
print(f"\nSaving results to {output_file}")
|
| 137 |
+
with open(output_file, "w") as f:
|
| 138 |
+
json.dump(results, f, indent=2)
|
| 139 |
+
print(f"✓ Results saved to {output_file}")
|
| 140 |
+
|
| 141 |
+
return results
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def main():
|
| 145 |
+
"""Main entry point."""
|
| 146 |
+
import argparse
|
| 147 |
+
|
| 148 |
+
parser = argparse.ArgumentParser(description="Compute metrics for all result folders in the results directory")
|
| 149 |
+
parser.add_argument(
|
| 150 |
+
"--results-dir", type=Path, default=Path("results"), help="Base results directory (default: results)"
|
| 151 |
+
)
|
| 152 |
+
parser.add_argument("--output", type=Path, help="Optional JSON file to save all results")
|
| 153 |
+
parser.add_argument(
|
| 154 |
+
"--type",
|
| 155 |
+
type=str,
|
| 156 |
+
choices=["jailbreak", "content_moderation"],
|
| 157 |
+
default="jailbreak",
|
| 158 |
+
help="Evaluation type (default: jailbreak). Sets default output path if --output is not specified.",
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
args = parser.parse_args()
|
| 162 |
+
|
| 163 |
+
# Set default output path based on type if not explicitly provided
|
| 164 |
+
output_file = args.output
|
| 165 |
+
if output_file is None:
|
| 166 |
+
if args.type == "jailbreak":
|
| 167 |
+
output_file = Path("data/jailbreak_metrics.json")
|
| 168 |
+
else:
|
| 169 |
+
output_file = Path("data/content_moderation_metrics.json")
|
| 170 |
+
|
| 171 |
+
results = compute_all_metrics(args.results_dir, output_file, eval_type=args.type)
|
| 172 |
+
|
| 173 |
+
print(f"\n{'=' * 60}")
|
| 174 |
+
print(f"Summary: Processed {len(results)} result combinations")
|
| 175 |
+
print(f"{'=' * 60}")
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
main()
|