Files
esh-pfi-infrastructure/tools/judge-bench/native.py
T
vh b8a535507a feat(judge-bench): keep the judge harness; warn about the 7-way alias collision
Operator: keep the benchmark. It has a named second use (brokkr-smithy-dev
wants gen vs a trained reward model once their tournament converges) and a
demonstrated first one -- it caught a seat that had been coin-flip-grade for
five weeks with nobody measuring it.

Harness promoted from scratch to tools/judge-bench/:
- paths de-hardcoded; runs from its own directory
- proper CLI: --models (REQUIRED), --repeats, --limit, --gateway.
  Required on purpose: a stale default would silently benchmark a retired
  seat, and the original default (selene-1-mini-8b) now 400s.
- README states the limitation rather than burying it: 24 items of the
  author's own design, a screen and not a verdict. This harness scored the
  same pair 83 vs 96 while brokkr's corpus ranking task scored it 47 (chance)
  vs 94. Both honest; absolute scoring on designed items is an easier task
  than ranking real text.
- records brokkr's technique, which is better than anything here: a control
  constructed so the correct answer is DEFINITIONAL rather than judged cannot
  inherit the designer's error (item vs itself, response vs its own
  truncation, text vs its own clauses permuted). Add those before adding more
  judged items.

Gateway: comment-only warning at the head of model_list. SEVEN aliases now
resolve to the same weights (chat-judge, classifier, gen, image-judge,
qwen-image-bench, summarizer, summarizer-large -> qwen3.8-27b-uncensored).
That is intended under ADR-0012, but it has a sharp edge brokkr flagged:
cross-checking a result against another alias measures NOTHING when they are
the same model -- agreement is an echo, not corroboration. The note names the
other current collisions (glm-5.2 x4, TTS x4, reranker x2), gives the
/model/info one-liner to check, and records that probes should resolve alias
-> backing at run start AND end because the response `model` field returns the
alias, so a swap is otherwise invisible.

Verified: config still parses, diff is comment-only, canonical re-synced.
2026-08-23 05:16:10 -07:00

156 lines
5.2 KiB
Python

"""Re-run selene on its NATIVE Atla template — the fairness check.
selene lost on the neutral prompt. Before recommending anyone repoint away
from it, test it on the prompt shape it was actually fine-tuned for: Atla's
Selene template uses an explicit rubric block and a `**Reasoning:** /
**Result:**` output contract rather than JSON.
Caveat worth stating: this template is reproduced from the published Selene
prompt format. If it is subtly off, that is a confound — but the structural
features that matter (rubric block, Reasoning/Result contract, no JSON
demand) are reproduced faithfully, and those are what a fine-tune keys on.
gen is run on the SAME native template too, so this is not "selene's best
prompt vs gen's worst" — it is both models on selene's home turf.
"""
import json
import pathlib
import re
import statistics
import sys
import time
import urllib.request
GATEWAY = "http://10.250.50.70:4000/v1/chat/completions"
KEY = sys.argv[1]
REPEATS = 3
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
from items import ABSOLUTE, PAIRWISE # noqa: E402
ABS_RUBRIC = """1: The response is wrong, evasive, or ignores the question.
2: The response is mostly unhelpful or contains a clear error.
3: The response is partially correct but incomplete or thin.
4: The response is correct and helpful with minor gaps.
5: The response is correct, complete, and follows all instructions."""
NATIVE_ABS = """You are tasked with evaluating a response based on a given instruction and a scoring rubric that serve as the evaluation standard. Provide a comprehensive feedback on the response quality strictly adhering to the scoring rubric, without any general evaluation. Follow this with a score between 1 and 5, referring to the scoring rubric. Avoid generating any additional opening, closing, or explanations.
Here are some rules of the evaluation:
(1) You should prioritize evaluating whether the response satisfies the provided rubric. The basis of your score should depend exactly on the rubric.
Your reply should strictly follow this format:
**Reasoning:** <Your feedback>
**Result:** <an integer between 1 and 5>
Here is the data:
Instruction:
```
{q}
```
Response:
```
{r}
```
Score Rubrics:
{rubric}
"""
NATIVE_PAIR = """You are tasked with evaluating two responses based on a given instruction and an evaluation criterion. Select the response that better satisfies the criterion. If the two responses are of equivalent quality, select Tie.
Your reply should strictly follow this format:
**Reasoning:** <Your feedback>
**Result:** <A, B, or Tie>
Here is the data:
Instruction:
```
{q}
```
Response A:
```
{a}
```
Response B:
```
{b}
```
Evaluation Criterion:
Factual accuracy, adherence to the instructions given, relevance, and completeness.
"""
def call(model, prompt):
body = json.dumps({"model": model, "messages": [{"role": "user", "content": prompt}],
"temperature": 0, "max_tokens": 400}).encode()
req = urllib.request.Request(GATEWAY, data=body, method="POST")
req.add_header("Authorization", "Bearer " + KEY)
req.add_header("Content-Type", "application/json")
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=120) as r:
d = json.loads(r.read().decode())
return d["choices"][0]["message"]["content"], time.time() - t0
except Exception as e:
return "ERROR %s" % e, time.time() - t0
def res_score(t):
m = re.search(r'\*\*Result:?\*\*\s*:?\s*([1-5])', t)
if m:
return int(m.group(1))
m = re.search(r'Result:?\s*([1-5])', t)
return int(m.group(1)) if m else None
def res_verdict(t):
m = re.search(r'\*\*Result:?\*\*\s*:?\s*(Tie|A|B)\b', t, re.I)
if not m:
m = re.search(r'Result:?\s*(Tie|A|B)\b', t, re.I)
if not m:
return None
v = m.group(1)
return "tie" if v.lower() == "tie" else v.upper()
for model in ("selene-1-mini-8b", "gen"):
print("\n=== %s on the NATIVE Atla template ===" % model, flush=True)
lat, pa, ab = [], 0, 0
for it in PAIRWISE:
vs = []
for _ in range(REPEATS):
t, dt = call(model, NATIVE_PAIR.format(q=it["q"], a=it["a"], b=it["b"]))
lat.append(dt)
vs.append(res_verdict(t))
modal = max(set(vs), key=vs.count)
ok = modal == it["truth"]
pa += ok
print(" %-16s truth=%-4s got=%-20s %s%s" % (
it["id"], it["truth"], str(vs), "OK " if ok else "MISS",
"" if len(set(vs)) == 1 else " <UNSTABLE>"), flush=True)
for it in ABSOLUTE:
ss = []
for _ in range(REPEATS):
t, dt = call(model, NATIVE_ABS.format(q=it["q"], r=it["r"], rubric=ABS_RUBRIC))
lat.append(dt)
ss.append(res_score(t))
got = [s for s in ss if s is not None]
med = statistics.median(got) if got else None
lo, hi = it["band"]
ok = med is not None and lo <= med <= hi
ab += ok
print(" %-16s band=%-6s got=%-16s %s%s" % (
it["id"], "%d-%d" % it["band"], str(ss), "OK " if ok else "MISS",
"" if len(set(ss)) == 1 else " <UNSTABLE>"), flush=True)
print(" --> pairwise %d/12 absolute %d/12 TOTAL %d/24 (%.0f%%) median lat %.2fs" % (
pa, ab, pa + ab, 100 * (pa + ab) / 24, statistics.median(lat)), flush=True)