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.
Files changed (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +506 -0
  3. package/README.zh-CN.md +434 -0
  4. package/SKILL.md +195 -0
  5. package/bin/eduevidence.js +127 -0
  6. package/domains/education/manifest.json +183 -0
  7. package/domains/education/outcome_taxonomy.json +127 -0
  8. package/domains/manifest.json +26 -0
  9. package/domains/policy/frame.schema.json +234 -0
  10. package/domains/policy/manifest.json +10 -0
  11. package/domains/policy/methodology_checklist.json +109 -0
  12. package/domains/policy/outcome_taxonomy.json +53 -0
  13. package/domains/policy/references/causal-identification.md +45 -0
  14. package/domains/policy/references/cost-evidence.md +44 -0
  15. package/domains/policy/references/equity.md +42 -0
  16. package/domains/policy/references/evidence-hierarchy.md +41 -0
  17. package/domains/policy/references/implementation-evidence.md +47 -0
  18. package/eduevidence_cli.py +26 -0
  19. package/engine/__init__.py +11 -0
  20. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  22. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  23. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  24. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  25. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  26. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  27. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  28. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  29. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  30. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  31. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  32. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  33. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  34. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  35. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  36. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  37. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  38. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  39. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  40. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  41. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  42. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  43. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  44. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  45. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  46. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  47. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  48. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  49. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  50. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  51. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  52. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  53. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  54. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  55. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  56. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  57. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  58. package/engine/analysis.py +308 -0
  59. package/engine/bias.py +178 -0
  60. package/engine/briefs.py +106 -0
  61. package/engine/capabilities.py +99 -0
  62. package/engine/citation_check.py +192 -0
  63. package/engine/contracts.py +117 -0
  64. package/engine/datasets.py +165 -0
  65. package/engine/events.py +67 -0
  66. package/engine/evidence_graph.py +571 -0
  67. package/engine/evidence_review.py +88 -0
  68. package/engine/evidencecore.py +182 -0
  69. package/engine/gap_lens.py +132 -0
  70. package/engine/gaps.py +169 -0
  71. package/engine/graph_store.py +335 -0
  72. package/engine/graph_validate.py +87 -0
  73. package/engine/ids.py +77 -0
  74. package/engine/library.py +268 -0
  75. package/engine/library_builtin.py +301 -0
  76. package/engine/living.py +671 -0
  77. package/engine/log.py +39 -0
  78. package/engine/meta_analysis.py +333 -0
  79. package/engine/meta_synthesis.py +111 -0
  80. package/engine/migration.py +397 -0
  81. package/engine/mode_router.py +72 -0
  82. package/engine/paths.py +15 -0
  83. package/engine/pilot.py +368 -0
  84. package/engine/planner.py +126 -0
  85. package/engine/project.py +118 -0
  86. package/engine/projections.py +240 -0
  87. package/engine/robustness.py +109 -0
  88. package/engine/run.py +85 -0
  89. package/engine/semantics.py +135 -0
  90. package/engine/study_design.py +87 -0
  91. package/engine/synthesis.py +187 -0
  92. package/engine/tribunal.py +408 -0
  93. package/engine/update.py +113 -0
  94. package/engine/versions.py +12 -0
  95. package/install.sh +510 -0
  96. package/integrations/__init__.py +1 -0
  97. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  98. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  99. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  100. package/integrations/agent_mcp.py +856 -0
  101. package/integrations/smart_web_fetch.py +59 -0
  102. package/package.json +50 -0
  103. package/pyproject.toml +55 -0
  104. package/references/applicability-policy.md +88 -0
  105. package/references/education-framing.md +132 -0
  106. package/references/effect_size_formulas.md +35 -0
  107. package/references/evaluation-design.md +111 -0
  108. package/references/evidence-quality.md +79 -0
  109. package/references/grade_framework.md +29 -0
  110. package/references/intervention-design.md +98 -0
  111. package/references/methodology-audit.md +103 -0
  112. package/references/outcome-taxonomy.md +106 -0
  113. package/references/retrieval-protocol.md +142 -0
  114. package/references/skeptic-protocol.md +93 -0
  115. package/references/social_science_pitfalls.md +48 -0
  116. package/references/source-validity.md +140 -0
  117. package/references/tribunal-policy.md +112 -0
  118. package/references/wwc_standards.md +29 -0
  119. package/retrieval/__init__.py +1 -0
  120. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  121. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  122. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  123. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  124. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  125. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  126. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  127. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  128. package/retrieval/corpus_store.py +181 -0
  129. package/retrieval/dedupe.py +127 -0
  130. package/retrieval/failures.py +90 -0
  131. package/retrieval/fetch.py +435 -0
  132. package/retrieval/search.py +493 -0
  133. package/retrieval/source.py +160 -0
  134. package/retrieval/validate.py +257 -0
  135. package/schemas/agent-mcp-approval.schema.json +57 -0
  136. package/schemas/chart-spec.schema.json +88 -0
  137. package/schemas/cross-model-review.schema.json +28 -0
  138. package/schemas/education-frame.schema.json +106 -0
  139. package/schemas/evaluation.schema.json +35 -0
  140. package/schemas/evidence.schema.json +81 -0
  141. package/schemas/fetch-result.schema.json +119 -0
  142. package/schemas/intervention.schema.json +46 -0
  143. package/schemas/methodology.schema.json +102 -0
  144. package/schemas/report-result.schema.json +381 -0
  145. package/schemas/report-spec.schema.json +130 -0
  146. package/schemas/source.schema.json +311 -0
  147. package/schemas/v2/analysis-plan.schema.json +28 -0
  148. package/schemas/v2/analysis-run.schema.json +33 -0
  149. package/schemas/v2/claim.schema.json +26 -0
  150. package/schemas/v2/dataset-asset.schema.json +40 -0
  151. package/schemas/v2/decision-snapshot.schema.json +53 -0
  152. package/schemas/v2/evidence-link.schema.json +38 -0
  153. package/schemas/v2/finding.schema.json +47 -0
  154. package/schemas/v2/graph-revision.schema.json +30 -0
  155. package/schemas/v2/knowledge-gap.schema.json +40 -0
  156. package/schemas/v2/methodology-audit.schema.json +30 -0
  157. package/schemas/v2/outcome.schema.json +18 -0
  158. package/schemas/v2/project.schema.json +31 -0
  159. package/schemas/v2/research-intent.schema.json +24 -0
  160. package/schemas/v2/run.schema.json +43 -0
  161. package/schemas/v2/source.schema.json +24 -0
  162. package/schemas/v2/study-design.schema.json +67 -0
  163. package/schemas/v2/study.schema.json +37 -0
  164. package/schemas/v3/pilot-outcome.schema.json +132 -0
  165. package/schemas/v3/run-manifest.schema.json +193 -0
  166. package/schemas/v3/synthesis.schema.json +86 -0
  167. package/schemas/v4/drift-report.schema.json +66 -0
  168. package/schemas/v4/evidence-library.schema.json +42 -0
  169. package/schemas/v4/living-subscription.schema.json +55 -0
  170. package/schemas/v4/meta-analysis.schema.json +152 -0
  171. package/schemas/verdict.schema.json +56 -0
  172. package/scripts/__init__.py +0 -0
  173. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  175. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  176. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  177. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  178. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  179. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  180. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  181. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  182. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  183. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  184. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  185. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  186. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  187. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  188. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  189. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  190. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  191. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  192. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  193. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  194. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  195. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  196. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  197. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  198. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  199. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  200. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  201. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  202. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  203. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  204. package/scripts/audit_dois.py +292 -0
  205. package/scripts/bake_pack.sh +37 -0
  206. package/scripts/benchmark.py +183 -0
  207. package/scripts/benchmark_evaluator.py +371 -0
  208. package/scripts/benchmark_judge.py +535 -0
  209. package/scripts/benchmark_routing.py +120 -0
  210. package/scripts/benchmark_v2.py +304 -0
  211. package/scripts/benchmark_v3.py +552 -0
  212. package/scripts/build_esl_artifacts.py +1921 -0
  213. package/scripts/build_evidence_library.py +307 -0
  214. package/scripts/build_killer_demo.py +295 -0
  215. package/scripts/build_result.py +311 -0
  216. package/scripts/check_version_consistency.py +96 -0
  217. package/scripts/citation_check.py +123 -0
  218. package/scripts/claim_audit.py +157 -0
  219. package/scripts/complexity_gate.py +180 -0
  220. package/scripts/compute_confidence.py +176 -0
  221. package/scripts/dashboard_server.py +536 -0
  222. package/scripts/did_regression.py +315 -0
  223. package/scripts/effect_calculator.py +99 -0
  224. package/scripts/enrich_projects_human_and_lieflat.py +315 -0
  225. package/scripts/evidence_matrix.py +129 -0
  226. package/scripts/evidence_score.py +234 -0
  227. package/scripts/evidence_semantics.py +87 -0
  228. package/scripts/fetch_benchmark.py +177 -0
  229. package/scripts/generate_metrics.py +99 -0
  230. package/scripts/generate_new_projects.py +686 -0
  231. package/scripts/generate_promo.py +192 -0
  232. package/scripts/lint_report_layout.py +182 -0
  233. package/scripts/orchestrator.py +1456 -0
  234. package/scripts/pre_verdict_gate.py +513 -0
  235. package/scripts/quickstart.py +121 -0
  236. package/scripts/rebake_all_5themes.py +88 -0
  237. package/scripts/recompute_demo_quality.py +205 -0
  238. package/scripts/render_report.py +270 -0
  239. package/scripts/render_report_html.py +356 -0
  240. package/scripts/retraction_watch.py +110 -0
  241. package/scripts/run_workspace.py +337 -0
  242. package/scripts/serve_web.py +54 -0
  243. package/scripts/skill_lint.py +150 -0
  244. package/scripts/startup_probe.py +265 -0
  245. package/scripts/sync_killer_demo_report.py +270 -0
  246. package/scripts/test_adversarial_empirical.py +541 -0
  247. package/scripts/validate_schema.py +256 -0
  248. package/skill/agents/education-planner.md +80 -0
  249. package/skill/agents/evaluation-designer.md +74 -0
  250. package/skill/agents/evidence-analyst.md +106 -0
  251. package/skill/agents/evidence-judge.md +111 -0
  252. package/skill/agents/evidence-retriever.md +80 -0
  253. package/skill/agents/intervention-designer.md +82 -0
  254. package/skill/agents/method-reviewer.md +104 -0
  255. package/skill/agents/skeptic.md +89 -0
  256. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
  257. package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
  258. package/skill/sub-skills/data-analysis/SKILL.md +23 -0
  259. package/skill/sub-skills/ethics-review/SKILL.md +25 -0
  260. package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
  261. package/skill/sub-skills/evidence-review/SKILL.md +18 -0
  262. package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
  263. package/skill/sub-skills/literature-review/SKILL.md +21 -0
  264. package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
  265. package/skill/sub-skills/report-generation/SKILL.md +51 -0
  266. package/skill/sub-skills/research-planning/SKILL.md +21 -0
  267. package/skill/sub-skills/study-design/SKILL.md +16 -0
  268. package/skill/task-briefs/adjudicate.md +17 -0
  269. package/skill/task-briefs/audit.md +15 -0
  270. package/skill/task-briefs/challenge.md +15 -0
  271. package/skill/task-briefs/evaluate.md +13 -0
  272. package/skill/task-briefs/extract.md +16 -0
  273. package/skill/task-briefs/frame.md +17 -0
  274. package/skill/task-briefs/intervene.md +14 -0
  275. package/skill/task-briefs/present.md +16 -0
  276. package/skill/task-briefs/retrieve.md +15 -0
  277. package/visualization/eduevidence-report/assets/base.css +337 -0
  278. package/visualization/eduevidence-report/motion/motion.css +157 -0
  279. package/visualization/eduevidence-report/motion/motion.js +107 -0
  280. package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
  281. package/visualization/eduevidence-report/references/component-catalog.md +145 -0
  282. package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
  283. package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
  284. package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
  285. package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
  286. package/visualization/eduevidence-report/references/motion-system.md +31 -0
  287. package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
  288. package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
  289. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  290. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  291. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  292. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  293. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  294. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  295. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  296. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  297. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
  298. package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
  299. package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
  300. package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
  301. package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
  302. package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
  303. package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
  304. package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
  305. package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
  306. package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
  307. package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
  308. package/visualization/eduevidence-report/themes/academic.css +94 -0
  309. package/visualization/eduevidence-report/themes/claude.css +96 -0
  310. package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
  311. package/visualization/eduevidence-report/themes/datalab.css +151 -0
  312. package/visualization/eduevidence-report/themes/presentation.css +140 -0
@@ -0,0 +1,552 @@
1
+ #!/usr/bin/env python3
2
+ """benchmark_v3.py — Layer B Empirical Benchmark harness (v3).
3
+
4
+ Runs B0-B4 baselines over benchmarks/questions.jsonl with REAL model calls and
5
+ records a full run manifest (schemas/v3/run-manifest.schema.json). The
6
+ deterministic simulation (benchmark_v2) remains only as harness validation and
7
+ is always labelled SIMULATED — empirical numbers are the only performance
8
+ evidence (docs/benchmark.md Layer A vs Layer B).
9
+
10
+ Baselines:
11
+ B0_direct_llm 直接问模型,无检索无协议
12
+ B1_search_llm 一次搜索再回答(驱动不支持真实搜索时降级并如实记录)
13
+ B2_standard_agent 有检索无教育协议
14
+ B3_eduevidence_single 完整 EvidenceFlow 单 Agent(精简协议)
15
+ B4_eduevidence_agent_mcp 增强模式(API 驱动下如实标注 agent_mcp_used=false)
16
+
17
+ Drivers:
18
+ api OpenAI-compatible chat completions
19
+ env: EDUEVIDENCE_LLM_API_KEY / EDUEVIDENCE_LLM_BASE_URL / EDUEVIDENCE_LLM_MODEL
20
+ sim deterministic simulation (SIMULATED) — harness validation only
21
+
22
+ Usage:
23
+ python3 scripts/benchmark_v3.py run --baselines B2_standard_agent,B3_eduevidence_single \
24
+ --questions benchmarks/questions.jsonl --annotations benchmarks/annotations \
25
+ --repeats 3 --out benchmarks/empirical/run-20260813-120000
26
+ python3 scripts/benchmark_v3.py eval --run benchmarks/empirical/run-20260813-120000
27
+ python3 scripts/benchmark_v3.py report --run benchmarks/empirical/run-20260813-120000
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import os
34
+ import sys
35
+ import urllib.error
36
+ import urllib.request
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+ from typing import Any
40
+
41
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
42
+
43
+ from benchmark import load_questions, validate_questions # noqa: E402
44
+
45
+ BASELINES = (
46
+ "B0_direct_llm", "B1_search_llm", "B2_standard_agent",
47
+ "B3_eduevidence_single", "B4_eduevidence_agent_mcp",
48
+ )
49
+ DEFAULT_BUDGET_TOKENS = 1_000_000
50
+
51
+ # ---------------------------------------------------------------- prompts
52
+
53
+
54
+ def _prompt_b0(q: dict) -> str:
55
+ return (
56
+ "请直接回答以下教育决策问题,给出明确结论(采用/试点/拒绝/证据不足)和理由。\n\n"
57
+ f"问题:{q['question']}\n"
58
+ )
59
+
60
+
61
+ def _prompt_b1(q: dict) -> str:
62
+ return (
63
+ "请先使用可用检索工具搜索相关研究证据,再基于搜索结果回答以下教育决策问题,"
64
+ "给出明确结论(采用/试点/拒绝/证据不足)并引用来源。\n\n"
65
+ f"问题:{q['question']}\n"
66
+ )
67
+
68
+
69
+ def _prompt_b2(q: dict) -> str:
70
+ return (
71
+ "你是研究助理。请检索并分析相关文献,然后回答以下教育决策问题。"
72
+ "必须:列出支持与反对的证据并标注来源;评估证据质量;给出结论(采用/试点/拒绝/证据不足)。\n\n"
73
+ f"问题:{q['question']}\n"
74
+ )
75
+
76
+
77
+ _PROTOCOL_B3 = (
78
+ "你是 EduEvidence 教育证据决策引擎(单 Agent 完整协议)。对问题执行:\n"
79
+ "1) Frame:确定学习者/干预/对照/目标结果(结果必须使用枚举:knowledge_gain, concept_understanding, "
80
+ "retention, transfer, independent_problem_solving, completion_time, accuracy, code_quality, "
81
+ "assignment_score, engagement, motivation, cognitive_load, help_seeking, metacognition, ai_dependency, "
82
+ "over_reliance, reduced_effort, reduced_transfer, academic_integrity_risk, false_confidence);\n"
83
+ "2) 检索支持证据与独立反方证据(null/negative result、AI 依赖、迁移受损、新奇效应);\n"
84
+ "3) 方法学审查(任务完成表现 ≠ 学习效果,最高优先级);\n"
85
+ "4) 证据裁决(支持/反驳/中性分开;冲突不强行裁决);\n"
86
+ "5) 结论边界(能主张什么/不能主张什么/是否超出证据范围);\n"
87
+ "6) 决策动作(adopt / pilot / reject / insufficient_evidence)。\n"
88
+ "严格区分:任务表现提升 ≠ 学习效果提升。"
89
+ )
90
+
91
+
92
+ def _prompt_b3(q: dict) -> str:
93
+ return (
94
+ _PROTOCOL_B3 + "\n\n" + f"问题:{q['question']}\n" +
95
+ "\n请以 JSON 输出:{\"frame\": {...}, \"claims\": [{\"claim\": ..., \"outcome_type\": ..., "
96
+ "\"direction\": \"support|contradict|neutral\", \"source\": ...}], \"contradictions\": [...], "
97
+ "\"scope\": {\"can_claim\": [...], \"cannot_claim\": [...], \"exceeds_boundary\": [...]}, "
98
+ "\"recommended_action\": \"adopt|pilot|reject|insufficient_evidence\", "
99
+ "\"confidence\": \"High|Moderate|Low|Insufficient\"}\n"
100
+ )
101
+
102
+
103
+ def _prompt_b4(q: dict) -> str:
104
+ return (
105
+ _PROTOCOL_B3 + "\n" +
106
+ "(增强模式:检索者、反证挑战者、方法学审查者、证据裁决者由独立上下文分别执行并交叉复核。)\n\n"
107
+ + f"问题:{q['question']}\n" +
108
+ "\n请以 JSON 输出:{\"frame\": {...}, \"claims\": [...], \"contradictions\": [...], "
109
+ "\"scope\": {...}, \"recommended_action\": ..., \"confidence\": ..., "
110
+ "\"cross_review\": {\"agreement\": \"agree|disagree\", \"final_recommendation\": ...}}\n"
111
+ )
112
+
113
+
114
+ def build_prompt(baseline: str, q: dict) -> str:
115
+ fn = {"B0_direct_llm": _prompt_b0, "B1_search_llm": _prompt_b1,
116
+ "B2_standard_agent": _prompt_b2, "B3_eduevidence_single": _prompt_b3,
117
+ "B4_eduevidence_agent_mcp": _prompt_b4}[baseline]
118
+ return fn(q)
119
+
120
+
121
+ # ---------------------------------------------------------------- drivers
122
+
123
+
124
+ class ApiDriver:
125
+ """OpenAI-compatible chat completions driver (no SDK dependency)."""
126
+
127
+ name = "api"
128
+
129
+ def __init__(self, *, model: str | None = None,
130
+ base_url: str | None = None, api_key: str | None = None,
131
+ temperature: float = 0.0, timeout: int = 180):
132
+ self.model = model or os.environ.get("EDUEVIDENCE_LLM_MODEL", "")
133
+ self.base_url = (base_url or os.environ.get("EDUEVIDENCE_LLM_BASE_URL", "")
134
+ or "https://api.openai.com/v1").rstrip("/")
135
+ self.api_key = api_key or os.environ.get("EDUEVIDENCE_LLM_API_KEY", "")
136
+ self.temperature = temperature
137
+ self.timeout = timeout
138
+
139
+ def available(self) -> bool:
140
+ return bool(self.model and self.api_key)
141
+
142
+ def call(self, prompt: str) -> tuple[str, dict[str, Any]]:
143
+ import time
144
+ t0 = time.monotonic()
145
+ body = json.dumps({
146
+ "model": self.model,
147
+ "temperature": self.temperature,
148
+ "messages": [{"role": "user", "content": prompt}],
149
+ }).encode("utf-8")
150
+ req = urllib.request.Request(
151
+ self.base_url + "/chat/completions", data=body,
152
+ headers={"Content-Type": "application/json",
153
+ "Authorization": f"Bearer {self.api_key}"},
154
+ method="POST")
155
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp: # noqa: S310 (user-configured endpoint)
156
+ payload = json.loads(resp.read().decode("utf-8"))
157
+ usage = payload.get("usage") or {}
158
+ text = (payload.get("choices") or [{}])[0].get("message", {}).get("content", "")
159
+ latency = time.monotonic() - t0
160
+ usage_out = {
161
+ "prompt_tokens": usage.get("prompt_tokens"),
162
+ "completion_tokens": usage.get("completion_tokens"),
163
+ "latency_s": round(latency, 2),
164
+ }
165
+ return text, usage_out
166
+
167
+
168
+ class CliDriver:
169
+ """omp CLI driver - host agent runtime (user-approved).
170
+
171
+ Calls `omp -p --no-session --model=<model> <prompt>` in a scratch dir;
172
+ captures stdout as the response. Token usage is estimated from text
173
+ length and recorded as such (manifest usage fields may stay null; the
174
+ run manifest environment records the exact invocation).
175
+ """
176
+
177
+ name = "cli"
178
+
179
+ def __init__(self, model: str | None = None, thinking: str = "minimal",
180
+ timeout: int = 600):
181
+ self.model = model or os.environ.get("EDUEVIDENCE_LLM_MODEL", "deepseek-v4-flash")
182
+ self.thinking = thinking
183
+ self.timeout = timeout
184
+
185
+ def available(self) -> bool:
186
+ import shutil
187
+ return shutil.which("omp") is not None
188
+
189
+ def call(self, prompt: str, *, no_tools: bool = False) -> tuple[str, dict]:
190
+ import subprocess
191
+ import tempfile
192
+ import time
193
+
194
+ cmd = ["omp", "-p", "--no-session", f"--model={self.model}",
195
+ f"--thinking={self.thinking}", "--no-extensions", "--no-skills"]
196
+ if no_tools:
197
+ cmd.append("--no-tools")
198
+ cmd.append("--")
199
+ cmd.append(prompt)
200
+ t0 = time.monotonic()
201
+ with tempfile.TemporaryDirectory(prefix="eduevidence-bench-") as workdir:
202
+ proc = subprocess.run(cmd, capture_output=True, text=True,
203
+ timeout=self.timeout, cwd=workdir)
204
+ latency = time.monotonic() - t0
205
+ if proc.returncode != 0:
206
+ raise RuntimeError(
207
+ f"omp failed rc={proc.returncode}: "
208
+ f"{(proc.stderr or proc.stdout or '')[:300]}")
209
+ text = (proc.stdout or "").strip()
210
+ usage = {
211
+ "prompt_tokens": max(1, len(prompt) // 2),
212
+ "completion_tokens": max(1, len(text) // 2),
213
+ "latency_s": round(latency, 2),
214
+ }
215
+ return text, usage
216
+
217
+ class SimDriver:
218
+ """Deterministic simulation — harness validation ONLY. Never performance evidence."""
219
+
220
+ name = "sim"
221
+
222
+ def __init__(self, temperature: float = 0.0):
223
+ self.temperature = temperature
224
+
225
+ def available(self) -> bool:
226
+ return True
227
+
228
+ def call(self, prompt: str, *, no_tools: bool = False) -> tuple[str, dict[str, Any]]:
229
+ from benchmark_v2 import simulate_question_result # noqa: PLC0415
230
+
231
+ # Deterministic pseudo-usage from prompt length; response is a stub
232
+ # that the evaluator must never use as model performance.
233
+ import random
234
+ rng = random.Random(len(prompt) * 7919 % 2**31)
235
+ usage = {
236
+ "prompt_tokens": len(prompt) // 4,
237
+ "completion_tokens": 300 + rng.randint(0, 200),
238
+ "latency_s": round(0.01 + rng.random() * 0.05, 3),
239
+ }
240
+ return (
241
+ '{"claims": [], "contradictions": [], "scope": {"can_claim": [], '
242
+ '"cannot_claim": [], "exceeds_boundary": []}, '
243
+ '"recommended_action": "insufficient_evidence", '
244
+ '"confidence": "Insufficient", "simulated": true}',
245
+ usage,
246
+ )
247
+
248
+
249
+ def make_driver(name: str) -> Any:
250
+ if name == "api":
251
+ return ApiDriver()
252
+ if name == "cli":
253
+ return CliDriver()
254
+ if name == "sim":
255
+ return SimDriver()
256
+ raise ValueError(f"unknown driver: {name}")
257
+
258
+
259
+ # ---------------------------------------------------------------- run
260
+
261
+
262
+ def _now_iso() -> str:
263
+ return datetime.now(timezone.utc).isoformat()
264
+
265
+
266
+ def run_benchmark(*, questions: list[dict], baselines: list[str], repeats: int,
267
+ out_dir: Path, driver_name: str, budget_tokens: int | None,
268
+ temperature: float = 0.0, resume: bool = False) -> dict[str, Any]:
269
+ out_dir.mkdir(parents=True, exist_ok=True)
270
+ driver = make_driver(driver_name)
271
+ if not driver.available():
272
+ raise RuntimeError(
273
+ f"driver '{driver_name}' unavailable (api needs EDUEVIDENCE_LLM_MODEL "
274
+ "and EDUEVIDENCE_LLM_API_KEY)")
275
+
276
+ run_id = "run-" + datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
277
+ manifest: dict[str, Any] = {
278
+ "run_id": run_id,
279
+ "run_mode": "simulated" if driver_name == "sim" else "empirical",
280
+ "created_at": _now_iso(),
281
+ "questions_version": _questions_version(),
282
+ "baselines": list(baselines),
283
+ "repeats": repeats,
284
+ "budget_tokens": budget_tokens,
285
+ "environment": {
286
+ "driver": driver.name,
287
+ "model_family": getattr(driver, "model", "sim") or "unknown",
288
+ "model_version": getattr(driver, "model", "sim") or "unknown",
289
+ "temperature": temperature,
290
+ "tools": ([] if driver_name == "sim"
291
+ else (["host agent tools"] if driver_name == "cli"
292
+ else [])),
293
+ "search_provider": ("none" if driver_name == "sim"
294
+ else ("host_tools" if driver_name == "cli"
295
+ else "none")),
296
+ "agent_mcp_used": False,
297
+ },
298
+ "attempts": [],
299
+ "notes": ("SIMULATED: harness validation only, not model performance" if driver_name == "sim"
300
+ else "empirical run; see per-attempt artifacts"),
301
+ }
302
+
303
+ total_tokens = 0
304
+ budget_stopped = False
305
+ import re as _re
306
+ # --resume: reuse previously completed attempts (their response artifacts
307
+ # live in out_dir); only unfinished attempts are re-run.
308
+ done_ids: set[str] = set()
309
+ resumed: dict[str, dict[str, Any]] = {}
310
+ if resume and out_dir.is_dir():
311
+ import json as _json
312
+ for art in out_dir.glob("*.response.json"):
313
+ try:
314
+ data = _json.loads(art.read_text(encoding="utf-8"))
315
+ except (OSError, _json.JSONDecodeError):
316
+ continue
317
+ aid = data.get("attempt_id")
318
+ if aid and (out_dir / art.name).is_file():
319
+ done_ids.add(aid)
320
+ resumed[aid] = data
321
+ if done_ids:
322
+ print(f"resume: {len(done_ids)} attempt(s) already present, skipping")
323
+ for question in questions:
324
+ if budget_stopped:
325
+ break
326
+ if not _re.fullmatch(r"[A-Za-z0-9_-]+", question.get("id", "")):
327
+ raise ValueError(
328
+ f"question id {question.get('id')!r} contains unsafe characters "
329
+ "(P2-14: ids are used in artifact filenames)")
330
+ for baseline in baselines:
331
+ for attempt in range(1, repeats + 1):
332
+ if budget_stopped:
333
+ break
334
+ attempt_id = f"{question['id']}-{baseline}-a{attempt}"
335
+ if attempt_id in done_ids:
336
+ # Re-register resumed attempts in the manifest (status +
337
+ # usage read back from their artifact) so eval/report see
338
+ # the complete run.
339
+ art = out_dir / f"{attempt_id}.response.json"
340
+ data = resumed.get(attempt_id, {})
341
+ usage = data.get("usage") or {}
342
+ manifest["attempts"].append({
343
+ "attempt_id": attempt_id,
344
+ "question_id": question["id"],
345
+ "baseline": baseline,
346
+ "attempt": attempt,
347
+ "status": "completed",
348
+ "error": None,
349
+ "started_at": _now_iso(),
350
+ "finished_at": _now_iso(),
351
+ "prompt_tokens": usage.get("prompt_tokens"),
352
+ "completion_tokens": usage.get("completion_tokens"),
353
+ "latency_s": usage.get("latency_s"),
354
+ "cost_usd": None,
355
+ "artifacts": [art.name] if art.is_file() else [],
356
+ })
357
+ continue
358
+ started = _now_iso()
359
+ entry: dict[str, Any] = {
360
+ "attempt_id": attempt_id,
361
+ "question_id": question["id"],
362
+ "baseline": baseline,
363
+ "attempt": attempt,
364
+ "status": "completed",
365
+ "error": None,
366
+ "started_at": started,
367
+ "finished_at": None,
368
+ "prompt_tokens": None,
369
+ "completion_tokens": None,
370
+ "latency_s": None,
371
+ "cost_usd": None,
372
+ "artifacts": [],
373
+ }
374
+ try:
375
+ prompt = build_prompt(baseline, question)
376
+ text, usage = driver.call(
377
+ prompt, no_tools=(baseline == "B0_direct_llm"))
378
+ entry.update({
379
+ "finished_at": _now_iso(),
380
+ "prompt_tokens": usage.get("prompt_tokens"),
381
+ "completion_tokens": usage.get("completion_tokens"),
382
+ "latency_s": usage.get("latency_s"),
383
+ })
384
+ pt = usage.get("prompt_tokens") or 0
385
+ ct = usage.get("completion_tokens") or 0
386
+ total_tokens += pt + ct
387
+ artifact = out_dir / f"{attempt_id}.response.json"
388
+ artifact.write_text(json.dumps({
389
+ "attempt_id": attempt_id,
390
+ "prompt": prompt,
391
+ "response": text,
392
+ "usage": usage,
393
+ }, ensure_ascii=False, indent=2), encoding="utf-8")
394
+ entry["artifacts"] = [artifact.name]
395
+ except (urllib.error.URLError, OSError, ValueError, KeyError,
396
+ subprocess.TimeoutExpired) as exc: # P2-1: a hung model call must not kill the whole run
397
+ entry.update({"status": "failed", "error": str(exc),
398
+ "finished_at": _now_iso()})
399
+ manifest["attempts"].append(entry)
400
+
401
+ if budget_tokens is not None and total_tokens >= budget_tokens:
402
+ budget_stopped = True
403
+ manifest["notes"] = (manifest["notes"] + " BUDGET STOPPED at "
404
+ f"{total_tokens} tokens.")
405
+ break
406
+
407
+ if budget_stopped:
408
+ # P2-2: record remaining attempts as budget_stopped so the report can
409
+ # distinguish "stopped by budget" from "never scheduled".
410
+ for question in questions:
411
+ if any(a["question_id"] == question["id"] for a in manifest["attempts"]):
412
+ continue
413
+ for baseline in baselines:
414
+ for attempt in range(1, repeats + 1):
415
+ manifest["attempts"].append({
416
+ "attempt_id": f"{question['id']}-{baseline}-a{attempt}",
417
+ "question_id": question["id"],
418
+ "baseline": baseline,
419
+ "attempt": attempt,
420
+ "status": "budget_stopped",
421
+ "error": "budget exhausted",
422
+ "started_at": _now_iso(),
423
+ "finished_at": _now_iso(),
424
+ "prompt_tokens": None, "completion_tokens": None,
425
+ "latency_s": None, "cost_usd": None, "artifacts": [],
426
+ })
427
+
428
+ manifest_path = out_dir / "manifest.json"
429
+ tmp = out_dir / "manifest.json.tmp"
430
+ tmp.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
431
+ encoding="utf-8")
432
+ tmp.replace(manifest_path) # atomic write (P2-4)
433
+ _validate_manifest(manifest_path)
434
+ print(f"wrote {manifest_path} (attempts={len(manifest['attempts'])}, "
435
+ f"mode={manifest['run_mode']}, total_tokens~{total_tokens})")
436
+ return manifest
437
+
438
+
439
+ def _questions_version() -> str:
440
+ import subprocess as _sp
441
+
442
+ repo = Path(__file__).resolve().parent.parent
443
+ try:
444
+ proc = _sp.run(["git", "-C", str(repo), "rev-parse", "--short", "HEAD"],
445
+ capture_output=True, text=True, timeout=10)
446
+ return proc.stdout.strip() or "unknown"
447
+ except Exception: # noqa: BLE001 - version lookup must never fail a run
448
+ return "unknown"
449
+
450
+
451
+ def _validate_manifest(path: Path) -> None:
452
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
453
+ from validate_schema import Validator, SchemaError # noqa: PLC0415
454
+
455
+ import json as _json
456
+ schema = _json.loads(
457
+ (Path(__file__).resolve().parent.parent / "schemas" / "v3"
458
+ / "run-manifest.schema.json").read_text(encoding="utf-8"))
459
+ data = _json.loads(path.read_text(encoding="utf-8"))
460
+ try:
461
+ Validator(schema).validate(data, schema, "$")
462
+ except SchemaError as exc:
463
+ raise ValueError(f"manifest failed schema validation: {exc}") from exc
464
+
465
+
466
+ def _cmd_run(args: argparse.Namespace) -> int:
467
+ questions = load_questions(Path(args.questions))
468
+ errors = validate_questions(questions)
469
+ if errors:
470
+ print("invalid questions:", errors, file=sys.stderr)
471
+ return 2
472
+ baselines = [b for b in BASELINES if b in args.baselines.split(",")]
473
+ if not baselines:
474
+ print(f"--baselines must be a subset of {BASELINES}", file=sys.stderr)
475
+ return 2
476
+ run_benchmark(
477
+ questions=[q for q in questions if q["id"] in (args.ids.split(",") if args.ids else
478
+ [q["id"] for q in questions])],
479
+ baselines=baselines, repeats=args.repeats,
480
+ out_dir=Path(args.out), driver_name=args.driver,
481
+ budget_tokens=args.budget_tokens, temperature=args.temperature,
482
+ resume=args.resume)
483
+ return 0
484
+
485
+
486
+ def _cmd_report(args: argparse.Namespace) -> int:
487
+ from benchmark_evaluator import report_from_run # noqa: PLC0415
488
+
489
+ run_dir = Path(args.run)
490
+ manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
491
+ markdown = report_from_run(run_dir, manifest, Path(args.out))
492
+ print(markdown if args.stdout else f"wrote {args.out}")
493
+ return 0
494
+
495
+
496
+ def main(argv: list[str] | None = None) -> int:
497
+ parser = argparse.ArgumentParser(description="EduEvidence Layer B empirical benchmark (v3)")
498
+ sub = parser.add_subparsers(dest="command", required=True)
499
+
500
+ p_run = sub.add_parser("run", help="run baselines and write a manifest")
501
+ p_run.add_argument("--baselines", required=True,
502
+ help="comma-separated subset of " + ",".join(BASELINES))
503
+ p_run.add_argument("--questions", default="benchmarks/questions.jsonl")
504
+ p_run.add_argument("--ids", default=None, help="comma-separated question ids to run")
505
+ p_run.add_argument("--repeats", type=int, default=3)
506
+ p_run.add_argument("--driver", choices=["api", "cli", "sim"], default=None,
507
+ help="api (needs env keys), cli (omp), or sim (harness validation only)")
508
+ p_run.add_argument("--out", required=True)
509
+ p_run.add_argument("--budget-tokens", type=int, default=DEFAULT_BUDGET_TOKENS)
510
+ p_run.add_argument("--temperature", type=float, default=0.0)
511
+ p_run.add_argument("--resume", action="store_true",
512
+ help="skip attempts whose response artifacts already exist in --out")
513
+ p_run.set_defaults(func=_cmd_run)
514
+
515
+ p_eval = sub.add_parser("eval", help="evaluate a run against gold annotations")
516
+ p_eval.add_argument("--run", required=True)
517
+ p_eval.add_argument("--annotations", default="benchmarks/annotations")
518
+ p_eval.add_argument("--out", default=None)
519
+ p_eval.set_defaults(func=_cmd_eval)
520
+
521
+ p_report = sub.add_parser("report", help="render the empirical benchmark report (markdown)")
522
+ p_report.add_argument("--run", required=True)
523
+ p_report.add_argument("--out", required=True)
524
+ p_report.add_argument("--stdout", action="store_true")
525
+ p_report.set_defaults(func=_cmd_report)
526
+
527
+ args = parser.parse_args(argv)
528
+ if args.command == "run" and getattr(args, "driver", None) is None:
529
+ if ApiDriver().available():
530
+ args.driver = "api"
531
+ elif CliDriver().available():
532
+ args.driver = "cli"
533
+ else:
534
+ args.driver = "sim"
535
+ return args.func(args)
536
+
537
+
538
+ def _cmd_eval(args: argparse.Namespace) -> int:
539
+ from benchmark_evaluator import evaluate_run # noqa: PLC0415
540
+
541
+ run_dir = Path(args.run)
542
+ manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
543
+ summary = evaluate_run(run_dir, manifest, Path(args.annotations))
544
+ out_path = Path(args.out) if args.out else run_dir / "evaluation.json"
545
+ out_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
546
+ encoding="utf-8")
547
+ print(f"wrote {out_path}")
548
+ return 0
549
+
550
+
551
+ if __name__ == "__main__":
552
+ sys.exit(main())