|
| 1 | +""" |
| 2 | +name: |
| 3 | +MT-Bench (inspect_ai) |
| 4 | +
|
| 5 | +dataset: |
| 6 | +lighteval/mt-bench |
| 7 | +
|
| 8 | +abstract: |
| 9 | +inspect_ai-compatible version of MT-Bench. Uses the scorer model server |
| 10 | +(SCORER_MODEL_BASE_URL / SCORER_MODEL_PATH env vars) as the judge. |
| 11 | +Supports 2-turn conversation; scores each turn independently. |
| 12 | +
|
| 13 | +languages: |
| 14 | +english |
| 15 | +
|
| 16 | +tags: |
| 17 | +conversational, generation, multi-turn |
| 18 | +""" |
| 19 | + |
| 20 | +import os |
| 21 | +import re |
| 22 | + |
| 23 | +from inspect_ai.dataset import Sample |
| 24 | +from inspect_ai.model import ChatMessageUser, GenerateConfig, get_model |
| 25 | +from inspect_ai.scorer import Score, mean, scorer, stderr |
| 26 | +from inspect_ai.solver import Generate, TaskState, generate, solver |
| 27 | + |
| 28 | +from lighteval.tasks.lighteval_task import LightevalTaskConfig |
| 29 | +from lighteval.tasks.tasks.mt_bench.judge_prompt_templates import ( |
| 30 | + flow_judge_prompt_mt_bench_with_ref, |
| 31 | + flow_judge_prompt_mt_bench_without_ref, |
| 32 | +) |
| 33 | + |
| 34 | + |
| 35 | +def _scorer_endpoint_config(task_name: str = "mt_bench_inspect"): |
| 36 | + base_url = os.environ.get("SCORER_MODEL_BASE_URL") |
| 37 | + if not base_url: |
| 38 | + raise RuntimeError(f"SCORER_MODEL_BASE_URL must be set for {task_name}.") |
| 39 | + |
| 40 | + model_name = os.environ.get("SCORER_MODEL_PATH") |
| 41 | + if not model_name: |
| 42 | + raise RuntimeError(f"SCORER_MODEL_PATH must be set for {task_name}.") |
| 43 | + |
| 44 | + return base_url, model_name, os.environ.get("VLLM_API_KEY", "inspectai") |
| 45 | + |
| 46 | + |
| 47 | +def _get_scorer_model(task_name: str = "mt_bench_inspect"): |
| 48 | + base_url, model_name, api_key = _scorer_endpoint_config(task_name) |
| 49 | + return get_model( |
| 50 | + f"openai-api/scorer/{model_name}", |
| 51 | + config=GenerateConfig( |
| 52 | + extra_body={"chat_template_kwargs": {"enable_thinking": False}}, |
| 53 | + ), |
| 54 | + base_url=base_url, |
| 55 | + api_key=api_key, |
| 56 | + ) |
| 57 | + |
| 58 | + |
| 59 | +def _parse_judge_score(text: str) -> int: |
| 60 | + match = re.search(r"<score>\s*(\d)\s*</score>", text) |
| 61 | + return int(match.group(1)) if match else 0 |
| 62 | + |
| 63 | + |
| 64 | +def record_to_sample(record): |
| 65 | + return Sample( |
| 66 | + input=record["turns"][0], |
| 67 | + metadata={ |
| 68 | + "turns": record["turns"], |
| 69 | + "reference": record.get("reference", []), |
| 70 | + "category": record.get("category", ""), |
| 71 | + "question_id": record.get("question_id", ""), |
| 72 | + }, |
| 73 | + ) |
| 74 | + |
| 75 | + |
| 76 | +@solver |
| 77 | +def require_scorer_endpoint(task_name: str = "mt_bench_inspect"): |
| 78 | + async def solve(state: TaskState, generate: Generate) -> TaskState: |
| 79 | + _scorer_endpoint_config(task_name) |
| 80 | + return state |
| 81 | + |
| 82 | + return solve |
| 83 | + |
| 84 | + |
| 85 | +@solver |
| 86 | +def append_second_turn(): |
| 87 | + async def solve(state: TaskState, generate: Generate) -> TaskState: |
| 88 | + turns = state.metadata["turns"] |
| 89 | + if len(turns) > 1: |
| 90 | + state.messages.append(ChatMessageUser(content=turns[1])) |
| 91 | + return state |
| 92 | + |
| 93 | + return solve |
| 94 | + |
| 95 | + |
| 96 | +@scorer(metrics={"turn_1": [mean(), stderr()], "turn_2": [mean(), stderr()]}) |
| 97 | +def mt_bench_scorer(): |
| 98 | + judge = None |
| 99 | + |
| 100 | + async def score(state: TaskState, target) -> Score: |
| 101 | + nonlocal judge |
| 102 | + if judge is None: |
| 103 | + judge = _get_scorer_model() |
| 104 | + |
| 105 | + turns = state.metadata["turns"] |
| 106 | + references = state.metadata.get("reference", []) |
| 107 | + assistant_messages = [m for m in state.messages if m.role == "assistant"] |
| 108 | + |
| 109 | + scores = {} |
| 110 | + for i, question in enumerate(turns): |
| 111 | + if i >= len(assistant_messages): |
| 112 | + scores[f"turn_{i + 1}"] = 0 |
| 113 | + continue |
| 114 | + |
| 115 | + message = assistant_messages[i] |
| 116 | + raw = message.text if hasattr(message, "text") else str(message.content) |
| 117 | + answer = raw |
| 118 | + ref = references[i] if references and i < len(references) else None |
| 119 | + |
| 120 | + if ref: |
| 121 | + messages = flow_judge_prompt_mt_bench_with_ref(question, [], answer, ref) |
| 122 | + else: |
| 123 | + messages = flow_judge_prompt_mt_bench_without_ref(question, [], answer, None) |
| 124 | + |
| 125 | + judge_input = [ChatMessageUser(content=m["content"]) for m in messages if m["role"] == "user"] |
| 126 | + output = await judge.generate( |
| 127 | + input=judge_input, |
| 128 | + config=GenerateConfig(temperature=0, max_tokens=512), |
| 129 | + ) |
| 130 | + scores[f"turn_{i + 1}"] = _parse_judge_score(output.completion) |
| 131 | + |
| 132 | + if "turn_2" not in scores: |
| 133 | + scores["turn_2"] = 0 |
| 134 | + |
| 135 | + return Score( |
| 136 | + value=scores, |
| 137 | + explanation=f"category={state.metadata.get('category', '')} id={state.metadata.get('question_id', '')}", |
| 138 | + ) |
| 139 | + |
| 140 | + return score |
| 141 | + |
| 142 | + |
| 143 | +mt_bench_inspect = LightevalTaskConfig( |
| 144 | + name="mt_bench_inspect", |
| 145 | + prompt_function=None, |
| 146 | + hf_repo="lighteval/mt-bench", |
| 147 | + hf_subset="default", |
| 148 | + hf_avail_splits=["train"], |
| 149 | + evaluation_splits=["train"], |
| 150 | + few_shots_split="", |
| 151 | + few_shots_select="random", |
| 152 | + metrics=[], |
| 153 | + generation_size=1024, |
| 154 | + stop_sequence=[], |
| 155 | + sample_fields=record_to_sample, |
| 156 | + solver=[require_scorer_endpoint(), generate(cache=True), append_second_turn(), generate(cache=True)], |
| 157 | + scorer=mt_bench_scorer(), |
| 158 | +) |
| 159 | + |
| 160 | + |
| 161 | +TASKS_TABLE = [mt_bench_inspect] |
0 commit comments