rasyn-iris / pfbmax /tournament.py
ansh-rasyn's picture
Upload folder using huggingface_hub
406a5e6 verified
Raw
History Blame Contribute Delete
9.09 kB
"""Listwise tournament over the judge's contested band.
Why this exists, in numbers: the pointwise judge strands 458/910 gold-perfect
papers at scores 0.3-0.6 (only 35 clear the >0.99 gate), yet blind A/B tests
prove the distinction IS visible in the text: gold vs same-score non-gold is
called correctly 71.5% of the time by a strong reasoning model at high
effort and 72.0% by gpt-5-mini (low), versus 64.7% for gpt-4o-mini.
Absolute "is it Perfect?" scoring throws that comparative signal away;
ordering papers directly recovers it. RankGPT-style sliding windows, Borda
aggregation, and the pointwise score kept as a weak prior.
Scope guard: only the CONTESTED ZONE is reordered: papers below the >0.99
gate, in score-sorted order, up to ZONE_MAX of them. Gate-passers stay
locked on top (they are 63% precise and already sorted), and the tail below
the zone is untouched, so a bad window can cost a few ranks, never a gold's
window membership from above.
Cost: ~2 passes x zone/stride windows x ~1.7k tokens with gpt-5-mini(low)
~= $0.02-0.05/query.
Env: PFBMAX_TOURN=1 enables (router wiring), PFBMAX_TOURN_MODEL,
PFBMAX_TOURN_ZONE (default 150), PFBMAX_TOURN_WIN (12),
PFBMAX_TOURN_STRIDE (6), PFBMAX_TOURN_PASSES (2).
"""
from __future__ import annotations
import json
import os
import re
import urllib.request
from concurrent.futures import ThreadPoolExecutor
HERE = os.path.dirname(os.path.abspath(__file__))
ENDPOINT = "https://api.openai.com/v1/chat/completions"
TIMEOUT_S = 180.0
ALIAS = "ABCDEFGHIJKLMNOP"
SYS = ("You rank papers for a literature search. Given the request, its "
"relevance criteria, and a lettered list of papers, order the papers "
"from MOST to LEAST likely to satisfy EVERY criterion fully. Compare "
"the papers against each other; do not grade them in isolation. "
'Reply JSON only: {"ranking": ["X", "Y", ...]} using each letter '
"exactly once.")
def _key():
k = os.environ.get("OPENAI_API_KEY", "")
if k:
return k
return open(os.path.join(HERE, ".openai_key"),
encoding="utf-8").read().strip()
def _envi(name, default):
raw = (os.environ.get(name) or "").strip()
try:
return int(raw) if raw else default
except ValueError:
return default
def _rank_window(query, criteria, docs, model, meter=None):
"""docs: [(cid, text)]; returns cids best-first, or None on failure."""
letters = ALIAS[:len(docs)]
crit = "\n".join(f"- {c}" for c in criteria)
papers = "\n\n".join(f"PAPER {l}:\n{t[:900]}"
for l, (_c, t) in zip(letters, docs))
user = (f"request: {query}\ncriteria:\n{crit}\n\n{papers}\n\n"
f"Order the {len(docs)} papers best-first.")
payload = {"model": model,
"messages": [{"role": "system", "content": SYS},
{"role": "user", "content": user}],
"response_format": {"type": "json_object"}}
if model.startswith(("gpt-5", "o")):
payload["max_completion_tokens"] = 4096
payload["reasoning_effort"] = os.environ.get(
"PFBMAX_TOURN_EFFORT", "low")
else:
payload["temperature"] = 0.0
payload["max_tokens"] = 120
req = urllib.request.Request(
ENDPOINT, data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + _key()})
try:
with urllib.request.urlopen(req, timeout=TIMEOUT_S) as r:
obj = json.loads(r.read())
usage = obj.get("usage") or {}
if meter is not None:
try:
meter.add(obj.get("model") or model,
usage.get("prompt_tokens") or 0,
usage.get("completion_tokens") or 0)
except Exception:
pass
raw = json.loads(obj["choices"][0]["message"]["content"] or "{}")
got = [str(x).strip().upper()[:1] for x in raw.get("ranking", [])]
seen = []
for g in got:
if g in letters and g not in seen:
seen.append(g)
for l in letters: # letters the model dropped keep order
if l not in seen:
seen.append(l)
idx = {l: i for i, l in enumerate(letters)}
return [docs[idx[l]][0] for l in seen]
except Exception:
return None
def rerank_zone(query, criteria, ordered, scores, texts,
model=None, meter=None, trace=None):
"""Reorder the contested zone of ``ordered`` (score-sorted cids).
ordered: full head order AFTER pointwise sort (best first)
scores: {cid: pointwise score}
texts: {cid: doc text}
Returns the full reordered list (locked top + tournament zone + tail).
"""
tr = trace if trace is not None else {}
model = model or os.environ.get("PFBMAX_TOURN_MODEL", "gpt-5-mini")
zone_max = _envi("PFBMAX_TOURN_ZONE", 150)
win = _envi("PFBMAX_TOURN_WIN", 12)
stride = _envi("PFBMAX_TOURN_STRIDE", 6)
passes = _envi("PFBMAX_TOURN_PASSES", 2)
locked = [c for c in ordered if (scores.get(c) or 0.0) > 0.99]
rest = [c for c in ordered if (scores.get(c) or 0.0) <= 0.99]
zone = [c for c in rest if texts.get(c)][:zone_max]
tail = [c for c in rest if c not in set(zone)]
if len(zone) < win:
return ordered
order = list(zone)
base_rank = {c: i for i, c in enumerate(zone)}
# Points POOL across passes: the offline sweep on dumped windows showed
# pooled Borda (+cap 8) at +0.0151 vs +(-0.0105..-0.0001) for the
# sequential per-pass reorder this loop originally applied. Pass 2 still
# re-slides windows over the pass-1 order (new match-ups), but the final
# order comes from ALL windows at once.
pooled = {c: [] for c in zone}
for _p in range(passes):
windows = []
i = 0
while i < len(order):
w = order[i:i + win]
if len(w) >= 4:
windows.append((i, w))
if i + win >= len(order):
break
i += stride
results = {}
with ThreadPoolExecutor(max_workers=_envi("PFBMAX_TOURN_WORKERS", 6)) as ex:
futs = {ex.submit(_rank_window, query, criteria,
[(c, texts[c]) for c in w], model, meter): (s, w)
for s, w in windows}
for f in futs:
s, w = futs[f]
got = f.result()
if got:
results[s] = got
# accumulate Borda points into the cross-pass pool; the within-pass
# reorder below only shapes the NEXT pass's window composition.
for s, w in windows:
got = results.get(s)
if not got:
continue
for pos, c in enumerate(got):
pooled[c].append(1.0 - pos / max(1, len(got) - 1))
try:
prior_w = float(os.environ.get("PFBMAX_TOURN_PRIOR") or 0.4)
except ValueError:
prior_w = 0.4
def key(c):
p = (sum(pooled[c]) / len(pooled[c])) if pooled[c] else 0.5
prior = 1.0 - base_rank[c] / max(1, len(zone) - 1)
return -( (1 - prior_w) * p + prior_w * prior )
order = sorted(order, key=key)
# dump raw window outputs: aggregation becomes a $0 offline sweep
dump = os.environ.get("PFBMAX_TOURN_DUMP", "").strip()
if dump:
try:
with open(dump, "a", encoding="utf-8") as fh:
fh.write(json.dumps({
"query": query, "pass": _p, "zone": zone,
"windows": [{"start": s, "in": w,
"out": results.get(s)}
for s, w in windows],
}, ensure_ascii=False) + "\n")
except Exception:
pass
# DEMOTION CAP. Pilot 1 (no cap): net -0.0001 -- one +0.094 rescue but
# two catastrophes (-0.035, -0.071 to zero) where the tournament dragged
# base-trusted papers out of the scored window. Rescues need unlimited
# ascent; base papers need bounded descent: a paper may fall at most
# DEMOTE_CAP ranks below where the base order had it.
cap = _envi("PFBMAX_TOURN_DEMOTE_CAP", 8)
if cap > 0:
final = list(order)
for c in zone:
want = base_rank[c] + cap
cur = final.index(c)
if cur > want:
final.remove(c)
final.insert(want, c)
order = final
tr["tournament"] = {"model": model, "zone": len(zone),
"windows": len(windows), "passes": passes,
"failed": sum(1 for s, _w in windows
if s not in results)}
return locked + order + tail