#!/usr/bin/env python3 import argparse import asyncio import csv import hashlib import json import os import random import re import time from pathlib import Path from openai import AsyncOpenAI from dotenv import load_dotenv from tqdm.asyncio import tqdm_asyncio load_dotenv() api_key = os.getenv("OPENAI_API_KEY") if not api_key: print("Error: OPENAI_API_KEY not set") exit(1) client = AsyncOpenAI(api_key=api_key) MAX_COMPLETION_TOKENS = 500 # Base system prompts (external markdown files) with open("judge_prompt.md") as f: judge_system_prompt = f.read() # Load prompts from CSV and deduplicate prompts = [] seen = set() with open("chi_test_case.csv") as f: reader = csv.DictReader(f) for row in reader: prompt = row["gpt_description_prompt"].strip() if prompt and prompt not in seen: prompts.append(prompt) seen.add(prompt) METADATA_REGEX = re.compile(r"\{(.+?)\}") Q_AND_A_PROMPT = re.compile( r"""---\n \n? Q:.*\n A:.*""", re.VERBOSE, ) BANNED_GENRES = frozenset({"instrumental", "catchy", "festive", "energetic", "upbeat"}) ANODYNE_GENRES = ["pop", "rock", "folk", "cumbia", "metal", "funk", "dance", "jazz", "country", "rap"] LONG_GENRE_EXAMPLE = ( "This country anthem begins with sparse acoustic steel guitar and builds through verses with gradual " "instrumentation. It explodes into a honky-tonk chorus with full band energy. The production balances " "authentic country elements with commercial polish, designed for singalong crowd participation in chorus sections." ) POP_GENRE_INFO = ( "You are a Grammy-winning pop singer known for your catchy lyrics. Your style is often compared to poetry, " "and artists such as the Beatles, Michael Jackson and Elton John. You use rough, unpolished vocabulary and " "catchy lines that allow your lyrics to breathe. Don't use commas. Write unexpected and witty lyrics." ) ROCK_GENRE_INFO = ( "You are a Grammy-winning rock singer known for your frustrated, questioning and aggressive lyrics. Your style " "is often compared to beat poetry, and artists such as Bob Dylan and Neil Young. Use rough, unpolished vocabulary, " "and phrases rather than complete sentences. Don't ever use commas." ) RAP_GENRE_INFO = ( "You are a Grammy-winning rapper known for your humorous wordplay, internal rhymes, and pop-cultural allusions, " "often compared to artists like Jay-Z, Nas, Earl Sweatshirt and MF DOOM. Use rough, unpolished vocabulary, and " "phrases rather than complete sentences. Use creative similes, but don't use the 'like' or 'as'." ) FOLK_GENRE_INFO = ( "You are a famous folk singer known for your narrative storytelling in plaintive and understated lyrics. Your " "style is often compared to poetry, and artists such as Bob Dylan and Joan Baez. Use rough, unpolished vocabulary " "and unexplained references." ) FUNK_GENRE_INFO = ( "You are a Grammy-winning funk singer known for your rhythmic, groovy lyrics. Your style contains spontaneous " "ad-libs (like '(oo-yeah!)') and is hypnotizingly repetitive. Your lyrics make heavy use of slang, humor, and " "call and response." ) CUMBIA_GENRE_INFO = ( "You are a beloved cumbia singer known for your simple, sparing and traditional lyrics that are easy to sing along " "to. You tell stories through your songs. Don't be afraid to shout out ad-libs in parentheses where you give the " "audience dance instructions!" ) METAL_GENRE_INFO = ( "You are a metal singer infamous for your INDIFFERENCE TO BRUTALITY!! 🀘THE UNIVERSE IS VAST AND CRUEL! You love " "violence, suffering and power, and you often tear the heads off of bats with your teeth. You use short, brutal " "phrases, never complete sentences." ) DANCE_GENRE_INFO = ( "You are a Grammy-winning dance pop singer known for your simple and infectious lyrics. Your lyrics keep the " "people on the dance floor all night long. You use extremely simple vocabulary, short lines that allow your lyrics " "to breathe, short phrases rather than complete sentences, and repetition repetition repetition. You use " "spontaneous ad-libs (like '(ooh-yeah!)') and repeat words at the ends of lines." ) JAZZ_GENRE_INFO = ( "You are a Grammy-winning Tin Pan Alley jazz singer known for your classic, witty and wry lyrics. Your charming " "style is full of simple, understated wordplay, often compared to songwriters such as the Gershwin brothers, Hoagy " "Carmichael and Jerome Kern. Your lyrics are cheeky and sly, speaking to love, timeless romance and melancholy." ) COUNTRY_GENRE_INFO = ( "You are a Grammy-winning Nashville country singer known for your frank, stark and poignant storytelling. Your " "style is simple and full of concrete details about small-town rural life, often compared to songwriters such as " "Hank Williams, Townes van Zandt, Doc Watson, Merle Haggard and Dolly Parton." ) GENRE_PROMPTS = { "pop": POP_GENRE_INFO, "rock": ROCK_GENRE_INFO, "rap": RAP_GENRE_INFO, "folk": FOLK_GENRE_INFO, "funk": FUNK_GENRE_INFO, "cumbia": CUMBIA_GENRE_INFO, "metal": METAL_GENRE_INFO, "dance": DANCE_GENRE_INFO, "jazz": JAZZ_GENRE_INFO, "country": COUNTRY_GENRE_INFO, } GENRE_KEYWORDS = { "rap": ["rap", "hip hop", "hip-hop", "trap"], "rock": ["rock", "grunge", "punk"], "folk": ["folk", "acoustic", "story"], "funk": ["funk", "groove"], "cumbia": ["cumbia"], "metal": ["metal", "scream"], "dance": ["dance", "edm", "club"], "jazz": ["jazz", "swing"], "country": ["country", "western"], "pop": ["pop", "catchy"], } JUDGE_FAILURE_KEYS = [ "instrumental_mismatch", "format_failure", "language_mismatch", "non_english_metadata", "ban_violation", "empty_lyrics", "refusal", "tone_mismatch", ] PROMPT_STRATEGIES = {"5_1", "4o", "raw"} DEFAULT_ELO_RATING = 1000.0 ELO_K_FACTOR = 24.0 DEFAULT_CONFIDENCE_WEIGHT = 0.5 def _normalize_confidence_value(value): try: confidence = float(value) except (TypeError, ValueError): return None if confidence != confidence: # NaN check without math.isnan return None return max(0.0, min(1.0, confidence)) def _confidence_weight(value): normalized = _normalize_confidence_value(value) return normalized if normalized is not None else DEFAULT_CONFIDENCE_WEIGHT def _init_elo_bucket(): return { "rating": DEFAULT_ELO_RATING, "matches": 0, "confidence_sum": 0.0, "confidence_samples": 0, } def load_prompt_text(path): with open(path) as f: return f.read() def infer_prompt_strategy(model_name): lowered = (model_name or "").lower() if "5.1" in lowered or "5_1" in lowered: return "5_1" if "4o" in lowered: return "4o" return "raw" def build_slot_config(label, model_name, prompt_path, strategy=None, key=None): prompt_text = load_prompt_text(prompt_path) inferred = strategy or infer_prompt_strategy(model_name) prompt_strategy = inferred if inferred in PROMPT_STRATEGIES else "raw" marker = f"{os.path.basename(prompt_path)}:{hashlib.md5(prompt_text.encode()).hexdigest()[:8]}" return { "key": key or label.lower(), "label": label, "model": model_name, "prompt_path": prompt_path, "prompt_text": prompt_text, "prompt_strategy": prompt_strategy, "prompt_marker": marker, } def _strip_lyrics_prompt_hallucinations(lyrics): return Q_AND_A_PROMPT.sub("", lyrics) def get_stanzas(text): """Split lyrics into stanzas based on blank lines.""" if not text: return [] return [stanza.strip() for stanza in text.split("\n\n") if stanza.strip()] def truncate_lyrics(text, num_stanzas=3): """Keep only the first num_stanzas stanzas.""" stanzas = get_stanzas(text) if not stanzas: return "", False truncated_stanzas = stanzas[:num_stanzas] was_truncated = len(stanzas) > num_stanzas return "\n\n".join(truncated_stanzas), was_truncated def detect_genre(user_prompt): lowered = user_prompt.lower() for genre, keywords in GENRE_KEYWORDS.items(): if any(keyword in lowered for keyword in keywords): return genre, GENRE_PROMPTS[genre] return None, None def is_instrumental_prompt(user_prompt): return "instrumental" in user_prompt.lower() def construct_5_1_system_prompt(base_prompt, genre_block): """Prepend genre-specific persona to the 5.1 base prompt.""" return f"{genre_block}\n\n{base_prompt}" def construct_4o_system_prompt(base_prompt, user_prompt): genre_key, genre_block = detect_genre(user_prompt) prompt_parts = [] if genre_block: prompt_parts.append(genre_block) prompt_parts.append(base_prompt) prompt = "\n\n".join(prompt_parts) return prompt, genre_key or "pop" def format_prompt_preview(text, max_len=60): """Collapse whitespace and truncate long prompts for logging.""" if not text: return "" collapsed = " ".join(text.split()) if len(collapsed) <= max_len: return collapsed return f"{collapsed[: max_len - 3].rstrip()}..." def build_slot_system_prompt(slot_config, user_prompt): strategy = slot_config.get("prompt_strategy") or "raw" prompt_text = slot_config.get("prompt_text") or "" if strategy == "4o": return construct_4o_system_prompt(prompt_text, user_prompt) genre_key, genre_block = detect_genre(user_prompt) if strategy == "5_1" and genre_block: return construct_5_1_system_prompt(prompt_text, genre_block), genre_key return prompt_text, genre_key def _empty_failure_flags(): base = {key: False for key in JUDGE_FAILURE_KEYS} base["details"] = "" return base def normalize_failure_flags(raw_flags): flags = _empty_failure_flags() if not raw_flags: return flags for key in JUDGE_FAILURE_KEYS: flags[key] = bool(raw_flags.get(key, False)) if raw_flags.get("details"): flags["details"] = str(raw_flags["details"]) return flags def _ensure_model_tracking(tracking, model): if model is None or tracking is None: return if model not in tracking["model_stats"]: tracking["model_stats"][model] = {"wins": 0, "losses": 0, "ties": 0} if model not in tracking["failure_counts"]: tracking["failure_counts"][model] = {key: 0 for key in JUDGE_FAILURE_KEYS} if model not in tracking["model_observations"]: tracking["model_observations"][model] = 0 if "elo_ratings" not in tracking: tracking["elo_ratings"] = {} if model not in tracking["elo_ratings"]: tracking["elo_ratings"][model] = _init_elo_bucket() def _apply_elo_result(tracking, model_a, model_b, score_a, confidence): if not tracking or not model_a or not model_b: return _ensure_model_tracking(tracking, model_a) _ensure_model_tracking(tracking, model_b) bucket_a = tracking["elo_ratings"][model_a] bucket_b = tracking["elo_ratings"][model_b] rating_a = bucket_a["rating"] rating_b = bucket_b["rating"] expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400)) expected_b = 1 - expected_a score_b = 1 - score_a if score_a in (0.0, 1.0) else 0.5 weight = _confidence_weight(confidence) k = ELO_K_FACTOR * weight bucket_a["rating"] = rating_a + k * (score_a - expected_a) bucket_b["rating"] = rating_b + k * (score_b - expected_b) bucket_a["matches"] += 1 bucket_b["matches"] += 1 normalized_conf = _normalize_confidence_value(confidence) if normalized_conf is not None: bucket_a["confidence_sum"] += normalized_conf bucket_a["confidence_samples"] += 1 bucket_b["confidence_sum"] += normalized_conf bucket_b["confidence_samples"] += 1 def format_candidate_block(label, payload): status = payload.get("status") or "unknown" markers = [] if payload.get("was_token_truncated"): markers.append("token_truncated") if payload.get("was_stanza_truncated"): markers.append("stanza_truncated") marker_text = f" ({', '.join(markers)})" if markers else "" output_text = payload.get("formatted_output") or "" return ( f"Candidate {label} β€” status: {status}{marker_text}\n" "```\n" f"{output_text}\n" "```\n" ) def build_judge_message(user_prompt, candidates): parts = [ "User prompt:", user_prompt.strip(), "", format_candidate_block("A", candidates["A"]), format_candidate_block("B", candidates["B"]), "Respond with strict JSON following the specified schema.", ] return "\n".join(parts) def make_candidate_payload(completion): metadata = completion.get("metadata", {}) or {} title = metadata.get("title", "").strip() style = metadata.get("style", "").strip() lyrics = (completion.get("content") or "").strip() formatted_output = "\n".join( [ f"{{{title}}}" if title else "{ }", f"{{{style}}}" if style else "{ }", "", lyrics or "", ] ).strip() return { "title": title, "style": style, "lyrics": lyrics, "status": completion.get("status"), "formatted_output": formatted_output, "was_token_truncated": metadata.get("was_token_truncated"), "was_stanza_truncated": metadata.get("was_stanza_truncated"), } def init_judge_tracking(model_keys, judge_model_name): return { "judge_model": judge_model_name, "total_judged": 0, "model_stats": {model: {"wins": 0, "losses": 0, "ties": 0} for model in model_keys}, "failure_counts": {model: {key: 0 for key in JUDGE_FAILURE_KEYS} for model in model_keys}, "model_observations": {model: 0 for model in model_keys}, "elo_ratings": {model: _init_elo_bucket() for model in model_keys}, } def update_judge_tracking(tracking, judge_payload): if not tracking or not judge_payload: return assignments = judge_payload.get("model_assignment", {}) if len(assignments) < 2: return tracking["total_judged"] += 1 failures = judge_payload.get("failures", {}) for label, model in assignments.items(): _ensure_model_tracking(tracking, model) if model not in tracking["model_observations"]: continue tracking["model_observations"][model] += 1 label_failures = failures.get(label, {}) for key in JUDGE_FAILURE_KEYS: if label_failures.get(key): tracking["failure_counts"][model][key] += 1 winner_label = judge_payload.get("winner", "tie") confidence_value = judge_payload.get("confidence") if winner_label in assignments: winner_model = assignments[winner_label] loser_label = "B" if winner_label == "A" else "A" loser_model = assignments.get(loser_label) _ensure_model_tracking(tracking, winner_model) _ensure_model_tracking(tracking, loser_model) if winner_model in tracking["model_stats"]: tracking["model_stats"][winner_model]["wins"] += 1 if loser_model and loser_model in tracking["model_stats"]: tracking["model_stats"][loser_model]["losses"] += 1 if winner_model and loser_model: _apply_elo_result(tracking, winner_model, loser_model, 1.0, confidence_value) else: for model in assignments.values(): _ensure_model_tracking(tracking, model) if model in tracking["model_stats"]: tracking["model_stats"][model]["ties"] += 1 model_a = assignments.get("A") model_b = assignments.get("B") if model_a and model_b: _apply_elo_result(tracking, model_a, model_b, 0.5, confidence_value) def finalize_judge_summary(tracking): if not tracking or tracking["total_judged"] == 0: return None failure_rates = {} for model, counts in tracking["failure_counts"].items(): obs = tracking["model_observations"].get(model, 0) failure_rates[model] = {} for key, count in counts.items(): rate = count / obs if obs else 0.0 failure_rates[model][key] = {"count": count, "rate": round(rate, 4)} elo_summary = {} for model, metrics in tracking.get("elo_ratings", {}).items(): if not metrics: continue avg_conf = ( metrics["confidence_sum"] / metrics["confidence_samples"] if metrics["confidence_samples"] else None ) elo_summary[model] = { "rating": round(metrics["rating"], 2), "matches": metrics["matches"], "avg_confidence": round(avg_conf, 4) if avg_conf is not None else None, } return { "judge_model": tracking["judge_model"], "total_judged": tracking["total_judged"], "model_stats": tracking["model_stats"], "failure_rates": failure_rates, "elo_ratings": elo_summary, } def parse_completion_output(raw_text, make_instrumental=False, lyrics_model=None): if make_instrumental and not raw_text.strip(): return { "status": "instrumental", "title": "", "style": "", "tags": ["instrumental"], "lyrics": "[Instrumental]", "lyrics_raw": "[Instrumental]", "was_token_truncated": False, "was_stanza_truncated": False, "line_count_raw": 1, "line_count_final": 1, } matches = METADATA_REGEX.findall(raw_text) title = matches[0].strip() if matches else "" style = matches[1].strip() if len(matches) > 1 else "" raw_tags = {tag.strip().lower() for tag in style.split(",") if tag.strip()} tags = [tag for tag in raw_tags if tag not in BANNED_GENRES] or [random.choice(ANODYNE_GENRES)] lyrics_raw = METADATA_REGEX.sub("", raw_text).strip() lyrics_raw = _strip_lyrics_prompt_hallucinations(lyrics_raw) line_count_raw = len(lyrics_raw.split("\n")) if lyrics_raw else 0 should_truncate = not (lyrics_model and "gpt-5.1" in lyrics_model) if should_truncate: lyrics, was_stanza_truncated = truncate_lyrics(lyrics_raw) else: lyrics = lyrics_raw was_stanza_truncated = False was_token_truncated = bool(raw_text and not raw_text.rstrip().endswith((".", "!", "?", '"', "'", "]", ")"))) status = "ok" if not matches: status = "missing_metadata" if not lyrics: status = "empty_lyrics" if make_instrumental and raw_text.strip(): lyrics = "[Instrumental]" status = "instrumental" if "instrumental" not in tags: tags = ["instrumental"] + tags line_count_final = len(lyrics.split("\n")) if lyrics else 0 return { "status": status, "title": title, "style": style, "tags": tags, "lyrics": lyrics, "lyrics_raw": lyrics_raw, "was_token_truncated": was_token_truncated, "was_stanza_truncated": was_stanza_truncated, "line_count_raw": line_count_raw, "line_count_final": line_count_final, } def hash_file(path): return hashlib.md5(Path(path).read_bytes()).hexdigest() def get_latest_output(): files = sorted(Path(".").glob("outputs_*.json"), reverse=True) if not files: return None try: with open(files[0]) as f: return json.load(f) except: # noqa: E722 return None async def stream_completion_once(slot_config, user_prompt, semaphore, log_status=None, prompt_preview=""): logger = log_status or print prompt_label = prompt_preview or format_prompt_preview(user_prompt) async with semaphore: model_name = slot_config["model"] system_prompt, genre_hint = build_slot_system_prompt(slot_config, user_prompt) make_instrumental = is_instrumental_prompt(user_prompt) params = { "model": model_name, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "max_completion_tokens": MAX_COMPLETION_TOKENS, "temperature": 1.0, "n": 1, "stream": True, "stream_options": {"include_usage": True}, } if model_name == "gpt-5.1": params["reasoning_effort"] = "none" start_time = time.time() ttft = None full_content = "" usage_stats = None stream = await client.chat.completions.create(**params) async for chunk in stream: if hasattr(chunk, "usage") and chunk.usage: usage_stats = chunk.usage if chunk.choices and chunk.choices[0].delta.content: full_content += chunk.choices[0].delta.content if ttft is None: ttft = time.time() - start_time total_duration = time.time() - start_time completion_tokens = usage_stats.completion_tokens if usage_stats else 0 tokens_per_second = 0 if ttft and total_duration > ttft and completion_tokens > 1: tokens_per_second = (completion_tokens - 1) / (total_duration - ttft) elif total_duration > 0 and completion_tokens > 0: tokens_per_second = completion_tokens / total_duration metric_bits = [] if ttft is not None: metric_bits.append(f"TTFT {ttft:.2f}s") if tokens_per_second: metric_bits.append(f"{tokens_per_second:.1f} tok/s") metrics_display = f" ({', '.join(metric_bits)})" if metric_bits else "" logger(f"βœ“ {model_name} β€’ {prompt_label}{metrics_display}") parsed = parse_completion_output( full_content, make_instrumental=make_instrumental, lyrics_model=model_name, ) return { "model": model_name, "prompt": user_prompt, "content": parsed["lyrics"], "status": parsed["status"], "prompt_path": slot_config.get("prompt_path"), "prompt_marker": slot_config.get("prompt_marker"), "prompt_strategy": slot_config.get("prompt_strategy"), "metadata": { "title": parsed["title"], "style": parsed["style"], "tags": parsed["tags"], "genre_hint": genre_hint, "raw_output": full_content, "lyrics_raw": parsed.get("lyrics_raw"), "was_token_truncated": parsed.get("was_token_truncated"), "was_stanza_truncated": parsed.get("was_stanza_truncated"), "line_count_raw": parsed.get("line_count_raw"), "line_count_final": parsed.get("line_count_final"), }, "metrics": { "ttft": round(ttft, 3) if ttft else None, "tokens_per_second": round(tokens_per_second, 2), "total_duration": round(total_duration, 3), "total_tokens": completion_tokens, }, } async def stream_completion_with_retry( slot_config, user_prompt, semaphore, log_status=None, prompt_preview="", max_retries=2, ): attempts = 0 last_error = None while attempts <= max_retries: try: return await stream_completion_once( slot_config, user_prompt, semaphore, log_status=log_status, prompt_preview=prompt_preview, ) except Exception as err: attempts += 1 last_error = err logger = log_status or print logger( f"βœ— {slot_config['model']} β€’ {prompt_preview or format_prompt_preview(user_prompt)} " f"β€” attempt {attempts}/{max_retries + 1}: {err}" ) if attempts > max_retries: break await asyncio.sleep(min(2 ** attempts, 5)) return { "model": slot_config["model"], "prompt": user_prompt, "content": f"ERROR: {last_error}", "status": "error", "prompt_path": slot_config.get("prompt_path"), "prompt_marker": slot_config.get("prompt_marker"), "prompt_strategy": slot_config.get("prompt_strategy"), "metadata": {"genre_hint": None}, "metrics": {"error": str(last_error) if last_error else "unknown"}, } async def judge_prompt_pair( prompt_text, slot_payloads, judge_cfg, semaphore, log_status=None, prompt_preview="", ): logger = log_status or print prompt_label = prompt_preview or format_prompt_preview(prompt_text) available = [ (slot_key, data) for slot_key, data in slot_payloads.items() if data and data.get("content") is not None and data.get("status") != "error" ] if len(available) < 2: return None random.shuffle(available) selected = available[:2] assignments = {} slot_assignments = {} candidates = {} for label, (slot_key, data) in zip(("A", "B"), selected): assignments[label] = data.get("model") slot_assignments[label] = slot_key candidates[label] = make_candidate_payload(data) user_message = build_judge_message(prompt_text, candidates) params = { "model": judge_cfg["model"], "messages": [ {"role": "system", "content": judge_cfg["prompt"]}, {"role": "user", "content": user_message}, ], "temperature": judge_cfg["temperature"], "max_completion_tokens": judge_cfg["max_tokens"], "response_format": {"type": "json_object"}, } attempts = 0 max_retries = judge_cfg["max_retries"] while attempts <= max_retries: try: async with semaphore: response = await client.chat.completions.create(**params) content = response.choices[0].message.content parsed = json.loads(content) failures = parsed.get("failures", {}) normalized = { "model_assignment": assignments, "slot_assignment": slot_assignments, "winner": parsed.get("winner", "tie"), "confidence": parsed.get("confidence"), "commentary": (parsed.get("commentary") or "").strip(), "failures": { "A": normalize_failure_flags(failures.get("A")), "B": normalize_failure_flags(failures.get("B")), }, } logger( f"βš–οΈ {judge_cfg['model']} β€’ {prompt_label} β€” winner {normalized['winner']} (conf {normalized['confidence']})" ) return normalized except Exception as e: attempts += 1 if attempts > max_retries: logger(f"βœ— Judge β€’ {prompt_label} β€” {e}") return { "model_assignment": assignments, "slot_assignment": slot_assignments, "winner": "tie", "confidence": None, "commentary": f"Judge error: {e}", "failures": { "A": _empty_failure_flags(), "B": _empty_failure_flags(), }, "error": str(e), } await asyncio.sleep(min(2 ** attempts, 5)) async def evaluate_prompt( index, prompt_text, slot_configs, completion_semaphore, judge_enabled, judge_cfg, judge_semaphore, judge_state, judge_lock, log_status, ): prompt_preview = format_prompt_preview(prompt_text) entry = {"prompt": prompt_text, "completions": {}} slot_payloads = {} for slot in slot_configs: payload = await stream_completion_with_retry( slot, prompt_text, completion_semaphore, log_status=log_status, prompt_preview=prompt_preview, ) entry["completions"][slot["key"]] = payload slot_payloads[slot["key"]] = payload should_judge = judge_enabled and random.random() <= judge_cfg.get("sample_rate", 1.0) if should_judge: judge_result = await judge_prompt_pair( prompt_text, slot_payloads, judge_cfg, judge_semaphore, log_status=log_status, prompt_preview=prompt_preview, ) if judge_result: entry["judge"] = judge_result if judge_lock: async with judge_lock: update_judge_tracking(judge_state, judge_result) else: update_judge_tracking(judge_state, judge_result) return index, entry async def process_prompts( prompts_to_process, slot_configs, max_concurrent=8, judge_options=None, checkpoint_interval=50, checkpoint_path=None, system_prompt_markers=None, slot_metadata=None, prompts_hash=None, ): total = len(prompts_to_process) if total == 0: return [], None log_status = tqdm_asyncio.write completion_semaphore = asyncio.Semaphore(max(1, max_concurrent)) judge_cfg = judge_options or {} judge_enabled = judge_cfg.get("enabled", False) and judge_cfg.get("prompt") judge_state = None judge_semaphore = None if judge_enabled: unique_models = sorted({slot["model"] for slot in slot_configs}) judge_state = init_judge_tracking(unique_models, judge_cfg["model"]) judge_semaphore = asyncio.Semaphore(max(1, judge_cfg.get("max_concurrent", 4))) judge_lock = asyncio.Lock() else: judge_cfg = {} judge_lock = None ordered_results = [] buffer = [None] * total next_commit = 0 completed = 0 result_lock = asyncio.Lock() prompt_queue = asyncio.Queue() for idx, prompt in enumerate(prompts_to_process): prompt_queue.put_nowait((idx, prompt)) async def worker(): nonlocal next_commit, completed while True: try: idx, prompt = await prompt_queue.get() except asyncio.CancelledError: break try: index, result = await evaluate_prompt( idx, prompt, slot_configs, completion_semaphore, judge_enabled, judge_cfg, judge_semaphore, judge_state, judge_lock, log_status, ) async with result_lock: buffer[index] = result while next_commit < total and buffer[next_commit] is not None: ordered_results.append(buffer[next_commit]) buffer[next_commit] = None next_commit += 1 completed += 1 if ( checkpoint_interval and checkpoint_path and completed % checkpoint_interval == 0 ): snapshot = ( finalize_judge_summary(judge_state) if judge_state else None ) write_results_file( checkpoint_path, system_prompt_markers, prompts_hash, slot_metadata, ordered_results, snapshot, ) finally: prompt_queue.task_done() worker_count = min(total, max(1, max_concurrent)) workers = [asyncio.create_task(worker()) for _ in range(worker_count)] await prompt_queue.join() for task in workers: task.cancel() await asyncio.gather(*workers, return_exceptions=True) return ordered_results, judge_state def find_next_version(): version = 1 while Path(f"outputs_{version:03d}.json").exists(): version += 1 return version def write_results_file( path, system_prompt_markers, prompts_hash, slot_metadata, results, judge_summary, ): payload = { "system_prompts": system_prompt_markers or {}, "slots": slot_metadata or {}, "prompts_hash": prompts_hash, "results": results, } if judge_summary: payload["judge_summary"] = judge_summary with open(path, "w") as f: json.dump(payload, f, indent=2) async def main(): parser = argparse.ArgumentParser(description="Eval harness for prompt comparison") parser.add_argument("-n", "--num-prompts", type=int, help="Number of prompts to process (default: all)") parser.add_argument("--max-concurrent", type=int, default=8, help="Max concurrent generation requests") parser.add_argument("--no-judge", action="store_true", help="Disable LLM judge comparisons") parser.add_argument("--judge-model", default="gpt-5.1", help="Model used for judging outputs") parser.add_argument("--judge-temperature", type=float, default=0.3, help="Sampling temperature for judge calls") parser.add_argument("--judge-max-tokens", type=int, default=400, help="Max completion tokens for judge responses") parser.add_argument("--judge-max-retries", type=int, default=2, help="Number of retries for failed judge calls") parser.add_argument( "--judge-max-concurrent", type=int, default=4, help="Maximum concurrent judge API calls", ) parser.add_argument( "--judge-sample-rate", type=float, default=1.0, help="Fraction of prompts to pass through the judge (0-1)", ) parser.add_argument("--model-a", default="gpt-5.1", help="API model used for Slot A") parser.add_argument("--model-b", default="gpt-4o", help="API model used for Slot B") parser.add_argument("--prompt-a", default="5_1.md", help="System prompt file for Slot A") parser.add_argument("--prompt-b", default="4o.md", help="System prompt file for Slot B") parser.add_argument( "--checkpoint-interval", type=int, default=50, help="Write partial outputs every N completed prompts (0 disables checkpointing)", ) args = parser.parse_args() prompts_hash = hash_file("chi_test_case.csv") slot_a = build_slot_config("Slot A", args.model_a, args.prompt_a, key="slot_a") slot_b = build_slot_config("Slot B", args.model_b, args.prompt_b, key="slot_b") slot_configs = [slot_a, slot_b] slot_metadata = { slot["key"]: { "label": slot["label"], "model": slot["model"], "prompt_path": slot["prompt_path"], "prompt_marker": slot["prompt_marker"], "prompt_strategy": slot["prompt_strategy"], } for slot in slot_configs } judge_marker = f"judge:{hashlib.md5(judge_system_prompt.encode()).hexdigest()[:8]}" system_prompt_markers = { slot["key"]: slot["prompt_marker"] for slot in slot_configs } system_prompt_markers["judge"] = judge_marker prompts_to_process = prompts[:args.num_prompts] if args.num_prompts else prompts judge_sample_rate = min(1.0, max(0.0, args.judge_sample_rate)) judge_options = None if not args.no_judge and judge_sample_rate > 0: judge_options = { "enabled": True, "model": args.judge_model, "prompt": judge_system_prompt, "temperature": args.judge_temperature, "max_tokens": args.judge_max_tokens, "max_retries": max(0, args.judge_max_retries), "sample_rate": judge_sample_rate, "max_concurrent": max(1, args.judge_max_concurrent), } checkpoint_interval = max(0, args.checkpoint_interval) version = find_next_version() output_file = f"outputs_{version:03d}.json" print(f"Processing {len(prompts_to_process)} prompts with output -> {output_file}") results, judge_state = await process_prompts( prompts_to_process, slot_configs, max_concurrent=args.max_concurrent, judge_options=judge_options, checkpoint_interval=checkpoint_interval, checkpoint_path=output_file if checkpoint_interval else None, system_prompt_markers=system_prompt_markers, slot_metadata=slot_metadata, prompts_hash=prompts_hash, ) judge_summary = finalize_judge_summary(judge_state) write_results_file( output_file, system_prompt_markers, prompts_hash, slot_metadata, results, judge_summary, ) print(f"\nβœ“ Results saved to {output_file}") if __name__ == "__main__": asyncio.run(main())