darkrange-eval 0.1.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.
- darkrange_eval-0.1.0.dist-info/METADATA +103 -0
- darkrange_eval-0.1.0.dist-info/RECORD +32 -0
- darkrange_eval-0.1.0.dist-info/WHEEL +5 -0
- darkrange_eval-0.1.0.dist-info/entry_points.txt +3 -0
- darkrange_eval-0.1.0.dist-info/top_level.txt +1 -0
- dreval/__init__.py +2 -0
- dreval/_mock.py +24 -0
- dreval/adapter.py +89 -0
- dreval/app.py +458 -0
- dreval/banner.py +71 -0
- dreval/cli.py +246 -0
- dreval/criteria.py +150 -0
- dreval/graders/__init__.py +47 -0
- dreval/graders/command_lint.py +102 -0
- dreval/graders/cve_struct.py +70 -0
- dreval/graders/cwe_label.py +91 -0
- dreval/graders/execute.py +71 -0
- dreval/graders/extract_f1.py +52 -0
- dreval/graders/fabrication_scan.py +87 -0
- dreval/graders/json_schema.py +92 -0
- dreval/graders/mcq.py +40 -0
- dreval/graders/paired_divergence.py +59 -0
- dreval/graders/plan_check.py +96 -0
- dreval/graders/refusal.py +50 -0
- dreval/history.py +59 -0
- dreval/judge.py +48 -0
- dreval/logo.py +4 -0
- dreval/runner.py +93 -0
- dreval/scoring.py +92 -0
- dreval/task.py +74 -0
- dreval/threshold.py +66 -0
- dreval/tui.py +167 -0
dreval/app.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Interactive TUI app for darkrange-eval.
|
|
2
|
+
|
|
3
|
+
Every screen carries the company logo + DARKRANGE wordmark. A scan shows a live
|
|
4
|
+
per-criterion spinner + score, then a prominent FINAL SCORE and the full detailed
|
|
5
|
+
report inline. Reports/results let you browse passed AND failed test cases.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import textwrap
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import urllib.request
|
|
13
|
+
|
|
14
|
+
from . import banner, history, threshold, tui
|
|
15
|
+
from .criteria import CRITERIA, GATES, POOLS
|
|
16
|
+
from .runner import run_suite
|
|
17
|
+
from .task import load_suite, smoke_subset
|
|
18
|
+
|
|
19
|
+
CLAUDE_MODELS = ["claude-sonnet-5", "claude-opus-4-8", "claude-haiku-4-5-20251001"]
|
|
20
|
+
_CRIT_NAME = {c["key"]: c["name"] for c in CRITERIA}
|
|
21
|
+
_ASCII = bool(os.environ.get("DR_EVAL_ASCII"))
|
|
22
|
+
_SPIN = "|/-\\" if _ASCII else "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _suite_default():
|
|
26
|
+
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
27
|
+
"suites", "darkrange")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _detect_provider(url):
|
|
31
|
+
return "anthropic" if "anthropic" in (url or "").lower() else "openai"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fetch_models(endpoint, api_key=None):
|
|
35
|
+
url = endpoint.rstrip("/") + "/models"
|
|
36
|
+
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
|
37
|
+
try:
|
|
38
|
+
with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=6) as r:
|
|
39
|
+
import json
|
|
40
|
+
return [m.get("id") for m in json.load(r).get("data", []) if m.get("id")]
|
|
41
|
+
except Exception:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _wrap(text, maxchars=900, indent=" "):
|
|
46
|
+
text = "" if text is None else str(text)
|
|
47
|
+
if len(text) > maxchars:
|
|
48
|
+
text = text[:maxchars] + " ...(truncated)"
|
|
49
|
+
out = []
|
|
50
|
+
for para in text.split("\n"):
|
|
51
|
+
out.extend(textwrap.wrap(para, 86) or [""])
|
|
52
|
+
return "\n".join(indent + ln for ln in out) or (indent + "(empty)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _bar(x):
|
|
56
|
+
x = max(0.0, min(1.0, x))
|
|
57
|
+
f = int(round(x * 12))
|
|
58
|
+
return ("#" * f + "-" * (12 - f)) if _ASCII else ("█" * f + "░" * (12 - f))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _mk(passed, errored=False):
|
|
62
|
+
if errored:
|
|
63
|
+
return "ERR"
|
|
64
|
+
return ("OK" if passed else "XX") if _ASCII else ("✔" if passed else "✗")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _pulse(msg, cycles=8):
|
|
68
|
+
"""A brief loading animation (only in a real terminal)."""
|
|
69
|
+
if not sys.stdout.isatty():
|
|
70
|
+
return
|
|
71
|
+
for i in range(cycles):
|
|
72
|
+
sys.stdout.write(f"\r {_SPIN[i % len(_SPIN)]} {msg}{'.' * (i % 4)} ")
|
|
73
|
+
sys.stdout.flush()
|
|
74
|
+
time.sleep(0.11)
|
|
75
|
+
sys.stdout.write("\r" + " " * (len(msg) + 12) + "\r")
|
|
76
|
+
sys.stdout.flush()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------ live progress
|
|
80
|
+
class _Progress:
|
|
81
|
+
"""on_item callback + a background thread that keeps the current criterion's
|
|
82
|
+
spinner animating even while the model is thinking (continuous 'running' feel)."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, tasks):
|
|
85
|
+
self.totals, self.order = {}, []
|
|
86
|
+
for t in tasks:
|
|
87
|
+
if t.criterion not in self.totals:
|
|
88
|
+
self.order.append(t.criterion)
|
|
89
|
+
self.totals[t.criterion] = 0
|
|
90
|
+
self.totals[t.criterion] += 1
|
|
91
|
+
self.cur, self.scores, self.k, self.tick = None, [], 0, 0
|
|
92
|
+
self.lock = threading.Lock()
|
|
93
|
+
self._stop = False
|
|
94
|
+
self.th = threading.Thread(target=self._animate, daemon=True)
|
|
95
|
+
self.th.start()
|
|
96
|
+
|
|
97
|
+
def _animate(self):
|
|
98
|
+
while not self._stop:
|
|
99
|
+
with self.lock:
|
|
100
|
+
if self.cur is not None:
|
|
101
|
+
self._draw()
|
|
102
|
+
time.sleep(0.1)
|
|
103
|
+
|
|
104
|
+
def _draw(self):
|
|
105
|
+
self.tick += 1
|
|
106
|
+
nm = _CRIT_NAME.get(self.cur, self.cur)[:30]
|
|
107
|
+
dots = "." * (self.tick % 4)
|
|
108
|
+
sys.stdout.write(f"\r {_SPIN[self.tick % len(_SPIN)]} {nm:32} "
|
|
109
|
+
f"[{self.k}/{self.totals.get(self.cur, '?')}] running{dots} ")
|
|
110
|
+
sys.stdout.flush()
|
|
111
|
+
|
|
112
|
+
def __call__(self, i, n, task, res):
|
|
113
|
+
with self.lock:
|
|
114
|
+
c = task.criterion
|
|
115
|
+
if c != self.cur:
|
|
116
|
+
self._finish_locked()
|
|
117
|
+
self.cur, self.scores, self.k = c, [], 0
|
|
118
|
+
self.k += 1
|
|
119
|
+
if res is not None and "endpoint_error" not in (res.flags or []) and res.score is not None:
|
|
120
|
+
self.scores.append(res.score)
|
|
121
|
+
|
|
122
|
+
def _finish_locked(self):
|
|
123
|
+
if self.cur is None:
|
|
124
|
+
return
|
|
125
|
+
vals = [s for s in self.scores if s is not None]
|
|
126
|
+
sc = sum(vals) / len(vals) if vals else 0.0
|
|
127
|
+
nm = _CRIT_NAME.get(self.cur, self.cur)[:30]
|
|
128
|
+
body = f" {_mk(sc >= 0.6)} {nm:32} {_bar(sc)} {sc:.2f} "
|
|
129
|
+
sys.stdout.write("\r" + (tui.ok(body) if sc >= 0.6 else tui.bad(body)) + "\n")
|
|
130
|
+
sys.stdout.flush()
|
|
131
|
+
|
|
132
|
+
def finish(self):
|
|
133
|
+
self._stop = True
|
|
134
|
+
try:
|
|
135
|
+
self.th.join(timeout=0.6)
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
with self.lock:
|
|
139
|
+
self._finish_locked()
|
|
140
|
+
self.cur = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------ report render
|
|
144
|
+
def _print_final(report):
|
|
145
|
+
v = threshold.verdict(report)
|
|
146
|
+
des = report.get("DES") or 0.0
|
|
147
|
+
print(" " + "=" * 72)
|
|
148
|
+
head = f" FINAL SCORE {des:.4f} ({int(round(des * 100))}/100) band: {v['band']}"
|
|
149
|
+
print(tui.ok(head) if v["passed"] else tui.bad(head))
|
|
150
|
+
if v["passed"]:
|
|
151
|
+
tag = f" VERDICT PASS — clears the {v['pass_des']} pass line with all gates green"
|
|
152
|
+
elif not v["gates_ok"]:
|
|
153
|
+
tag = " VERDICT GATED — failing gate(s): " + ", ".join(v["failed_gates"])
|
|
154
|
+
else:
|
|
155
|
+
tag = f" VERDICT below the {v['pass_des']} pass line"
|
|
156
|
+
print(tui.ok(tag) if v["passed"] else tui.bad(tag))
|
|
157
|
+
if report.get("errors"):
|
|
158
|
+
print(f" note: {report['errors']} task(s) errored (endpoint) and were excluded from scoring")
|
|
159
|
+
print(" " + "=" * 72)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _render_scorecard(report):
|
|
163
|
+
print(f" {'CRITERION':16} {'SCORE':>6} METRICS")
|
|
164
|
+
for c, e in sorted(report["criteria"].items()):
|
|
165
|
+
extra = " ".join(f"{k}={x}" for k, x in e.items() if k not in ("score", "n"))
|
|
166
|
+
score = e["score"] if e["score"] is not None else "-"
|
|
167
|
+
print(f" {c:16} {score:>6} {extra}")
|
|
168
|
+
gline = " ".join(f"{k}:{g}" for k, g in report.get("gates", {}).items())
|
|
169
|
+
print(f" gates: {gline or 'n/a'}")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------ case browsing
|
|
173
|
+
def _show_case(io, r, breadcrumb):
|
|
174
|
+
tui.clear(); tui.header(breadcrumb + " / case")
|
|
175
|
+
out = r.get("output")
|
|
176
|
+
out = "\n---\n".join(str(o) for o in out) if isinstance(out, list) else str(out)
|
|
177
|
+
err = "endpoint_error" in (r.get("flags") or [])
|
|
178
|
+
status = "ERROR" if err else ("PASS" if (r.get("passed") and not r.get("flags")) else "FAIL")
|
|
179
|
+
flags = (" [" + ",".join(r["flags"]) + "]") if r.get("flags") else ""
|
|
180
|
+
print(f"\n {status} {r['criterion']} · {r['id']} · score {r.get('score')}{flags}\n")
|
|
181
|
+
print(" PROMPT"); print(_wrap(r.get("prompt"), 900))
|
|
182
|
+
print("\n EXPECTED"); print(" " + str(r.get("expected")))
|
|
183
|
+
print("\n MODEL OUTPUT"); print(_wrap(out, 1100))
|
|
184
|
+
print("\n GRADER SAID"); print(" " + (r.get("detail") or "(n/a)"))
|
|
185
|
+
tui.pause(io)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _browse_group(io, rows, breadcrumb):
|
|
189
|
+
while True:
|
|
190
|
+
labels = []
|
|
191
|
+
for r in rows:
|
|
192
|
+
err = "endpoint_error" in (r.get("flags") or [])
|
|
193
|
+
mk = _mk(r.get("passed") and not r.get("flags"), err)
|
|
194
|
+
sc = " - " if r.get("score") is None else f"{r['score']:.2f}"
|
|
195
|
+
fl = (" [" + ",".join(r["flags"]) + "]") if r.get("flags") else ""
|
|
196
|
+
labels.append(f"{mk:3} {r['id']:16} {sc:>5}{fl}")
|
|
197
|
+
sel = tui.menu(labels + ["Back"], io, title="Enter to inspect the prompt / expected / output",
|
|
198
|
+
breadcrumb=breadcrumb + " / cases")
|
|
199
|
+
if sel is None or sel == len(rows):
|
|
200
|
+
return
|
|
201
|
+
_show_case(io, rows[sel], breadcrumb)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _browse_cases(io, journal, breadcrumb, mode="all"):
|
|
205
|
+
def keep(r):
|
|
206
|
+
passed = r.get("passed") and not r.get("flags")
|
|
207
|
+
return {"failed": not passed, "passed": passed}.get(mode, True)
|
|
208
|
+
rows = [r for r in journal if keep(r)]
|
|
209
|
+
if not rows:
|
|
210
|
+
tui.clear(); tui.header(breadcrumb + " / cases")
|
|
211
|
+
print(f"\n No {mode} test cases in this run.")
|
|
212
|
+
tui.pause(io)
|
|
213
|
+
return
|
|
214
|
+
order = [c["key"] for c in CRITERIA]
|
|
215
|
+
groups = [(c, [r for r in rows if r["criterion"] == c]) for c in order]
|
|
216
|
+
groups = [(c, rs) for c, rs in groups if rs]
|
|
217
|
+
while True:
|
|
218
|
+
labels = []
|
|
219
|
+
for c, rs in groups:
|
|
220
|
+
npass = sum(1 for r in rs if r.get("passed") and not r.get("flags"))
|
|
221
|
+
labels.append(f"{_CRIT_NAME.get(c, c)[:32]:32} {npass}/{len(rs)} passed")
|
|
222
|
+
sel = tui.menu(labels + ["Back"], io,
|
|
223
|
+
title=f"{mode.upper()} cases by criterion ({len(rows)} tasks) — Enter a criterion",
|
|
224
|
+
breadcrumb=breadcrumb + " / cases")
|
|
225
|
+
if sel is None or sel == len(groups):
|
|
226
|
+
return
|
|
227
|
+
_browse_group(io, groups[sel][1], breadcrumb)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------ run flow
|
|
231
|
+
def _flow_run(io, adapter_factory):
|
|
232
|
+
mode = tui.menu(
|
|
233
|
+
["Quick smoke — 2 per criterion (20 tasks · seconds)",
|
|
234
|
+
"Mid scan — 25 per criterion (250 tasks · minutes)",
|
|
235
|
+
"Full scan — all criteria (506 tasks · long)",
|
|
236
|
+
"Back"],
|
|
237
|
+
io, title="Choose scan depth", breadcrumb="Run a test")
|
|
238
|
+
if mode is None or mode == 3:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
tui.clear(); tui.header("Run a test / target")
|
|
242
|
+
print("\n Paste the LLM endpoint — provider + model are auto-detected.")
|
|
243
|
+
print(" e.g. http://localhost:11434/v1 (ollama) · http://HOST:8000/v1 (vllm) · https://api.anthropic.com\n")
|
|
244
|
+
endpoint = io.text(" endpoint: ", "http://localhost:11434/v1").strip()
|
|
245
|
+
if not endpoint:
|
|
246
|
+
return
|
|
247
|
+
provider = _detect_provider(endpoint)
|
|
248
|
+
api_key = None
|
|
249
|
+
if provider == "anthropic":
|
|
250
|
+
api_key = io.text(" anthropic api key: ", "").strip() or None
|
|
251
|
+
models = CLAUDE_MODELS
|
|
252
|
+
else:
|
|
253
|
+
print(f"\n detected {provider} — fetching models ...")
|
|
254
|
+
models = _fetch_models(endpoint)
|
|
255
|
+
if len(models) == 1:
|
|
256
|
+
model = models[0]
|
|
257
|
+
elif len(models) > 1:
|
|
258
|
+
sel = tui.menu(models + ["(type it manually)"], io,
|
|
259
|
+
title=f"{provider}: pick the model", breadcrumb="Run a test / model")
|
|
260
|
+
model = models[sel] if (sel is not None and sel < len(models)) else io.text(" model: ", "").strip()
|
|
261
|
+
else:
|
|
262
|
+
model = io.text(" model name: ", "").strip()
|
|
263
|
+
if not model:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
tasks = load_suite(_suite_default())
|
|
267
|
+
if mode == 0:
|
|
268
|
+
tasks = smoke_subset(tasks, 2)
|
|
269
|
+
elif mode == 1:
|
|
270
|
+
tasks = smoke_subset(tasks, 25)
|
|
271
|
+
label = {0: "smoke", 1: "mid", 2: "full"}[mode]
|
|
272
|
+
|
|
273
|
+
adapter = adapter_factory(endpoint, model, provider=provider, api_key=api_key)
|
|
274
|
+
|
|
275
|
+
tui.clear(); tui.header("Run a test / scanning")
|
|
276
|
+
print(f"\n target: {provider}:{model} @ {endpoint} ({len(tasks)} tasks · {label})")
|
|
277
|
+
try:
|
|
278
|
+
pong = adapter.complete([{"role": "user", "content": "Reply with: ok"}], max_tokens=8).text
|
|
279
|
+
print(" " + tui.ok(f"preflight ok (replied {pong.strip()[:30]!r})"))
|
|
280
|
+
except Exception as e: # noqa: BLE001
|
|
281
|
+
print(" " + tui.bad(f"preflight failed: {e}"))
|
|
282
|
+
tui.pause(io)
|
|
283
|
+
return
|
|
284
|
+
print("\n scanning — a score appears as each criterion finishes:\n")
|
|
285
|
+
_pulse(f"initializing {len(tasks)} tests")
|
|
286
|
+
|
|
287
|
+
prog = _Progress(tasks)
|
|
288
|
+
report, journal = run_suite(tasks, adapter, on_item=prog)
|
|
289
|
+
prog.finish()
|
|
290
|
+
report["model"] = f"{provider}:{model}"
|
|
291
|
+
run_id = history.new_run_id(model)
|
|
292
|
+
meta = {"model": f"{provider}:{model}", "endpoint": endpoint, "suite": label,
|
|
293
|
+
"n": len(tasks), "DES": report["DES"]}
|
|
294
|
+
history.save_run(run_id, report, journal, meta)
|
|
295
|
+
|
|
296
|
+
# FINAL SCORE + full detailed report, inline on the same screen
|
|
297
|
+
print()
|
|
298
|
+
_print_final(report)
|
|
299
|
+
print()
|
|
300
|
+
_render_scorecard(report)
|
|
301
|
+
print(f"\n saved as {run_id} — available anytime under Reports")
|
|
302
|
+
tui.pause(io, "press any key to inspect test cases ")
|
|
303
|
+
|
|
304
|
+
while True:
|
|
305
|
+
tui.clear(); tui.header("Run a test / report")
|
|
306
|
+
print()
|
|
307
|
+
_print_final(report)
|
|
308
|
+
print()
|
|
309
|
+
sel = tui.menu(["View ALL test cases (passed + failed)",
|
|
310
|
+
"View failed / flagged only", "Back to main menu"],
|
|
311
|
+
io, breadcrumb="Run a test / report")
|
|
312
|
+
if sel == 0:
|
|
313
|
+
_browse_cases(io, journal, "Run a test", "all")
|
|
314
|
+
elif sel == 1:
|
|
315
|
+
_browse_cases(io, journal, "Run a test", "failed")
|
|
316
|
+
else:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ------------------------------------------------------------------ reports flow
|
|
321
|
+
def _flow_reports(io):
|
|
322
|
+
while True:
|
|
323
|
+
runs = history.list_runs()
|
|
324
|
+
if not runs:
|
|
325
|
+
tui.clear(); tui.header("Reports")
|
|
326
|
+
print("\n No saved scans yet. Run a test first.")
|
|
327
|
+
tui.pause(io)
|
|
328
|
+
return
|
|
329
|
+
labels = [f"{r['id']:30} {str(r.get('model'))[:20]:20} {str(r.get('suite', '')):5} DES {r.get('DES')}"
|
|
330
|
+
for r in runs]
|
|
331
|
+
sel = tui.menu(labels + ["Back"], io, title="Saved scans (newest first)", breadcrumb="Reports")
|
|
332
|
+
if sel is None or sel == len(runs):
|
|
333
|
+
return
|
|
334
|
+
data = history.load_run(runs[sel]["id"])
|
|
335
|
+
if not data:
|
|
336
|
+
continue
|
|
337
|
+
while True:
|
|
338
|
+
tui.clear(); tui.header("Reports / detail")
|
|
339
|
+
m = data["meta"]
|
|
340
|
+
print(f"\n {runs[sel]['id']} {m.get('model')} {m.get('endpoint', '')} ({m.get('n')} tasks)\n")
|
|
341
|
+
_print_final(data["report"])
|
|
342
|
+
print()
|
|
343
|
+
_render_scorecard(data["report"])
|
|
344
|
+
sub = tui.menu(["View ALL test cases (passed + failed)",
|
|
345
|
+
"View failed / flagged only", "Back to scan list"],
|
|
346
|
+
io, breadcrumb="Reports / detail")
|
|
347
|
+
if sub == 0:
|
|
348
|
+
_browse_cases(io, data["journal"], "Reports", "all")
|
|
349
|
+
elif sub == 1:
|
|
350
|
+
_browse_cases(io, data["journal"], "Reports", "failed")
|
|
351
|
+
else:
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ------------------------------------------------------------------ info flow
|
|
356
|
+
def _metrics_lines():
|
|
357
|
+
return ["METRICS & SCORING", "",
|
|
358
|
+
" Every task is graded 0..1 by a dedicated grader (deterministic where possible).",
|
|
359
|
+
" Per-criterion = mean of its tasks. FINAL SCORE (DES) = weighted composite:"] + \
|
|
360
|
+
[f" {c['n']:>2}. {c['name']:34} {int(c['weight'] * 100):>3}%" for c in CRITERIA] + \
|
|
361
|
+
["", " Special metrics: refusal->ORR/CRR · vuln_ident->detection/false-alarm ·",
|
|
362
|
+
" planning->distinct-ratio · format->valid short/long · groundedness->fabrication-rate.",
|
|
363
|
+
" Endpoint errors are retried, then excluded from scoring (never graded as answers)."]
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _gates_lines():
|
|
367
|
+
out = ["HARD GATES (auto-flag even at a high score)", ""]
|
|
368
|
+
out += [f" · {n:14} {d}" for n, d in GATES]
|
|
369
|
+
out += ["", "DATA POOLS (contamination policy)", ""]
|
|
370
|
+
out += [f" · {n:14} {d}" for n, d in POOLS]
|
|
371
|
+
return out
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _working_lines():
|
|
375
|
+
return ["HOW THE BENCHMARK WORKS", "",
|
|
376
|
+
" 1. A suite = task files (~50 per criterion; 25 mid, 2 smoke).",
|
|
377
|
+
" 2. Each task is sent to your endpoint — the model is isolated from the agent.",
|
|
378
|
+
" 3. The matching grader scores the raw output. No agent loop, no tools.",
|
|
379
|
+
" 4. Scores roll up to per-criterion + a weighted FINAL SCORE; gates are checked.",
|
|
380
|
+
" 5. The run is saved with the real prompts + outputs so you can review every case.",
|
|
381
|
+
"", " Model-agnostic · contamination-safe · reproducible (temp 0, fixed seed)."]
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _info_page(io, title, lines):
|
|
385
|
+
tui.clear(); tui.header("Info / " + title)
|
|
386
|
+
print()
|
|
387
|
+
for ln in lines:
|
|
388
|
+
print(" " + ln)
|
|
389
|
+
tui.pause(io)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _info_criteria(io):
|
|
393
|
+
while True:
|
|
394
|
+
labels = [f"{c['n']:>2}. {c['name']:34} [{int(c['weight'] * 100)}% · tier {c['tier']}]"
|
|
395
|
+
for c in CRITERIA]
|
|
396
|
+
sel = tui.menu(labels + ["Back"], io,
|
|
397
|
+
title="The 10 criteria — pick one for full detail", breadcrumb="Info / criteria")
|
|
398
|
+
if sel is None or sel == len(CRITERIA):
|
|
399
|
+
return
|
|
400
|
+
c = CRITERIA[sel]
|
|
401
|
+
lines = [f"CRITERION {c['n']} — {c['name']}", "",
|
|
402
|
+
f" weight {int(c['weight'] * 100)}% · tier {c['tier']} · graders: {', '.join(c['graders'])}",
|
|
403
|
+
"", "MEASURES", " " + c["measures"],
|
|
404
|
+
"", "HOW IT IS JUDGED", " " + c["judged"],
|
|
405
|
+
"", "WHY IT MATTERS", " " + c["why"],
|
|
406
|
+
"", "EXAMPLE", " " + c["example"]]
|
|
407
|
+
_info_page(io, c["name"], lines)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _flow_info(io):
|
|
411
|
+
while True:
|
|
412
|
+
sel = tui.menu(
|
|
413
|
+
["Browse the 10 criteria (measures, judging, why, example)",
|
|
414
|
+
"Metrics & scoring", "Gates & data pools",
|
|
415
|
+
"Ideal threshold (how it was set)", "How the benchmark works", "Back"],
|
|
416
|
+
io, title="What do you want to know?", breadcrumb="Info")
|
|
417
|
+
if sel is None or sel == 5:
|
|
418
|
+
return
|
|
419
|
+
if sel == 0:
|
|
420
|
+
_info_criteria(io)
|
|
421
|
+
elif sel == 1:
|
|
422
|
+
_info_page(io, "Metrics & scoring", _metrics_lines())
|
|
423
|
+
elif sel == 2:
|
|
424
|
+
_info_page(io, "Gates & data pools", _gates_lines())
|
|
425
|
+
elif sel == 3:
|
|
426
|
+
_info_page(io, "Ideal threshold", threshold.RATIONALE.splitlines())
|
|
427
|
+
elif sel == 4:
|
|
428
|
+
_info_page(io, "How the benchmark works", _working_lines())
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ------------------------------------------------------------------ main loop
|
|
432
|
+
def run_app(io=None, adapter_factory=None):
|
|
433
|
+
if io is None:
|
|
434
|
+
io = tui.IO()
|
|
435
|
+
if adapter_factory is None:
|
|
436
|
+
from .adapter import ModelAdapter
|
|
437
|
+
adapter_factory = ModelAdapter
|
|
438
|
+
|
|
439
|
+
tui.enable_utf8()
|
|
440
|
+
tui.clear()
|
|
441
|
+
banner.print_brand(big=True)
|
|
442
|
+
print("\n Interactive mode — use the arrow keys. Press Enter to begin.")
|
|
443
|
+
io.key()
|
|
444
|
+
|
|
445
|
+
while True:
|
|
446
|
+
sel = tui.menu(
|
|
447
|
+
["Run a test", "Reports (view past scans)", "Info (criteria, metrics, how it works)", "Quit"],
|
|
448
|
+
io, title="Main menu", hint="up/down move | Enter select | Esc/Q quit")
|
|
449
|
+
if sel is None or sel == 3:
|
|
450
|
+
tui.clear()
|
|
451
|
+
print(" bye.")
|
|
452
|
+
return
|
|
453
|
+
if sel == 0:
|
|
454
|
+
_flow_run(io, adapter_factory)
|
|
455
|
+
elif sel == 1:
|
|
456
|
+
_flow_reports(io)
|
|
457
|
+
elif sel == 2:
|
|
458
|
+
_flow_info(io)
|
dreval/banner.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""DarkRange-Eval branding — a horizontal lockup: the company icon on the LEFT
|
|
2
|
+
and the DARKRANGE wordmark on the RIGHT (like darkrange-logo-full-white.svg),
|
|
3
|
+
in red, on every screen. Pure-ASCII fallback when truecolor isn't available."""
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from .logo import LOGO_ANSI, LOGO_BIG # red half-block icons: 20px (header), 32px (splash)
|
|
8
|
+
except Exception:
|
|
9
|
+
LOGO_ANSI = LOGO_BIG = ""
|
|
10
|
+
|
|
11
|
+
# Compact "DARKRANGE" wordmark that fits beside the icon (6 rows).
|
|
12
|
+
DR_TEXT = r""" ██████╗ █████╗ ██████╗ ██╗ ██╗██████╗ █████╗ ███╗ ██╗ ██████╗ ███████╗
|
|
13
|
+
██╔══██╗██╔══██╗██╔══██╗██║ ██╔╝██╔══██╗██╔══██╗████╗ ██║██╔════╝ ██╔════╝
|
|
14
|
+
██║ ██║███████║██████╔╝█████╔╝ ██████╔╝███████║██╔██╗ ██║██║ ███╗█████╗
|
|
15
|
+
██║ ██║██╔══██║██╔══██╗██╔═██╗ ██╔══██╗██╔══██║██║╚██╗██║██║ ██║██╔══╝
|
|
16
|
+
██████╔╝██║ ██║██║ ██║██║ ██╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝███████╗
|
|
17
|
+
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝"""
|
|
18
|
+
|
|
19
|
+
RED = "\033[38;5;196m"
|
|
20
|
+
RST = "\033[0m"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ascii_mode():
|
|
24
|
+
import os
|
|
25
|
+
return bool(os.environ.get("DR_EVAL_ASCII"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _use_color():
|
|
29
|
+
import os
|
|
30
|
+
return sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _lockup(icon_str):
|
|
34
|
+
"""icon (left) + DARKRANGE text (right, vertically centered) on the same rows."""
|
|
35
|
+
color = _use_color()
|
|
36
|
+
icon = icon_str.split("\n")
|
|
37
|
+
text = DR_TEXT.split("\n")
|
|
38
|
+
pad = max(0, (len(icon) - len(text)) // 2)
|
|
39
|
+
lines = []
|
|
40
|
+
for r, left in enumerate(icon):
|
|
41
|
+
ti = r - pad
|
|
42
|
+
right = text[ti] if 0 <= ti < len(text) else ""
|
|
43
|
+
lines.append(left + " " + ((RED + right + RST) if (color and right) else right))
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _safe_print(text):
|
|
48
|
+
try:
|
|
49
|
+
sys.stdout.write(text)
|
|
50
|
+
except UnicodeEncodeError:
|
|
51
|
+
sys.stdout.buffer.write(text.encode('utf-8'))
|
|
52
|
+
|
|
53
|
+
def print_brand(big=False):
|
|
54
|
+
"""The DarkRange logo lockup — shown on every screen. big=True for the splash."""
|
|
55
|
+
color = _use_color()
|
|
56
|
+
icon = LOGO_BIG if big else LOGO_ANSI
|
|
57
|
+
if _ascii_mode() or not color or not icon:
|
|
58
|
+
# no truecolor icon available -> just the DARKRANGE wordmark
|
|
59
|
+
_safe_print((RED + DR_TEXT + RST + "\n") if color else DR_TEXT + "\n")
|
|
60
|
+
else:
|
|
61
|
+
_safe_print(_lockup(icon) + "\n")
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# back-compat aliases
|
|
66
|
+
def print_wordmark():
|
|
67
|
+
print_brand()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_banner(subtitle="", version="0.1.0"):
|
|
71
|
+
print_brand()
|