"""Shared helpers for the SenseNova-Loop NVFP4 eval harness (stdlib + requests only)."""
from __future__ import annotations
import json
import math
import re
import time
from typing import Any
EVAL_VERSION = "sn-loop-eval-v1"
# ----------------------------------------------------------------------------- stats
def wilson_ci(k: int, n: int, z: float = 1.959964) -> tuple[float, float]:
if n == 0:
return (float("nan"), float("nan"))
p = k / n
den = 1 + z * z / n
centre = (p + z * z / (2 * n)) / den
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / den
return (max(0.0, centre - half), min(1.0, centre + half))
# ----------------------------------------------------------------------------- text utils
THINK_RE = re.compile(r".*?\s*", re.S)
def strip_think(text: str | None) -> str:
"""Remove any ... block (and a dangling '...' prefix)."""
if not text:
return ""
text = THINK_RE.sub("", text)
if "" in text:
text = text.split("")[-1]
return text.strip()
# ----------------------------------------------------------------------------- tool-call parsing
# Chat template (Qwen3.5-style XML) emits:
# \n\n\nVALUE\n\n...\n
# String values are written raw; dict/list values as JSON (tojson); numbers/bools via |string
# (so Python-ish 'True'/'False' are possible).
TOOL_CALL_RE = re.compile(r"\s*\n]+)>(.*?)\s*", re.S)
PARAM_RE = re.compile(r"\n]+)>\n?(.*?)\n?", re.S)
def _coerce(value: str, schema: dict | None) -> Any:
typ = (schema or {}).get("type")
if typ == "string":
return value
v = value.strip()
if typ == "boolean" or v in ("True", "False"):
if v.lower() in ("true", "false"):
return v.lower() == "true"
try:
return json.loads(v)
except Exception:
pass
if typ in ("integer", "number"):
try:
return float(v) if "." in v else int(v)
except ValueError:
return v
return value
def parse_xml_tool_calls(text: str, tools: list[dict] | None = None) -> tuple[list[dict], dict]:
"""Fallback parser for raw-text tool calls. Returns (calls, format_report)."""
schemas: dict[str, dict] = {}
for t in tools or []:
fn = t.get("function", t)
schemas[fn.get("name")] = (fn.get("parameters") or {}).get("properties") or {}
calls = []
for m in TOOL_CALL_RE.finditer(text or ""):
name = m.group(1).strip()
args = {}
for pm in PARAM_RE.finditer(m.group(2)):
pname = pm.group(1).strip()
args[pname] = _coerce(pm.group(2), schemas.get(name, {}).get(pname))
calls.append({"name": name, "arguments": args})
n_open = (text or "").count("")
n_close = (text or "").count("")
tail = (text or "").split("")[-1].strip() if n_close else ""
report = {
"n_tool_call_tags": n_open,
"balanced_tags": n_open == n_close,
"all_blocks_parsed": n_open == len(calls),
"suffix_after_last_call": bool(tail), # template says: NO suffix after the call
"well_formed": n_open > 0 and n_open == n_close == len(calls),
}
return calls, report
def calls_from_message(msg: dict, tools: list[dict] | None) -> tuple[list[dict], str, dict]:
"""Return (calls, source, format_report). source in {'server','fallback_xml','none'}."""
tcs = msg.get("tool_calls") or []
if tcs:
calls, ok = [], True
for tc in tcs:
fn = tc.get("function", tc)
raw = fn.get("arguments")
if isinstance(raw, str):
try:
args = json.loads(raw) if raw.strip() else {}
except Exception:
args, ok = {"__unparseable__": raw}, False
else:
args = raw or {}
calls.append({"name": fn.get("name"), "arguments": args})
return calls, "server", {"well_formed": ok, "args_valid_json": ok}
content = strip_think(msg.get("content") or "")
calls, rep = parse_xml_tool_calls(content, tools)
return calls, ("fallback_xml" if calls else "none"), rep
# ----------------------------------------------------------------------------- HTTP client
def chat(session, base_url: str, model: str, messages: list, *, max_tokens: int,
tools: list | None = None, enable_thinking: bool = False, timeout: float = 600,
retries: int = 2, extra: dict | None = None) -> dict:
"""One deterministic chat completion. Returns a dict with response + timing (never raises)."""
body: dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": 0.0,
"top_p": 1.0,
"max_tokens": max_tokens,
"seed": 0,
"chat_template_kwargs": {"enable_thinking": enable_thinking},
}
if tools:
body["tools"] = tools
body["tool_choice"] = "auto"
if extra:
body.update(extra)
url = base_url.rstrip("/") + "/chat/completions"
last_err = None
for attempt in range(retries + 1):
t0 = time.time()
try:
r = session.post(url, json=body, timeout=timeout)
dt = time.time() - t0
if r.status_code != 200:
last_err = f"HTTP {r.status_code}: {r.text[:500]}"
if r.status_code < 500:
break
time.sleep(1 + attempt)
continue
j = r.json()
ch = j["choices"][0]
msg = ch.get("message") or {}
return {
"ok": True,
"content": msg.get("content") or "",
"reasoning_content": msg.get("reasoning_content") or msg.get("reasoning") or "",
"tool_calls": msg.get("tool_calls") or [],
"message": msg,
"finish_reason": ch.get("finish_reason"),
"usage": j.get("usage") or {},
"latency_s": round(dt, 3),
"error": None,
}
except Exception as e: # noqa: BLE001
last_err = f"{type(e).__name__}: {e}"
time.sleep(1 + attempt)
return {"ok": False, "content": "", "reasoning_content": "", "tool_calls": [], "message": {},
"finish_reason": None, "usage": {}, "latency_s": None, "error": last_err}
def read_jsonl(path) -> list[dict]:
with open(path) as f:
return [json.loads(l) for l in f if l.strip()]
def write_jsonl(path, rows) -> None:
with open(path, "w") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")