"""ReVeR reference pipeline: write a rubric for an instruction, judge each response against it, score. rubric generator instruction -> rubric; every item ends with [Hard Rule] or [Principle] judge (instruction, one response, rubric) -> 0/1 per Hard Rule, 0-100 per Principle; it may call a Python tool for mechanical checks score number of satisfied Hard Rules + mean Principle score / 101 Both models are reached through OpenAI-compatible endpoints (see README.md for the vLLM commands). python rever.py --rubric-url http://localhost:8001/v1 --judge-url http://localhost:8002/v1 \ --instruction "Write exactly two sentences about tea." \ --response "Tea is a drink. It is made from leaves." --response "Tea is nice." """ from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any from openai import OpenAI from python_tool import RestrictedPythonExecutor HERE = Path(__file__).resolve().parent RUBRIC_PROMPT = (HERE / "prompts" / "rubric_generator_prompt.txt").read_text(encoding="utf-8") JUDGE_PROMPT = (HERE / "prompts" / "judge_system_prompt.txt").read_text(encoding="utf-8") PYTHON_TOOLS = [ { "type": "function", "function": { "name": "python", "description": "Execute Python code for deterministic verification.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ] # Decoding used for both models in all our evaluations. Thinking stays on. SAMPLING = { "temperature": 1.0, "top_p": 0.95, "presence_penalty": 1.5, "max_tokens": 8192, "extra_body": { "top_k": 20, "min_p": 0.0, "repetition_penalty": 1.0, "chat_template_kwargs": {"enable_thinking": True}, }, } MAX_TOOL_CALLS = 3 # executed tool calls per judgment MAX_JUDGE_TURNS = 4 # model calls per judgment; the last one is made without tools TOOL_TIMEOUT_SECONDS = 3.0 TOOL_OUTPUT_LIMIT = 4000 TOOL_MEMORY_LIMIT_MB = 512 THINK_RE = re.compile(r"]*>.*?", re.IGNORECASE | re.DOTALL) RUBRIC_OPEN_RE = re.compile(r"", re.IGNORECASE) RUBRIC_CLOSE_RE = re.compile(r"", re.IGNORECASE) RUBRIC_LABEL_RE = re.compile(r"\[(Hard Rule|Principle)\]\s*$") # --------------------------------------------------------------------------------------------- rubric def build_rubric_prompt(instruction: str) -> str: return RUBRIC_PROMPT.format(instruction=instruction, context="") def parse_rubric(text: str) -> tuple[list[dict[str, Any]], str | None]: """Items of the last block -> ([{rubric_id, criterion, kind}], error).""" text = THINK_RE.sub("", text or "") blocks = [] for opening in RUBRIC_OPEN_RE.finditer(text): closing = RUBRIC_CLOSE_RE.search(text, opening.end()) if closing is not None: blocks.append(text[opening.end():closing.start()]) elif RUBRIC_LABEL_RE.search(text[opening.end():]): # block cut off right after its last item blocks.append(text[opening.end():]) if not blocks: return [], "missing ... block" block = blocks[-1].strip().replace("```", "") matches = list(re.finditer(r"(?m)^\s*(\d+)\s*[.)]\s+", block)) if not matches: return [], "rubric block contains no numbered items" items = [] for position, match in enumerate(matches, start=1): if int(match.group(1)) != position: return [], f"rubric item numbering must be consecutive from 1; got {match.group(1)}" end = matches[position].start() if position < len(matches) else len(block) criterion = re.sub(r"\s+", " ", block[match.end():end]).strip() label = RUBRIC_LABEL_RE.search(criterion) if label is None: return [], f"rubric item {position} must end with [Hard Rule] or [Principle]" kind = "hard_rule" if label.group(1) == "Hard Rule" else "principle" items.append({"rubric_id": position, "criterion": criterion, "kind": kind}) return items, None def generate_rubric(client: OpenAI, model: str, instruction: str, max_attempts: int = 3) -> list[dict[str, Any]]: """One rubric for the instruction. The prompt is a single user message; there is no system message.""" error = None for _ in range(max_attempts): response = client.chat.completions.create( model=model, messages=[{"role": "user", "content": build_rubric_prompt(instruction)}], **SAMPLING ) items, error = parse_rubric(response.choices[0].message.content or "") if error is None: return items raise RuntimeError(f"no parseable rubric after {max_attempts} attempts: {error}") # ---------------------------------------------------------------------------------------------- judge def _reasoning(message: Any) -> str | None: """The thinking of a turn: vLLM >= 0.11 returns it as `reasoning`, older servers as `reasoning_content`.""" for key in ("reasoning_content", "reasoning"): value = getattr(message, key, None) if isinstance(value, str) and value: return value return None def _assistant_turn(message: Any, call: Any = None) -> dict[str, Any]: """The assistant turn to send back. Its thinking goes back under both keys, so that the chat template renders the block again; without it the judge answers after a tool call without its own earlier reasoning.""" turn: dict[str, Any] = {"role": "assistant", "content": message.content or ""} reasoning = _reasoning(message) if reasoning: turn["reasoning_content"] = reasoning turn["reasoning"] = reasoning if call is not None: turn["tool_calls"] = [ {"id": call.id, "type": "function", "function": {"name": call.function.name, "arguments": call.function.arguments}} ] return turn def _run_tool(call: Any, context: dict[str, Any]) -> str: try: arguments = json.loads(call.function.arguments or "{}") except json.JSONDecodeError: arguments = {} code = arguments.get("code") if isinstance(arguments, dict) else None if call.function.name != "python": return json.dumps({"ok": False, "stdout": "", "result": None, "error": "unsupported tool"}) if not isinstance(code, str): return json.dumps({"ok": False, "stdout": "", "result": None, "error": "missing string argument: code"}) executor = RestrictedPythonExecutor( timeout_seconds=TOOL_TIMEOUT_SECONDS, output_limit=TOOL_OUTPUT_LIMIT, memory_limit_mb=TOOL_MEMORY_LIMIT_MB ) return executor.run(code, context) def _balanced_object(text: str) -> str | None: start = text.find("{") if start < 0: return None depth, in_string, escaped = 0, False, False for index, char in enumerate(text[start:], start=start): if in_string: if escaped: escaped = False elif char == "\\": escaped = True elif char == '"': in_string = False continue if char == '"': in_string = True elif char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: return text[start:index + 1] return None def parse_judgments(text: str, items: list[dict[str, Any]]) -> tuple[list[dict[str, Any]] | None, str | None]: """The judgment object of the final answer -> ([{rubric_id, explanation, score}], error).""" by_id = {item["rubric_id"]: item for item in items} cleaned = THINK_RE.sub("", text or "") blocks = re.findall(r"(.*?)", cleaned, flags=re.IGNORECASE | re.DOTALL) body = re.sub(r"```(?:json)?|```", "", blocks[-1] if blocks else cleaned, flags=re.IGNORECASE).strip() candidates = [body] balanced = _balanced_object(body) if balanced and balanced not in candidates: candidates.append(balanced) candidates += [c.replace("\\'", "'") for c in candidates if "\\'" in c] # \' is not a JSON escape parsed, error = None, "no judge JSON object found" for candidate in candidates: try: parsed = json.loads(candidate) break except json.JSONDecodeError as exc: error = str(exc) if parsed is None: return None, error if not isinstance(parsed, dict) or set(parsed) != {"judgments"} or not isinstance(parsed["judgments"], list): return None, "the judge JSON must be an object with the single key 'judgments' holding a list" judgments = parsed["judgments"] if len(judgments) != len(items): return None, f"expected {len(items)} judgments, got {len(judgments)}" if all(isinstance(j, dict) and j.get("rubric_id") in (None, "") for j in judgments): judgments = [dict(j, rubric_id=item["rubric_id"]) for j, item in zip(judgments, items)] out, seen = [], set() for judgment in judgments: if not isinstance(judgment, dict): return None, "each judgment must be an object" rubric_id, score = judgment.get("rubric_id"), judgment.get("score") if isinstance(rubric_id, str) and re.fullmatch(r"\d+", rubric_id.strip()): rubric_id = int(rubric_id.strip()) if isinstance(score, str) and re.fullmatch(r"\d+", score.strip()): score = int(score.strip()) if type(rubric_id) is not int or rubric_id not in by_id or rubric_id in seen: return None, f"unknown or repeated rubric_id: {judgment.get('rubric_id')!r}" if not isinstance(judgment.get("explanation"), str): return None, f"explanation for rubric {rubric_id} must be a string" if type(score) is not int: return None, f"score for rubric {rubric_id} must be an integer" if by_id[rubric_id]["kind"] == "hard_rule" and score not in (0, 1): return None, f"Hard Rule rubric {rubric_id} score must be 0 or 1" if by_id[rubric_id]["kind"] != "hard_rule" and not 0 <= score <= 100: return None, f"Principle rubric {rubric_id} score must be between 0 and 100" seen.add(rubric_id) out.append({"rubric_id": rubric_id, "explanation": judgment["explanation"], "score": score}) return sorted(out, key=lambda j: j["rubric_id"]), None def judge_once(client: OpenAI, model: str, instruction: str, response: str, items: list[dict[str, Any]]) -> str: """One judgment episode; returns the text of the final answer.""" rubrics = [{"rubric_id": item["rubric_id"], "criterion": item["criterion"]} for item in items] payload = {"instruction": instruction, "response": response, "rubrics": rubrics} conversation: list[dict[str, Any]] = [ {"role": "system", "content": JUDGE_PROMPT}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False, indent=2)}, ] # variables the judge's code can read context = { "conversation": [{"role": "user", "content": instruction}, {"role": "assistant", "content": response}], "instruction": instruction, "response": response, "rubrics": rubrics, "rubric_items": items, "last_assistant_response": response, } executed = 0 for turn in range(1, MAX_JUDGE_TURNS + 1): offer_tools = executed < MAX_TOOL_CALLS and turn < MAX_JUDGE_TURNS extra = {"tools": PYTHON_TOOLS, "tool_choice": "auto"} if offer_tools else {} message = client.chat.completions.create(model=model, messages=conversation, **SAMPLING, **extra).choices[0].message calls = (message.tool_calls or []) if offer_tools else [] if not calls: return message.content if (message.content or "").strip() else (_reasoning(message) or "") call = calls[0] # one call is executed per turn conversation.append(_assistant_turn(message, call)) conversation.append({"role": "tool", "tool_call_id": call.id, "content": _run_tool(call, context)}) executed += 1 raise RuntimeError("the judge loop ended without a final answer") def score_of(items: list[dict[str, Any]], judgments: list[dict[str, Any]]) -> float: """Number of satisfied Hard Rules + mean Principle score / 101. The second term is below 1, so Principles only order responses that satisfy the same number of Hard Rules.""" by_id = {j["rubric_id"]: j["score"] for j in judgments} hard = sum(by_id[i["rubric_id"]] for i in items if i["kind"] == "hard_rule") principles = [by_id[i["rubric_id"]] for i in items if i["kind"] != "hard_rule"] return hard + (sum(principles) / len(principles) if principles else 0.0) / 101 def judge(client: OpenAI, model: str, instruction: str, response: str, items: list[dict[str, Any]], max_attempts: int = 3) -> dict[str, Any]: """Judge one response against the rubric -> {score, judgments}. The judge never sees another response.""" error = None for _ in range(max_attempts): judgments, error = parse_judgments(judge_once(client, model, instruction, response, items), items) if error is None: return {"score": score_of(items, judgments), "judgments": judgments} raise RuntimeError(f"no parseable judgment after {max_attempts} attempts: {error}") # ------------------------------------------------------------------------------------------------ CLI def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--rubric-url", required=True, help="base URL of the rubric generator endpoint") parser.add_argument("--judge-url", required=True, help="base URL of the judge endpoint") parser.add_argument("--rubric-model", default="rever-rubric", help="served model name of the rubric generator") parser.add_argument("--judge-model", default="rever-judge", help="served model name of the judge") parser.add_argument("--instruction", required=True) parser.add_argument("--response", action="append", required=True, help="repeat for several responses") parser.add_argument("--api-key", default="EMPTY") args = parser.parse_args() rubric_client = OpenAI(base_url=args.rubric_url, api_key=args.api_key, timeout=600) judge_client = OpenAI(base_url=args.judge_url, api_key=args.api_key, timeout=600) items = generate_rubric(rubric_client, args.rubric_model, args.instruction) print("Rubric") for item in items: print(f" {item['rubric_id']}. {item['criterion']}") for number, response in enumerate(args.response, start=1): result = judge(judge_client, args.judge_model, args.instruction, response, items) print(f"\nResponse {number}: score {result['score']:.4f}") for judgment in result["judgments"]: print(f" {judgment['rubric_id']}. score {judgment['score']:>3} {judgment['explanation']}") if __name__ == "__main__": main()