eduevidence 6.2.0 → 6.3.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/CHANGELOG.md +395 -0
- package/README.md +22 -13
- package/README.zh-CN.md +15 -8
- package/SKILL.md +10 -9
- package/benchmarks/evidence-library.json +277 -1
- package/docs/architecture.md +6 -3
- package/docs/j-ev-experimental.md +250 -0
- package/docs/reproducibility.md +138 -0
- package/domains/_neutral/copy/few_shots.json +21 -0
- package/domains/_neutral/copy/framing_lexicon.json +19 -0
- package/domains/_neutral/copy/module_labels.json +5 -0
- package/domains/_neutral/copy/module_labels_footer.json +102 -0
- package/domains/_neutral/copy/module_labels_modules.json +204 -0
- package/domains/_neutral/copy/module_labels_nav.json +126 -0
- package/domains/_neutral/copy/module_labels_summary.json +98 -0
- package/domains/_neutral/copy/module_labels_tables.json +164 -0
- package/domains/_neutral/copy/module_labels_v2.json +90 -0
- package/domains/_neutral/copy/risk_constructs.json +20 -0
- package/domains/_neutral/copy/section_titles.json +66 -0
- package/domains/_neutral/copy/terminology.json +11 -0
- package/domains/check_copy_packs.py +103 -0
- package/domains/education/copy/few_shots.json +22 -0
- package/domains/education/copy/framing_enums.json +167 -0
- package/domains/education/copy/framing_lexicon.json +166 -0
- package/domains/education/copy/module_labels.json +169 -0
- package/domains/education/copy/risk_constructs.json +48 -0
- package/domains/education/copy/section_titles.json +186 -0
- package/domains/education/copy/terminology.json +70 -0
- package/domains/education/manifest.json +1 -1
- package/domains/education/outcome_taxonomy.json +2 -2
- package/domains/manifest.json +1 -1
- package/domains/policy/copy/few_shots.json +22 -0
- package/domains/policy/copy/framing_enums.json +94 -0
- package/domains/policy/copy/framing_lexicon.json +174 -0
- package/domains/policy/copy/module_labels.json +168 -0
- package/domains/policy/copy/risk_constructs.json +33 -0
- package/domains/policy/copy/section_titles.json +186 -0
- package/domains/policy/copy/terminology.json +64 -0
- package/engine/capabilities.py +57 -5
- package/engine/decision_policy.py +88 -17
- package/engine/library_builtin.py +7 -4
- package/engine/tribunal.py +17 -23
- package/engine/versions.py +1 -1
- package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +4 -4
- package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +4 -4
- package/examples/spaced-retrieval-practice/EduEvidence_Report.html +2728 -0
- package/examples/spaced-retrieval-practice/report.html +2522 -0
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +4 -4
- package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +4 -4
- package/examples/workplace-ai-assistant/EduEvidence_Report.html +2814 -0
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +36 -36
- package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +36 -36
- package/integrations/jev/__init__.py +115 -0
- package/integrations/jev/approval.py +212 -0
- package/integrations/jev/cli.py +84 -0
- package/integrations/jev/config.py +112 -0
- package/integrations/jev/gateway.py +128 -0
- package/integrations/jev/modes.py +38 -0
- package/integrations/jev/tools_classify.py +88 -0
- package/integrations/jev/tools_extract.py +111 -0
- package/integrations/jev/tools_rerank.py +71 -0
- package/integrations/jev/tools_screen.py +87 -0
- package/integrations/jev/tools_verify.py +95 -0
- package/integrations/jev_mcp.py +22 -0
- package/integrations/semantic_decide.py +286 -0
- package/integrations/semdecide_cli.py +55 -0
- package/package.json +9 -1
- package/pyproject.toml +1 -1
- package/references/report-copy-style.md +43 -3
- package/schemas/v2/decision-snapshot.schema.json +20 -9
- package/schemas/v2/intake.schema.json +191 -0
- package/scripts/build_evidence_library.py +15 -5
- package/scripts/dashboard_server.py +13 -2
- package/scripts/intake/__init__.py +31 -0
- package/scripts/intake/__main__.py +18 -0
- package/scripts/intake/background.py +78 -0
- package/scripts/intake/browser.py +79 -0
- package/scripts/intake/cli.py +57 -0
- package/scripts/intake/constants.py +57 -0
- package/scripts/intake/depth.py +53 -0
- package/scripts/intake/enhancements.py +106 -0
- package/scripts/intake/hooks.py +90 -0
- package/scripts/intake/prefs.py +76 -0
- package/scripts/intake/prompts.py +85 -0
- package/scripts/intake/session.py +152 -0
- package/scripts/lint_file_layers.py +126 -0
- package/scripts/orchestrator.py +68 -17
- package/scripts/pre_verdict_gate.py +21 -7
- package/scripts/skill_lint.py +11 -1
- package/scripts/skill_payload.py +3 -3
- package/scripts/test_adversarial_empirical.py +70 -6
- package/skill/agents/evidence-judge.md +49 -7
- package/skill/workflows/experimental-jev.md +170 -0
- package/skill/workflows/intake.md +120 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +32 -14
- package/visualization/eduevidence-report/scripts/build_report.py +75 -662
- package/visualization/eduevidence-report/scripts/report_copy_pack.py +296 -0
- package/visualization/eduevidence-report/scripts/report_copy_policy_guard.py +47 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +61 -0
- package/scripts/build_esl_artifacts.py +0 -1921
- package/scripts/build_killer_demo.py +0 -295
- package/scripts/enrich_projects_human_and_lieflat.py +0 -315
- package/scripts/generate_new_projects.py +0 -686
- package/scripts/sync_killer_demo_report.py +0 -270
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://eduevidence.dev/schemas/v2/intake.schema.json",
|
|
4
|
+
"title": "Intake",
|
|
5
|
+
"description": "One-shot two-round user intake for a run: research question, execution enhancements, depth, optional enhancement mappings, Frame boundary hints, and wrap-up confirmation. User answers only mode and questions; the agent infers the rest. Non-question preferences live in ~/.eduevidence/prefs.json (see definitions.Prefs) and never record the research question. All five report themes stay rendered; default_main_theme only selects which one to open.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"research_question",
|
|
11
|
+
"enhancements",
|
|
12
|
+
"depth",
|
|
13
|
+
"summary_confirmed"
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"schema_version": { "type": "integer", "minimum": 1 },
|
|
17
|
+
"research_question": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"minLength": 1,
|
|
20
|
+
"description": "Canonical research question collected in round 1. Stored on the run, never in prefs."
|
|
21
|
+
},
|
|
22
|
+
"enhancements": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"description": "Execution enhancements selected in round 1. Use [\"none\"] when the user enables nothing.",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["agent_mcp", "jev", "semdecide", "none"]
|
|
28
|
+
},
|
|
29
|
+
"minItems": 1,
|
|
30
|
+
"uniqueItems": true
|
|
31
|
+
},
|
|
32
|
+
"depth": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["S", "M", "L"],
|
|
35
|
+
"description": "Resolved complexity depth after auto rules (S quick check / M standard / L deep)."
|
|
36
|
+
},
|
|
37
|
+
"depth_choice": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"enum": ["S", "M", "L", "auto"],
|
|
40
|
+
"description": "What the user picked in round 1. auto means the agent applies the depth guideline."
|
|
41
|
+
},
|
|
42
|
+
"depth_rationale": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Why depth resolved to S/M/L (auto guideline or explicit choice)."
|
|
45
|
+
},
|
|
46
|
+
"mcp": {
|
|
47
|
+
"type": ["object", "null"],
|
|
48
|
+
"description": "Round-2 Agent MCP block. Present only when agent_mcp is selected.",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"properties": {
|
|
51
|
+
"authorized": { "type": "boolean" },
|
|
52
|
+
"role_mapping": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"description": "role -> {cli, model} authorization table confirmed by the user.",
|
|
55
|
+
"additionalProperties": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"required": ["cli", "model"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"cli": { "type": "string", "minLength": 1 },
|
|
61
|
+
"model": { "type": "string", "minLength": 1 }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"note": { "type": "string" }
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"jev": {
|
|
69
|
+
"type": ["object", "null"],
|
|
70
|
+
"description": "Round-2 Jev block. Present only when jev is selected.",
|
|
71
|
+
"additionalProperties": false,
|
|
72
|
+
"properties": {
|
|
73
|
+
"tool_subset": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"items": { "type": "string", "minLength": 1 },
|
|
76
|
+
"uniqueItems": true
|
|
77
|
+
},
|
|
78
|
+
"note": { "type": "string" }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"semdecide": {
|
|
82
|
+
"type": ["object", "null"],
|
|
83
|
+
"description": "Round-2 SemDecide block. Present only when semdecide is selected.",
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"properties": {
|
|
86
|
+
"purpose": { "type": "string", "minLength": 1 },
|
|
87
|
+
"note": { "type": "string" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"frame_hints": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"description": "Background deep-dive boundary hints (population / intervention / comparison / primary_outcome / context). Inferred values are proposals only; unknown fields stay absent (never fabricated).",
|
|
94
|
+
"properties": {
|
|
95
|
+
"population": { "type": "string" },
|
|
96
|
+
"intervention": { "type": "string" },
|
|
97
|
+
"comparison": { "type": "string" },
|
|
98
|
+
"primary_outcome": { "type": "string" },
|
|
99
|
+
"context": { "type": "string" },
|
|
100
|
+
"domain": { "type": "string" },
|
|
101
|
+
"confirmed": { "type": "boolean" }
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"summary_confirmed": {
|
|
105
|
+
"type": "boolean",
|
|
106
|
+
"description": "Wrap-up summary accepted once; the run then continues unattended."
|
|
107
|
+
},
|
|
108
|
+
"open_browser": {
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"description": "Whether to open the main report in a browser when the run finishes."
|
|
111
|
+
},
|
|
112
|
+
"default_main_theme": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"enum": ["claude", "academic", "datalab", "datalab-dark", "presentation"],
|
|
115
|
+
"description": "Which of the five baked themes to open as the main report. All five remain rendered."
|
|
116
|
+
},
|
|
117
|
+
"interactive": { "type": "boolean" },
|
|
118
|
+
"prefs_path": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Absolute path of the prefs file written for this intake (never contains the research question)."
|
|
121
|
+
},
|
|
122
|
+
"source": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"enum": ["interactive", "silent", "yes_flag", "prefs"]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"definitions": {
|
|
128
|
+
"Prefs": {
|
|
129
|
+
"title": "Prefs",
|
|
130
|
+
"description": "Durable non-question preferences at ~/.eduevidence/prefs.json. Never store a research question here.",
|
|
131
|
+
"type": "object",
|
|
132
|
+
"additionalProperties": false,
|
|
133
|
+
"properties": {
|
|
134
|
+
"schema_version": { "type": "integer", "minimum": 1 },
|
|
135
|
+
"enhancement": {
|
|
136
|
+
"type": "array",
|
|
137
|
+
"items": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"enum": ["agent_mcp", "jev", "semdecide", "none"]
|
|
140
|
+
},
|
|
141
|
+
"uniqueItems": true
|
|
142
|
+
},
|
|
143
|
+
"mcp": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": false,
|
|
146
|
+
"properties": {
|
|
147
|
+
"role_mapping": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"additionalProperties": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"additionalProperties": false,
|
|
152
|
+
"required": ["cli", "model"],
|
|
153
|
+
"properties": {
|
|
154
|
+
"cli": { "type": "string", "minLength": 1 },
|
|
155
|
+
"model": { "type": "string", "minLength": 1 }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"jev": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"additionalProperties": false,
|
|
164
|
+
"properties": {
|
|
165
|
+
"tool_subset": {
|
|
166
|
+
"type": "array",
|
|
167
|
+
"items": { "type": "string", "minLength": 1 },
|
|
168
|
+
"uniqueItems": true
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"semdecide": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"additionalProperties": false,
|
|
175
|
+
"properties": {
|
|
176
|
+
"purpose": { "type": "string" }
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"depth_preference": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"enum": ["S", "M", "L", "auto"]
|
|
182
|
+
},
|
|
183
|
+
"open_browser": { "type": "boolean" },
|
|
184
|
+
"default_main_theme": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"enum": ["claude", "academic", "datalab", "datalab-dark", "presentation"]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -17,9 +17,15 @@ schemas/v4/evidence-library.schema.json using the repo's zero-dependency
|
|
|
17
17
|
validator (scripts/validate_schema.py).
|
|
18
18
|
|
|
19
19
|
Direction semantics (adoption-relevant, conservative):
|
|
20
|
-
support -> evidence favors adopting the intervention
|
|
21
|
-
|
|
20
|
+
support -> evidence favors adopting the intervention
|
|
21
|
+
(offline screening cap => pilot; never adopt)
|
|
22
|
+
contradict -> evidence opposes adopting the intervention
|
|
23
|
+
(oppose-only => reject; mixed with support is an unresolved
|
|
24
|
+
conflict and must fall to INSUFFICIENT_EVIDENCE via
|
|
25
|
+
engine.decision_policy.decision_outcome)
|
|
22
26
|
neutral -> inconclusive
|
|
27
|
+
The library never emits adopt: full ADOPT is decided only by
|
|
28
|
+
engine.decision_policy.decision_outcome (High + support + direct primary).
|
|
23
29
|
For gold units the coarse rule is: if a question's expected decision range is
|
|
24
30
|
purely reject-oriented ("reject" present and "pilot" absent), its
|
|
25
31
|
key_claims/key_supporting_sources are harmful evidence => contradict, and its
|
|
@@ -266,10 +272,14 @@ def build(generated_at: str | None = None) -> tuple[dict[str, Any], int]:
|
|
|
266
272
|
"key_claims/key_supporting_sources/known_contradictions/correct_outcome_types)"
|
|
267
273
|
"+ 3 个示例工作流 evidence.jsonl(ai-coding-assistant / ai-tutor / ai-writing-assistant)"
|
|
268
274
|
"抽取生成;按 (source_id, outcome_token, claim_text) 去重合并。"
|
|
269
|
-
"direction 语义为采纳方向:support
|
|
270
|
-
"contradict
|
|
275
|
+
"direction 语义为采纳方向:support=支持采纳(离线初筛上限 => pilot,永不输出 adopt),"
|
|
276
|
+
"contradict=反对采纳(oppose-only 时 => reject;与 support 并存属未决冲突,应交 "
|
|
277
|
+
"engine.decision_policy.decision_outcome 判为 INSUFFICIENT_EVIDENCE),neutral=中性;"
|
|
278
|
+
"金标准条目按 expected_decision_range "
|
|
271
279
|
"粗粒度映射方向(纯 reject 问题反向映射),conflict 与混合方向问题的单条断言方向可能不精确。"
|
|
272
|
-
"仅用于离线初步裁决(preliminary,保守),从不直接给出 adopt
|
|
280
|
+
"仅用于离线初步裁决(preliminary,保守),从不直接给出 adopt;"
|
|
281
|
+
"完整 ADOPT 只能由 engine.decision_policy.decision_outcome 给出"
|
|
282
|
+
"(High + support + 主要结果 directness 2)。"
|
|
273
283
|
),
|
|
274
284
|
}
|
|
275
285
|
_validate(library)
|
|
@@ -524,7 +524,8 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
524
524
|
self._send_report_bytes(html_path.read_bytes())
|
|
525
525
|
|
|
526
526
|
|
|
527
|
-
def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765
|
|
527
|
+
def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765,
|
|
528
|
+
open_browser: bool = False) -> None:
|
|
528
529
|
server = None
|
|
529
530
|
actual_port = port
|
|
530
531
|
for offset in range(10):
|
|
@@ -541,12 +542,20 @@ def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
|
541
542
|
if server is None:
|
|
542
543
|
print(f"❌ 端口 {port}-{port + 9} 均被占用。")
|
|
543
544
|
return
|
|
545
|
+
studio_url = f"http://{host}:{actual_port}/studio/"
|
|
544
546
|
print("============================================================")
|
|
545
547
|
print(f"🚀 EduEvidence Web Studio running at http://{host}:{actual_port}/")
|
|
546
548
|
print(" Research Studio /studio/ (read-only)")
|
|
547
549
|
print(" Projects, evidence, revisions and five-theme reports")
|
|
548
550
|
print(" Projection API /api/studio/catalog")
|
|
549
551
|
print("============================================================")
|
|
552
|
+
if open_browser:
|
|
553
|
+
try:
|
|
554
|
+
import webbrowser
|
|
555
|
+
webbrowser.open(studio_url)
|
|
556
|
+
print(f"🌐 opened {studio_url}")
|
|
557
|
+
except Exception as exc: # pragma: no cover - browser may be absent
|
|
558
|
+
print(f"⚠️ failed to open browser: {exc}")
|
|
550
559
|
try:
|
|
551
560
|
server.serve_forever()
|
|
552
561
|
except KeyboardInterrupt:
|
|
@@ -557,8 +566,10 @@ def main() -> None:
|
|
|
557
566
|
parser = argparse.ArgumentParser()
|
|
558
567
|
parser.add_argument("--host", default="127.0.0.1")
|
|
559
568
|
parser.add_argument("--port", type=int, default=8765)
|
|
569
|
+
parser.add_argument("--open", action="store_true",
|
|
570
|
+
help="open the Studio URL in the system browser once the server is up")
|
|
560
571
|
args = parser.parse_args()
|
|
561
|
-
run_dashboard_server(args.host, args.port)
|
|
572
|
+
run_dashboard_server(args.host, args.port, open_browser=args.open)
|
|
562
573
|
|
|
563
574
|
|
|
564
575
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""intake — one-shot two-round user intake + durable prefs + report open."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .browser import (
|
|
5
|
+
maybe_open_after_run,
|
|
6
|
+
open_report,
|
|
7
|
+
open_url,
|
|
8
|
+
resolve_main_report,
|
|
9
|
+
)
|
|
10
|
+
from .constants import (
|
|
11
|
+
DEPTH_CHOICES,
|
|
12
|
+
ENHANCEMENTS,
|
|
13
|
+
ENHANCEMENT_BLURBS,
|
|
14
|
+
DEPTH_BLURBS,
|
|
15
|
+
FRAME_SLOTS,
|
|
16
|
+
THEME_NAMES,
|
|
17
|
+
)
|
|
18
|
+
from .depth import auto_resolve_depth, resolve_depth
|
|
19
|
+
from .background import infer_frame_hints
|
|
20
|
+
from .prefs import default_prefs, load_prefs, prefs_path, save_prefs
|
|
21
|
+
from .prompts import PROMPT_SCRIPT
|
|
22
|
+
from .session import run_intake
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEPTH_BLURBS", "DEPTH_CHOICES", "ENHANCEMENTS", "ENHANCEMENT_BLURBS",
|
|
26
|
+
"FRAME_SLOTS", "PROMPT_SCRIPT", "THEME_NAMES",
|
|
27
|
+
"auto_resolve_depth", "default_prefs", "infer_frame_hints",
|
|
28
|
+
"load_prefs", "maybe_open_after_run", "open_report", "open_url",
|
|
29
|
+
"prefs_path", "resolve_depth", "resolve_main_report", "run_intake",
|
|
30
|
+
"save_prefs",
|
|
31
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""python -m intake — CLI entry."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Allow `python scripts/intake/__main__.py` without installing the package.
|
|
8
|
+
if __package__ in (None, ""):
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
10
|
+
|
|
11
|
+
from intake.cli import main # noqa: E402
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
try:
|
|
15
|
+
raise SystemExit(main())
|
|
16
|
+
except ValueError as exc:
|
|
17
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
18
|
+
raise SystemExit(2) from exc
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Frame skeleton inference + 3–6 boundary questions (never fabricate)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from .constants import (
|
|
8
|
+
CONTEXT_CUES,
|
|
9
|
+
FRAME_SLOTS,
|
|
10
|
+
OUTCOME_CUES,
|
|
11
|
+
POP_CUES,
|
|
12
|
+
SLOT_LABELS,
|
|
13
|
+
)
|
|
14
|
+
from .prompts import ask, ask_yes
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def infer_frame_hints(question: str, domain: str = "education") -> dict[str, Any]:
|
|
18
|
+
"""Propose Frame boundary hints from the question text only.
|
|
19
|
+
|
|
20
|
+
Anything not stated in the question stays absent. Never fabricate.
|
|
21
|
+
"""
|
|
22
|
+
hints: dict[str, Any] = {"domain": domain, "confirmed": False}
|
|
23
|
+
text = question or ""
|
|
24
|
+
low = text.lower()
|
|
25
|
+
for cue in POP_CUES:
|
|
26
|
+
if cue.lower() in low:
|
|
27
|
+
hints["population"] = cue
|
|
28
|
+
break
|
|
29
|
+
for cue in OUTCOME_CUES:
|
|
30
|
+
if cue.lower() in low:
|
|
31
|
+
hints["primary_outcome"] = cue
|
|
32
|
+
break
|
|
33
|
+
for cue in CONTEXT_CUES:
|
|
34
|
+
if cue in text or cue.lower() in low:
|
|
35
|
+
hints["context"] = cue
|
|
36
|
+
break
|
|
37
|
+
m = re.search(r"(AI[\w\s一-鿿]{0,20}(?:工具|助手|tutor|copilot|编程)|"
|
|
38
|
+
r"(?:翻转|项目式|同伴)教学|AI\s*编程助手)", text, re.I)
|
|
39
|
+
if m:
|
|
40
|
+
hints["intervention"] = m.group(0).strip()
|
|
41
|
+
if re.search(r"是否|影响|对比|比较|vs|与.*比|是否允许|该不该", text):
|
|
42
|
+
hints["comparison"] = "business as usual / 未使用该干预的常规做法"
|
|
43
|
+
return hints
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def boundary_questions(inferred: dict[str, Any]) -> list[tuple[str, str, str | None]]:
|
|
47
|
+
"""3–6 boundary questions: (slot, prompt, default)."""
|
|
48
|
+
questions: list[tuple[str, str, str | None]] = []
|
|
49
|
+
for slot in FRAME_SLOTS:
|
|
50
|
+
questions.append((slot, f" · {SLOT_LABELS[slot]}", inferred.get(slot)))
|
|
51
|
+
return questions[:6]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def collect_frame_hints(question: str, *, domain: str = "education",
|
|
55
|
+
interactive: bool,
|
|
56
|
+
printer: Callable[[str], None] = print) -> dict[str, Any]:
|
|
57
|
+
"""Infer skeleton; when interactive, ask 3–6 boundary Qs + one confirm."""
|
|
58
|
+
inferred = infer_frame_hints(question, domain=domain)
|
|
59
|
+
frame_hints = dict(inferred)
|
|
60
|
+
if not interactive:
|
|
61
|
+
frame_hints["confirmed"] = False
|
|
62
|
+
return frame_hints
|
|
63
|
+
|
|
64
|
+
printer("—— 背景智能深挖(Frame 边界,智能默认 + 一次确认;缺失不编造)——")
|
|
65
|
+
printer(f" 领域骨架:{domain}")
|
|
66
|
+
for slot, prompt, default in boundary_questions(inferred):
|
|
67
|
+
label = default if default is not None else "(未从题目推出,留空)"
|
|
68
|
+
raw = ask(prompt, label if default is not None else "")
|
|
69
|
+
if raw and not raw.startswith("("):
|
|
70
|
+
frame_hints[slot] = raw
|
|
71
|
+
elif default is not None:
|
|
72
|
+
frame_hints[slot] = default
|
|
73
|
+
# else: leave absent — never fabricate
|
|
74
|
+
printer(" Frame 边界摘要:")
|
|
75
|
+
for slot in FRAME_SLOTS:
|
|
76
|
+
printer(f" {slot}: {frame_hints.get(slot) or '—'}")
|
|
77
|
+
frame_hints["confirmed"] = ask_yes(" 确认以上边界?(Y/n)", True)
|
|
78
|
+
return frame_hints
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Main-report resolution and optional system-browser open."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import webbrowser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .constants import THEME_NAMES
|
|
8
|
+
from .prefs import load_prefs
|
|
9
|
+
from .prompts import is_interactive
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_main_report(directory: Path | str, theme: str | None = None) -> Path | None:
|
|
13
|
+
"""Pick the main report file. All five themes stay on disk.
|
|
14
|
+
|
|
15
|
+
Preference order:
|
|
16
|
+
reports-5themes/EduEvidence_Report_<theme>.html
|
|
17
|
+
reports-5themes/EduEvidence_Report_claude.html
|
|
18
|
+
report.html
|
|
19
|
+
EduEvidence_Report.html
|
|
20
|
+
"""
|
|
21
|
+
root = Path(directory)
|
|
22
|
+
if not root.is_dir():
|
|
23
|
+
return None
|
|
24
|
+
theme = theme if theme in THEME_NAMES else "claude"
|
|
25
|
+
variants = root / "reports-5themes"
|
|
26
|
+
candidates = [
|
|
27
|
+
variants / f"EduEvidence_Report_{theme}.html",
|
|
28
|
+
variants / "EduEvidence_Report_claude.html",
|
|
29
|
+
root / "report.html",
|
|
30
|
+
root / "EduEvidence_Report.html",
|
|
31
|
+
]
|
|
32
|
+
for path in candidates:
|
|
33
|
+
if path.is_file() and path.stat().st_size > 2:
|
|
34
|
+
return path
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def open_report(directory: Path | str, theme: str | None = None,
|
|
39
|
+
*, force: bool = False) -> bool:
|
|
40
|
+
"""Open the preferred main report in the system browser.
|
|
41
|
+
|
|
42
|
+
Only opens when `force` or prefs.open_browser is set. The caller should
|
|
43
|
+
also suppress this on non-TTY if silent behaviour is required.
|
|
44
|
+
"""
|
|
45
|
+
prefs = load_prefs()
|
|
46
|
+
if not force and not prefs.get("open_browser", True):
|
|
47
|
+
return False
|
|
48
|
+
path = resolve_main_report(directory, theme or prefs.get("default_main_theme"))
|
|
49
|
+
if path is None:
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
webbrowser.open(path.resolve().as_uri())
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def open_url(url: str, *, force: bool = False) -> bool:
|
|
59
|
+
prefs = load_prefs()
|
|
60
|
+
if not force and not prefs.get("open_browser", True):
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
webbrowser.open(url)
|
|
64
|
+
except Exception:
|
|
65
|
+
return False
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def maybe_open_after_run(run_dir: Path | str, *, theme: str | None = None,
|
|
70
|
+
interactive: bool | None = None) -> bool:
|
|
71
|
+
"""End-of-run auto-open. Silent on non-TTY so tests stay quiet."""
|
|
72
|
+
if interactive is None:
|
|
73
|
+
interactive = is_interactive()
|
|
74
|
+
if not interactive:
|
|
75
|
+
return False
|
|
76
|
+
prefs = load_prefs()
|
|
77
|
+
if not prefs.get("open_browser", True):
|
|
78
|
+
return False
|
|
79
|
+
return open_report(run_dir, theme or prefs.get("default_main_theme"), force=True)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""CLI for the one-shot two-round intake."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .constants import DEPTH_CHOICES, ENHANCEMENTS
|
|
9
|
+
from .prefs import load_prefs
|
|
10
|
+
from .prompts import PROMPT_SCRIPT
|
|
11
|
+
from .session import run_intake
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: list[str] | None = None) -> int:
|
|
15
|
+
parser = argparse.ArgumentParser(description="EduEvidence two-round intake")
|
|
16
|
+
parser.add_argument("--question", default=None, help="research question")
|
|
17
|
+
parser.add_argument("--depth", default=None,
|
|
18
|
+
choices=list(DEPTH_CHOICES) + ["quick", "standard", "deep"],
|
|
19
|
+
help="S/M/L/auto (or legacy quick/standard/deep)")
|
|
20
|
+
parser.add_argument("--enhancement", action="append", default=None,
|
|
21
|
+
choices=list(ENHANCEMENTS),
|
|
22
|
+
help="execution enhancement (repeatable)")
|
|
23
|
+
parser.add_argument("--domain", default="education")
|
|
24
|
+
parser.add_argument("--yes", action="store_true",
|
|
25
|
+
help="skip prompts; use prefs + explicit flags (unattended)")
|
|
26
|
+
parser.add_argument("--print-prompts", action="store_true",
|
|
27
|
+
help="print the fixed two-round prompt script and exit")
|
|
28
|
+
parser.add_argument("--show-prefs", action="store_true",
|
|
29
|
+
help="print current prefs.json and exit")
|
|
30
|
+
args = parser.parse_args(argv)
|
|
31
|
+
|
|
32
|
+
if args.print_prompts:
|
|
33
|
+
sys.stdout.write(PROMPT_SCRIPT)
|
|
34
|
+
return 0
|
|
35
|
+
if args.show_prefs:
|
|
36
|
+
print(json.dumps(load_prefs(), ensure_ascii=False, indent=2))
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
record = run_intake(
|
|
40
|
+
question=args.question,
|
|
41
|
+
depth=args.depth,
|
|
42
|
+
enhancements=args.enhancement,
|
|
43
|
+
domain=args.domain,
|
|
44
|
+
assume_yes=args.yes,
|
|
45
|
+
)
|
|
46
|
+
print(json.dumps(record, ensure_ascii=False, indent=2))
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
# Documented entry: `python3 -m intake.cli --print-prompts` from scripts/.
|
|
52
|
+
# Same failure contract as scripts/intake/__main__.py.
|
|
53
|
+
try:
|
|
54
|
+
raise SystemExit(main())
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
57
|
+
raise SystemExit(2) from exc
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Shared constants for the one-shot two-round intake."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
SCHEMA_VERSION = 1
|
|
5
|
+
PREFS_NAME = "prefs.json"
|
|
6
|
+
THEME_NAMES = ("claude", "academic", "datalab", "datalab-dark", "presentation")
|
|
7
|
+
ENHANCEMENTS = ("agent_mcp", "jev", "semdecide", "none")
|
|
8
|
+
DEPTH_CHOICES = ("S", "M", "L", "auto")
|
|
9
|
+
DEPTH_ALIASES = {"quick": "S", "standard": "M", "deep": "L"}
|
|
10
|
+
|
|
11
|
+
# Fixed round-1 labels (mirrored verbatim in skill/workflows/intake.md).
|
|
12
|
+
ENHANCEMENT_BLURBS: dict[str, str] = {
|
|
13
|
+
"agent_mcp": "Agent MCP — 多 CLI/多模型分工、独立子上下文、超时恢复(可选执行增强)",
|
|
14
|
+
"jev": "Jev — 轻量工具子集派发,适合把确定性小步交给受限工具面",
|
|
15
|
+
"semdecide": "SemDecide — 语义决策辅助,只结构化上游意图,不改下游确定性路由",
|
|
16
|
+
"none": "均不启用 — 平台原生模式,零外部执行增强",
|
|
17
|
+
}
|
|
18
|
+
DEPTH_BLURBS: dict[str, str] = {
|
|
19
|
+
"S": "S 快检 — 单点问题,最小检索面,直接给边界结论",
|
|
20
|
+
"M": "M 标准 — 常规采用/比较问题,标准证据到决策流程",
|
|
21
|
+
"L": "L 深研 — 高影响、有争议或要试点,完整深研与决策扩展",
|
|
22
|
+
"auto": "自动 — 不选则按指南判定:单点→S,常规采用→M,高影响/争议/要试点→L,不确定取 M",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Auto-depth guideline keywords (deterministic aid; the agent's judgment wins).
|
|
26
|
+
L_AUTO_SIGNALS = (
|
|
27
|
+
"试点", "pilot", "高影响", "争议", "全校", "全面部署", "政策",
|
|
28
|
+
"rollout", "high-stakes", "high stakes",
|
|
29
|
+
)
|
|
30
|
+
S_AUTO_SIGNALS = (
|
|
31
|
+
"单点", "一句话", "快速", "简要", "是不是", "对不对", "解释一下",
|
|
32
|
+
"定义", "公式", "quick", "trivial",
|
|
33
|
+
)
|
|
34
|
+
M_AUTO_SIGNALS = (
|
|
35
|
+
"是否", "有效", "影响", "比较", "对比", "采用", "adopt", "vs",
|
|
36
|
+
"效果", "evidence", "review",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Frame boundary slots and light inference cues (never invent; only extract).
|
|
40
|
+
FRAME_SLOTS = ("population", "intervention", "comparison", "primary_outcome", "context")
|
|
41
|
+
POP_CUES = (
|
|
42
|
+
"大一", "大二", "大三", "大四", "本科", "研究生", "高中", "初中", "小学",
|
|
43
|
+
"成人", "职业", "CS1", "大学生", "中学生", "小学生", "undergraduate", "K-12",
|
|
44
|
+
)
|
|
45
|
+
OUTCOME_CUES = (
|
|
46
|
+
"独立编程", "学习效果", "学习保持", "迁移", "学业成绩", "保持", "retention",
|
|
47
|
+
"transfer", "learning", "成绩", "能力", "依赖", "诚信",
|
|
48
|
+
)
|
|
49
|
+
CONTEXT_CUES = ("线上", "线下", "混合", "小班", "大班", "MOOC", "翻转", "课堂", "实验室")
|
|
50
|
+
|
|
51
|
+
SLOT_LABELS = {
|
|
52
|
+
"population": "人群(目标学习者/决策对象)",
|
|
53
|
+
"intervention": "干预(具体方法/工具及用法)",
|
|
54
|
+
"comparison": "对照(与什么比较;可用 BAU)",
|
|
55
|
+
"primary_outcome": "主结果(一个主要结果指标)",
|
|
56
|
+
"context": "情境(学制/班型/线上线下/支持条件)",
|
|
57
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Depth auto guideline: 单点→S,常规采用→M,高影响/争议/要试点→L,不确定取 M."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .constants import (
|
|
7
|
+
DEPTH_ALIASES,
|
|
8
|
+
DEPTH_CHOICES,
|
|
9
|
+
L_AUTO_SIGNALS,
|
|
10
|
+
M_AUTO_SIGNALS,
|
|
11
|
+
S_AUTO_SIGNALS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def auto_resolve_depth(question: str, hints: dict[str, Any] | None = None) -> tuple[str, str]:
|
|
16
|
+
text = (question or "").lower()
|
|
17
|
+
hints = hints or {}
|
|
18
|
+
blob = " ".join(str(v) for v in hints.values() if isinstance(v, str)).lower()
|
|
19
|
+
hay = f"{text} {blob}"
|
|
20
|
+
strong_l = any(k.lower() in hay for k in L_AUTO_SIGNALS)
|
|
21
|
+
if any(k.lower() in hay for k in S_AUTO_SIGNALS) and not strong_l:
|
|
22
|
+
return "S", "auto: single-point lookup signal → S"
|
|
23
|
+
if strong_l:
|
|
24
|
+
return "L", "auto: high-impact / contested / pilot-or-policy signal → L"
|
|
25
|
+
if any(k.lower() in hay for k in M_AUTO_SIGNALS):
|
|
26
|
+
return "M", "auto: routine adoption / comparison signal → M"
|
|
27
|
+
return "M", "auto: uncertain → M (guideline default)"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_depth(choice: str | None, question: str,
|
|
31
|
+
hints: dict[str, Any] | None = None,
|
|
32
|
+
prefs: dict[str, Any] | None = None) -> tuple[str, str, str]:
|
|
33
|
+
"""Return (depth_choice, resolved_depth, rationale)."""
|
|
34
|
+
prefs = prefs or {}
|
|
35
|
+
if choice is not None:
|
|
36
|
+
choice = DEPTH_ALIASES.get(str(choice), str(choice))
|
|
37
|
+
if choice in ("S", "M", "L"):
|
|
38
|
+
return choice, choice, f"explicit choice {choice}"
|
|
39
|
+
if choice == "auto":
|
|
40
|
+
depth, why = auto_resolve_depth(question, hints)
|
|
41
|
+
return "auto", depth, why
|
|
42
|
+
pref = prefs.get("depth_preference") or "auto"
|
|
43
|
+
if pref in ("S", "M", "L"):
|
|
44
|
+
return pref, pref, f"prefs.depth_preference={pref}"
|
|
45
|
+
depth, why = auto_resolve_depth(question, hints)
|
|
46
|
+
return "auto", depth, why
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normalize_depth_choice(raw: str | None) -> str | None:
|
|
50
|
+
if raw is None:
|
|
51
|
+
return None
|
|
52
|
+
mapped = DEPTH_ALIASES.get(str(raw), str(raw))
|
|
53
|
+
return mapped if mapped in DEPTH_CHOICES else raw
|