Keep K3 suite selection and report-schema scoring in bash, merge K3/vision dataset_args into dpv4 yamls, and pin EvalScope at 735d920ee911 with local patches. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
7.7 KiB
Python
215 lines
7.7 KiB
Python
import json
|
|
from typing import Any, List
|
|
|
|
import pytest
|
|
|
|
from evalscope.api.evaluator import TaskState
|
|
from evalscope.api.messages import ChatMessageSystem
|
|
from evalscope.api.metric import AggScore
|
|
from evalscope.api.model import ModelOutput
|
|
from evalscope.api.registry import get_benchmark
|
|
from evalscope.benchmarks.one_million_bench.one_million_bench_adapter import OneMillionBenchAdapter
|
|
from evalscope.config import TaskConfig
|
|
from evalscope.constants import JudgeScoreType, ScoreStatus
|
|
from evalscope.metrics.judge.llm_judge import DEFAULT_PROMPT_TEMPLATE, LLMJudge
|
|
from evalscope.report import gen_table
|
|
from evalscope.report.generator import ReportGenerator
|
|
|
|
|
|
class ScriptedJudge:
|
|
score_type = JudgeScoreType.PATTERN
|
|
score_mapping = {'A': 1.0, 'B': 0.0}
|
|
prompt_template = DEFAULT_PROMPT_TEMPLATE
|
|
system_prompt = None
|
|
build_prompt = LLMJudge.build_prompt
|
|
|
|
def __init__(self, replies: List[str]) -> None:
|
|
self.replies = replies
|
|
self.judge_id = self.model_id = 'scripted'
|
|
self.calls: List[Any] = []
|
|
|
|
def generate(self, messages: Any) -> ModelOutput:
|
|
self.calls.append(messages)
|
|
return ModelOutput.from_content('scripted', self.replies[min(len(self.calls) - 1, len(self.replies) - 1)])
|
|
|
|
|
|
def make_adapter() -> OneMillionBenchAdapter:
|
|
config = TaskConfig(
|
|
model='mock-model',
|
|
datasets=['one_million_bench'],
|
|
judge={'strategy': 'llm', 'models': [{'model_id': 'judge-model'}]},
|
|
)
|
|
adapter = get_benchmark('one_million_bench', config)
|
|
assert isinstance(adapter, OneMillionBenchAdapter)
|
|
return adapter
|
|
|
|
|
|
def make_record() -> dict:
|
|
return {
|
|
'id': 'sample-id',
|
|
'case_id': 7,
|
|
'language': 'global',
|
|
'system_prompt': '',
|
|
'question': 'Prepare a professional report.',
|
|
'tags': {
|
|
'topics': ['Economics and Finance', 'Investment'],
|
|
'time_sensitivity': {'time_sensitivity': 'Time-agnostic', 'year_month': 'NA', 'day': 'NA'},
|
|
},
|
|
'rubrics': [
|
|
{
|
|
'rubric_number': 1,
|
|
'rubric_detail': 'Includes the required evidence.',
|
|
'rubric_weight': 5,
|
|
'rubric_tag': 'Factual Information',
|
|
},
|
|
{
|
|
'rubric_number': 2,
|
|
'rubric_detail': 'Provides a complete analysis.',
|
|
'rubric_weight': 3,
|
|
'rubric_tag': 'Analytical Reasoning',
|
|
},
|
|
{
|
|
'rubric_number': 3,
|
|
'rubric_detail': 'Contains unsupported claims.',
|
|
'rubric_weight': -2,
|
|
'rubric_tag': 'Factual Information',
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def make_state(adapter: OneMillionBenchAdapter, prediction: str = 'A detailed report.') -> TaskState:
|
|
sample = adapter.record_to_sample(make_record())
|
|
sample.id = 0
|
|
sample.group_id = 0
|
|
return TaskState(
|
|
model='mock-model', sample=sample, output=ModelOutput.from_content('mock-model', prediction), completed=True
|
|
)
|
|
|
|
|
|
def test_registration_and_sample_conversion() -> None:
|
|
adapter = make_adapter()
|
|
record = make_record()
|
|
record['system_prompt'] = 'Act as a financial analyst.'
|
|
|
|
sample = adapter.record_to_sample(record)
|
|
|
|
assert adapter.dataset_id == 'evalscope/OneMillion-Bench'
|
|
assert adapter.scoring_policy.value == 'judge_only'
|
|
assert sample.subset_key == 'global_economics_and_finance'
|
|
assert isinstance(sample.input[0], ChatMessageSystem)
|
|
assert sample.input[0].text == 'Act as a financial analyst.'
|
|
assert sample.input[1].text == record['question']
|
|
assert json.loads(sample.target) == record['rubrics']
|
|
assert sample.metadata['case_id'] == 7
|
|
|
|
|
|
def test_official_weighted_score_and_negative_penalty() -> None:
|
|
adapter = make_adapter()
|
|
judge = ScriptedJudge(
|
|
[
|
|
json.dumps(
|
|
{
|
|
'results': [
|
|
{'rubric_id': 1, 'status': '是', 'justification': 'Evidence is present.'},
|
|
{'rubric_id': 2, 'status': '否', 'justification': 'Analysis is incomplete.'},
|
|
{'rubric_id': 3, 'status': '是', 'justification': 'An unsupported claim appears.'},
|
|
]
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
]
|
|
)
|
|
adapter.llm_judge = judge
|
|
|
|
score = adapter.calculate_metrics(make_state(adapter)).score
|
|
|
|
assert score.status is ScoreStatus.SUCCESS
|
|
assert score.value['expert_score'] == pytest.approx(3 / 8)
|
|
assert score.value['pass_rate'] == 0.0
|
|
assert score.main_score_name == 'expert_score'
|
|
assert score.metadata['raw_score'] == 3
|
|
assert score.metadata['max_score'] == 8
|
|
assert 'rubricWeight: -2分' in judge.calls[0][0].text
|
|
|
|
|
|
def test_expert_score_is_clipped_and_pass_threshold_is_inclusive() -> None:
|
|
adapter = make_adapter()
|
|
positive = [
|
|
{'rubric_id': 1, 'status': '是', 'justification': 'hit'},
|
|
{'rubric_id': 2, 'status': '是', 'justification': 'hit'},
|
|
{'rubric_id': 3, 'status': '否', 'justification': 'not hit'},
|
|
]
|
|
adapter.llm_judge = ScriptedJudge([json.dumps({'results': positive}, ensure_ascii=False)])
|
|
|
|
passing_score = adapter.calculate_metrics(make_state(adapter)).score
|
|
|
|
assert passing_score.value == {'expert_score': 1.0, 'pass_rate': 1.0}
|
|
|
|
|
|
def test_missing_rubric_verdict_excludes_sample() -> None:
|
|
adapter = make_adapter()
|
|
adapter.llm_judge = ScriptedJudge(
|
|
[
|
|
json.dumps(
|
|
{
|
|
'results': [
|
|
{'rubric_id': 1, 'status': '是', 'justification': 'hit'},
|
|
{'rubric_id': 2, 'status': '否', 'justification': 'miss'},
|
|
]
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
]
|
|
)
|
|
|
|
score = adapter.calculate_metrics(make_state(adapter)).score
|
|
|
|
assert score.status is ScoreStatus.EXCLUDED
|
|
assert score.value == {}
|
|
assert score.metadata['judge_attempts'][0]['status'] == 'parse_error'
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('language', 'domain', 'expected_subset'),
|
|
[
|
|
('global', 'Economics & Finance', 'global_economics_and_finance'),
|
|
('cn', '医疗健康', 'cn_healthcare_and_medicine'),
|
|
('cn', '自然科学', 'cn_natural_sciences'),
|
|
],
|
|
)
|
|
def test_observed_domain_aliases_are_normalized(language: str, domain: str, expected_subset: str) -> None:
|
|
adapter = make_adapter()
|
|
record = make_record()
|
|
record['language'] = language
|
|
record['tags']['topics'][0] = domain
|
|
|
|
assert adapter.record_to_sample(record).subset_key == expected_subset
|
|
|
|
|
|
def test_report_uses_official_metric_labels_and_sample_weighted_means() -> None:
|
|
adapter = make_adapter()
|
|
score_dict = {
|
|
'global_law': [
|
|
AggScore(score=0.5, metric_name='expert_score', aggregation='mean', num=2),
|
|
AggScore(score=0.5, metric_name='pass_rate', aggregation='mean', num=2),
|
|
],
|
|
'cn_law': [
|
|
AggScore(score=1.0, metric_name='expert_score', aggregation='mean', num=1),
|
|
AggScore(score=1.0, metric_name='pass_rate', aggregation='mean', num=1),
|
|
],
|
|
}
|
|
|
|
report = ReportGenerator.generate_report(score_dict, 'mock-model', adapter)
|
|
metrics = {metric.identity.name: metric for metric in report.metrics}
|
|
table = gen_table(report_list=[report], add_overall_metric=True)
|
|
|
|
assert adapter.aggregation == 'mean'
|
|
assert report.primary_metric_identity == metrics['expert_score'].identity
|
|
assert metrics['expert_score'].score == pytest.approx(2 / 3, abs=1e-4)
|
|
assert metrics['pass_rate'].score == pytest.approx(2 / 3, abs=1e-4)
|
|
assert 'Expert Score ↑' in table
|
|
assert 'Pass Rate ↑' in table
|
|
assert table.count('66.7%') == 2
|
|
assert table.count('OVERALL') == 2
|