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.
- auditkit/README.md +99 -0
- auditkit/__init__.py +177 -0
- auditkit/__main__.py +3 -0
- auditkit/_bootstrap.py +77 -0
- auditkit/_identity_guard.py +99 -0
- auditkit/adapter.py +264 -0
- auditkit/annotator.py +339 -0
- auditkit/api.py +502 -0
- auditkit/assets/auditkit_logo.png +0 -0
- auditkit/cache.py +47 -0
- auditkit/cli.py +417 -0
- auditkit/comparison.py +563 -0
- auditkit/diff.py +265 -0
- auditkit/errors.py +54 -0
- auditkit/evaluator.py +20 -0
- auditkit/experiment.py +145 -0
- auditkit/hf_publish.py +262 -0
- auditkit/lmeval_engine.py +550 -0
- auditkit/loaders.py +121 -0
- auditkit/logs.py +18 -0
- auditkit/metric.py +199 -0
- auditkit/metrics/README.md +15 -0
- auditkit/metrics/__init__.py +0 -0
- auditkit/metrics/code.py +222 -0
- auditkit/metrics/embedding.py +131 -0
- auditkit/metrics/encoder_judge.py +423 -0
- auditkit/metrics/generation.py +331 -0
- auditkit/metrics/guard.py +412 -0
- auditkit/metrics/hallucination.py +45 -0
- auditkit/metrics/judge.py +547 -0
- auditkit/metrics/pairwise.py +153 -0
- auditkit/metrics/perf.py +53 -0
- auditkit/metrics/rag.py +149 -0
- auditkit/metrics/security.py +64 -0
- auditkit/metrics/toxicity.py +238 -0
- auditkit/model/README.md +16 -0
- auditkit/model/__init__.py +485 -0
- auditkit/model/anthropic.py +94 -0
- auditkit/model/api_gen.py +133 -0
- auditkit/model/groq_gen.py +121 -0
- auditkit/model/hf_gen.py +385 -0
- auditkit/model/lexsi.py +155 -0
- auditkit/model/litellm_gen.py +65 -0
- auditkit/model/openai.py +90 -0
- auditkit/model/openrouter_gen.py +152 -0
- auditkit/model/vllm_gen.py +316 -0
- auditkit/model_compare.py +655 -0
- auditkit/redteam/README.md +9 -0
- auditkit/redteam/__init__.py +26 -0
- auditkit/redteam/detector.py +37 -0
- auditkit/redteam/detectors/README.md +5 -0
- auditkit/redteam/detectors/builtin.py +126 -0
- auditkit/redteam/probe.py +39 -0
- auditkit/redteam/probes/README.md +5 -0
- auditkit/redteam/probes/builtin.py +85 -0
- auditkit/redteam/runner.py +206 -0
- auditkit/registry.py +65 -0
- auditkit/report.py +278 -0
- auditkit/report_format.py +52 -0
- auditkit/router.py +54 -0
- auditkit/runner.py +575 -0
- auditkit/runspec.py +159 -0
- auditkit/sample.py +40 -0
- auditkit/scenario.py +88 -0
- auditkit/scenarios/README.md +10 -0
- auditkit/scenarios/__init__.py +4 -0
- auditkit/scenarios/arc.py +33 -0
- auditkit/scenarios/gsm8k.py +32 -0
- auditkit/scenarios/hellaswag.py +33 -0
- auditkit/scenarios/humaneval.py +32 -0
- auditkit/scenarios/mmlu.py +34 -0
- auditkit/scenarios/truthfulqa.py +33 -0
- auditkit/score.py +165 -0
- auditkit/scorers.py +117 -0
- auditkit/scoring.py +79 -0
- auditkit/types.py +69 -0
- auditkit-1.0.0.dist-info/METADATA +396 -0
- auditkit-1.0.0.dist-info/RECORD +81 -0
- auditkit-1.0.0.dist-info/WHEEL +4 -0
- auditkit-1.0.0.dist-info/entry_points.txt +2 -0
- 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()
|