ELBench-live / src /elbench /execution /preflight.py
adelevett's picture
Deploy ELBench-live dashboard
5db30ac verified
Raw History Blame Contribute Delete
14.9 kB
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from elbench.loaders import LoaderFactory
from elbench.registry import FileRegistry
from elbench.schemas.config import ModelConfig, ProjectConfig
from elbench.utils.secrets import get_api_key
from .basic_education import DEFAULT_BASIC_MODULE_NAME, load_basic_education_config
from .selection import ModuleSelection, resolve_module_selection
LLM_JUDGE_TASKS = {
"safety_refusal",
"safety_guidance",
"safety_answer",
"adversarial_safety",
"highlevel_edu",
}
class PreflightError(RuntimeError):
pass
@dataclass(slots=True)
class PreflightIssue:
level: str
code: str
message: str
details: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class PreflightReport:
ok: bool
selection: ModuleSelection
total_samples: int
by_module: dict[str, int]
by_source_file: dict[str, int]
llm_judge_tasks: list[str]
issues: list[PreflightIssue]
def to_dict(self) -> dict[str, Any]:
return {
"ok": self.ok,
"selected_modules": sorted(self.selection.selected_modules),
"standard_modules": sorted(self.selection.standard_modules),
"include_basic_education": self.selection.include_basic_education,
"total_samples": self.total_samples,
"by_module": self.by_module,
"by_source_file": self.by_source_file,
"llm_judge_tasks": self.llm_judge_tasks,
"issues": [
{
"level": issue.level,
"code": issue.code,
"message": issue.message,
"details": issue.details,
}
for issue in self.issues
],
}
class PreflightRunner:
def __init__(self, config: ProjectConfig) -> None:
self._config = config
def run(
self,
*,
model_id: str,
modules: set[str] | None = None,
subsets: set[str] | None = None,
source_files: set[str] | None = None,
dimensions: set[str] | None = None,
max_samples: int | None = None,
judge_enabled: bool = True,
require_real_judges: bool = True,
) -> PreflightReport:
issues: list[PreflightIssue] = []
model_config = self._get_model_config(model_id)
selection = resolve_module_selection(self._config, modules)
if not judge_enabled:
issues.append(
PreflightIssue(
level="error",
code="judge_disabled",
message="judge_enabled=False 会导致结果无法产出完整评测分数。",
)
)
total_samples = 0
by_module: dict[str, int] = {}
by_source_file: dict[str, int] = {}
llm_judge_tasks: set[str] = set()
registry = FileRegistry(self._config)
if selection.standard_modules:
resolved_items = registry.resolve(
modules=set(selection.standard_modules),
subsets=subsets,
source_files=source_files,
)
loaded_total = 0
for item in resolved_items:
loader = LoaderFactory.create(item.entry.loader_name)
count = 0
for sample in loader.iter_samples(item):
if dimensions and sample.dimension not in dimensions:
continue
count += 1
loaded_total += 1
if max_samples is not None and loaded_total >= max_samples:
break
by_module[item.entry.module] = by_module.get(item.entry.module, 0) + count
by_source_file[item.entry.canonical_name] = count
total_samples += count
task = item.entry.task or ""
if task in LLM_JUDGE_TASKS:
llm_judge_tasks.add(task)
if max_samples is not None and loaded_total >= max_samples:
break
if selection.include_basic_education:
basic_config = load_basic_education_config(
config_root=self._config.config_root,
project_root=self._config.project_root,
data_root=self._config.app.data_root,
)
if not basic_config.enabled:
issues.append(
PreflightIssue(
level="error",
code="basic_education_disabled",
message="基本教育模块已被选择,但 configs/basic_education.yaml 未启用。",
)
)
scenario_count = 0
for scenario in basic_config.scenarios:
if not scenario.enabled:
continue
if subsets and scenario.subset not in subsets and scenario.scenario_id not in subsets:
continue
if source_files and scenario.source_file not in source_files:
continue
scenario_dimension = scenario.dimension or scenario.scenario_id
if dimensions and scenario_dimension not in dimensions:
continue
count = int(scenario.expected_tasks or 0)
scenario_count += count
total_samples += count
by_module[DEFAULT_BASIC_MODULE_NAME] = (
by_module.get(DEFAULT_BASIC_MODULE_NAME, 0) + count
)
by_source_file[scenario.source_file] = count
if scenario_count == 0:
issues.append(
PreflightIssue(
level="error",
code="basic_education_empty",
message="基本教育模块已被选择,但当前筛选条件下没有任何可执行场景。",
)
)
self._validate_basic_education_test_endpoint(
issues=issues,
require_real_judges=require_real_judges,
fallback_model_id=next(iter(llm_judge_tasks), None),
)
if total_samples <= 0:
issues.append(
PreflightIssue(
level="error",
code="no_samples",
message="当前筛选条件下没有解析出任何样本。",
)
)
if llm_judge_tasks:
self._validate_llm_judge_model(
issues=issues,
tasks=sorted(llm_judge_tasks),
require_real_judges=require_real_judges,
)
self._validate_target_model(
issues=issues,
model_config=model_config,
require_real_model=True,
)
ok = not any(issue.level == "error" for issue in issues)
return PreflightReport(
ok=ok,
selection=selection,
total_samples=total_samples,
by_module=by_module,
by_source_file=by_source_file,
llm_judge_tasks=sorted(llm_judge_tasks),
issues=issues,
)
def ensure_ok(self, **kwargs: Any) -> PreflightReport:
report = self.run(**kwargs)
if report.ok:
return report
messages = [issue.message for issue in report.issues if issue.level == "error"]
raise PreflightError(" | ".join(messages))
def _validate_target_model(
self,
*,
issues: list[PreflightIssue],
model_config: ModelConfig,
require_real_model: bool,
) -> None:
if require_real_model and model_config.provider_name == "mock":
issues.append(
PreflightIssue(
level="warning",
code="mock_target_model",
message=f"当前目标模型 `{model_config.model_id}` 是 mock,仅适合本地冒烟,不适合正式评测。",
)
)
self._validate_api_key(issues=issues, model_config=model_config, issue_code="target_model_key_missing")
def _validate_llm_judge_model(
self,
*,
issues: list[PreflightIssue],
tasks: list[str],
require_real_judges: bool,
) -> None:
judge_model_id = self._config.judges.default_judge_model_id
if not judge_model_id:
issues.append(
PreflightIssue(
level="error",
code="judge_model_missing",
message=f"以下任务依赖 LLM judge,但 default_judge_model_id 未配置:{', '.join(tasks)}",
details={"tasks": tasks},
)
)
return
if judge_model_id not in self._config.models:
issues.append(
PreflightIssue(
level="error",
code="judge_model_unknown",
message=f"default_judge_model_id={judge_model_id!r} 未在 models.yaml 中注册。",
details={"judge_model_id": judge_model_id, "tasks": tasks},
)
)
return
judge_model = self._config.models[judge_model_id]
if require_real_judges and judge_model.provider_name == "mock":
issues.append(
PreflightIssue(
level="error",
code="judge_model_mock",
message=f"以下任务依赖真实 LLM judge,但当前 judge 模型仍是 `{judge_model_id}`(mock)。",
details={"tasks": tasks, "judge_model_id": judge_model_id},
)
)
self._validate_api_key(issues=issues, model_config=judge_model, issue_code="judge_model_key_missing")
def _validate_basic_education_test_endpoint(
self,
*,
issues: list[PreflightIssue],
require_real_judges: bool,
fallback_model_id: str | None,
) -> None:
basic_config = load_basic_education_config(
config_root=self._config.config_root,
project_root=self._config.project_root,
data_root=self._config.app.data_root,
)
test_endpoint_model_id = (
self._config.judges.basic_education_test_endpoint_model_id
or basic_config.test_endpoint_model_id
or self._config.judges.basic_education_evaluator_model_id
or basic_config.evaluator_model_id
or self._config.judges.default_judge_model_id
)
if not test_endpoint_model_id:
issues.append(
PreflightIssue(
level="error",
code="basic_education_test_endpoint_missing",
message=(
"基本教育模块需要测试端点模型,但当前未配置 test_endpoint_model_id,"
"也没有 evaluator_model_id 或 default_judge_model_id 可回退。"
),
)
)
return
if test_endpoint_model_id not in self._config.models:
issues.append(
PreflightIssue(
level="error",
code="basic_education_test_endpoint_unknown",
message=f"基本教育测试端点模型 `{test_endpoint_model_id}` 未在 models.yaml 中注册。",
)
)
return
evaluator_model = self._config.models[test_endpoint_model_id]
if require_real_judges and evaluator_model.provider_name == "mock":
issues.append(
PreflightIssue(
level="error",
code="basic_education_test_endpoint_mock",
message=f"基本教育测试端点当前会落到 `{test_endpoint_model_id}`(mock),无法产出正式分数。",
details={
"test_endpoint_model_id": test_endpoint_model_id,
"fallback": fallback_model_id,
},
)
)
self._validate_api_key(
issues=issues,
model_config=evaluator_model,
issue_code="basic_education_test_endpoint_key_missing",
)
explicit_eval_id = self._config.judges.basic_education_evaluator_model_id
if explicit_eval_id and explicit_eval_id != test_endpoint_model_id:
if explicit_eval_id not in self._config.models:
issues.append(
PreflightIssue(
level="error",
code="basic_education_evaluator_unknown",
message=f"基本教育评估模型 `{explicit_eval_id}` 未在 models.yaml 中注册。",
)
)
return
eval_only_model = self._config.models[explicit_eval_id]
self._validate_api_key(
issues=issues,
model_config=eval_only_model,
issue_code="basic_education_evaluator_key_missing",
)
def _validate_api_key(
self,
*,
issues: list[PreflightIssue],
model_config: ModelConfig,
issue_code: str,
) -> None:
if model_config.provider_name == "mock":
return
if not model_config.api_base:
issues.append(
PreflightIssue(
level="error",
code=f"{issue_code}_api_base",
message=f"模型 `{model_config.model_id}` 缺少 api_base 配置。",
)
)
if not model_config.api_key_env:
issues.append(
PreflightIssue(
level="error",
code=f"{issue_code}_env",
message=f"模型 `{model_config.model_id}` 缺少 api_key_env 配置。",
)
)
return
if not get_api_key(model_config.api_key_env):
issues.append(
PreflightIssue(
level="error",
code=issue_code,
message=(
f"模型 `{model_config.model_id}` 需要的密钥 `{model_config.api_key_env}` 未读取到。"
),
details={"api_key_env": model_config.api_key_env},
)
)
def _get_model_config(self, model_id: str) -> ModelConfig:
if model_id not in self._config.models:
available = ", ".join(sorted(self._config.models))
raise PreflightError(f"Unknown model_id={model_id!r}. Available: {available}")
return self._config.models[model_id]