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
auditkit/report.py ADDED
@@ -0,0 +1,278 @@
1
+ """The result of a run: :class:`Prediction` rows and the :class:`RunResult`.
2
+
3
+ Every run returns the same shape regardless of task or technique: aggregate
4
+ :class:`~auditkit.score.Stat` per metric, a ``headline`` of means, and one
5
+ :class:`Prediction` per sample — the answer browser. Reporting helpers
6
+ (:meth:`RunResult.summary`, :meth:`RunResult.metric_table`,
7
+ :meth:`RunResult.wrong_only`, :meth:`RunResult.save`) read off that.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Optional
15
+
16
+ from .score import Stat
17
+
18
+
19
+ @dataclass
20
+ class Prediction:
21
+ """One sample's record: what was asked, what came back, and how it scored."""
22
+
23
+ run_id: str
24
+ task: str
25
+ sample_id: str
26
+ prompt: str
27
+ raw_output: Optional[str]
28
+ parsed_answer: Optional[str]
29
+ expected: Optional[str]
30
+ correct: Optional[bool]
31
+ score: Optional[float]
32
+ choice_likelihoods: Optional[list[float]] = None
33
+ context: Any = None
34
+ metadata: dict[str, Any] = field(default_factory=dict)
35
+
36
+ def to_doc(self) -> dict[str, Any]:
37
+ return {
38
+ "run_id": self.run_id,
39
+ "task": self.task,
40
+ "sample_id": self.sample_id,
41
+ "prompt": self.prompt,
42
+ "raw_output": self.raw_output,
43
+ "parsed_answer": self.parsed_answer,
44
+ "expected": self.expected,
45
+ "correct": self.correct,
46
+ "score": self.score,
47
+ "choice_likelihoods": self.choice_likelihoods,
48
+ "context": self.context,
49
+ "metadata": self.metadata,
50
+ }
51
+
52
+
53
+ @dataclass
54
+ class RunResult:
55
+ """What ``evaluate()`` returns: aggregates, per-sample rows, and a headline."""
56
+
57
+ run_id: str
58
+ fingerprint: str
59
+ stats: dict[str, Stat]
60
+ predictions: list[Prediction]
61
+ headline: dict[str, float]
62
+ config: Any = None
63
+ model_spec: Any = None
64
+ errors: list[dict] = field(default_factory=list)
65
+ failed_count: int = 0
66
+ experiment_name: str | None = None
67
+ tags: list[str] = field(default_factory=list)
68
+ # Efficiency/systems metrics — latency (ms) and throughput (req/s),
69
+ # measured by timing the run's own model calls (see Runner.run()).
70
+ # None unless RunConfig.track_performance=True was set on this run (the
71
+ # default is False -- most callers scoring a quick eval don't want this
72
+ # measured). When tracking IS on: {} on the reads_actual_output path (no
73
+ # model call happens there at all) or when a run made zero requests --
74
+ # that's a genuinely different state from "never asked to measure" (None).
75
+ perf: Optional[dict[str, Any]] = None
76
+ # Model size/identity — never caller-supplied. Introspected (real
77
+ # parameter count/sparsity/size) for local backends; identity-only
78
+ # ({"is_local": False, "model_name": ...}) for hosted-API backends,
79
+ # since there is no checkpoint to measure. See Model.model_info().
80
+ # None unless RunConfig.track_performance=True (see `perf` above).
81
+ model_size: Optional[dict[str, Any]] = None
82
+ # Real, provider-reported token counts (prompt_tokens/completion_tokens/
83
+ # total_tokens) — populated automatically by Runner from each backend's
84
+ # actual API response (see model/openai.py etc.), never estimated. All
85
+ # zero for local/callable backends that don't report usage. This is the
86
+ # only input .cost() needs from the run itself; $/token pricing is
87
+ # deliberately NOT bundled in the library (see .cost()'s docstring).
88
+ # None unless RunConfig.track_performance=True (see `perf` above).
89
+ token_usage: Optional[dict[str, int]] = None
90
+
91
+ def cost(self, pricing: dict[str, float]) -> Optional[float]:
92
+ """Dollar cost of this run, given *pricing* — never a library default.
93
+
94
+ There is no live pricing API and provider prices change over time, so
95
+ a hardcoded table would silently go stale; you must supply
96
+ ``{"input_per_1m": <$ per 1M prompt tokens>, "output_per_1m": <$ per
97
+ 1M completion tokens>}`` yourself (check your provider's current
98
+ pricing page). Returns ``None`` if this run recorded no token usage
99
+ at all (a local/callable backend, nothing was ever generated, or
100
+ ``RunConfig.track_performance`` was never enabled on this run) —
101
+ there is nothing to price.
102
+ """
103
+ if not self.token_usage or not self.token_usage.get("total_tokens"):
104
+ return None
105
+ return (
106
+ self.token_usage["prompt_tokens"] / 1_000_000 * pricing["input_per_1m"]
107
+ + self.token_usage["completion_tokens"] / 1_000_000 * pricing["output_per_1m"]
108
+ )
109
+
110
+ @property
111
+ def samples(self) -> list[Prediction]:
112
+ return self.predictions
113
+
114
+ def metric_table(self) -> list[dict[str, Any]]:
115
+ """One row per metric: name, mean, stderr, and n."""
116
+ rows = []
117
+ for name, stat in self.stats.items():
118
+ rows.append({
119
+ "name": name,
120
+ "mean": stat.mean,
121
+ "stderr": stat.stderr,
122
+ "n": stat.count,
123
+ })
124
+ return rows
125
+
126
+ def summary(self) -> str:
127
+ """A compact human-readable metric table (mean ± stderr over n)."""
128
+ lines = [f"run {self.run_id} (fingerprint {self.fingerprint})"]
129
+ for row in self.metric_table():
130
+ lines.append(
131
+ f" {row['name']}: {row['mean']:.4f} ± {row['stderr']:.4f} (n={row['n']})"
132
+ )
133
+ perf_line = self._perf_line()
134
+ if perf_line:
135
+ lines.append(f" {perf_line}")
136
+ size_line = self._model_size_line()
137
+ if size_line:
138
+ lines.append(f" {size_line}")
139
+ if self.token_usage and self.token_usage.get("total_tokens"):
140
+ lines.append(
141
+ f" tokens: {self.token_usage['prompt_tokens']:,} in + "
142
+ f"{self.token_usage['completion_tokens']:,} out = "
143
+ f"{self.token_usage['total_tokens']:,} total "
144
+ f"(pass pricing to .cost() for a $ figure)"
145
+ )
146
+ return "\n".join(lines)
147
+
148
+ def _perf_line(self) -> str:
149
+ """One-line latency/throughput summary, or '' if nothing was measured
150
+ (the reads_actual_output path makes no model call to time, or
151
+ RunConfig.track_performance was never enabled on this run)."""
152
+ if not self.perf:
153
+ return ""
154
+ lat = self.perf.get("latency_ms") or {}
155
+ thr = self.perf.get("throughput") or {}
156
+ if not lat.get("count"):
157
+ return ""
158
+ line = (
159
+ f"perf: latency={lat['mean']:.0f}ms "
160
+ f"(n={lat['count']} calls) | throughput={thr.get('rps', 0.0):.2f} req/s"
161
+ )
162
+ # Only shown when the backend actually reported token usage (hosted
163
+ # APIs) -- 0 tok/s for a local/callable backend would misleadingly
164
+ # read as "measured zero" rather than "not available here".
165
+ if thr.get("total_tokens_per_sec"):
166
+ line += f" | {thr['output_tokens_per_sec']:.1f} out-tok/s, {thr['total_tokens_per_sec']:.1f} total-tok/s"
167
+ return line
168
+
169
+ def _model_size_line(self) -> str:
170
+ """One-line model size summary, or '' if nothing was recorded at all."""
171
+ ms = self.model_size
172
+ if not ms:
173
+ return ""
174
+ if not ms.get("is_local"):
175
+ return f"model: {ms.get('model_name', '?')} (not a local model -- no measurable size)"
176
+ return (
177
+ f"model: {ms.get('model_name', '?')} -- "
178
+ f"{ms.get('total_params', 0):,} params "
179
+ f"({ms.get('sparsity', 0.0):.1%} sparse), {ms.get('size_mb', 0.0):.1f} MB"
180
+ )
181
+
182
+ def wrong_only(self) -> list[Prediction]:
183
+ """The predictions that were scored incorrect (the regression browser)."""
184
+ return [p for p in self.predictions if p.correct is False]
185
+
186
+ def to_dict(self) -> dict[str, Any]:
187
+ return {
188
+ "run_id": self.run_id,
189
+ "fingerprint": self.fingerprint,
190
+ "headline": self.headline,
191
+ "stats": {name: stat.to_dict() for name, stat in self.stats.items()},
192
+ "predictions": [p.to_doc() for p in self.predictions],
193
+ "config": self.config.to_dict() if self.config else None,
194
+ "model_spec": self.model_spec,
195
+ "errors": self.errors,
196
+ "failed_count": self.failed_count,
197
+ "experiment_name": self.experiment_name,
198
+ "tags": self.tags,
199
+ "perf": self.perf,
200
+ "model_size": self.model_size,
201
+ "token_usage": self.token_usage,
202
+ }
203
+
204
+ @classmethod
205
+ def from_dict(cls, data: dict[str, Any]) -> RunResult:
206
+ from .score import Stat
207
+ from .runspec import RunConfig
208
+ stats = {}
209
+ for name, sdata in data.get("stats", {}).items():
210
+ stats[name] = Stat.from_dict({"name": name, **sdata})
211
+ predictions = [Prediction(**p) for p in data.get("predictions", [])]
212
+ return cls(
213
+ run_id=data["run_id"],
214
+ fingerprint=data["fingerprint"],
215
+ stats=stats,
216
+ predictions=predictions,
217
+ headline=data.get("headline", {}),
218
+ config=RunConfig.from_dict(data["config"]) if data.get("config") else None,
219
+ model_spec=data.get("model_spec"),
220
+ errors=data.get("errors", []),
221
+ failed_count=data.get("failed_count", 0),
222
+ experiment_name=data.get("experiment_name"),
223
+ tags=data.get("tags", []),
224
+ perf=data.get("perf", {}),
225
+ model_size=data.get("model_size", {}),
226
+ token_usage=data.get("token_usage", {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}),
227
+ )
228
+
229
+ @classmethod
230
+ def load(cls, path: str) -> RunResult:
231
+ import json
232
+ with open(path, "r", encoding="utf-8") as fh:
233
+ return cls.from_dict(json.load(fh))
234
+
235
+ def save(self, path: str, fmt: str = "json") -> None:
236
+ """Persist the run as ``json`` (full) or ``csv`` (per-prediction rows)."""
237
+ if fmt == "json":
238
+ with open(path, "w", encoding="utf-8") as fh:
239
+ json.dump(self.to_dict(), fh, indent=2, default=str)
240
+ elif fmt == "csv":
241
+ import csv as csv_mod
242
+ predictions = self.predictions
243
+ if not predictions:
244
+ with open(path, "w") as fh:
245
+ pass
246
+ return
247
+ fields = list(predictions[0].to_doc().keys())
248
+ with open(path, "w", newline="") as fh:
249
+ writer = csv_mod.DictWriter(fh, fieldnames=fields)
250
+ writer.writeheader()
251
+ for p in predictions:
252
+ writer.writerow(p.to_doc())
253
+ else:
254
+ raise ValueError(f"unknown format {fmt!r}; use 'json' or 'csv'")
255
+
256
+ def push_to_hub(
257
+ self,
258
+ repo_id: str,
259
+ private: bool = False,
260
+ token: str | None = None,
261
+ ) -> str:
262
+ """Push predictions to a Hub dataset and stamp the AuditKIT card."""
263
+ from .hf_publish import push_rows_to_hub
264
+
265
+ model = ""
266
+ if isinstance(self.model_spec, str):
267
+ model = self.model_spec
268
+ elif isinstance(self.model_spec, dict):
269
+ model = str(self.model_spec.get("name") or self.model_spec.get("model") or "")
270
+ return push_rows_to_hub(
271
+ [p.to_doc() for p in self.predictions],
272
+ repo_id,
273
+ private=private,
274
+ token=token,
275
+ kind="run",
276
+ method=self.experiment_name or "evaluate",
277
+ model=model,
278
+ )
@@ -0,0 +1,52 @@
1
+ """Pretty-printed and markdown reports for RunResult."""
2
+ from __future__ import annotations
3
+ from typing import Any
4
+ from .report import RunResult
5
+
6
+
7
+ class Report:
8
+ def __init__(self, result: RunResult) -> None:
9
+ self.result = result
10
+
11
+ def metric_rows(self) -> list[dict[str, Any]]:
12
+ rows = []
13
+ for name, stat in self.result.stats.items():
14
+ rows.append({
15
+ "name": name,
16
+ "mean": stat.mean,
17
+ "stderr": stat.stderr,
18
+ "std": stat.std,
19
+ "n": stat.count,
20
+ })
21
+ return rows
22
+
23
+ def __str__(self) -> str:
24
+ lines = [f"Run {self.result.run_id} (fingerprint {self.result.fingerprint})"]
25
+ lines.append("")
26
+ rows = self.metric_rows()
27
+ if rows:
28
+ lines.append(f"{'Metric':<30} {'Mean':>8} {'±Stderr':>8} {'Std':>8} {'N':>6}")
29
+ lines.append("-" * 62)
30
+ for r in rows:
31
+ lines.append(
32
+ f"{r['name']:<30} {r['mean']:>8.4f} {r['stderr']:>8.4f} "
33
+ f"{r['std']:>8.4f} {r['n']:>6}"
34
+ )
35
+ lines.append("")
36
+ lines.append(f"{len(self.result.predictions)} predictions")
37
+ wrong = len(self.result.wrong_only())
38
+ if wrong:
39
+ lines.append(f"{wrong} wrong / {len(self.result.predictions)} total")
40
+ return "\n".join(lines)
41
+
42
+ def markdown(self) -> str:
43
+ rows = self.metric_rows()
44
+ if not rows:
45
+ return ""
46
+ lines = ["| Metric | Mean | ±Stderr | Std | N |", "|---|---|---|---|---|"]
47
+ for r in rows:
48
+ lines.append(
49
+ f"| {r['name']} | {r['mean']:.4f} | {r['stderr']:.4f} "
50
+ f"| {r['std']:.4f} | {r['n']} |"
51
+ )
52
+ return "\n".join(lines)
auditkit/router.py ADDED
@@ -0,0 +1,54 @@
1
+ """Automatic adapter selection from the dataset's shape.
2
+
3
+ Routing is a pure function of the **task** — what each sample carries — never of
4
+ the model:
5
+
6
+ - ``choices`` present -> :class:`MCQAdapter`
7
+ - ``retrieval_context`` present -> :class:`RAGAdapter`
8
+ - otherwise -> :class:`GenerationAdapter`
9
+
10
+ Why not look at the model? Chat-vs-base formatting (an instruct model's chat
11
+ template vs a base model's raw text) is applied by the **model backend** at
12
+ generate-time, so any adapter's output is formatted correctly for whatever model
13
+ runs it. That means routing never has to know the model, never has to download a
14
+ tokenizer to probe it, and an MCQ/RAG/plain task is handled correctly on both
15
+ base and instruct models without picking a different adapter.
16
+
17
+ :class:`ChatAdapter` is intentionally **never** auto-selected: its only job now
18
+ is to inject a specific system prompt, which is an explicit user choice — not
19
+ something to guess (and auto-injecting a generic "You are a helpful assistant"
20
+ would silently confound an eval).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Sequence, Union
26
+
27
+ from .adapter import Adapter, GenerationAdapter, MCQAdapter, RAGAdapter
28
+ from .sample import Sample
29
+
30
+
31
+ def _first_sample(samples: Union[Sample, Sequence[Sample]]) -> Sample:
32
+ if isinstance(samples, Sample):
33
+ return samples
34
+ for s in samples:
35
+ return s
36
+ raise ValueError("route_adapter needs at least one sample to inspect")
37
+
38
+
39
+ def route_adapter(samples: Union[Sample, Sequence[Sample]], model: Any = None,
40
+ **_ignored: Any) -> Adapter:
41
+ """Pick an adapter for *samples* from their shape.
42
+
43
+ Routes on the field each adapter actually needs (``choices`` for MCQ,
44
+ ``retrieval_context`` for RAG), so a sample can never be routed to an
45
+ adapter that would then fail for lack of that field. ``model`` is accepted
46
+ for backward compatibility but ignored — routing is task-based; the model
47
+ backend handles model-specific formatting.
48
+ """
49
+ sample = _first_sample(samples)
50
+ if sample.choices:
51
+ return MCQAdapter()
52
+ if sample.retrieval_context:
53
+ return RAGAdapter()
54
+ return GenerationAdapter()