sora 13274243a0 Bump vendored EvalScope and add K3-ready DPV4 configs.
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>
2026-09-02 07:30:48 +00:00

244 lines
8.6 KiB
Python

# Copyright (c) Alibaba, Inc. and its affiliates.
# flake8: noqa: E501
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Literal
from pydantic import BaseModel
from evalscope.api.benchmark import BenchmarkMeta, DefaultDataAdapter
from evalscope.api.dataset import Sample
from evalscope.api.judge import (
CaseVerdict,
JudgeCase,
JudgeContext,
JudgeDefinition,
JudgeRequest,
OutputContract,
ReducedVerdict,
)
from evalscope.api.messages import ChatMessageUser
from evalscope.api.metric import Score
from evalscope.api.registry import register_benchmark
from evalscope.constants import DEFAULT_EVALSCOPE_CACHE_DIR, ScoringPolicy, Tags
from evalscope.utils.download_utils import download_url
from evalscope.utils.logger import get_logger
logger = get_logger()
# The judge prompt ends with "Reply only with CORRECT or INCORRECT."
class Grade(BaseModel):
reasoning: str = ''
verdict: Literal['CORRECT', 'INCORRECT']
GRADE_CONTRACT = OutputContract(schema_model=Grade)
# Default judge prompt template
JUDGE_PROMPT = """Assess whether the following CANDIDATE ANSWER is CORRECT or INCORRECT. For the CANDIDATE ANSWER to be correct, it must be consistent with the OFFICIAL ANSWER.
The question, for reference only: {question}
The OFFICIAL ANSWER: {correct_answer}
CANDIDATE ANSWER TO ASSESS: {response}
Reply only with CORRECT or INCORRECT."""
PROMPT_TEMPLATE = """
BEGIN INPUT DOCUMENTS
{documents_text}
END INPUT DOCUMENTS
Answer the following question using the input documents provided above.
START QUESTION
{question}
END QUESTION
"""
# New constants for auto-download
DOWNLOAD_URL: str = (
'https://modelscope.cn/datasets/evalscope/AA-LCR/resolve/master/extracted_text/AA-LCR_extracted-text.zip'
)
DEFAULT_CACHE_SUBDIR: str = 'aa_lcr'
DEFAULT_ZIP_NAME: str = 'AA-LCR_extracted-text.zip'
DEFAULT_EXTRACTED_DIR_NAME: str = 'lcr'
@register_benchmark(
BenchmarkMeta(
name='aa_lcr',
pretty_name='AA-LCR',
tags=[Tags.KNOWLEDGE, Tags.REASONING, Tags.LONG_CONTEXT],
description="""
## Overview
AA-LCR (Artificial Analysis Long Context Retrieval) is a benchmark for evaluating long-context retrieval and reasoning capabilities of language models. It requires models to find and synthesize information across multiple documents.
## Task Description
- **Task Type**: Long-Context Question Answering
- **Input**: Multiple documents + question requiring cross-document reasoning
- **Output**: Answer synthesized from document information
- **Context**: Very long context (multiple documents concatenated)
## Key Features
- Tests long-context retrieval abilities
- Multiple document understanding
- Cross-document reasoning required
- LLM-based judging for answer correctness
- Auto-download of document corpus
## Evaluation Notes
- Default configuration uses **0-shot** evaluation
- Primary metric: **Accuracy** (via LLM judge)
- Evaluates on **test** split
- Documents auto-downloaded if `text_dir` not specified
- Judge prompt compares candidate answer against reference
""", # noqa: E501
dataset_id='evalscope/AA-LCR',
metric_list=['acc'],
few_shot_num=0,
train_split=None,
eval_split='test',
prompt_template=PROMPT_TEMPLATE,
extra_params={
'text_dir': {
'type': 'str | null',
'description': 'Local directory containing extracted AA-LCR text files; if null will auto-download & extract.',
'value': None,
}
},
)
)
class AALCRAdapter(DefaultDataAdapter):
scoring_policy = ScoringPolicy.JUDGE_ONLY
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Get extra parameters
self.text_dir = self.extra_params.get('text_dir')
def load(self):
# Auto download and extract when text_dir is not provided
if not self.text_dir:
self.text_dir = self._ensure_text_dir_downloaded()
elif not Path(self.text_dir).exists():
raise ValueError(
'AA-LCR text_dir does not exist: '
f'{self.text_dir}. Please provide a valid directory or omit text_dir to auto-download.'
)
self.text_dir = Path(self.text_dir)
return super().load()
def _ensure_text_dir_downloaded(self) -> Path:
"""Ensure AA-LCR extracted texts are available locally; download and extract if missing."""
cache_root = Path(DEFAULT_EVALSCOPE_CACHE_DIR) / DEFAULT_CACHE_SUBDIR
extracted_dir = cache_root / DEFAULT_EXTRACTED_DIR_NAME
if extracted_dir.exists():
logger.info(f'AA-LCR documents found: {extracted_dir}')
return extracted_dir
cache_root.mkdir(parents=True, exist_ok=True)
zip_path = cache_root / DEFAULT_ZIP_NAME
try:
logger.info(f'Downloading AA-LCR documents from {DOWNLOAD_URL} to {zip_path}...')
download_url(DOWNLOAD_URL, str(zip_path))
logger.info(f'Extracting {zip_path} to {cache_root}...')
with zipfile.ZipFile(zip_path, 'r') as zf:
zf.extractall(cache_root)
if not extracted_dir.exists():
raise ValueError(f'Extraction succeeded but target directory not found: {extracted_dir}')
logger.info(f'AA-LCR documents ready at {extracted_dir}')
return extracted_dir
except Exception as e:
raise ValueError(
f'Failed to download or extract AA-LCR documents: {e}. '
'You can also manually download and set extra_params["text_dir"].'
) from e
finally:
# Best-effort cleanup of the zip file
try:
if zip_path.exists():
zip_path.unlink()
except Exception:
pass
def _get_context(self, record: Dict[str, Any]) -> str:
doc_folder = self.text_dir / record['document_category'] / record['document_set_id']
# Check if the document folder exists
if not doc_folder.exists() or not doc_folder.is_dir():
logger.warning(f'Document folder not found: {doc_folder}. Returning empty context.')
return ''
doc_blocks = []
try:
for file_path in doc_folder.iterdir():
if file_path.is_file():
try:
content = file_path.read_text(encoding='utf-8').strip()
if content:
doc_blocks.append(content)
except (IOError, UnicodeDecodeError) as e:
logger.warning(f'Could not read file {file_path}, skipping: {e}')
except OSError as e:
logger.warning(f'Could not access document folder {doc_folder}: {e}')
return f'ERROR: Could not read documents for {record["document_category"]}/{record["document_set_id"]}'
documents_text = '\n\n'.join(
f'BEGIN DOCUMENT {i + 1}:\n{doc}\nEND DOCUMENT {i + 1}' for i, doc in enumerate(doc_blocks)
)
return documents_text
def record_to_sample(self, record: Dict[str, Any]) -> Sample:
"""Convert a record to a Sample with long-context prompt."""
context = self._get_context(record)
prompt = self.prompt_template.format(documents_text=context, question=record['question'])
return Sample(
input=[ChatMessageUser(content=prompt)],
target=record['answer'],
metadata={
'question': record['question'],
'data_source_urls': record['data_source_urls'],
'input_tokens': record.get('input_tokens', 0),
},
)
def judge_definition(self, context: JudgeContext) -> JudgeDefinition:
def request(case, placement, completed_cases, judge_context) -> JudgeRequest:
prompt = (
JUDGE_PROMPT.format(
question=judge_context.task_state.metadata['question'],
correct_answer=judge_context.reference,
response=judge_context.filtered_prediction,
)
+ case.output_contract.instruction()
)
return JudgeRequest(messages=[ChatMessageUser(content=prompt)])
def reduce(case_verdicts, judge_context) -> ReducedVerdict:
return ReducedVerdict(value={'acc': 1.0 if case_verdicts[0].value.verdict == 'CORRECT' else 0.0})
return JudgeDefinition.workflow(
cases=[JudgeCase(case_id='grade', output_contract=GRADE_CONTRACT)],
request=request,
reduce=reduce,
main_score_name='acc',
)