eduevidence 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- package/visualization/eduevidence-report/themes/presentation.css +140 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""benchmark_judge.py - LLM judge evaluator for Layer B empirical runs (v3).
|
|
3
|
+
|
|
4
|
+
Runs an LLM judge (omp CLI, default model deepseek-v4-flash) over the
|
|
5
|
+
responses of an empirical run and scores every attempt on a 5-dimension
|
|
6
|
+
0-3 rubric, producing judge-evaluation.json next to the heuristic
|
|
7
|
+
evaluation.json. The report command renders judge-report.md with the
|
|
8
|
+
heuristic per_baseline metrics side by side.
|
|
9
|
+
|
|
10
|
+
Rubric dimensions (0-3, one-line rationale):
|
|
11
|
+
citation_support 引用关键支持证据的可信度与具体性
|
|
12
|
+
outcome_correctness 结局指标识别正确性(任务表现 != 学习效果)
|
|
13
|
+
scope_calibration 结论边界限定是否恰当(不过度泛化)
|
|
14
|
+
contradiction_handling 反方/负面/矛盾证据的识别与处理
|
|
15
|
+
decision_calibration 最终决策动作与证据强度/期望决策范围的一致性
|
|
16
|
+
|
|
17
|
+
Independence caveat: the judge model is the same family as the execution
|
|
18
|
+
model (deepseek-v4-flash), so its review is NOT an independent third-party
|
|
19
|
+
judgement - it is reported as a semantic-complement view to the heuristic
|
|
20
|
+
(token-overlap) metrics, never as a standalone verdict.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 scripts/benchmark_judge.py run --run benchmarks/empirical/run-empirical-01 \
|
|
24
|
+
--out benchmarks/empirical/judge-evaluation.json
|
|
25
|
+
python3 scripts/benchmark_judge.py report --out benchmarks/empirical/judge-report.md
|
|
26
|
+
|
|
27
|
+
Budget control: --limit (default 60) caps how many completed attempts are
|
|
28
|
+
judged; attempts beyond the limit are recorded as "skipped". A failed model
|
|
29
|
+
call or unparseable judge output marks that attempt "failed" and continues.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
41
|
+
|
|
42
|
+
from benchmark import load_questions # noqa: E402
|
|
43
|
+
from benchmark_v3 import CliDriver, build_prompt # noqa: E402,F401 (parity import per spec; judge uses build_judge_prompt)
|
|
44
|
+
from benchmark_evaluator import extract_json_block # noqa: E402
|
|
45
|
+
|
|
46
|
+
JUDGE_DIMS = ("citation_support", "outcome_correctness", "scope_calibration",
|
|
47
|
+
"contradiction_handling", "decision_calibration")
|
|
48
|
+
DEFAULT_JUDGE_MODEL = "deepseek-v4-flash"
|
|
49
|
+
DEFAULT_LIMIT = 60
|
|
50
|
+
HEURISTIC_METRICS = ("outcome_separation_accuracy", "decision_calibration",
|
|
51
|
+
"contradiction_recall", "contradiction_precision",
|
|
52
|
+
"citation_support_recall", "scope_calibration")
|
|
53
|
+
|
|
54
|
+
DIM_DESCRIPTIONS = {
|
|
55
|
+
"citation_support": (
|
|
56
|
+
"是否引用关键支持证据;来源标注是否具体可信(作者/年份/研究设计/效应量),"
|
|
57
|
+
"是否把相关但非直接证据当作直接证据"),
|
|
58
|
+
"outcome_correctness": (
|
|
59
|
+
"是否正确识别题目对应的结局指标(如 accuracy / independent_problem_solving),"
|
|
60
|
+
"是否混淆任务表现与学习效果(最高优先级)"),
|
|
61
|
+
"scope_calibration": (
|
|
62
|
+
"是否明确限定结论边界(能主张什么/不能主张什么/是否超出证据范围),"
|
|
63
|
+
"是否过度泛化或把相关性说成因果"),
|
|
64
|
+
"contradiction_handling": (
|
|
65
|
+
"是否识别并恰当处理反方/负面/矛盾证据(null 结果、AI 依赖、迁移受损、新奇效应),"
|
|
66
|
+
"冲突是否被强行裁决"),
|
|
67
|
+
"decision_calibration": (
|
|
68
|
+
"最终决策动作(adopt/pilot/reject/insufficient_evidence)是否与证据强度"
|
|
69
|
+
"及期望决策范围一致"),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
INDEPENDENCE_NOTE = (
|
|
73
|
+
"judge 模型 deepseek-v4-flash 与执行模型同族(benchmark 运行环境 "
|
|
74
|
+
"model_family=deepseek-v4-flash)。评审表达风格与执行模型同源,可能存在同族偏好,"
|
|
75
|
+
"独立性受限:judge 结果不视为独立第三方评审,应与 heuristic 指标对照阅读。"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
_OUTPUT_FORMAT = (
|
|
79
|
+
"## 输出格式(严格 JSON,不要输出 JSON 以外的任何内容)\n"
|
|
80
|
+
'{"citation_support": <0-3的整数>, "outcome_correctness": <0-3的整数>, '
|
|
81
|
+
'"scope_calibration": <0-3的整数>, "contradiction_handling": <0-3的整数>, '
|
|
82
|
+
'"decision_calibration": <0-3的整数>, '
|
|
83
|
+
'"rationale": "一行中文理由,指出最关键的得/失分点"}\n'
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
RUBRIC_INTRO = (
|
|
87
|
+
"你是 EduEvidence 实证基准的 LLM 评审员。请按 5 个维度,对下面的\"模型回答\"进行 "
|
|
88
|
+
"0-3 评分(0=完全不符合/严重错误;1=部分符合但有明显缺陷;2=基本符合;3=完全符合),"
|
|
89
|
+
"并给出一行中文理由。评审以\"参考答案要点(gold)\"为准绳:不要求回答逐字复述 gold,"
|
|
90
|
+
"只判断实质符合程度;评分必须严格落在 0-3。\n"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------- prompt
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_judge_prompt(question: dict, gold: dict, response: str) -> str:
|
|
98
|
+
"""Rubric prompt for one attempt: 题目 + gold 要点 + 模型回答 + 5 维 0-3 评分要求."""
|
|
99
|
+
q_text = question.get("question", "")
|
|
100
|
+
|
|
101
|
+
def _fmt(label: str, key: str) -> str | None:
|
|
102
|
+
val = gold.get(key)
|
|
103
|
+
if val is None:
|
|
104
|
+
return None
|
|
105
|
+
if isinstance(val, list):
|
|
106
|
+
val = "; ".join(str(x) for x in val if str(x).strip())
|
|
107
|
+
val = str(val).strip()
|
|
108
|
+
return f"- {label}: {val}" if val else None
|
|
109
|
+
|
|
110
|
+
gold_lines = [ln for ln in (
|
|
111
|
+
_fmt("关键结论", "key_claims"),
|
|
112
|
+
_fmt("正确结局指标", "correct_outcome_types"),
|
|
113
|
+
_fmt("关键支持证据", "key_supporting_sources"),
|
|
114
|
+
_fmt("已知反方/矛盾证据", "known_contradictions"),
|
|
115
|
+
_fmt("允许的结论范围", "allowed_scope"),
|
|
116
|
+
_fmt("期望决策范围", "expected_decision_range"),
|
|
117
|
+
_fmt("已知方法学局限", "known_methodological_limitations"),
|
|
118
|
+
) if ln]
|
|
119
|
+
gold_block = "\n".join(gold_lines) if gold_lines else "(无 gold 要点)"
|
|
120
|
+
|
|
121
|
+
dim_lines = "\n".join(
|
|
122
|
+
f"{i}. {dim} (0-3): {DIM_DESCRIPTIONS[dim]}"
|
|
123
|
+
for i, dim in enumerate(JUDGE_DIMS, 1))
|
|
124
|
+
|
|
125
|
+
return "\n".join([
|
|
126
|
+
RUBRIC_INTRO,
|
|
127
|
+
"## 题目",
|
|
128
|
+
q_text,
|
|
129
|
+
"",
|
|
130
|
+
"## 参考答案要点(gold)",
|
|
131
|
+
gold_block,
|
|
132
|
+
"",
|
|
133
|
+
"## 模型回答",
|
|
134
|
+
response.strip() or "(空)",
|
|
135
|
+
"",
|
|
136
|
+
"## 评审维度",
|
|
137
|
+
dim_lines,
|
|
138
|
+
"",
|
|
139
|
+
_OUTPUT_FORMAT,
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------- parsing
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _coerce_score(value: Any) -> float | None:
|
|
147
|
+
"""Tolerant 0-3 score extraction: int/float, '2', '2/3', '2.5', '2.0/3'."""
|
|
148
|
+
if isinstance(value, bool):
|
|
149
|
+
return None
|
|
150
|
+
if isinstance(value, (int, float)):
|
|
151
|
+
v = float(value)
|
|
152
|
+
elif isinstance(value, str):
|
|
153
|
+
s = value.strip()
|
|
154
|
+
m = re.search(r"(-?\d+(?:\.\d+)?)\s*/\s*3", s, re.IGNORECASE)
|
|
155
|
+
if m:
|
|
156
|
+
v = float(m.group(1))
|
|
157
|
+
else:
|
|
158
|
+
m = re.search(r"(-?\d+(?:\.\d+)?)", s)
|
|
159
|
+
if not m:
|
|
160
|
+
return None
|
|
161
|
+
v = float(m.group(1))
|
|
162
|
+
else:
|
|
163
|
+
return None
|
|
164
|
+
if v != v: # NaN
|
|
165
|
+
return None
|
|
166
|
+
return round(min(3.0, max(0.0, v)), 2) # clamp out-of-range to 0-3
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_judge_output(text: str) -> dict[str, Any] | None:
|
|
170
|
+
"""Parse judge output into {'scores': {dim: float|None}, 'rationale': str|None}.
|
|
171
|
+
|
|
172
|
+
Tries a JSON block first (preferred contract), then falls back to
|
|
173
|
+
per-dimension line regexes. Missing dims stay None; if NO dim parses,
|
|
174
|
+
returns None (caller marks the attempt failed).
|
|
175
|
+
"""
|
|
176
|
+
text = (text or "").strip()
|
|
177
|
+
if not text:
|
|
178
|
+
return None
|
|
179
|
+
scores: dict[str, float | None] = {}
|
|
180
|
+
rationale: str | None = None
|
|
181
|
+
|
|
182
|
+
data = extract_json_block(text)
|
|
183
|
+
if isinstance(data, dict):
|
|
184
|
+
for dim in JUDGE_DIMS:
|
|
185
|
+
if dim in data:
|
|
186
|
+
v = _coerce_score(data[dim])
|
|
187
|
+
if v is not None:
|
|
188
|
+
scores[dim] = v
|
|
189
|
+
r = data.get("rationale")
|
|
190
|
+
if isinstance(r, str) and r.strip():
|
|
191
|
+
rationale = r.strip()
|
|
192
|
+
|
|
193
|
+
if len(scores) < len(JUDGE_DIMS):
|
|
194
|
+
for dim in JUDGE_DIMS:
|
|
195
|
+
if dim in scores:
|
|
196
|
+
continue
|
|
197
|
+
m = re.search(
|
|
198
|
+
rf"{re.escape(dim)}\s*[::=]\s*(\d+(?:\.\d+)?(?:\s*/\s*3)?)",
|
|
199
|
+
text, re.IGNORECASE)
|
|
200
|
+
if m:
|
|
201
|
+
v = _coerce_score(m.group(1))
|
|
202
|
+
if v is not None:
|
|
203
|
+
scores[dim] = v
|
|
204
|
+
if rationale is None:
|
|
205
|
+
m = re.search(r"(?:rationale|理由)\s*[::=]\s*(.+)$", text,
|
|
206
|
+
re.IGNORECASE | re.MULTILINE)
|
|
207
|
+
if m:
|
|
208
|
+
cand = m.group(1).strip().strip('"').strip("'")
|
|
209
|
+
if cand:
|
|
210
|
+
rationale = cand[:500]
|
|
211
|
+
|
|
212
|
+
if not scores:
|
|
213
|
+
return None
|
|
214
|
+
for dim in JUDGE_DIMS:
|
|
215
|
+
scores.setdefault(dim, None)
|
|
216
|
+
return {"scores": scores, "rationale": rationale}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------- run
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_golds(annotations_dir: Path) -> dict[str, dict[str, Any]]:
|
|
223
|
+
golds: dict[str, dict[str, Any]] = {}
|
|
224
|
+
for path in sorted(Path(annotations_dir).glob("gold-*.json")):
|
|
225
|
+
g = json.loads(path.read_text(encoding="utf-8"))
|
|
226
|
+
golds[g.get("id", path.stem.replace("gold-", ""))] = g
|
|
227
|
+
return golds
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _mean(values: list[float]) -> float:
|
|
231
|
+
return sum(values) / len(values) if values else 0.0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _per_baseline_means(per_attempt: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
235
|
+
by: dict[str, list[dict[str, Any]]] = {}
|
|
236
|
+
for row in per_attempt:
|
|
237
|
+
if row.get("status") == "completed" and row.get("judge"):
|
|
238
|
+
by.setdefault(row["baseline"], []).append(row)
|
|
239
|
+
out: dict[str, dict[str, Any]] = {}
|
|
240
|
+
for baseline, rows in by.items():
|
|
241
|
+
means: dict[str, dict[str, Any]] = {}
|
|
242
|
+
for dim in JUDGE_DIMS:
|
|
243
|
+
values = [r["judge"].get(dim) for r in rows]
|
|
244
|
+
values = [v for v in values if isinstance(v, (int, float))]
|
|
245
|
+
# n=0 -> mean None (report renders "-"), never a misleading 0.0 (P2-4)
|
|
246
|
+
entry: dict[str, Any] = {
|
|
247
|
+
"mean": round(_mean(values), 4) if values else None,
|
|
248
|
+
"n": len(values)}
|
|
249
|
+
if values:
|
|
250
|
+
entry["min"] = min(values)
|
|
251
|
+
entry["max"] = max(values)
|
|
252
|
+
means[dim] = entry
|
|
253
|
+
out[baseline] = {"n": len(rows), "judge": means}
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def run_judge(*, run_dir: Path, annotations_dir: Path, questions: list[dict],
|
|
258
|
+
out_path: Path, driver: Any, limit: int | None = DEFAULT_LIMIT) -> dict[str, Any]:
|
|
259
|
+
"""Judge completed attempts of an empirical run; write judge-evaluation.json.
|
|
260
|
+
|
|
261
|
+
Failed driver calls / unparseable outputs mark the attempt failed and
|
|
262
|
+
never interrupt the run (P2-1 parity with benchmark_v3).
|
|
263
|
+
"""
|
|
264
|
+
run_dir = Path(run_dir)
|
|
265
|
+
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
|
|
266
|
+
golds = load_golds(annotations_dir)
|
|
267
|
+
q_by_id = {q.get("id"): q for q in questions}
|
|
268
|
+
model = getattr(driver, "model", DEFAULT_JUDGE_MODEL)
|
|
269
|
+
|
|
270
|
+
per_attempt: list[dict[str, Any]] = []
|
|
271
|
+
judged = 0
|
|
272
|
+
for entry in manifest.get("attempts", []):
|
|
273
|
+
if entry.get("status") != "completed":
|
|
274
|
+
continue # failed/budget_stopped attempts have no response to judge
|
|
275
|
+
# tolerant field access: a malformed manifest row must degrade to
|
|
276
|
+
# failed, never KeyError the whole run (review P2-3)
|
|
277
|
+
aid = entry.get("attempt_id") or "unknown"
|
|
278
|
+
row: dict[str, Any] = {
|
|
279
|
+
"attempt_id": aid,
|
|
280
|
+
"question_id": entry.get("question_id"),
|
|
281
|
+
"baseline": entry.get("baseline"),
|
|
282
|
+
"attempt": entry.get("attempt"),
|
|
283
|
+
"status": "completed", "error": None, "judge": None, "usage": None,
|
|
284
|
+
}
|
|
285
|
+
if limit is not None and limit > 0 and judged >= limit:
|
|
286
|
+
row["status"] = "skipped"
|
|
287
|
+
per_attempt.append(row)
|
|
288
|
+
continue
|
|
289
|
+
judged += 1
|
|
290
|
+
try:
|
|
291
|
+
artifact_name = (entry.get("artifacts") or [None])[0]
|
|
292
|
+
if not artifact_name:
|
|
293
|
+
raise RuntimeError("no response artifact recorded")
|
|
294
|
+
artifact = json.loads((run_dir / artifact_name).read_text(encoding="utf-8"))
|
|
295
|
+
gold = golds.get(entry["question_id"])
|
|
296
|
+
if not gold:
|
|
297
|
+
raise RuntimeError(f"no gold annotation for {entry['question_id']}")
|
|
298
|
+
question = q_by_id.get(entry["question_id"])
|
|
299
|
+
if not question:
|
|
300
|
+
raise RuntimeError(f"question {entry['question_id']} missing from questions file")
|
|
301
|
+
response = (artifact.get("response") or "").strip()
|
|
302
|
+
if not response:
|
|
303
|
+
raise RuntimeError("empty response")
|
|
304
|
+
|
|
305
|
+
prompt = build_judge_prompt(question, gold, response)
|
|
306
|
+
text, usage = driver.call(prompt)
|
|
307
|
+
parsed = parse_judge_output(text)
|
|
308
|
+
if parsed is None:
|
|
309
|
+
raise RuntimeError("judge output unparseable: " + (text or "")[:200])
|
|
310
|
+
row["judge"] = {**parsed["scores"], "rationale": parsed["rationale"],
|
|
311
|
+
"method": "llm_judge", "raw": (text or "")[:800]}
|
|
312
|
+
row["usage"] = usage
|
|
313
|
+
except Exception as exc: # noqa: BLE001 - one bad attempt must not kill the run
|
|
314
|
+
row["status"] = "failed"
|
|
315
|
+
row["error"] = str(exc)[:300]
|
|
316
|
+
|
|
317
|
+
per_attempt.append(row)
|
|
318
|
+
|
|
319
|
+
n_completed = sum(1 for r in per_attempt if r["status"] == "completed")
|
|
320
|
+
n_failed = sum(1 for r in per_attempt if r["status"] == "failed")
|
|
321
|
+
n_skipped = sum(1 for r in per_attempt if r["status"] == "skipped")
|
|
322
|
+
summary = {
|
|
323
|
+
"run_id": manifest.get("run_id"),
|
|
324
|
+
"run_dir": str(run_dir),
|
|
325
|
+
"judge": {
|
|
326
|
+
"driver": getattr(driver, "name", "cli"),
|
|
327
|
+
"model": model,
|
|
328
|
+
"temperature": getattr(driver, "temperature", 0.0),
|
|
329
|
+
"scale": "0-3",
|
|
330
|
+
"dims": list(JUDGE_DIMS),
|
|
331
|
+
"independence_note": INDEPENDENCE_NOTE,
|
|
332
|
+
},
|
|
333
|
+
"heuristic_evaluation": str(run_dir / "evaluation.json"),
|
|
334
|
+
"limit": limit,
|
|
335
|
+
"summary": {"attempts_total": len(per_attempt),
|
|
336
|
+
"completed": n_completed, "failed": n_failed, "skipped": n_skipped},
|
|
337
|
+
"per_attempt": per_attempt,
|
|
338
|
+
"per_baseline": _per_baseline_means(per_attempt),
|
|
339
|
+
}
|
|
340
|
+
out_path = Path(out_path)
|
|
341
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
out_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
|
|
343
|
+
encoding="utf-8")
|
|
344
|
+
print(f"wrote {out_path} (judged={n_completed}, failed={n_failed}, "
|
|
345
|
+
f"skipped={n_skipped}, judge_model={model})")
|
|
346
|
+
return summary
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------- report
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def render_report(eval_data: dict[str, Any], heuristic_data: dict[str, Any],
|
|
353
|
+
out_path: Path) -> str:
|
|
354
|
+
"""Side-by-side judge (0-3) vs heuristic (0-1) report, with the
|
|
355
|
+
independence limitation statement."""
|
|
356
|
+
judge_meta = eval_data.get("judge", {})
|
|
357
|
+
model = judge_meta.get("model", DEFAULT_JUDGE_MODEL)
|
|
358
|
+
per_baseline = eval_data.get("per_baseline", {})
|
|
359
|
+
heur_pb = heuristic_data.get("per_baseline", {})
|
|
360
|
+
run_dir = eval_data.get("run_dir", "?")
|
|
361
|
+
summary = eval_data.get("summary", {})
|
|
362
|
+
lines = [
|
|
363
|
+
"# LLM Judge 评估报告",
|
|
364
|
+
"",
|
|
365
|
+
f"- run_id: {eval_data.get('run_id')}",
|
|
366
|
+
f"- judge 模型: **{model}**({judge_meta.get('driver', 'cli')} driver, "
|
|
367
|
+
f"scale 0-3,3=完全符合)",
|
|
368
|
+
f"- 对照来源: {eval_data.get('heuristic_evaluation', str(Path(run_dir) / 'evaluation.json'))} "
|
|
369
|
+
f"(method:heuristic,0-1)",
|
|
370
|
+
f"- attempts: judged={summary.get('completed')}, failed={summary.get('failed')}, "
|
|
371
|
+
f"skipped={summary.get('skipped')}, limit={eval_data.get('limit')}",
|
|
372
|
+
"",
|
|
373
|
+
f"> ⚠️ **独立性受限声明**: {INDEPENDENCE_NOTE}",
|
|
374
|
+
"",
|
|
375
|
+
"## 对照表(每 baseline:judge 均值(0-3) vs heuristic 均值(0-1),并排展示)",
|
|
376
|
+
"",
|
|
377
|
+
"| Baseline | n | J citation | H cit_recall | J outcome | H out_sep | "
|
|
378
|
+
"J scope | H scope | J contra | H contra_recall | J decision | H decision |",
|
|
379
|
+
"|---|---|---|---|---|---|---|---|---|---|---|---|",
|
|
380
|
+
]
|
|
381
|
+
for baseline in sorted(set(per_baseline) | set(heur_pb)):
|
|
382
|
+
j = per_baseline.get(baseline, {}).get("judge", {})
|
|
383
|
+
h = heur_pb.get(baseline, {}).get("metrics", {})
|
|
384
|
+
n = per_baseline.get(baseline, {}).get("n", heur_pb.get(baseline, {}).get("n", 0))
|
|
385
|
+
|
|
386
|
+
def jm(dim: str) -> str:
|
|
387
|
+
m = j.get(dim, {}).get("mean")
|
|
388
|
+
return f"{m:.3f}" if isinstance(m, (int, float)) else "-"
|
|
389
|
+
|
|
390
|
+
def hm(metric: str) -> str:
|
|
391
|
+
m = h.get(metric, {}).get("mean")
|
|
392
|
+
return f"{m:.3f}" if isinstance(m, (int, float)) else "-"
|
|
393
|
+
|
|
394
|
+
lines.append(
|
|
395
|
+
f"| {baseline} | {n} | {jm('citation_support')} | {hm('citation_support_recall')} "
|
|
396
|
+
f"| {jm('outcome_correctness')} | {hm('outcome_separation_accuracy')} "
|
|
397
|
+
f"| {jm('scope_calibration')} | {hm('scope_calibration')} "
|
|
398
|
+
f"| {jm('contradiction_handling')} | {hm('contradiction_recall')} "
|
|
399
|
+
f"| {jm('decision_calibration')} | {hm('decision_calibration')} |")
|
|
400
|
+
|
|
401
|
+
lines += [
|
|
402
|
+
"",
|
|
403
|
+
"## 维度对应说明",
|
|
404
|
+
"- judge citation_support ↔ heuristic citation_support_recall",
|
|
405
|
+
"- judge outcome_correctness ↔ heuristic outcome_separation_accuracy",
|
|
406
|
+
"- judge scope_calibration ↔ heuristic scope_calibration",
|
|
407
|
+
"- judge contradiction_handling ↔ heuristic contradiction_recall / contradiction_precision",
|
|
408
|
+
"- judge decision_calibration ↔ heuristic decision_calibration",
|
|
409
|
+
"- judge 为语义符合度(0-3),heuristic 为确定性 token 匹配(0-1),尺度不同不可直接相减。",
|
|
410
|
+
"",
|
|
411
|
+
"## 一致性摘要",
|
|
412
|
+
]
|
|
413
|
+
for baseline in sorted(per_baseline):
|
|
414
|
+
j = per_baseline[baseline].get("judge", {})
|
|
415
|
+
j_vals = [j[d]["mean"] for d in JUDGE_DIMS
|
|
416
|
+
if isinstance(j.get(d, {}).get("mean"), (int, float))]
|
|
417
|
+
h = heur_pb.get(baseline, {}).get("metrics", {})
|
|
418
|
+
h_vals = [h[k]["mean"] for k in HEURISTIC_METRICS
|
|
419
|
+
if isinstance(h.get(k, {}).get("mean"), (int, float))]
|
|
420
|
+
j_avg = _mean(j_vals) if j_vals else 0.0
|
|
421
|
+
h_avg = _mean(h_vals) if h_vals else 0.0
|
|
422
|
+
if j_vals:
|
|
423
|
+
best = max(JUDGE_DIMS, key=lambda d: j.get(d, {}).get("mean", -1))
|
|
424
|
+
worst = min(JUDGE_DIMS, key=lambda d: j.get(d, {}).get("mean", 4))
|
|
425
|
+
lines.append(
|
|
426
|
+
f"- **{baseline}**: judge 五维均值 **{j_avg:.3f}/3.0**,"
|
|
427
|
+
f"heuristic 六指标均值 {h_avg:.3f}/1.0;judge 最高分维度 "
|
|
428
|
+
f"{best},最低分维度 {worst}。")
|
|
429
|
+
else:
|
|
430
|
+
lines.append(f"- **{baseline}**: 无有效 judge 结果(attempt 全部失败)。")
|
|
431
|
+
lines.append(
|
|
432
|
+
"- 判读:judge 与 heuristic 是互补视角;若某维度 judge 高分而 heuristic 低分,"
|
|
433
|
+
"提示启发式漏检或 judge 同族偏好,建议人工抽查该维度样本。")
|
|
434
|
+
|
|
435
|
+
lines += ["", "## 逐 attempt"]
|
|
436
|
+
lines.append(
|
|
437
|
+
"| attempt_id | baseline | status | citation | outcome | scope | contra | "
|
|
438
|
+
"decision | rationale |")
|
|
439
|
+
lines.append("|---|---|---|---|---|---|---|---|---|")
|
|
440
|
+
for row in eval_data.get("per_attempt", []):
|
|
441
|
+
j = row.get("judge") or {}
|
|
442
|
+
rationale = (j.get("rationale") or "").replace("|", "\\|")
|
|
443
|
+
if len(rationale) > 80:
|
|
444
|
+
rationale = rationale[:80] + "…"
|
|
445
|
+
status = row.get("status", "?")
|
|
446
|
+
if status != "completed":
|
|
447
|
+
err = (row.get("error") or "")[:40].replace("|", "\\|")
|
|
448
|
+
lines.append(f"| {row['attempt_id']} | {row.get('baseline')} | {status} "
|
|
449
|
+
f"| - | - | - | - | - | {err} |")
|
|
450
|
+
continue
|
|
451
|
+
lines.append(
|
|
452
|
+
f"| {row['attempt_id']} | {row.get('baseline')} | completed "
|
|
453
|
+
f"| {j.get('citation_support', '-')} | {j.get('outcome_correctness', '-')} "
|
|
454
|
+
f"| {j.get('scope_calibration', '-')} | {j.get('contradiction_handling', '-')} "
|
|
455
|
+
f"| {j.get('decision_calibration', '-')} | {rationale} |")
|
|
456
|
+
|
|
457
|
+
markdown = "\n".join(lines) + "\n"
|
|
458
|
+
out_path = Path(out_path)
|
|
459
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
out_path.write_text(markdown, encoding="utf-8")
|
|
461
|
+
return markdown
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ---------------------------------------------------------------- cli
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
468
|
+
run_dir = Path(args.run)
|
|
469
|
+
if not (run_dir / "manifest.json").is_file():
|
|
470
|
+
print(f"no manifest.json in {run_dir}", file=sys.stderr)
|
|
471
|
+
return 2
|
|
472
|
+
driver = CliDriver(model=args.model, thinking=args.thinking)
|
|
473
|
+
if not driver.available():
|
|
474
|
+
print("cli driver unavailable: 'omp' not found on PATH", file=sys.stderr)
|
|
475
|
+
return 2
|
|
476
|
+
questions = load_questions(Path(args.questions))
|
|
477
|
+
out_path = Path(args.out) if args.out else run_dir / "judge-evaluation.json"
|
|
478
|
+
run_judge(run_dir=run_dir, annotations_dir=Path(args.annotations),
|
|
479
|
+
questions=questions, out_path=out_path, driver=driver,
|
|
480
|
+
limit=args.limit)
|
|
481
|
+
return 0
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _cmd_report(args: argparse.Namespace) -> int:
|
|
485
|
+
out_path = Path(args.out)
|
|
486
|
+
eval_path = (Path(args.evaluation) if args.evaluation
|
|
487
|
+
else out_path.parent / "judge-evaluation.json")
|
|
488
|
+
if not eval_path.is_file():
|
|
489
|
+
print(f"no judge evaluation at {eval_path} (run the run command first, or pass --evaluation)",
|
|
490
|
+
file=sys.stderr)
|
|
491
|
+
return 2
|
|
492
|
+
eval_data = json.loads(eval_path.read_text(encoding="utf-8"))
|
|
493
|
+
run_dir = Path(args.run) if args.run else Path(eval_data.get("run_dir", "."))
|
|
494
|
+
heur_path = run_dir / "evaluation.json"
|
|
495
|
+
if not heur_path.is_file():
|
|
496
|
+
print(f"no heuristic evaluation at {heur_path} (expected run_dir/evaluation.json)",
|
|
497
|
+
file=sys.stderr)
|
|
498
|
+
return 2
|
|
499
|
+
heuristic_data = json.loads(heur_path.read_text(encoding="utf-8"))
|
|
500
|
+
render_report(eval_data, heuristic_data, out_path)
|
|
501
|
+
print(f"wrote {out_path}")
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def main(argv: list[str] | None = None) -> int:
|
|
506
|
+
parser = argparse.ArgumentParser(
|
|
507
|
+
description="EduEvidence LLM judge evaluator (omp CLI, deepseek-v4-flash)")
|
|
508
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
509
|
+
|
|
510
|
+
p_run = sub.add_parser("run", help="judge the responses of an empirical run")
|
|
511
|
+
p_run.add_argument("--run", required=True, help="run directory with manifest.json")
|
|
512
|
+
p_run.add_argument("--out", default=None,
|
|
513
|
+
help="judge-evaluation.json path (default: <run>/judge-evaluation.json)")
|
|
514
|
+
p_run.add_argument("--questions", default="benchmarks/questions.jsonl")
|
|
515
|
+
p_run.add_argument("--annotations", default="benchmarks/annotations")
|
|
516
|
+
p_run.add_argument("--model", default=DEFAULT_JUDGE_MODEL)
|
|
517
|
+
p_run.add_argument("--thinking", default="minimal")
|
|
518
|
+
p_run.add_argument("--limit", type=int, default=DEFAULT_LIMIT,
|
|
519
|
+
help="max completed attempts to judge (default 60; <=0 = unlimited)")
|
|
520
|
+
p_run.set_defaults(func=_cmd_run)
|
|
521
|
+
|
|
522
|
+
p_report = sub.add_parser("report", help="render judge-report.md vs heuristic metrics")
|
|
523
|
+
p_report.add_argument("--out", required=True)
|
|
524
|
+
p_report.add_argument("--evaluation", default=None,
|
|
525
|
+
help="judge-evaluation.json (default: <out dir>/judge-evaluation.json)")
|
|
526
|
+
p_report.add_argument("--run", default=None,
|
|
527
|
+
help="override run dir for heuristic evaluation.json")
|
|
528
|
+
p_report.set_defaults(func=_cmd_report)
|
|
529
|
+
|
|
530
|
+
args = parser.parse_args(argv)
|
|
531
|
+
return args.func(args)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
if __name__ == "__main__":
|
|
535
|
+
sys.exit(main())
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""scripts/benchmark_routing.py — Benchmark Intent & Complexity Routing Evaluator.
|
|
3
|
+
|
|
4
|
+
Evaluates intent classification, PICO decomposition, and stage planning
|
|
5
|
+
accuracy against gold-standard benchmark inquiries.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Any
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
if str(ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(ROOT))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ROUTING_BENCHMARK_CASES = [
|
|
21
|
+
{
|
|
22
|
+
"query": "Is there evidence that spaced repetition flashcards improve vocabulary retention?",
|
|
23
|
+
"expected_depth": "S",
|
|
24
|
+
"expected_domain": "education",
|
|
25
|
+
"keywords": ["spaced repetition", "vocabulary", "retention"]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"query": "Should our high school adopt AI writing assistants in Grade 10 English essay classes?",
|
|
29
|
+
"expected_depth": "M",
|
|
30
|
+
"expected_domain": "education",
|
|
31
|
+
"keywords": ["AI writing assistant", "essay", "high school"]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"query": "We want to run a 12-week trial of AI pair programming in computer science undergraduate labs and evaluate with pre/post transfer tests and classroom CSV data.",
|
|
35
|
+
"expected_depth": "L",
|
|
36
|
+
"expected_domain": "education",
|
|
37
|
+
"keywords": ["AI pair programming", "12-week trial", "transfer test", "CSV"]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"query": "Evaluate the causal impact of reducing elementary school class sizes to 15 students on long-term graduation rates.",
|
|
41
|
+
"expected_depth": "M",
|
|
42
|
+
"expected_domain": "policy",
|
|
43
|
+
"keywords": ["class size", "graduation rates"]
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def classify_inquiry(query: str) -> Dict[str, Any]:
|
|
49
|
+
"""Deterministic heuristic/NLP rule classifier for inquiry routing."""
|
|
50
|
+
ql = query.lower()
|
|
51
|
+
|
|
52
|
+
# Domain detection
|
|
53
|
+
policy_signals = ("policy", "governance", "funding", "class size", "tuition", "graduation rate", "district", "legislation")
|
|
54
|
+
domain = "policy" if any(w in ql for w in policy_signals) else "education"
|
|
55
|
+
|
|
56
|
+
# Depth detection
|
|
57
|
+
if any(w in ql for w in ("12-week", "trial", "csv", "pre/post", "regression", "full cycle", "field experiment", "pilot data")):
|
|
58
|
+
depth = "L"
|
|
59
|
+
elif any(w in ql for w in ("adopt", "should we", "curriculum", "comprehensive", "review", "evaluate", "meta-analysis")):
|
|
60
|
+
depth = "M"
|
|
61
|
+
else:
|
|
62
|
+
depth = "S"
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"query": query,
|
|
66
|
+
"predicted_domain": domain,
|
|
67
|
+
"predicted_depth": depth,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_benchmark() -> Dict[str, Any]:
|
|
72
|
+
total = len(ROUTING_BENCHMARK_CASES)
|
|
73
|
+
depth_matches = 0
|
|
74
|
+
domain_matches = 0
|
|
75
|
+
details = []
|
|
76
|
+
|
|
77
|
+
for case in ROUTING_BENCHMARK_CASES:
|
|
78
|
+
res = classify_inquiry(case["query"])
|
|
79
|
+
depth_ok = res["predicted_depth"] == case["expected_depth"]
|
|
80
|
+
domain_ok = res["predicted_domain"] == case["expected_domain"]
|
|
81
|
+
if depth_ok:
|
|
82
|
+
depth_matches += 1
|
|
83
|
+
if domain_ok:
|
|
84
|
+
domain_matches += 1
|
|
85
|
+
details.append({
|
|
86
|
+
"query": case["query"],
|
|
87
|
+
"expected_depth": case["expected_depth"],
|
|
88
|
+
"predicted_depth": res["predicted_depth"],
|
|
89
|
+
"depth_ok": depth_ok,
|
|
90
|
+
"expected_domain": case["expected_domain"],
|
|
91
|
+
"predicted_domain": res["predicted_domain"],
|
|
92
|
+
"domain_ok": domain_ok,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"total_cases": total,
|
|
97
|
+
"depth_accuracy": round(depth_matches / total, 3),
|
|
98
|
+
"domain_accuracy": round(domain_matches / total, 3),
|
|
99
|
+
"details": details,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
print("[*] Running EduEvidence Routing Benchmark...")
|
|
105
|
+
res = run_benchmark()
|
|
106
|
+
print(f"[+] Total Cases: {res['total_cases']}")
|
|
107
|
+
print(f" • Depth Accuracy: {res['depth_accuracy'] * 100}%")
|
|
108
|
+
print(f" • Domain Accuracy: {res['domain_accuracy'] * 100}%")
|
|
109
|
+
for d in res["details"]:
|
|
110
|
+
status = "✓ PASS" if (d["depth_ok"] and d["domain_ok"]) else "✗ FAIL"
|
|
111
|
+
print(f" [{status}] Depth: {d['predicted_depth']} (Exp: {d['expected_depth']}), Domain: {d['predicted_domain']} — {d['query'][:60]}...")
|
|
112
|
+
if res["depth_accuracy"] == 1.0 and res["domain_accuracy"] == 1.0:
|
|
113
|
+
print("[+] Benchmark Passed with 100% Accuracy!")
|
|
114
|
+
sys.exit(0)
|
|
115
|
+
else:
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|