Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| #!/usr/bin/env env python3 | |
| """ | |
| Rubric Generation Script for HF-Agent Benchmark | |
| Generates instance-specific evaluation rubrics following the "Rubrics as Rewards" paper. | |
| Uses LiteLLM to call LLM models for rubric synthesis with expert grounding via reference answers. | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from pathlib import Path | |
| from typing import Any, Dict, List | |
| import litellm | |
| import pandas as pd | |
| from dotenv import load_dotenv | |
| from pydantic import BaseModel | |
| from eval.hf_io import df_to_hub | |
| class Rubric(BaseModel): | |
| title: str | |
| description: str | |
| weight: int | |
| class RubricList(BaseModel): | |
| rubrics: List[Rubric] | |
| # Load environment variables | |
| load_dotenv() | |
| # Rubric generation prompt template based on RaR paper | |
| PROMPT_TEMPLATE = """You are an expert rubric writer. Your job is to generate a self-contained set of evaluation criteria (“rubrics”) for judging | |
| how good, helpful and complete an agent's trajectory is to a given user question/request. | |
| Rubrics can cover aspects of a response such as, but not limited to, factual correctness, helpfulness, completeness, harmlessness, correctness of using Hugging Face best practices (based on HF documentation), depth of | |
| reasoning, contextual relevance and usefulness. Each item must be self-contained – non expert readers should not need to | |
| infer anything or consult external information. Begin each description with its category: “Essential Criteria: . . . ”, “Important | |
| Criteria: . . . ”, “Optional Criteria: . . . ”, or “Pitfall Criteria: Does not mention . . . ”. | |
| Inputs: !!! | |
| - question: <<<{question}>>> | |
| - reference_answer (ideal solution): <<<{reference_answer}>>> | |
| - thread: <<<{thread}>>> | |
| Total items: | |
| • Choose 7–20 rubric items based on the complexity of the question. | |
| Each rubric item: | |
| • title (2–4 words). | |
| • description: One sentence starting with its category prefix that explicitly states exactly what to look for. For example: | |
| – Essential Criteria: Writes a up-to-date, correct, complete and working training loop using the latest Hugging Face best practices. Launches the training with hf-jobs. | |
| – Pitfall Criteria: Deprecated launcher usage. Uses python -m torch.distributed.launch instead of torchrun / accelerate. | |
| – Important Criteria: Explains common DDP knobs. Mentions ddp_find_unused_parameters=False for models with conditional branches; optional ddp_timeout; brief note on when they matter and why. | |
| – Optional Criteria: Briefly notes --deepspeed ds_config.json as an alternative scaler when models get big (but stays on DDP for this Q). | |
| • weight: For Essential/Important/Optional, use 1–5 (5 = most important); for Pitfall, use –1 or –2. | |
| Category guidance: | |
| • Essential: Critical actions to answer/complete the user's question/request; if missing, the response is invalid and useless (weight 5). | |
| • Important: Key reasoning, completeness, or clarity; strongly affects quality and usefulness (weight 3–4). | |
| • Optional: Helpfulness in educating the user or providing extra depth; nice to have but not deal-breaking (weight 1–2). | |
| • Pitfall: Common mistakes or omissions specific to this prompt—identify things a respondent often forgets or misstates. | |
| Each Pitfall description must begin with “Pitfall Criteria: Does not mention . . . ” or “Pitfall Criteria: Recommends . . . ” | |
| and use weight –1 or –2. | |
| To ensure self-contained guidance: | |
| • When referring to answer choices, explicitly say “Identifies (A)”, “Identifies (B)”, etc., rather than vague phrasing. | |
| • If the format requires an action like calling a tool or launching a training run, include a rubric item such as: | |
| – Essential Criteria: Includes a clear statement "Launches the training with hf-jobs.". | |
| • If reasoning should precede the answer, include a rubric like: | |
| – Important Criteria: Presents the explanation and reasoning before stating the final answer. | |
| • If brevity is valued, include a rubric like: | |
| – Optional Criteria: Remains concise and avoids unnecessary detail. | |
| • If the question context demands mention of specific findings/best practices, include that explicitly (e.g., “Essential Criteria: Mentions | |
| that training data must be in "messages" column for LLM training”). | |
| Output: Provide a JSON array of rubric objects. Each object must contain exactly three keys—title, description, and weight. | |
| Do not copy large blocks of the question or reference_answer into the text. Each description must begin with its category | |
| prefix, and no extra keys are allowed. | |
| Now, given the question, thread and reference_answer, generate the rubric as described. The reference answer is an good and helpful response | |
| but not necessarily exhaustive; use it only as guidance.""" | |
| def build_prompt( | |
| question: str, reference_answer: str, thread: List[Dict[str, str]] | |
| ) -> List[Dict[str, str]]: | |
| """ | |
| Build the messages list for LiteLLM completion. | |
| Args: | |
| question: The question/task to evaluate | |
| reference_answer: The reference/accepted solution | |
| Returns: | |
| List of message dicts for LiteLLM | |
| """ | |
| prompt = PROMPT_TEMPLATE.format( | |
| question=question, reference_answer=reference_answer, thread=thread | |
| ) | |
| return [{"role": "user", "content": prompt}] | |
| def validate_rubric(rubric_list: List[Dict[str, Any]]) -> bool: | |
| """ | |
| Validate that rubric meets basic requirements. | |
| Args: | |
| rubric_list: List of rubric items to validate | |
| Returns: | |
| True if valid, False otherwise | |
| """ | |
| # Check count | |
| if not (7 <= len(rubric_list) <= 20): | |
| return False | |
| # Check each item | |
| category_prefixes = [ | |
| "Essential Criteria:", | |
| "Important Criteria:", | |
| "Optional Criteria:", | |
| "Pitfall Criteria:", | |
| ] | |
| for item in rubric_list: | |
| # Check keys | |
| if set(item.keys()) != {"title", "description", "weight"}: | |
| return False | |
| # Check description starts with category prefix | |
| if not any( | |
| item["description"].startswith(prefix) for prefix in category_prefixes | |
| ): | |
| return False | |
| return True | |
| def generate_rubric(row: pd.Series, model: str, timeout: int = 120) -> Dict[str, Any]: | |
| """ | |
| Generate rubric for a single question using LiteLLM. | |
| Args: | |
| question: The question text | |
| reference_answer: The reference solution | |
| model: Model name for LiteLLM | |
| timeout: Request timeout in seconds | |
| Returns: | |
| Dict with rubric_list and rubric_count, or None on failure | |
| """ | |
| messages = build_prompt(row["question"], row["solution"], row["thread"]) | |
| try: | |
| response = litellm.completion( | |
| model=model, | |
| messages=messages, | |
| timeout=timeout, | |
| response_format=RubricList, | |
| ) | |
| # Parse structured output | |
| rubric_list: RubricList = RubricList.model_validate_json( | |
| response.choices[0].message.content | |
| ) | |
| return rubric_list.model_dump_json() | |
| except Exception as e: | |
| print(f"Error generating rubric: {e}", file=sys.stderr) | |
| return None | |
| def load_input_data(infile: str) -> pd.DataFrame: | |
| """ | |
| Load input data from CSV or JSONL file. | |
| Args: | |
| infile: Path to input file | |
| Returns: | |
| DataFrame with loaded data | |
| """ | |
| path = Path(infile) | |
| if not path.exists(): | |
| raise FileNotFoundError(f"Input file not found: {infile}") | |
| if path.suffix == ".csv": | |
| # Try to auto-detect delimiter (comma or semicolon) | |
| df = pd.read_csv(infile, sep=None, engine="python") | |
| elif path.suffix == ".jsonl": | |
| df = pd.read_json(infile, lines=True) | |
| else: | |
| raise ValueError(f"Unsupported file format: {path.suffix}. Use .csv or .jsonl") | |
| # Validate required columns | |
| required_cols = [ | |
| "discussion_title", | |
| "discussion_url", | |
| "question", | |
| "thread", | |
| "solution", | |
| ] | |
| missing_cols = [col for col in required_cols if col not in df.columns] | |
| if missing_cols: | |
| raise ValueError(f"Missing required columns: {missing_cols}") | |
| return df | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Generate rubrics for HF-agent benchmark evaluation" | |
| ) | |
| parser.add_argument( | |
| "--infile", type=str, required=True, help="Input file path (.csv or .jsonl)" | |
| ) | |
| parser.add_argument( | |
| "--outfile", type=str, required=True, help="Output JSONL file path" | |
| ) | |
| parser.add_argument( | |
| "--model", | |
| type=str, | |
| default="anthropic/claude-sonnet-4-5-20250929", | |
| help="LiteLLM model name (default: from LITELLM_MODEL env or gpt-4o-mini)", | |
| ) | |
| parser.add_argument( | |
| "--timeout", | |
| type=int, | |
| default=120, | |
| help="Request timeout in seconds (default: 120)", | |
| ) | |
| parser.add_argument( | |
| "--max-concurrent", | |
| type=int, | |
| default=30, | |
| help="Maximum number of concurrent workers (default: 30)", | |
| ) | |
| parser.add_argument( | |
| "--push-to-hub", | |
| type=str, | |
| default=None, | |
| help="Push to HuggingFace dataset (e.g., username/dataset@rubrics)", | |
| ) | |
| args = parser.parse_args() | |
| # Determine model | |
| model = args.model or os.getenv("LITELLM_MODEL", "gpt-4o-mini") | |
| print(f"Using model: {model}") | |
| # Load input data | |
| print(f"Loading data from {args.infile}...") | |
| df = load_input_data(args.infile) | |
| print(f"Loaded {len(df)} examples") | |
| # Run rubric generation in parallel using ThreadPoolExecutor | |
| print(f"Running generation with {args.max_concurrent} parallel workers...") | |
| with ThreadPoolExecutor(max_workers=args.max_concurrent) as executor: | |
| # Submit all tasks | |
| future_to_idx = {} | |
| for idx, row in df.iterrows(): | |
| future = executor.submit( | |
| generate_rubric, | |
| row=row, | |
| model=model, | |
| timeout=args.timeout, | |
| ) | |
| future_to_idx[future] = idx | |
| # Collect results in order | |
| results = [None] * len(df) | |
| completed = 0 | |
| for future in as_completed(future_to_idx): | |
| idx = future_to_idx[future] | |
| results[idx] = future.result() | |
| completed += 1 | |
| print(f"Completed: {completed}/{len(df)}", end="\r") | |
| print() # New line after progress | |
| # Prepare results DataFrame | |
| print("Preparing results...") | |
| output_rows = [] | |
| success_count = 0 | |
| failure_count = 0 | |
| for idx, (_, row) in enumerate(df.iterrows()): | |
| rubric_result = results[idx] | |
| if rubric_result is None: | |
| failure_count += 1 | |
| continue | |
| # Merge with original data | |
| output_row = row.to_dict() | |
| output_row["rubric"] = rubric_result | |
| output_rows.append(output_row) | |
| success_count += 1 | |
| # Create DataFrame with results | |
| results_df = pd.DataFrame(output_rows) | |
| # Upload to HuggingFace if specified (before saving JSONL) | |
| if args.push_to_hub: | |
| print(f"\nUploading to HuggingFace: {args.push_to_hub}") | |
| upload_success = df_to_hub( | |
| df=results_df, | |
| dataset_spec=args.push_to_hub, | |
| split="train", | |
| private=False, | |
| ) | |
| if not upload_success: | |
| print("Warning: HuggingFace push failed, but continuing to save JSONL...") | |
| # Write results to JSONL file | |
| print(f"\nWriting results to {args.outfile}...") | |
| with open(args.outfile, "w") as outf: | |
| for output_row in output_rows: | |
| outf.write(json.dumps(output_row, default=str) + "\n") | |
| print("\nComplete!") | |
| print(f"Success: {success_count}/{len(df)}") | |
| print(f"Failures: {failure_count}/{len(df)}") | |
| print(f"Output written to: {args.outfile}") | |
| if args.push_to_hub and upload_success: | |
| print(f"Pushed to: {args.push_to_hub}") | |
| if __name__ == "__main__": | |
| main() | |