from __future__ import annotations import math import re from .config import RewardConfig from .schemas import TaskRecord, WorkerResult def _normalize_text(value: str, case_sensitive: bool) -> str: normalized = " ".join(value.strip().split()) return normalized if case_sensitive else normalized.casefold() def deterministic_quality(task: TaskRecord, candidate: str) -> float: """Grade tasks whose correctness can be checked locally.""" grader = task.grader reference = task.reference_answer if grader.type == "llm_judge": raise ValueError("llm_judge tasks must be scored by the configured judge model") if reference is None and grader.type != "regex": raise ValueError(f"Task {task.task_id} needs reference_answer for {grader.type}") if grader.type == "exact": return float( _normalize_text(candidate, grader.case_sensitive) == _normalize_text(reference or "", grader.case_sensitive) ) if grader.type == "contains": return float( _normalize_text(reference or "", grader.case_sensitive) in _normalize_text(candidate, grader.case_sensitive) ) if grader.type == "numeric": candidate_match = re.search(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?", candidate) reference_match = re.search( r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?", reference or "" ) if not candidate_match or not reference_match: return 0.0 candidate_value = float(candidate_match.group(0)) reference_value = float(reference_match.group(0)) return float(math.isclose(candidate_value, reference_value, abs_tol=grader.tolerance)) if grader.type == "regex": pattern = grader.pattern or reference if not pattern: raise ValueError(f"Task {task.task_id} needs grader.pattern") flags = 0 if grader.case_sensitive else re.IGNORECASE return float(re.search(pattern, candidate, flags=flags) is not None) raise ValueError(f"Unsupported grader: {grader.type}") def compute_utility(result: WorkerResult, config: RewardConfig) -> float: cost = result.cost_usd or 0.0 cost_penalty = min(cost / config.cost_scale_usd, 5.0) latency_penalty = min((result.latency_ms / 1000.0) / config.latency_scale_seconds, 5.0) return float( config.quality_weight * result.quality - config.cost_weight * cost_penalty - config.latency_weight * latency_penalty ) def soft_targets(rewards, temperature: float): """Torch-free reference implementation used by tests and data inspection.""" import numpy as np values = np.asarray(rewards, dtype=np.float64) if temperature <= 0: raise ValueError("temperature must be positive") shifted = (values - values.max(axis=-1, keepdims=True)) / temperature probabilities = np.exp(shifted) return probabilities / probabilities.sum(axis=-1, keepdims=True) def grader_prompt(task: TaskRecord, candidate: str) -> str: reference = task.reference_answer or "(no reference answer supplied)" rubric = task.grader.rubric or "Correct, relevant, complete, and follows the requested format." return ( f"TASK:\n{task.prompt}\n\n" f"RUBRIC:\n{rubric}\n\n" f"REFERENCE:\n{reference}\n\n" f"CANDIDATE:\n{candidate}\n\n" "SCORE (0 to 1):" ) def parse_judge_score(text: str) -> float: match = re.search(r"(?:^|\s)(?:0(?:\.\d+)?|1(?:\.0+)?)(?:\s|$)", text.strip()) if not match: match = re.search(r"(?:0(?:\.\d+)?|1(?:\.0+)?)", text) if not match: raise ValueError(f"Judge did not return a 0..1 score: {text[:200]!r}") return min(1.0, max(0.0, float(match.group(0).strip())))