logitgate 0.2.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.
- logitgate/__init__.py +103 -0
- logitgate/backends/__init__.py +25 -0
- logitgate/backends/base.py +259 -0
- logitgate/backends/dory_pytorch.py +330 -0
- logitgate/backends/llamacpp.py +352 -0
- logitgate/backends/ollama.py +286 -0
- logitgate/backends/openai_compat.py +195 -0
- logitgate/backends/recurrent.py +245 -0
- logitgate/cascade.py +394 -0
- logitgate/cli.py +264 -0
- logitgate/config.py +172 -0
- logitgate/debiasing/__init__.py +35 -0
- logitgate/debiasing/inversion.py +180 -0
- logitgate/debiasing/permutation.py +287 -0
- logitgate/debiasing/softmax.py +98 -0
- logitgate/gates/__init__.py +14 -0
- logitgate/gates/ast_gate.py +544 -0
- logitgate/gates/base.py +68 -0
- logitgate/gates/judge_gate.py +318 -0
- logitgate/gates/logit_gate.py +951 -0
- logitgate/integrations/__init__.py +10 -0
- logitgate/integrations/deepeval.py +212 -0
- logitgate/integrations/langgraph.py +30 -0
- logitgate/integrations/pytest_plugin.py +68 -0
- logitgate/models.py +109 -0
- logitgate/py.typed +0 -0
- logitgate/snapshot.py +224 -0
- logitgate-0.2.0.dist-info/METADATA +1236 -0
- logitgate-0.2.0.dist-info/RECORD +33 -0
- logitgate-0.2.0.dist-info/WHEEL +5 -0
- logitgate-0.2.0.dist-info/entry_points.txt +5 -0
- logitgate-0.2.0.dist-info/licenses/LICENSE +201 -0
- logitgate-0.2.0.dist-info/top_level.txt +1 -0
logitgate/cli.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from logitgate.cascade import Cascade
|
|
8
|
+
from logitgate.gates.ast_gate import ASTGate
|
|
9
|
+
from logitgate.gates.logit_gate import LogitGate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="logitgate",
|
|
15
|
+
description="LogitGate CLI: Calibrated first-token logit evaluation and cascaded routing.",
|
|
16
|
+
)
|
|
17
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
18
|
+
|
|
19
|
+
eval_parser = subparsers.add_parser(
|
|
20
|
+
"eval",
|
|
21
|
+
help="Evaluate candidate artifact against a rubric",
|
|
22
|
+
)
|
|
23
|
+
eval_parser.add_argument(
|
|
24
|
+
"--rubric",
|
|
25
|
+
required=True,
|
|
26
|
+
help="Evaluation rubric or question to verify against candidate",
|
|
27
|
+
)
|
|
28
|
+
eval_parser.add_argument(
|
|
29
|
+
"--candidate",
|
|
30
|
+
type=str,
|
|
31
|
+
default=None,
|
|
32
|
+
help="Candidate text/artifact to evaluate",
|
|
33
|
+
)
|
|
34
|
+
eval_parser.add_argument(
|
|
35
|
+
"--file",
|
|
36
|
+
type=str,
|
|
37
|
+
default=None,
|
|
38
|
+
help="Path to file containing candidate text to evaluate",
|
|
39
|
+
)
|
|
40
|
+
eval_parser.add_argument(
|
|
41
|
+
"--backend",
|
|
42
|
+
type=str,
|
|
43
|
+
default=None,
|
|
44
|
+
help="Backend specifier (defaults to LOGITGATE_BACKEND or stub://default)",
|
|
45
|
+
)
|
|
46
|
+
eval_parser.add_argument(
|
|
47
|
+
"--model",
|
|
48
|
+
type=str,
|
|
49
|
+
default=None,
|
|
50
|
+
help="Optional model name for backend",
|
|
51
|
+
)
|
|
52
|
+
eval_parser.add_argument(
|
|
53
|
+
"--threshold",
|
|
54
|
+
type=float,
|
|
55
|
+
default=None,
|
|
56
|
+
help="Pass probability threshold (defaults to LOGITGATE_PASS_THRESHOLD or 0.85)",
|
|
57
|
+
)
|
|
58
|
+
eval_parser.add_argument(
|
|
59
|
+
"--fail-threshold",
|
|
60
|
+
type=float,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Fail probability threshold (defaults to LOGITGATE_FAIL_THRESHOLD or 0.20)",
|
|
63
|
+
)
|
|
64
|
+
eval_parser.add_argument(
|
|
65
|
+
"--ast-syntax",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="Enable Layer 0 AST syntax validation before logit evaluation",
|
|
68
|
+
)
|
|
69
|
+
eval_parser.add_argument(
|
|
70
|
+
"--json",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Output verdict and traces in JSON format",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
cal_parser = subparsers.add_parser(
|
|
76
|
+
"calibrate",
|
|
77
|
+
help="Run micro-calibration dataset to fit and cache optimal logit temperature",
|
|
78
|
+
)
|
|
79
|
+
cal_parser.add_argument(
|
|
80
|
+
"--backend",
|
|
81
|
+
type=str,
|
|
82
|
+
default=None,
|
|
83
|
+
help="Backend specifier (defaults to LOGITGATE_BACKEND or stub://default)",
|
|
84
|
+
)
|
|
85
|
+
cal_parser.add_argument(
|
|
86
|
+
"--model",
|
|
87
|
+
type=str,
|
|
88
|
+
default=None,
|
|
89
|
+
help="Optional model name for backend",
|
|
90
|
+
)
|
|
91
|
+
cal_parser.add_argument(
|
|
92
|
+
"--cache-path",
|
|
93
|
+
type=str,
|
|
94
|
+
default=None,
|
|
95
|
+
help="Optional custom path to calibration cache JSON file",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
explain_parser = subparsers.add_parser(
|
|
99
|
+
"explain",
|
|
100
|
+
help="Replay frozen failure snapshot and generate full Layer-2 diagnostic explanation",
|
|
101
|
+
)
|
|
102
|
+
explain_parser.add_argument(
|
|
103
|
+
"snapshot_path",
|
|
104
|
+
nargs="?",
|
|
105
|
+
default=".logitgate/failures",
|
|
106
|
+
help="Path to snapshot JSON file or directory containing latest.json (default: .logitgate/failures)",
|
|
107
|
+
)
|
|
108
|
+
explain_parser.add_argument(
|
|
109
|
+
"--backend",
|
|
110
|
+
type=str,
|
|
111
|
+
default=None,
|
|
112
|
+
help="Generative backend specifier (e.g. stub://..., ollama://...)",
|
|
113
|
+
)
|
|
114
|
+
explain_parser.add_argument(
|
|
115
|
+
"--model",
|
|
116
|
+
type=str,
|
|
117
|
+
default=None,
|
|
118
|
+
help="Optional model name for backend",
|
|
119
|
+
)
|
|
120
|
+
explain_parser.add_argument(
|
|
121
|
+
"--max-tokens",
|
|
122
|
+
type=int,
|
|
123
|
+
default=None,
|
|
124
|
+
help="Maximum tokens for generative explanation",
|
|
125
|
+
)
|
|
126
|
+
explain_parser.add_argument(
|
|
127
|
+
"--no-think",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Disable Chain-of-Thought reasoning tokens in generative explanation",
|
|
130
|
+
)
|
|
131
|
+
explain_parser.add_argument(
|
|
132
|
+
"--json",
|
|
133
|
+
action="store_true",
|
|
134
|
+
help="Output enriched verdict and traces in JSON format",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return parser
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
141
|
+
parser = create_parser()
|
|
142
|
+
args = parser.parse_args(argv)
|
|
143
|
+
|
|
144
|
+
if args.command == "calibrate":
|
|
145
|
+
from pathlib import Path
|
|
146
|
+
|
|
147
|
+
from logitgate.backends.base import resolve_logit_backend
|
|
148
|
+
from logitgate.debiasing.permutation import auto_calibrate_backend
|
|
149
|
+
|
|
150
|
+
backend = resolve_logit_backend(args.backend, model=args.model)
|
|
151
|
+
cache_path = Path(args.cache_path) if args.cache_path else None
|
|
152
|
+
effective_model = args.model or getattr(backend, "model", "default")
|
|
153
|
+
|
|
154
|
+
print(f"Calibrating model '{effective_model}' on backend {backend.__class__.__name__}...")
|
|
155
|
+
result = auto_calibrate_backend(
|
|
156
|
+
backend,
|
|
157
|
+
model_key=effective_model,
|
|
158
|
+
save_cache=True,
|
|
159
|
+
cache_path=cache_path,
|
|
160
|
+
)
|
|
161
|
+
print(f"Optimal Temperature: {result.optimal_temperature:.4f}")
|
|
162
|
+
print(f"Brier Score: {result.brier_score_before:.6f} -> {result.brier_score_after:.6f}")
|
|
163
|
+
print(f"ECE: {result.ece_before:.6f} -> {result.ece_after:.6f}")
|
|
164
|
+
print("Calibration successfully saved to cache.")
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
if args.command == "explain":
|
|
168
|
+
from logitgate.snapshot import VerdictSnapshot
|
|
169
|
+
try:
|
|
170
|
+
snapshot = VerdictSnapshot.load(args.snapshot_path)
|
|
171
|
+
except (OSError, ValueError, KeyError) as e:
|
|
172
|
+
sys.stderr.write(f"Error loading snapshot from '{args.snapshot_path}': {e}\n")
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
think = not args.no_think
|
|
176
|
+
verdict = snapshot.explain(
|
|
177
|
+
backend=args.backend,
|
|
178
|
+
model=args.model,
|
|
179
|
+
max_tokens=args.max_tokens,
|
|
180
|
+
think=think,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if args.json:
|
|
184
|
+
print(verdict.model_dump_json(indent=2))
|
|
185
|
+
else:
|
|
186
|
+
print("=" * 60)
|
|
187
|
+
print("LOGITGATE FAILURE REPLAY & EXPLANATION")
|
|
188
|
+
print("=" * 60)
|
|
189
|
+
print(f"Snapshot ID: {snapshot.snapshot_id}")
|
|
190
|
+
if snapshot.test_name:
|
|
191
|
+
print(f"Test Name: {snapshot.test_name}")
|
|
192
|
+
print(f"Timestamp: {snapshot.timestamp}")
|
|
193
|
+
print(f"Rubric: {snapshot.rubric}")
|
|
194
|
+
orig_prob = snapshot.verdict.probability
|
|
195
|
+
prob_str = f"{orig_prob:.4f}" if orig_prob is not None else "N/A"
|
|
196
|
+
print(f"Original Prob: {prob_str}")
|
|
197
|
+
print(f"Triggered Layer: {snapshot.verdict.triggered_layer}")
|
|
198
|
+
print("-" * 60)
|
|
199
|
+
print(f"Replay Verdict: {verdict.status.value.upper()}")
|
|
200
|
+
print(f"Replay Latency: {verdict.total_latency_ms:.2f}ms")
|
|
201
|
+
print(f"Tokens Spent: {verdict.total_tokens_generated}")
|
|
202
|
+
print("-" * 60)
|
|
203
|
+
print("Diagnostic Critique:")
|
|
204
|
+
print(verdict.reason or "(No reason provided)")
|
|
205
|
+
print("=" * 60)
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
if args.command != "eval":
|
|
209
|
+
parser.print_help()
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
candidate: str | None = None
|
|
213
|
+
if args.file:
|
|
214
|
+
try:
|
|
215
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
216
|
+
candidate = f.read()
|
|
217
|
+
except OSError as e:
|
|
218
|
+
sys.stderr.write(f"Error reading file '{args.file}': {e}\n")
|
|
219
|
+
return 1
|
|
220
|
+
elif args.candidate is not None:
|
|
221
|
+
candidate = args.candidate
|
|
222
|
+
else:
|
|
223
|
+
if not sys.stdin.isatty():
|
|
224
|
+
candidate = sys.stdin.read()
|
|
225
|
+
else:
|
|
226
|
+
sys.stderr.write("Error: Either --candidate or --file must be specified.\n")
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
logit_gate = LogitGate(
|
|
230
|
+
rubric=args.rubric,
|
|
231
|
+
backend=args.backend,
|
|
232
|
+
model=args.model,
|
|
233
|
+
pass_threshold=args.threshold,
|
|
234
|
+
fail_threshold=args.fail_threshold,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
evaluator: Cascade | LogitGate
|
|
238
|
+
if args.ast_syntax:
|
|
239
|
+
evaluator = Cascade([
|
|
240
|
+
ASTGate(syntax=True),
|
|
241
|
+
logit_gate,
|
|
242
|
+
])
|
|
243
|
+
else:
|
|
244
|
+
evaluator = logit_gate
|
|
245
|
+
|
|
246
|
+
verdict = evaluator.evaluate(candidate)
|
|
247
|
+
|
|
248
|
+
if args.json:
|
|
249
|
+
print(verdict.model_dump_json(indent=2))
|
|
250
|
+
else:
|
|
251
|
+
verdict_str = "PASSED" if verdict.passed else "FAILED"
|
|
252
|
+
print(f"Verdict: {verdict_str} (status: {verdict.status.value})")
|
|
253
|
+
if verdict.probability is not None:
|
|
254
|
+
print(f"Confidence: {verdict.probability:.4f}")
|
|
255
|
+
print(f"Triggered Layer: {verdict.triggered_layer}")
|
|
256
|
+
print(f"Total Latency: {verdict.total_latency_ms:.2f}ms")
|
|
257
|
+
if verdict.reason:
|
|
258
|
+
print(f"Reason: {verdict.reason}")
|
|
259
|
+
|
|
260
|
+
return 0 if verdict.passed else 1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
sys.exit(main())
|
logitgate/config.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _ConfigState:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self.backend: str | None = None
|
|
9
|
+
self.model: str | None = None
|
|
10
|
+
self.pass_threshold: float | None = None
|
|
11
|
+
self.fail_threshold: float | None = None
|
|
12
|
+
self.logit_temperature: float | None = None
|
|
13
|
+
self.num_ctx: int | None = None
|
|
14
|
+
self.save_failures: bool | None = None
|
|
15
|
+
self.snapshot_dir: str | None = None
|
|
16
|
+
|
|
17
|
+
def reset(self) -> None:
|
|
18
|
+
self.backend = None
|
|
19
|
+
self.model = None
|
|
20
|
+
self.pass_threshold = None
|
|
21
|
+
self.fail_threshold = None
|
|
22
|
+
self.logit_temperature = None
|
|
23
|
+
self.num_ctx = None
|
|
24
|
+
self.save_failures = None
|
|
25
|
+
self.snapshot_dir = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_state = _ConfigState()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _DefaultsMeta(type):
|
|
32
|
+
@property
|
|
33
|
+
def backend(cls) -> str:
|
|
34
|
+
if _state.backend is not None:
|
|
35
|
+
return _state.backend
|
|
36
|
+
return os.environ.get("LOGITGATE_BACKEND", "stub://default")
|
|
37
|
+
|
|
38
|
+
@backend.setter
|
|
39
|
+
def backend(cls, val: str | None) -> None:
|
|
40
|
+
_state.backend = val
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def model(cls) -> str | None:
|
|
44
|
+
if _state.model is not None:
|
|
45
|
+
return _state.model
|
|
46
|
+
return os.environ.get("LOGITGATE_MODEL", None)
|
|
47
|
+
|
|
48
|
+
@model.setter
|
|
49
|
+
def model(cls, val: str | None) -> None:
|
|
50
|
+
_state.model = val
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def pass_threshold(cls) -> float:
|
|
54
|
+
if _state.pass_threshold is not None:
|
|
55
|
+
return _state.pass_threshold
|
|
56
|
+
val = os.environ.get("LOGITGATE_PASS_THRESHOLD")
|
|
57
|
+
return float(val) if val is not None else 0.85
|
|
58
|
+
|
|
59
|
+
@pass_threshold.setter
|
|
60
|
+
def pass_threshold(cls, val: float | None) -> None:
|
|
61
|
+
_state.pass_threshold = val
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def fail_threshold(cls) -> float:
|
|
65
|
+
if _state.fail_threshold is not None:
|
|
66
|
+
return _state.fail_threshold
|
|
67
|
+
val = os.environ.get("LOGITGATE_FAIL_THRESHOLD")
|
|
68
|
+
return float(val) if val is not None else 0.20
|
|
69
|
+
|
|
70
|
+
@fail_threshold.setter
|
|
71
|
+
def fail_threshold(cls, val: float | None) -> None:
|
|
72
|
+
_state.fail_threshold = val
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def logit_temperature(cls) -> float:
|
|
76
|
+
if _state.logit_temperature is not None:
|
|
77
|
+
return _state.logit_temperature
|
|
78
|
+
val = os.environ.get("LOGITGATE_TEMPERATURE")
|
|
79
|
+
return float(val) if val is not None else 1.0
|
|
80
|
+
|
|
81
|
+
@logit_temperature.setter
|
|
82
|
+
def logit_temperature(cls, val: float | None) -> None:
|
|
83
|
+
_state.logit_temperature = val
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def num_ctx(cls) -> int | None:
|
|
87
|
+
if _state.num_ctx is not None:
|
|
88
|
+
return _state.num_ctx
|
|
89
|
+
val = os.environ.get("LOGITGATE_NUM_CTX")
|
|
90
|
+
return int(val) if val is not None else None
|
|
91
|
+
|
|
92
|
+
@num_ctx.setter
|
|
93
|
+
def num_ctx(cls, val: int | None) -> None:
|
|
94
|
+
_state.num_ctx = val
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def save_failures(cls) -> bool:
|
|
98
|
+
if _state.save_failures is not None:
|
|
99
|
+
return _state.save_failures
|
|
100
|
+
return os.environ.get("LOGITGATE_SAVE_FAILURES", "0").lower() in ("1", "true", "yes")
|
|
101
|
+
|
|
102
|
+
@save_failures.setter
|
|
103
|
+
def save_failures(cls, val: bool | None) -> None:
|
|
104
|
+
_state.save_failures = val
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def snapshot_dir(cls) -> str:
|
|
108
|
+
if _state.snapshot_dir is not None:
|
|
109
|
+
return _state.snapshot_dir
|
|
110
|
+
return os.environ.get("LOGITGATE_SNAPSHOT_DIR", ".logitgate/failures")
|
|
111
|
+
|
|
112
|
+
@snapshot_dir.setter
|
|
113
|
+
def snapshot_dir(cls, val: str | None) -> None:
|
|
114
|
+
_state.snapshot_dir = val
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class LogitGateDefaults(metaclass=_DefaultsMeta):
|
|
118
|
+
"""Global configuration defaults for LogitGate."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def configure(
|
|
122
|
+
backend: str | None = None,
|
|
123
|
+
model: str | None = None,
|
|
124
|
+
pass_threshold: float | None = None,
|
|
125
|
+
fail_threshold: float | None = None,
|
|
126
|
+
logit_temperature: float | None = None,
|
|
127
|
+
num_ctx: int | None = None,
|
|
128
|
+
save_failures: bool | None = None,
|
|
129
|
+
snapshot_dir: str | None = None,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Configures global defaults for LogitGate."""
|
|
132
|
+
if backend is not None:
|
|
133
|
+
_state.backend = backend
|
|
134
|
+
if model is not None:
|
|
135
|
+
_state.model = model
|
|
136
|
+
if pass_threshold is not None:
|
|
137
|
+
_state.pass_threshold = pass_threshold
|
|
138
|
+
if fail_threshold is not None:
|
|
139
|
+
_state.fail_threshold = fail_threshold
|
|
140
|
+
if logit_temperature is not None:
|
|
141
|
+
_state.logit_temperature = logit_temperature
|
|
142
|
+
if num_ctx is not None:
|
|
143
|
+
_state.num_ctx = num_ctx
|
|
144
|
+
if save_failures is not None:
|
|
145
|
+
_state.save_failures = save_failures
|
|
146
|
+
if snapshot_dir is not None:
|
|
147
|
+
_state.snapshot_dir = snapshot_dir
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def reset_config() -> None:
|
|
151
|
+
"""Resets global configuration to environment variables and defaults."""
|
|
152
|
+
_state.reset()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_default_backend() -> str:
|
|
156
|
+
"""Resolves default backend string via configure() or LOGITGATE_BACKEND."""
|
|
157
|
+
return LogitGateDefaults.backend
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_default_model() -> str | None:
|
|
161
|
+
"""Resolves default model string via configure() or LOGITGATE_MODEL."""
|
|
162
|
+
return LogitGateDefaults.model
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_default_save_failures() -> bool:
|
|
166
|
+
"""Resolves default failure saving flag via configure() or LOGITGATE_SAVE_FAILURES."""
|
|
167
|
+
return LogitGateDefaults.save_failures
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_default_snapshot_dir() -> str:
|
|
171
|
+
"""Resolves default snapshot directory via configure() or LOGITGATE_SNAPSHOT_DIR."""
|
|
172
|
+
return LogitGateDefaults.snapshot_dir
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from logitgate.debiasing.inversion import (
|
|
2
|
+
compile_batch_binary_prompt,
|
|
3
|
+
compile_binary_prompt,
|
|
4
|
+
compile_choice_prompt,
|
|
5
|
+
compile_score_prompt,
|
|
6
|
+
)
|
|
7
|
+
from logitgate.debiasing.permutation import (
|
|
8
|
+
KNOWN_MODEL_TEMPERATURES,
|
|
9
|
+
CalibrationResult,
|
|
10
|
+
auto_calibrate_backend,
|
|
11
|
+
combine_symmetric_log_odds,
|
|
12
|
+
fit_calibration,
|
|
13
|
+
get_model_temperature,
|
|
14
|
+
)
|
|
15
|
+
from logitgate.debiasing.softmax import (
|
|
16
|
+
compute_binary_log_odds,
|
|
17
|
+
compute_normalized_probabilities,
|
|
18
|
+
extract_token_logprob,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"KNOWN_MODEL_TEMPERATURES",
|
|
23
|
+
"CalibrationResult",
|
|
24
|
+
"auto_calibrate_backend",
|
|
25
|
+
"combine_symmetric_log_odds",
|
|
26
|
+
"compile_batch_binary_prompt",
|
|
27
|
+
"compile_binary_prompt",
|
|
28
|
+
"compile_choice_prompt",
|
|
29
|
+
"compile_score_prompt",
|
|
30
|
+
"compute_binary_log_odds",
|
|
31
|
+
"compute_normalized_probabilities",
|
|
32
|
+
"extract_token_logprob",
|
|
33
|
+
"fit_calibration",
|
|
34
|
+
"get_model_temperature",
|
|
35
|
+
]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compile_binary_prompt(
|
|
8
|
+
rubric: str,
|
|
9
|
+
candidate: Any,
|
|
10
|
+
pass_token: str = "A",
|
|
11
|
+
fail_token: str = "B",
|
|
12
|
+
query_first: bool = True,
|
|
13
|
+
pass_description: str = "The candidate artifact complies fully with the rubric.",
|
|
14
|
+
fail_description: str = "The candidate artifact violates the rubric or has defects.",
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Compiles a prefix-cache-friendly binary evaluation prompt.
|
|
18
|
+
When query_first=True, the rubric and candidate artifact form a 100% byte-identical
|
|
19
|
+
static prefix across both Pass 1 and Pass 2, allowing KV-cache hit rates > 99%.
|
|
20
|
+
"""
|
|
21
|
+
cand_str = str(candidate).strip()
|
|
22
|
+
rubric_str = rubric.strip()
|
|
23
|
+
|
|
24
|
+
if query_first:
|
|
25
|
+
prefix = f"[EVALUATION RUBRIC]\n{rubric_str}\n\n[CANDIDATE ARTIFACT]\n{cand_str}\n\n"
|
|
26
|
+
else:
|
|
27
|
+
prefix = f"[CANDIDATE ARTIFACT]\n{cand_str}\n\n[EVALUATION RUBRIC]\n{rubric_str}\n\n"
|
|
28
|
+
|
|
29
|
+
t_pass = pass_token.strip().upper()
|
|
30
|
+
t_fail = fail_token.strip().upper()
|
|
31
|
+
|
|
32
|
+
mapping = {
|
|
33
|
+
t_pass: pass_description,
|
|
34
|
+
t_fail: fail_description,
|
|
35
|
+
}
|
|
36
|
+
sorted_tokens = sorted(mapping.keys())
|
|
37
|
+
option_lines = "\n".join(f"{tok}: {mapping[tok]}" for tok in sorted_tokens)
|
|
38
|
+
tokens_str = f"{sorted_tokens[0]} or {sorted_tokens[1]}" if len(sorted_tokens) == 2 else " or ".join(sorted_tokens)
|
|
39
|
+
|
|
40
|
+
suffix = (
|
|
41
|
+
f"[DECISION MAPPING]\n"
|
|
42
|
+
f"{option_lines}\n"
|
|
43
|
+
f"Respond with a single character ({tokens_str}).\n"
|
|
44
|
+
f"Verdict:"
|
|
45
|
+
)
|
|
46
|
+
return prefix + suffix
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compile_batch_binary_prompt(
|
|
50
|
+
rubrics: Sequence[str],
|
|
51
|
+
candidate: Any,
|
|
52
|
+
rubric_index: int,
|
|
53
|
+
pass_token: str = "A",
|
|
54
|
+
fail_token: str = "B",
|
|
55
|
+
query_first: bool = False,
|
|
56
|
+
pass_description: str = "The candidate artifact complies fully with the rubric.",
|
|
57
|
+
fail_description: str = "The candidate artifact violates the rubric or has defects.",
|
|
58
|
+
) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Compiles an isolated, prefix-cache-friendly binary evaluation prompt for an individual rubric
|
|
61
|
+
within a multi-rubric evaluation set.
|
|
62
|
+
|
|
63
|
+
Architecture:
|
|
64
|
+
The candidate artifact forms the shared static prefix:
|
|
65
|
+
[CANDIDATE ARTIFACT]
|
|
66
|
+
{candidate}
|
|
67
|
+
This prefix is 100% byte-identical across ALL K rubrics and across both forward (Pass 1) and
|
|
68
|
+
reverse (Pass 2) passes, allowing the inference server's KV-cache to retain the candidate tokens
|
|
69
|
+
across all queries.
|
|
70
|
+
|
|
71
|
+
Crucially, each rubric is evaluated in complete isolation: no other rubrics are loaded into the
|
|
72
|
+
prompt context. This prevents cross-rubric attention leakage and context bloat, scaling
|
|
73
|
+
consistently whether evaluating 2 rubrics or 500 rubrics.
|
|
74
|
+
"""
|
|
75
|
+
if rubric_index < 0 or rubric_index >= len(rubrics):
|
|
76
|
+
raise IndexError(f"rubric_index {rubric_index} out of bounds for rubrics list of size {len(rubrics)}")
|
|
77
|
+
|
|
78
|
+
cand_str = str(candidate).strip()
|
|
79
|
+
target_rubric = rubrics[rubric_index].strip()
|
|
80
|
+
|
|
81
|
+
t_pass = pass_token.strip().upper()
|
|
82
|
+
t_fail = fail_token.strip().upper()
|
|
83
|
+
|
|
84
|
+
mapping = {
|
|
85
|
+
t_pass: pass_description,
|
|
86
|
+
t_fail: fail_description,
|
|
87
|
+
}
|
|
88
|
+
sorted_tokens = sorted(mapping.keys())
|
|
89
|
+
option_lines = "\n".join(f"{tok}: {mapping[tok]}" for tok in sorted_tokens)
|
|
90
|
+
tokens_str = f"{sorted_tokens[0]} or {sorted_tokens[1]}" if len(sorted_tokens) == 2 else " or ".join(sorted_tokens)
|
|
91
|
+
|
|
92
|
+
if not query_first:
|
|
93
|
+
prefix = f"[CANDIDATE ARTIFACT]\n{cand_str}\n\n"
|
|
94
|
+
suffix = (
|
|
95
|
+
f"[EVALUATION RUBRIC]\n"
|
|
96
|
+
f"{target_rubric}\n\n"
|
|
97
|
+
f"[DECISION MAPPING]\n"
|
|
98
|
+
f"{option_lines}\n"
|
|
99
|
+
f"Respond with a single character ({tokens_str}).\n"
|
|
100
|
+
f"Verdict:"
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
prefix = f"[EVALUATION RUBRIC]\n{target_rubric}\n\n[CANDIDATE ARTIFACT]\n{cand_str}\n\n"
|
|
104
|
+
suffix = (
|
|
105
|
+
f"[DECISION MAPPING]\n"
|
|
106
|
+
f"{option_lines}\n"
|
|
107
|
+
f"Respond with a single character ({tokens_str}).\n"
|
|
108
|
+
f"Verdict:"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return prefix + suffix
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compile_choice_prompt(
|
|
115
|
+
question: str,
|
|
116
|
+
candidate: Any,
|
|
117
|
+
options: Mapping[str, str],
|
|
118
|
+
query_first: bool = True,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Compiles a discrete multi-choice prompt with identical static prefix.
|
|
122
|
+
"""
|
|
123
|
+
cand_str = str(candidate).strip()
|
|
124
|
+
q_str = question.strip()
|
|
125
|
+
|
|
126
|
+
if query_first:
|
|
127
|
+
prefix = f"[QUESTION]\n{q_str}\n\n[CANDIDATE ARTIFACT]\n{cand_str}\n\n"
|
|
128
|
+
else:
|
|
129
|
+
prefix = f"[CANDIDATE ARTIFACT]\n{cand_str}\n\n[QUESTION]\n{q_str}\n\n"
|
|
130
|
+
|
|
131
|
+
option_lines = "\n".join(f"{k}: {v}" for k, v in options.items())
|
|
132
|
+
keys = list(options.keys())
|
|
133
|
+
keys_str = " or ".join(keys)
|
|
134
|
+
|
|
135
|
+
suffix = (
|
|
136
|
+
f"[DECISION MAPPING]\n"
|
|
137
|
+
f"{option_lines}\n"
|
|
138
|
+
f"Respond with a single character ({keys_str}).\n"
|
|
139
|
+
f"Choice:"
|
|
140
|
+
)
|
|
141
|
+
return prefix + suffix
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def compile_score_prompt(
|
|
145
|
+
rubric: str,
|
|
146
|
+
candidate: Any,
|
|
147
|
+
min_score: int = 1,
|
|
148
|
+
max_score: int = 5,
|
|
149
|
+
query_first: bool = True,
|
|
150
|
+
inverted: bool = False,
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Compiles a bounded integer scoring prompt with identical static prefix.
|
|
154
|
+
When inverted=True, min_score corresponds to highest quality and max_score
|
|
155
|
+
corresponds to lowest quality, enabling symmetric score debiasing with
|
|
156
|
+
byte-identical prefix caching.
|
|
157
|
+
"""
|
|
158
|
+
cand_str = str(candidate).strip()
|
|
159
|
+
rubric_str = rubric.strip()
|
|
160
|
+
|
|
161
|
+
if query_first:
|
|
162
|
+
prefix = f"[EVALUATION RUBRIC]\n{rubric_str}\n\n[CANDIDATE ARTIFACT]\n{cand_str}\n\n"
|
|
163
|
+
else:
|
|
164
|
+
prefix = f"[CANDIDATE ARTIFACT]\n{cand_str}\n\n[EVALUATION RUBRIC]\n{rubric_str}\n\n"
|
|
165
|
+
|
|
166
|
+
if inverted:
|
|
167
|
+
scale_desc = (
|
|
168
|
+
f"Rate the candidate artifact on an inverted scale where {min_score} is highest quality "
|
|
169
|
+
f"and {max_score} is lowest quality based on the rubric.\n"
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
scale_desc = f"Rate the candidate artifact from {min_score} to {max_score} based on the rubric.\n"
|
|
173
|
+
|
|
174
|
+
suffix = (
|
|
175
|
+
f"[SCORING SCALE]\n"
|
|
176
|
+
f"{scale_desc}"
|
|
177
|
+
f"Respond with a single integer ({min_score}-{max_score}).\n"
|
|
178
|
+
f"Score:"
|
|
179
|
+
)
|
|
180
|
+
return prefix + suffix
|