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/cli.py ADDED
@@ -0,0 +1,417 @@
1
+ """auditkit CLI — run evaluations from the command line."""
2
+ from __future__ import annotations
3
+ import argparse
4
+ import sys
5
+ import os
6
+ from .api import evaluate
7
+ from .loaders import load_csv
8
+ from .sample import Sample
9
+ from .report_format import Report
10
+
11
+
12
+ def _load_yaml_config(path: str) -> dict:
13
+ """Load a YAML config file (pyyaml optional)."""
14
+ with open(path) as f:
15
+ raw = f.read()
16
+ try:
17
+ import yaml as _yaml
18
+ return _yaml.safe_load(raw) or {}
19
+ except ImportError:
20
+ import json as _json
21
+ try:
22
+ return _json.loads(raw)
23
+ except _json.JSONDecodeError:
24
+ raise SystemExit(
25
+ "PyYAML is not installed. Install it with 'pip install pyyaml' "
26
+ "or provide a JSON config file instead."
27
+ )
28
+
29
+
30
+ def _merge_yaml(args: argparse.Namespace, cfg: dict) -> argparse.Namespace:
31
+ """Overlay YAML keys onto CLI args (CLI flags take precedence when not None)."""
32
+ mapping = {
33
+ "model": "model", "system_prompt": "system_prompt",
34
+ "instruction": "instruction", "template": "template",
35
+ "temperature": "temperature", "top_p": "top_p",
36
+ "max_tokens": "max_tokens", "seed": "seed",
37
+ "limit": "limit", "trials": "trials",
38
+ "num_fewshot": "num_fewshot", "concurrency": "concurrency",
39
+ "experiment": "experiment", "output": "output",
40
+ "format": "format", "mlflow_uri": "mlflow_uri",
41
+ "dataset": "dataset", "subject": "subject",
42
+ "adapter": "adapter",
43
+ "gpu_memory_utilization": "gpu_memory_utilization",
44
+ }
45
+ for yaml_key, ns_key in mapping.items():
46
+ yv = cfg.get(yaml_key)
47
+ if yv is not None and getattr(args, ns_key, None) is None:
48
+ setattr(args, ns_key, yv)
49
+
50
+ if "tags" in cfg and not args.tag:
51
+ args.tag = cfg["tags"]
52
+ if "stop" in cfg and not args.stop:
53
+ args.stop = cfg["stop"]
54
+ if "split" in cfg:
55
+ s = cfg["split"]
56
+ if args.split_strategy is None:
57
+ args.split_strategy = s.get("strategy")
58
+ args.train_ratio = s.get("train_ratio", args.train_ratio)
59
+ args.val_ratio = s.get("val_ratio", args.val_ratio)
60
+ args.test_ratio = s.get("test_ratio", args.test_ratio)
61
+ args.split_seed = s.get("seed", args.split_seed)
62
+ return args
63
+
64
+
65
+ def _build_eval_parser(prog: str = "auditkit eval") -> argparse.ArgumentParser:
66
+ p = argparse.ArgumentParser(prog=prog, description="Evaluate any model on any task")
67
+ p.add_argument("--config", help="Path to YAML/JSON config file")
68
+ p.add_argument("--model", required=True, help="Model spec (e.g. hf:gpt2, groq:llama-3.3-70b-versatile)")
69
+ p.add_argument("--csv", help="Path to CSV dataset")
70
+ p.add_argument("--input-col", default="input", help="CSV input column")
71
+ p.add_argument("--target-col", default="target", help="CSV target column")
72
+ p.add_argument("--output", "-o", help="Save results to path")
73
+ p.add_argument("--format", choices=["json", "csv", "md"], default="json", help="Output format")
74
+ p.add_argument("--temperature", type=float, help="Generation temperature")
75
+ p.add_argument("--top-p", type=float, help="Nucleus sampling top-p")
76
+ p.add_argument("--max-tokens", type=int, help="Max tokens to generate")
77
+ p.add_argument("--stop", nargs="*", help="Stop sequences")
78
+ p.add_argument("--seed", type=int, help="Random seed")
79
+ p.add_argument("--limit", type=int, help="Max samples")
80
+ p.add_argument("--trials", type=int, help="Number of trials")
81
+ p.add_argument("--num-fewshot", type=int, help="Few-shot examples count")
82
+ p.add_argument("--concurrency", type=int, help="Max concurrency")
83
+ p.add_argument("--verbose", action="store_true", help="Verbose output")
84
+ p.add_argument("--split-strategy", choices=["sequential", "random"], help="Dataset split strategy")
85
+ p.add_argument("--train-ratio", type=float, default=0.0, help="Train split ratio")
86
+ p.add_argument("--val-ratio", type=float, default=0.0, help="Validation split ratio")
87
+ p.add_argument("--test-ratio", type=float, default=1.0, help="Test split ratio")
88
+ p.add_argument("--split-seed", type=int, help="Split random seed")
89
+ p.add_argument("--experiment", help="Experiment name for tracking")
90
+ p.add_argument("--mlflow-uri", help="MLflow tracking URI")
91
+ p.add_argument("--tag", action="append", help="Tags (can repeat)")
92
+ p.add_argument("--dataset", help="Built-in dataset name (mmlu, gsm8k, arc)")
93
+ p.add_argument("--subject", help="MMLU subject (when --dataset=mmlu)")
94
+ p.add_argument("--engine", choices=["native", "lmeval"], default="native",
95
+ help="Eval engine: 'native' spine (default) or 'lmeval' (lm-eval harness)")
96
+ p.add_argument("--tasks", help="lm-eval task name(s), comma-separated (with --engine lmeval)")
97
+ p.add_argument("--adapter", default="generation", choices=["generation", "chat", "instruction", "fewshot", "rag", "template"], help="Adapter type")
98
+ p.add_argument("--system-prompt", default="You are a helpful assistant.", help="System prompt (for chat adapter)")
99
+ p.add_argument("--instruction", default="Answer the following question:", help="Instruction prefix (for instruction adapter)")
100
+ p.add_argument("--template", default="{input}", help="Template string (for template adapter)")
101
+ p.add_argument(
102
+ "--gpu-memory-utilization",
103
+ type=float,
104
+ help="vLLM GPU memory fraction (forwarded to AutoModel.resolve)",
105
+ )
106
+ return p
107
+
108
+
109
+ def _run_eval(args: argparse.Namespace, yaml_prompts: list[str] | None = None) -> None:
110
+ """Execute an evaluation from parsed args."""
111
+ cfg = {}
112
+ if args.config:
113
+ cfg = _load_yaml_config(args.config)
114
+ args = _merge_yaml(args, cfg)
115
+
116
+ from .runspec import RunConfig, SplitConfig
117
+
118
+ split = None
119
+ if args.split_strategy:
120
+ split_kwargs = dict(
121
+ strategy=args.split_strategy,
122
+ train_ratio=args.train_ratio,
123
+ val_ratio=args.val_ratio,
124
+ test_ratio=args.test_ratio,
125
+ )
126
+ # Only override SplitConfig's own seed=0 default when --split-seed
127
+ # was actually passed -- args.split_seed is None whenever it wasn't
128
+ # (argparse has no default= on that flag), and passing seed=None
129
+ # explicitly would silently defeat SplitConfig's own reproducibility
130
+ # guarantee (None draws fresh OS entropy every call, reshuffling
131
+ # differently each run instead of the same way every time).
132
+ if args.split_seed is not None:
133
+ split_kwargs["seed"] = args.split_seed
134
+ split = SplitConfig(**split_kwargs)
135
+
136
+ config = RunConfig(
137
+ temperature=args.temperature,
138
+ top_p=args.top_p,
139
+ max_tokens=args.max_tokens,
140
+ stop_sequences=args.stop,
141
+ seed=args.seed,
142
+ limit=args.limit,
143
+ trials=args.trials,
144
+ num_fewshot=args.num_fewshot,
145
+ concurrency=args.concurrency if args.concurrency is not None else 1,
146
+ split=split,
147
+ )
148
+
149
+ engine = getattr(args, "engine", "native")
150
+ tasks = getattr(args, "tasks", None) or cfg.get("tasks")
151
+ if engine == "lmeval" or tasks:
152
+ from .api import run_lmeval
153
+ if not tasks:
154
+ print("--engine lmeval needs --tasks (e.g. --tasks mmlu,gsm8k).")
155
+ return
156
+ result = run_lmeval(tasks, model=args.model, config=config,
157
+ experiment_name=args.experiment, tags=args.tag)
158
+ if args.output:
159
+ if args.format == "md":
160
+ with open(args.output, "w") as fh:
161
+ fh.write(str(Report(result)))
162
+ else:
163
+ result.save(args.output, fmt=args.format)
164
+ else:
165
+ print(result.summary())
166
+ return
167
+
168
+ dataset_kwargs = {}
169
+ yaml_prompts = yaml_prompts or cfg.get("prompts")
170
+ if args.dataset:
171
+ dataset = args.dataset
172
+ if args.subject:
173
+ dataset_kwargs["subject"] = args.subject
174
+ elif args.csv:
175
+ samples = load_csv(args.csv, input_col=args.input_col, target_col=args.target_col)
176
+ dataset = samples
177
+ elif yaml_prompts:
178
+ dataset = [Sample(input=p) for p in yaml_prompts]
179
+ else:
180
+ dataset = [Sample(input=line.rstrip()) for line in sys.stdin if line.strip()]
181
+
182
+ if not dataset:
183
+ print("No dataset provided. Use --csv, --dataset, --config with prompts:, or pipe input via stdin.")
184
+ return
185
+
186
+ if args.adapter == "chat":
187
+ from .adapter import ChatAdapter
188
+ adapter = ChatAdapter(system_prompt=args.system_prompt)
189
+ elif args.adapter == "instruction":
190
+ from .adapter import InstructionAdapter
191
+ adapter = InstructionAdapter(instruction=args.instruction)
192
+ elif args.adapter == "fewshot":
193
+ from .adapter import FewShotAdapter
194
+ adapter = FewShotAdapter(num_shots=args.num_fewshot or 3)
195
+ elif args.adapter == "rag":
196
+ from .adapter import RAGAdapter
197
+ adapter = RAGAdapter()
198
+ elif args.adapter == "template":
199
+ from .adapter import TemplateAdapter
200
+ adapter = TemplateAdapter(template=args.template)
201
+ else:
202
+ adapter = None
203
+
204
+ eval_opts = dict(dataset_kwargs)
205
+ gpu_util = getattr(args, "gpu_memory_utilization", None)
206
+ if gpu_util is not None:
207
+ eval_opts["gpu_memory_utilization"] = gpu_util
208
+ result = evaluate(dataset, model=args.model, adapter=adapter, config=config,
209
+ experiment_name=args.experiment, tags=args.tag, **eval_opts)
210
+
211
+ if args.mlflow_uri:
212
+ from .experiment import Experiment
213
+ exp = Experiment(name=args.experiment or "cli_run")
214
+ exp.add(result)
215
+ exp.log_mlflow(experiment_name=args.experiment, tracking_uri=args.mlflow_uri)
216
+
217
+ if args.output:
218
+ if args.format == "md":
219
+ with open(args.output, "w") as fh:
220
+ fh.write(str(Report(result)))
221
+ else:
222
+ result.save(args.output, fmt=args.format)
223
+ else:
224
+ print(result.summary())
225
+
226
+
227
+ def cmd_init(args: argparse.Namespace) -> None:
228
+ """Scaffold a new auditkit project."""
229
+ path = args.path or "."
230
+ config_path = os.path.join(path, "auditkit.yaml")
231
+ if os.path.exists(config_path):
232
+ print(f"File already exists: {config_path}")
233
+ return
234
+ os.makedirs(path, exist_ok=True)
235
+ with open(config_path, "w") as f:
236
+ f.write("""# auditkit evaluation config
237
+ model: hf:gpt2
238
+ prompts:
239
+ - "What is the capital of France?"
240
+ - "Explain quantum computing in one sentence."
241
+ temperature: 0.0
242
+ max_tokens: 128
243
+ concurrency: 4
244
+ output: results.json
245
+ """)
246
+ print(f"Created {config_path}")
247
+ print("Edit the file then run: auditkit eval --config auditkit.yaml")
248
+
249
+
250
+ def cmd_list(args: argparse.Namespace) -> None:
251
+ """List available resources."""
252
+ from .registry import ADAPTERS, ANNOTATORS, METRICS, SCENARIOS
253
+
254
+ resource = getattr(args, "resource", "all")
255
+ if resource in ("metrics", "all"):
256
+ print("Built-in metrics:")
257
+ print(" " + ", ".join(METRICS.names()))
258
+ print("")
259
+ if resource in ("datasets", "all"):
260
+ print("Built-in datasets:")
261
+ print(" " + ", ".join(SCENARIOS.names()))
262
+ print("")
263
+ if resource in ("adapters", "all"):
264
+ print("Built-in adapters:")
265
+ print(" " + ", ".join(ADAPTERS.names()))
266
+ print("")
267
+ if resource in ("annotators", "all"):
268
+ print("Built-in annotators:")
269
+ print(" " + ", ".join(ANNOTATORS.names()))
270
+ print("")
271
+ if resource in ("models", "all"):
272
+ print("Model backends:")
273
+ print(" precomputed - Scores samples with actual_output already set")
274
+ print(" openai:<model> - OpenAI (e.g. openai:gpt-4o)")
275
+ print(" anthropic:<model> - Anthropic (e.g. anthropic:claude-3-opus)")
276
+ print(" hf:<model> - HuggingFace Transformers")
277
+ print(" lexsi:<model> - Lexsi gateway (OpenAI-compatible)")
278
+ print(" vllm:<model> - vLLM")
279
+ print(" litellm:<model> - LiteLLM proxy (e.g. litellm:ollama/llama3.1)")
280
+ print(" api:<model> - Generic OpenAI-compatible endpoint (needs base_url=)")
281
+ print(" groq:<model> - Groq (e.g. groq:llama-3.3-70b-versatile)")
282
+ print("")
283
+
284
+
285
+ def cmd_compare(args: argparse.Namespace) -> None:
286
+ """Compare multiple models on the same dataset."""
287
+ from .model_compare import compare_models
288
+ from .sample import Sample
289
+
290
+ models = args.models.split(",")
291
+ dataset = [Sample(input=line.rstrip()) for line in sys.stdin if line.strip()] if not args.csv else None
292
+
293
+ if args.csv:
294
+ from .loaders import load_csv
295
+ dataset = load_csv(args.csv, input_col=args.input_col, target_col=args.target_col)
296
+ elif args.dataset:
297
+ dataset = args.dataset
298
+
299
+ if not dataset:
300
+ print("Provide a dataset via --csv, --dataset, or stdin")
301
+ return
302
+
303
+ scorers = args.scorers.split(",") if getattr(args, "scorers", None) else None
304
+ result = compare_models(models, dataset, scorers=scorers, model_names=models)
305
+
306
+ baseline = getattr(args, "baseline", None)
307
+ if baseline:
308
+ if baseline not in result.runs:
309
+ print(f"--baseline {baseline!r} is not one of --models ({', '.join(models)})")
310
+ return
311
+ for name in models:
312
+ if name != baseline:
313
+ print(result.pairwise(baseline, name).summary())
314
+ print()
315
+ else:
316
+ print(result.summary())
317
+
318
+ if getattr(args, "output", None):
319
+ import json
320
+ from .api import compare as _leaderboard
321
+ payload = {
322
+ "models": models,
323
+ "baseline": baseline,
324
+ "leaderboard": _leaderboard(list(result.runs.values())),
325
+ "per_metric": [
326
+ {"metric": m.metric, "scores": m.scores, "winner": m.winner}
327
+ for m in result.per_metric()
328
+ ],
329
+ }
330
+ with open(args.output, "w", encoding="utf-8") as fh:
331
+ json.dump(payload, fh, indent=2, default=str)
332
+ print(f"wrote {args.output}")
333
+
334
+
335
+ def cmd_redteam(args: argparse.Namespace) -> None:
336
+ """Run red team evaluation."""
337
+ from .redteam import RedTeamRunner
338
+
339
+ probes = args.probes.split(",") if args.probes else None
340
+ detectors = args.detectors.split(",") if args.detectors else None
341
+
342
+ runner = RedTeamRunner(model=args.model)
343
+ result = runner.run(probes=probes, detectors=detectors)
344
+ print(result.summary())
345
+ if args.output:
346
+ import json
347
+ data = [
348
+ {
349
+ "probe": r.probe_name,
350
+ "prompt": r.prompt,
351
+ "output": r.output,
352
+ "passed": r.passed,
353
+ "detector": r.detector_name,
354
+ }
355
+ for r in result.results
356
+ ]
357
+ with open(args.output, "w") as f:
358
+ json.dump(data, f, indent=2)
359
+
360
+
361
+ def main(argv: list[str] | None = None) -> None:
362
+ # Detect subcommand manually so --flags work in flat mode
363
+ args_list = argv if argv is not None else sys.argv[1:]
364
+ first_arg = args_list[0] if args_list else None
365
+ if first_arg and not first_arg.startswith("-") and first_arg in ("init", "list", "eval", "redteam", "compare"):
366
+ command = first_arg
367
+ rest = args_list[1:] if len(args_list) > 1 else []
368
+ else:
369
+ command = None
370
+ rest = args_list
371
+
372
+ if command == "init":
373
+ p = argparse.ArgumentParser(prog="auditkit init", description="Scaffold a new project")
374
+ p.add_argument("path", nargs="?", default=".", help="Project directory")
375
+ args = p.parse_args(rest)
376
+ cmd_init(args)
377
+ return
378
+
379
+ if command == "list":
380
+ p = argparse.ArgumentParser(prog="auditkit list", description="List available resources")
381
+ p.add_argument("resource", nargs="?", default="all", choices=["all", "metrics", "datasets", "adapters", "annotators", "models"], help="Resource type to list")
382
+ args = p.parse_args(rest)
383
+ cmd_list(args)
384
+ return
385
+
386
+ if command == "redteam":
387
+ p = argparse.ArgumentParser(prog="auditkit redteam", description="Run red team evaluation")
388
+ p.add_argument("--model", required=True, help="Model spec (e.g. hf:gpt2, groq:llama-3.3-70b-versatile)")
389
+ p.add_argument("--probes", help="Comma-separated probe names")
390
+ p.add_argument("--detectors", help="Comma-separated detector names")
391
+ p.add_argument("--output", "-o", help="Save results to JSON file")
392
+ args = p.parse_args(rest)
393
+ cmd_redteam(args)
394
+ return
395
+
396
+ if command == "compare":
397
+ p = argparse.ArgumentParser(prog="auditkit compare", description="Compare multiple models on the same dataset")
398
+ p.add_argument("--models", required=True, help="Comma-separated model specs")
399
+ p.add_argument("--csv", help="Path to CSV dataset")
400
+ p.add_argument("--input-col", default="input", help="CSV input column")
401
+ p.add_argument("--target-col", default="target", help="CSV target column")
402
+ p.add_argument("--dataset", help="Built-in dataset name")
403
+ p.add_argument("--scorers", help="Comma-separated scorer/metric names (default: auto)")
404
+ p.add_argument("--baseline", help="Model spec (from --models) to anchor a base-vs-candidate comparison against")
405
+ p.add_argument("--output", "-o", help="Save results to JSON file")
406
+ args = p.parse_args(rest)
407
+ cmd_compare(args)
408
+ return
409
+
410
+ # Default: eval (with backwards compat for flat argument style)
411
+ parser = _build_eval_parser("auditkit" if command is None else "auditkit eval")
412
+ args = parser.parse_args(rest)
413
+ _run_eval(args)
414
+
415
+
416
+ if __name__ == "__main__":
417
+ main()