auditkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. auditkit/README.md +99 -0
  2. auditkit/__init__.py +177 -0
  3. auditkit/__main__.py +3 -0
  4. auditkit/_bootstrap.py +77 -0
  5. auditkit/_identity_guard.py +99 -0
  6. auditkit/adapter.py +264 -0
  7. auditkit/annotator.py +339 -0
  8. auditkit/api.py +502 -0
  9. auditkit/assets/auditkit_logo.png +0 -0
  10. auditkit/cache.py +47 -0
  11. auditkit/cli.py +417 -0
  12. auditkit/comparison.py +563 -0
  13. auditkit/diff.py +265 -0
  14. auditkit/errors.py +54 -0
  15. auditkit/evaluator.py +20 -0
  16. auditkit/experiment.py +145 -0
  17. auditkit/hf_publish.py +262 -0
  18. auditkit/lmeval_engine.py +550 -0
  19. auditkit/loaders.py +121 -0
  20. auditkit/logs.py +18 -0
  21. auditkit/metric.py +199 -0
  22. auditkit/metrics/README.md +15 -0
  23. auditkit/metrics/__init__.py +0 -0
  24. auditkit/metrics/code.py +222 -0
  25. auditkit/metrics/embedding.py +131 -0
  26. auditkit/metrics/encoder_judge.py +423 -0
  27. auditkit/metrics/generation.py +331 -0
  28. auditkit/metrics/guard.py +412 -0
  29. auditkit/metrics/hallucination.py +45 -0
  30. auditkit/metrics/judge.py +547 -0
  31. auditkit/metrics/pairwise.py +153 -0
  32. auditkit/metrics/perf.py +53 -0
  33. auditkit/metrics/rag.py +149 -0
  34. auditkit/metrics/security.py +64 -0
  35. auditkit/metrics/toxicity.py +238 -0
  36. auditkit/model/README.md +16 -0
  37. auditkit/model/__init__.py +485 -0
  38. auditkit/model/anthropic.py +94 -0
  39. auditkit/model/api_gen.py +133 -0
  40. auditkit/model/groq_gen.py +121 -0
  41. auditkit/model/hf_gen.py +385 -0
  42. auditkit/model/lexsi.py +155 -0
  43. auditkit/model/litellm_gen.py +65 -0
  44. auditkit/model/openai.py +90 -0
  45. auditkit/model/openrouter_gen.py +152 -0
  46. auditkit/model/vllm_gen.py +316 -0
  47. auditkit/model_compare.py +655 -0
  48. auditkit/redteam/README.md +9 -0
  49. auditkit/redteam/__init__.py +26 -0
  50. auditkit/redteam/detector.py +37 -0
  51. auditkit/redteam/detectors/README.md +5 -0
  52. auditkit/redteam/detectors/builtin.py +126 -0
  53. auditkit/redteam/probe.py +39 -0
  54. auditkit/redteam/probes/README.md +5 -0
  55. auditkit/redteam/probes/builtin.py +85 -0
  56. auditkit/redteam/runner.py +206 -0
  57. auditkit/registry.py +65 -0
  58. auditkit/report.py +278 -0
  59. auditkit/report_format.py +52 -0
  60. auditkit/router.py +54 -0
  61. auditkit/runner.py +575 -0
  62. auditkit/runspec.py +159 -0
  63. auditkit/sample.py +40 -0
  64. auditkit/scenario.py +88 -0
  65. auditkit/scenarios/README.md +10 -0
  66. auditkit/scenarios/__init__.py +4 -0
  67. auditkit/scenarios/arc.py +33 -0
  68. auditkit/scenarios/gsm8k.py +32 -0
  69. auditkit/scenarios/hellaswag.py +33 -0
  70. auditkit/scenarios/humaneval.py +32 -0
  71. auditkit/scenarios/mmlu.py +34 -0
  72. auditkit/scenarios/truthfulqa.py +33 -0
  73. auditkit/score.py +165 -0
  74. auditkit/scorers.py +117 -0
  75. auditkit/scoring.py +79 -0
  76. auditkit/types.py +69 -0
  77. auditkit-1.0.0.dist-info/METADATA +396 -0
  78. auditkit-1.0.0.dist-info/RECORD +81 -0
  79. auditkit-1.0.0.dist-info/WHEEL +4 -0
  80. auditkit-1.0.0.dist-info/entry_points.txt +2 -0
  81. auditkit-1.0.0.dist-info/licenses/LICENSE.md +92 -0
@@ -0,0 +1,331 @@
1
+ """Text generation quality metrics (BLEU, ROUGE, chrF, WER, Perplexity, BERTScore)."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from typing import Any
6
+
7
+ from auditkit.metric import Metric
8
+ from auditkit.registry import METRICS
9
+ from auditkit.sample import Sample
10
+ from auditkit.score import Score
11
+ from auditkit.types import Direction, ScoreKind
12
+ from auditkit.errors import ExtraNotInstalled
13
+
14
+
15
+ @METRICS.register("bleu")
16
+ class Bleu(Metric):
17
+ name = "bleu"
18
+ kind = ScoreKind.BENCHMARK
19
+ direction = Direction.MAXIMIZE
20
+ is_deterministic = True
21
+ required_fields = frozenset({"target"})
22
+
23
+ def __init__(self, max_n: int = 4, smooth: bool = True) -> None:
24
+ self._max_n = max_n
25
+ self._smooth = smooth
26
+
27
+ def identity(self) -> dict:
28
+ return {"name": self.name, "max_n": self._max_n, "smooth": self._smooth}
29
+
30
+ @staticmethod
31
+ def _ngrams(tokens: list[str], n: int) -> list[tuple[str, ...]]:
32
+ return [tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1)]
33
+
34
+ @staticmethod
35
+ def _brevity_penalty(ref_len: int, hyp_len: int) -> float:
36
+ if hyp_len == 0:
37
+ return 0.0
38
+ if hyp_len >= ref_len:
39
+ return 1.0
40
+ return math.exp(1.0 - ref_len / hyp_len)
41
+
42
+ def score(self, sample: Sample, output: str, context: Any = None) -> Score:
43
+ ref = (sample.target or "").split()
44
+ hyp = output.split()
45
+ if not ref or not hyp:
46
+ return Score(name=self.name, value=0.0, kind=self.kind)
47
+
48
+ max_n = min(self._max_n, max(len(ref), len(hyp)))
49
+ precisions = []
50
+ for n in range(1, max_n + 1):
51
+ hyp_ngrams = self._ngrams(hyp, n)
52
+ ref_ngrams = self._ngrams(ref, n)
53
+ if not hyp_ngrams:
54
+ precisions.append(1e-12 if self._smooth else 0.0)
55
+ continue
56
+
57
+ ref_counts = {}
58
+ for ng in ref_ngrams:
59
+ ref_counts[ng] = ref_counts.get(ng, 0) + 1
60
+
61
+ count = 0
62
+ clipped = 0
63
+ for ng in hyp_ngrams:
64
+ count += 1
65
+ if ref_counts.get(ng, 0) > 0:
66
+ clipped += 1
67
+ ref_counts[ng] -= 1
68
+
69
+ p = clipped / count if count > 0 else 0.0
70
+ if self._smooth and n > 1 and p == 0.0:
71
+ p = 1e-12
72
+ precisions.append(p)
73
+
74
+ if not precisions:
75
+ return Score(name=self.name, value=0.0, kind=self.kind)
76
+ if any(p <= 0.0 for p in precisions):
77
+ if self._smooth:
78
+ precisions = [max(p, 1e-12) for p in precisions]
79
+ else:
80
+ return Score(name=self.name, value=0.0, kind=self.kind)
81
+ log_sum = sum(math.log(p) for p in precisions)
82
+ geo_mean = math.exp(log_sum / len(precisions))
83
+ bp = self._brevity_penalty(len(ref), len(hyp))
84
+ return Score(name=self.name, value=bp * geo_mean, kind=self.kind)
85
+
86
+
87
+ @METRICS.register("rouge_l")
88
+ class RogueL(Metric):
89
+ name = "rouge_l"
90
+ kind = ScoreKind.BENCHMARK
91
+ direction = Direction.MAXIMIZE
92
+ is_deterministic = True
93
+ required_fields = frozenset({"target"})
94
+
95
+ @staticmethod
96
+ def _lcs_length(a: list[str], b: list[str]) -> int:
97
+ m, n = len(a), len(b)
98
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
99
+ for i in range(1, m + 1):
100
+ for j in range(1, n + 1):
101
+ if a[i - 1] == b[j - 1]:
102
+ dp[i][j] = dp[i - 1][j - 1] + 1
103
+ else:
104
+ dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
105
+ return dp[m][n]
106
+
107
+ def score(self, sample: Sample, output: str, context: Any = None) -> Score:
108
+ ref = (sample.target or "").split()
109
+ hyp = output.split()
110
+ if not ref and not hyp:
111
+ return Score(name=self.name, value=1.0, kind=self.kind)
112
+ if not ref or not hyp:
113
+ return Score(name=self.name, value=0.0, kind=self.kind)
114
+
115
+ lcs_len = self._lcs_length(hyp, ref)
116
+ precision = lcs_len / len(hyp)
117
+ recall = lcs_len / len(ref)
118
+ if precision == 0.0 and recall == 0.0:
119
+ f1 = 0.0
120
+ else:
121
+ f1 = 2.0 * precision * recall / (precision + recall)
122
+ return Score(name=self.name, value=f1, kind=self.kind)
123
+
124
+
125
+ @METRICS.register("chrf")
126
+ class ChrF(Metric):
127
+ name = "chrf"
128
+ kind = ScoreKind.BENCHMARK
129
+ direction = Direction.MAXIMIZE
130
+ is_deterministic = True
131
+ required_fields = frozenset({"target"})
132
+
133
+ def __init__(self, n: int = 6, beta: float = 1.0) -> None:
134
+ self._n = n
135
+ self._beta = beta
136
+
137
+ def identity(self) -> dict:
138
+ return {"name": self.name, "n": self._n, "beta": self._beta}
139
+
140
+ def score(self, sample: Sample, output: str, context: Any = None) -> Score:
141
+ ref = sample.target or ""
142
+ hyp = output
143
+ if not ref or not hyp:
144
+ return Score(name=self.name, value=0.0, kind=self.kind)
145
+
146
+ # Cap by the shorter string's length, same as Bleu does for its word
147
+ # n-grams -- without this, a short string (e.g. "hi"/"hi", 2 chars)
148
+ # still gets n=3..6 character n-gram orders folded into the average,
149
+ # each contributing a spurious 0 precision/recall since no n-gram of
150
+ # that length can even exist, understating an otherwise-perfect match.
151
+ max_n = min(self._n, max(len(ref), len(hyp)))
152
+ total_p = 0.0
153
+ total_r = 0.0
154
+ for n in range(1, max_n + 1):
155
+ ref_ngrams: dict[str, int] = {}
156
+ for i in range(len(ref) - n + 1):
157
+ ng = ref[i:i + n]
158
+ ref_ngrams[ng] = ref_ngrams.get(ng, 0) + 1
159
+
160
+ hyp_ngrams: dict[str, int] = {}
161
+ for i in range(len(hyp) - n + 1):
162
+ ng = hyp[i:i + n]
163
+ hyp_ngrams[ng] = hyp_ngrams.get(ng, 0) + 1
164
+
165
+ match_count = 0
166
+ for ng, count in hyp_ngrams.items():
167
+ match_count += min(count, ref_ngrams.get(ng, 0))
168
+
169
+ hyp_count = sum(hyp_ngrams.values())
170
+ ref_count = sum(ref_ngrams.values())
171
+
172
+ p = match_count / hyp_count if hyp_count > 0 else 0.0
173
+ r = match_count / ref_count if ref_count > 0 else 0.0
174
+ total_p += p
175
+ total_r += r
176
+
177
+ avg_p = total_p / max_n
178
+ avg_r = total_r / max_n
179
+ beta2 = self._beta ** 2
180
+ if avg_p == 0.0 and avg_r == 0.0:
181
+ f_beta = 0.0
182
+ else:
183
+ f_beta = (1.0 + beta2) * avg_p * avg_r / (beta2 * avg_p + avg_r)
184
+ return Score(name=self.name, value=f_beta, kind=self.kind)
185
+
186
+
187
+ @METRICS.register("word_error_rate")
188
+ class WordErrorRate(Metric):
189
+ name = "wer"
190
+ kind = ScoreKind.BENCHMARK
191
+ # MAXIMIZE, not MINIMIZE despite the class name: .score() below already
192
+ # inverts to `max(0.0, 1.0 - wer)`, a 0-1 "higher is better" value, not
193
+ # raw word-error-rate. Direction describes the returned VALUE's
194
+ # convention, not the underlying named phenomenon.
195
+ direction = Direction.MAXIMIZE
196
+ is_deterministic = True
197
+ required_fields = frozenset({"target"})
198
+
199
+ @staticmethod
200
+ def _word_edit_distance(ref_words: list[str], hyp_words: list[str]) -> int:
201
+ m, n = len(ref_words), len(hyp_words)
202
+ prev = list(range(n + 1))
203
+ for i in range(1, m + 1):
204
+ curr = [i] * (n + 1)
205
+ for j in range(1, n + 1):
206
+ cost = 0 if ref_words[i - 1] == hyp_words[j - 1] else 1
207
+ curr[j] = min(
208
+ prev[j] + 1,
209
+ curr[j - 1] + 1,
210
+ prev[j - 1] + cost,
211
+ )
212
+ prev = curr
213
+ return prev[n]
214
+
215
+ def score(self, sample: Sample, output: str, context: Any = None) -> Score:
216
+ ref = (sample.target or "").split()
217
+ hyp = output.split()
218
+ if not ref and not hyp:
219
+ return Score(name=self.name, value=1.0, kind=self.kind)
220
+ if not ref:
221
+ return Score(name=self.name, value=0.0, kind=self.kind)
222
+
223
+ distance = self._word_edit_distance(ref, hyp)
224
+ wer = distance / len(ref)
225
+ # Word error rate itself is unbounded above (a much-longer/garbled
226
+ # hyp can need more edits than ref has words), so 1.0 - wer can go
227
+ # arbitrarily negative -- clamp to the [0, 1] range every other
228
+ # normalized metric in this module promises, rather than leaking an
229
+ # unbounded value into aggregation/thresholding code that assumes it.
230
+ return Score(name=self.name, value=max(0.0, 1.0 - wer), kind=self.kind)
231
+
232
+
233
+ @METRICS.register("perplexity")
234
+ class Perplexity(Metric):
235
+ name = "perplexity"
236
+ kind = ScoreKind.BENCHMARK
237
+ # Genuinely MINIMIZE: .score() returns raw exp(loss), unbounded and
238
+ # not inverted -- lower perplexity is a more confident/correct model.
239
+ direction = Direction.MINIMIZE
240
+ is_deterministic = False
241
+ required_fields = frozenset({"target"})
242
+
243
+ def __init__(self, model_name: str = "gpt2") -> None:
244
+ self._model_name = model_name
245
+ self._model = None
246
+ self._tokenizer = None
247
+
248
+ def identity(self) -> dict:
249
+ return {"name": self.name, "model_name": self._model_name}
250
+
251
+ def score(self, sample: Sample, output: str, context: Any = None) -> Score:
252
+ if self._model is None:
253
+ try:
254
+ import torch # noqa: F401
255
+ import transformers # noqa: F401
256
+ except ImportError:
257
+ raise ExtraNotInstalled("transformers", "pip install auditkit[transformers]") from None
258
+ self._tokenizer = transformers.AutoTokenizer.from_pretrained(self._model_name)
259
+ self._model = transformers.AutoModelForCausalLM.from_pretrained(self._model_name)
260
+
261
+ import torch
262
+ encodings = self._tokenizer(output, return_tensors="pt")
263
+ input_ids = encodings.input_ids
264
+ # A causal-LM loss needs at least 2 tokens (one to predict from, one
265
+ # to predict) -- a single-token `output` (a bare word/number/label,
266
+ # exactly what an annotator's extraction commonly produces) has
267
+ # nothing to shift against and silently returns nan. Prepend a
268
+ # BOS-equivalent token so there's always a predecessor; GPT-2-family
269
+ # tokenizers have no distinct bos_token, so eos_token_id doubles as
270
+ # the conventional sequence start there.
271
+ bos_id = self._tokenizer.bos_token_id
272
+ if bos_id is None:
273
+ bos_id = self._tokenizer.eos_token_id
274
+ if bos_id is not None:
275
+ bos = torch.tensor([[bos_id]], dtype=input_ids.dtype)
276
+ input_ids = torch.cat([bos, input_ids], dim=1)
277
+ with torch.no_grad():
278
+ outputs = self._model(input_ids, labels=input_ids)
279
+ loss = outputs.loss.item()
280
+ ppl = math.exp(loss)
281
+ return Score(name=self.name, value=ppl, kind=self.kind)
282
+
283
+
284
+ @METRICS.register("bert_score")
285
+ class BertScore(Metric):
286
+ name = "bert_score"
287
+ kind = ScoreKind.BENCHMARK
288
+ direction = Direction.MAXIMIZE
289
+ is_deterministic = False
290
+ required_fields = frozenset({"target"})
291
+
292
+ def __init__(self, model_name: str = "microsoft/deberta-xlarge-mnli") -> None:
293
+ self._model_name = model_name
294
+
295
+ def identity(self) -> dict:
296
+ return {"name": self.name, "model_name": self._model_name}
297
+
298
+ def score(self, sample: Sample, output: str, context: Any = None) -> Score:
299
+ try:
300
+ import sys
301
+ import bert_score.score # noqa: F401 -- ensures sys.modules entry exists
302
+ from bert_score import score as bert_scorer
303
+ except ImportError:
304
+ raise ExtraNotInstalled("bert-score", "pip install bert-score") from None
305
+ ref = [sample.target or ""]
306
+ hyp = [output]
307
+ # bert_score's own package __init__ does `from .score import score`,
308
+ # which shadows the `bert_score.score` attribute with that function --
309
+ # so the submodule must be reached via sys.modules, not attribute access.
310
+ _bs_score_mod = sys.modules["bert_score.score"]
311
+ original_get_tokenizer = _bs_score_mod.get_tokenizer
312
+
313
+ def _patched_get_tokenizer(model_type, use_fast_tokenizer=False):
314
+ # Some tokenizers (e.g. microsoft/deberta-xlarge-mnli, this
315
+ # metric's own default) report model_max_length as HuggingFace's
316
+ # "no limit configured" sentinel (~1e30). transformers>=5's
317
+ # Rust-backed truncation setup tries to convert that into a
318
+ # bounded int and raises OverflowError. bert_score itself never
319
+ # clamps this, so we do it here before it's used for truncation.
320
+ tok = original_get_tokenizer(model_type, use_fast_tokenizer)
321
+ if getattr(tok, "model_max_length", 0) > 100_000:
322
+ tok.model_max_length = 512
323
+ return tok
324
+
325
+ _bs_score_mod.get_tokenizer = _patched_get_tokenizer
326
+ try:
327
+ P, R, F1 = bert_scorer(hyp, ref, model_type=self._model_name, verbose=False)
328
+ finally:
329
+ _bs_score_mod.get_tokenizer = original_get_tokenizer
330
+ f1 = F1.item()
331
+ return Score(name=self.name, value=f1, kind=self.kind)