413 lines
15 KiB
Python
413 lines
15 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
"""
|
|||
|
|
Concept Graph Material → OpenMAIC Requirement 转换器
|
|||
|
|
|
|||
|
|
将 WeKnora wiki 知识图谱中的 concept 页面及其关联 entity 转换为
|
|||
|
|
结构化的 OpenMAIC 课程生成需求描述。两阶段转换:
|
|||
|
|
1. Concept Graph Material → Pedagogical Design JSON
|
|||
|
|
2. Pedagogical Design JSON → requirement string
|
|||
|
|
|
|||
|
|
此脚本仅做数据转换,不涉及网络调用。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
echo '{"concept": {...}, "entities": [...]}' | python scripts/concept-to-requirement.py
|
|||
|
|
python scripts/concept-to-requirement.py --file input.json
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _tokenize(text: str) -> list[str]:
|
|||
|
|
"""Simple whitespace + punctuation tokenizer for title/slug overlap."""
|
|||
|
|
return [t.lower() for t in re.split(r"[\s_\-/]+", text) if t]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _score_entity(
|
|||
|
|
entity: dict[str, Any],
|
|||
|
|
concept_title_tokens: list[str],
|
|||
|
|
concept_summary_keywords: list[str],
|
|||
|
|
concept_slug_tokens: list[str],
|
|||
|
|
) -> int:
|
|||
|
|
"""Score an entity by relevance to the concept (pure text heuristics)."""
|
|||
|
|
score = 0
|
|||
|
|
|
|||
|
|
# link_type scoring
|
|||
|
|
link_type = entity.get("link_type", "")
|
|||
|
|
if link_type == "bidirectional":
|
|||
|
|
score += 3
|
|||
|
|
elif link_type == "outlink":
|
|||
|
|
score += 2
|
|||
|
|
elif link_type == "inlink":
|
|||
|
|
score += 1
|
|||
|
|
|
|||
|
|
# title token overlap
|
|||
|
|
entity_title_tokens = set(_tokenize(entity.get("title", "")))
|
|||
|
|
overlap = entity_title_tokens & set(concept_title_tokens)
|
|||
|
|
score += min(len(overlap), 2)
|
|||
|
|
|
|||
|
|
# summary keyword hit
|
|||
|
|
entity_summary = (entity.get("summary") or "").lower()
|
|||
|
|
keyword_hits = sum(1 for kw in concept_summary_keywords if kw in entity_summary)
|
|||
|
|
score += min(keyword_hits, 2)
|
|||
|
|
|
|||
|
|
# slug token hit
|
|||
|
|
entity_slug_tokens = set(_tokenize(entity.get("slug", "")))
|
|||
|
|
slug_overlap = entity_slug_tokens & set(concept_slug_tokens)
|
|||
|
|
score += min(len(slug_overlap), 1)
|
|||
|
|
|
|||
|
|
# penalty for empty summary
|
|||
|
|
if not entity.get("summary"):
|
|||
|
|
score -= 2
|
|||
|
|
|
|||
|
|
return score
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _classify_entity(
|
|||
|
|
entity: dict[str, Any], concept_title: str
|
|||
|
|
) -> str:
|
|||
|
|
"""Classify an entity into a pedagogical role."""
|
|||
|
|
title = (entity.get("title") or "").lower()
|
|||
|
|
summary = (entity.get("summary") or "").lower()
|
|||
|
|
text = f"{title} {summary}"
|
|||
|
|
|
|||
|
|
tool_keywords = ["工具", "平台", "框架", "库", "sdk", "api", "tool", "platform", "framework", "library"]
|
|||
|
|
example_keywords = ["案例", "实例", "示例", "应用", "case", "example", "application", "demo"]
|
|||
|
|
prereq_keywords = ["前提", "基础", "前置", "先决", "prerequisite", "foundation", "basic"]
|
|||
|
|
|
|||
|
|
if any(kw in text for kw in tool_keywords):
|
|||
|
|
return "Tools"
|
|||
|
|
if any(kw in text for kw in example_keywords):
|
|||
|
|
return "Examples"
|
|||
|
|
if any(kw in text for kw in prereq_keywords):
|
|||
|
|
return "Prerequisites"
|
|||
|
|
return "Application Scenarios"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_markdown_sections(content: str) -> dict[str, str]:
|
|||
|
|
"""Parse markdown content into sections keyed by heading."""
|
|||
|
|
sections: dict[str, str] = {}
|
|||
|
|
current_heading = ""
|
|||
|
|
current_lines: list[str] = []
|
|||
|
|
|
|||
|
|
for line in content.split("\n"):
|
|||
|
|
heading_match = re.match(r"^(#{1,4})\s+(.+)$", line)
|
|||
|
|
if heading_match:
|
|||
|
|
if current_heading:
|
|||
|
|
sections[current_heading] = "\n".join(current_lines).strip()
|
|||
|
|
current_heading = heading_match.group(2).strip()
|
|||
|
|
current_lines = []
|
|||
|
|
else:
|
|||
|
|
current_lines.append(line)
|
|||
|
|
|
|||
|
|
if current_heading:
|
|||
|
|
sections[current_heading] = "\n".join(current_lines).strip()
|
|||
|
|
|
|||
|
|
return sections
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _extract_key_points(sections: dict[str, str]) -> list[str]:
|
|||
|
|
"""Extract key points from markdown sections (definitions, mechanisms)."""
|
|||
|
|
points: list[str] = []
|
|||
|
|
definition_headings = {"定义", "概念", "概述", "简介", "Definition", "Overview", "Introduction"}
|
|||
|
|
mechanism_headings = {"机制", "原理", "工作原理", "Mechanism", "How it works", "Principle"}
|
|||
|
|
|
|||
|
|
for heading, body in sections.items():
|
|||
|
|
if any(d in heading for d in definition_headings):
|
|||
|
|
first_para = body.split("\n\n")[0].strip()
|
|||
|
|
if first_para:
|
|||
|
|
points.append(first_para[:200])
|
|||
|
|
elif any(m in heading for m in mechanism_headings):
|
|||
|
|
bullets = [l.strip().lstrip("-*• ") for l in body.split("\n") if l.strip().startswith(("- ", "* ", "• "))]
|
|||
|
|
points.extend(bullets[:3])
|
|||
|
|
|
|||
|
|
return points[:5]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _extract_examples(sections: dict[str, str]) -> list[str]:
|
|||
|
|
"""Extract examples from markdown sections."""
|
|||
|
|
example_headings = {"案例", "示例", "实例", "应用场景", "Example", "Use Case", "Application"}
|
|||
|
|
examples: list[str] = []
|
|||
|
|
|
|||
|
|
for heading, body in sections.items():
|
|||
|
|
if any(e in heading for e in example_headings):
|
|||
|
|
bullets = [l.strip().lstrip("-*• ") for l in body.split("\n") if l.strip().startswith(("- ", "* ", "• "))]
|
|||
|
|
examples.extend(bullets[:3])
|
|||
|
|
|
|||
|
|
return examples[:3]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _extract_misconceptions(sections: dict[str, str]) -> list[str]:
|
|||
|
|
"""Extract common misconceptions from markdown sections."""
|
|||
|
|
misconception_headings = {"误区", "常见错误", "误解", "Misconception", "Common mistake", "Pitfall"}
|
|||
|
|
misconceptions: list[str] = []
|
|||
|
|
|
|||
|
|
for heading, body in sections.items():
|
|||
|
|
if any(m in heading for m in misconception_headings):
|
|||
|
|
bullets = [l.strip().lstrip("-*• ") for l in body.split("\n") if l.strip().startswith(("- ", "* ", "• "))]
|
|||
|
|
misconceptions.extend(bullets[:3])
|
|||
|
|
|
|||
|
|
return misconceptions[:3]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_pedagogical_design(data: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""Stage 1: Concept Graph Material → Pedagogical Design JSON."""
|
|||
|
|
concept = data["concept"]
|
|||
|
|
entities = data.get("entities", [])
|
|||
|
|
language = data.get("language", "zh-CN")
|
|||
|
|
depth = data.get("depth", "intermediate")
|
|||
|
|
audience = data.get("audience", "相关领域的学习者")
|
|||
|
|
|
|||
|
|
concept_title = concept.get("title", "")
|
|||
|
|
concept_summary = concept.get("summary", "")
|
|||
|
|
concept_content = concept.get("content", "")
|
|||
|
|
concept_slug = concept.get("slug", "")
|
|||
|
|
|
|||
|
|
# Parse markdown sections from content
|
|||
|
|
sections = _parse_markdown_sections(concept_content) if concept_content else {}
|
|||
|
|
|
|||
|
|
# Extract pedagogical elements from content
|
|||
|
|
key_points = _extract_key_points(sections)
|
|||
|
|
examples_from_content = _extract_examples(sections)
|
|||
|
|
misconceptions = _extract_misconceptions(sections)
|
|||
|
|
|
|||
|
|
# Score and rank entities
|
|||
|
|
concept_title_tokens = _tokenize(concept_title)
|
|||
|
|
concept_summary_keywords = [w.lower() for w in re.findall(r"\w+", concept_summary) if len(w) > 1]
|
|||
|
|
concept_slug_tokens = _tokenize(concept_slug)
|
|||
|
|
|
|||
|
|
scored_entities = []
|
|||
|
|
for entity in entities:
|
|||
|
|
score = _score_entity(entity, concept_title_tokens, concept_summary_keywords, concept_slug_tokens)
|
|||
|
|
scored_entities.append((score, entity))
|
|||
|
|
scored_entities.sort(key=lambda x: x[0], reverse=True)
|
|||
|
|
|
|||
|
|
# Select top entities (3-5)
|
|||
|
|
top_count = min(max(3, len(scored_entities)), 5)
|
|||
|
|
top_entities = scored_entities[:top_count]
|
|||
|
|
|
|||
|
|
# Classify entities into pedagogical roles
|
|||
|
|
classified: dict[str, list[dict[str, Any]]] = {
|
|||
|
|
"Examples": [],
|
|||
|
|
"Tools": [],
|
|||
|
|
"Application Scenarios": [],
|
|||
|
|
"Prerequisites": [],
|
|||
|
|
}
|
|||
|
|
for score, entity in top_entities:
|
|||
|
|
role = _classify_entity(entity, concept_title)
|
|||
|
|
classified[role].append({
|
|||
|
|
"slug": entity.get("slug", ""),
|
|||
|
|
"title": entity.get("title", ""),
|
|||
|
|
"summary": entity.get("summary", ""),
|
|||
|
|
"link_type": entity.get("link_type", ""),
|
|||
|
|
"relevance_score": score,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# Build learning objectives from concept summary
|
|||
|
|
learning_objectives: list[str] = []
|
|||
|
|
if concept_summary:
|
|||
|
|
sentences = re.split(r"[。!?.!?]", concept_summary)
|
|||
|
|
learning_objectives = [f"理解{s.strip()}" for s in sentences if s.strip()][:3]
|
|||
|
|
if not learning_objectives:
|
|||
|
|
learning_objectives = [f"掌握 {concept_title} 的核心概念"]
|
|||
|
|
|
|||
|
|
# Build practice tasks from entity examples
|
|||
|
|
practice_tasks: list[str] = []
|
|||
|
|
for ent in classified.get("Examples", []):
|
|||
|
|
practice_tasks.append(f"通过 {ent['title']} 实践 {concept_title} 的应用")
|
|||
|
|
for ent in classified.get("Application Scenarios", []):
|
|||
|
|
practice_tasks.append(f"分析 {ent['title']} 在 {concept_title} 中的作用")
|
|||
|
|
if not practice_tasks and top_entities:
|
|||
|
|
_, first_ent = top_entities[0]
|
|||
|
|
practice_tasks.append(f"结合 {first_ent.get('title', '相关实体')} 理解 {concept_title} 的实际应用")
|
|||
|
|
|
|||
|
|
# Build prerequisites from entity prerequisites
|
|||
|
|
prerequisites: list[str] = []
|
|||
|
|
for ent in classified.get("Prerequisites", []):
|
|||
|
|
prerequisites.append(ent["title"])
|
|||
|
|
|
|||
|
|
# Build assessment prompts
|
|||
|
|
assessment_prompts: list[str] = []
|
|||
|
|
if concept_summary:
|
|||
|
|
assessment_prompts.append(f"请解释 {concept_title} 的核心定义")
|
|||
|
|
if key_points:
|
|||
|
|
assessment_prompts.append(f"请描述 {concept_title} 的工作机制")
|
|||
|
|
if classified.get("Examples"):
|
|||
|
|
assessment_prompts.append(f"请举例说明 {concept_title} 的实际应用")
|
|||
|
|
|
|||
|
|
# Build warnings from misconceptions
|
|||
|
|
warnings: list[str] = []
|
|||
|
|
for m in misconceptions:
|
|||
|
|
warnings.append(f"常见误区:{m}")
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"concept_slug": concept_slug,
|
|||
|
|
"title": concept_title,
|
|||
|
|
"teaching_anchor": concept_summary or concept_title,
|
|||
|
|
"learning_objectives": learning_objectives,
|
|||
|
|
"key_points": key_points,
|
|||
|
|
"examples": examples_from_content,
|
|||
|
|
"practice_tasks": practice_tasks,
|
|||
|
|
"prerequisites": prerequisites,
|
|||
|
|
"misconception_checks": misconceptions,
|
|||
|
|
"assessment_prompts": assessment_prompts,
|
|||
|
|
"warnings": warnings,
|
|||
|
|
"classified_entities": classified,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_requirement(design: dict[str, Any], data: dict[str, Any]) -> str:
|
|||
|
|
"""Stage 2: Pedagogical Design JSON → requirement string."""
|
|||
|
|
concept = data["concept"]
|
|||
|
|
depth = data.get("depth", "intermediate")
|
|||
|
|
audience = data.get("audience", "相关领域的学习者")
|
|||
|
|
language = data.get("language", "zh-CN")
|
|||
|
|
|
|||
|
|
depth_map = {"beginner": "入门", "intermediate": "中级", "advanced": "高级"}
|
|||
|
|
depth_cn = depth_map.get(depth, "中级")
|
|||
|
|
|
|||
|
|
parts: list[str] = []
|
|||
|
|
|
|||
|
|
# Header
|
|||
|
|
parts.append(f"基于知识图谱概念「{design['title']}」,为{audience}创建一个{depth_cn}微课堂(micro-classroom)。")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Teaching anchor
|
|||
|
|
parts.append(f"教学锚点:{design['teaching_anchor']}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Learning objectives
|
|||
|
|
if design["learning_objectives"]:
|
|||
|
|
parts.append("学习目标:")
|
|||
|
|
for obj in design["learning_objectives"]:
|
|||
|
|
parts.append(f" - {obj}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Key points
|
|||
|
|
if design["key_points"]:
|
|||
|
|
parts.append("核心知识点:")
|
|||
|
|
for kp in design["key_points"]:
|
|||
|
|
parts.append(f" - {kp}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Classified entities as practice context
|
|||
|
|
classified = design.get("classified_entities", {})
|
|||
|
|
entity_sections = []
|
|||
|
|
for role in ("Examples", "Tools", "Application Scenarios", "Prerequisites"):
|
|||
|
|
ents = classified.get(role, [])
|
|||
|
|
if ents:
|
|||
|
|
role_cn = {
|
|||
|
|
"Examples": "案例",
|
|||
|
|
"Tools": "工具",
|
|||
|
|
"Application Scenarios": "应用场景",
|
|||
|
|
"Prerequisites": "前置知识",
|
|||
|
|
}[role]
|
|||
|
|
ent_descs = [f"{e['title']}" + (f":{e['summary'][:80]}" if e.get("summary") else "") for e in ents]
|
|||
|
|
entity_sections.append(f"{role_cn}:{';'.join(ent_descs)}")
|
|||
|
|
|
|||
|
|
if entity_sections:
|
|||
|
|
parts.append("关联实体(实践环节):")
|
|||
|
|
for section in entity_sections:
|
|||
|
|
parts.append(f" - {section}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Practice tasks
|
|||
|
|
if design["practice_tasks"]:
|
|||
|
|
parts.append("实践任务:")
|
|||
|
|
for task in design["practice_tasks"]:
|
|||
|
|
parts.append(f" - {task}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Misconception checks
|
|||
|
|
if design["misconception_checks"]:
|
|||
|
|
parts.append("常见误区检查:")
|
|||
|
|
for mc in design["misconception_checks"]:
|
|||
|
|
parts.append(f" - {mc}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Assessment
|
|||
|
|
if design["assessment_prompts"]:
|
|||
|
|
parts.append("评估提示:")
|
|||
|
|
for ap in design["assessment_prompts"]:
|
|||
|
|
parts.append(f" - {ap}")
|
|||
|
|
parts.append("")
|
|||
|
|
|
|||
|
|
# Language directive
|
|||
|
|
if language == "zh-CN":
|
|||
|
|
parts.append("请使用中文生成课程内容。")
|
|||
|
|
|
|||
|
|
# Concept content fallback
|
|||
|
|
concept_content = concept.get("content", "")
|
|||
|
|
if concept_content and len(concept_content) > 200:
|
|||
|
|
parts.append("")
|
|||
|
|
parts.append(f"参考内容(前500字):{concept_content[:500]}")
|
|||
|
|
|
|||
|
|
return "\n".join(parts)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def process(input_data: dict[str, Any]) -> dict[str, Any]:
|
|||
|
|
"""Main processing: two-stage conversion."""
|
|||
|
|
concept = input_data.get("concept")
|
|||
|
|
if not concept:
|
|||
|
|
return {
|
|||
|
|
"requirement": "",
|
|||
|
|
"pedagogical_design": {},
|
|||
|
|
"metadata": {"error": "Missing 'concept' in input"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
entities = input_data.get("entities", [])
|
|||
|
|
|
|||
|
|
# Stage 1: Build pedagogical design
|
|||
|
|
design = build_pedagogical_design(input_data)
|
|||
|
|
|
|||
|
|
# Stage 2: Build requirement string
|
|||
|
|
requirement = build_requirement(design, input_data)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"requirement": requirement,
|
|||
|
|
"pedagogical_design": design,
|
|||
|
|
"metadata": {
|
|||
|
|
"concept_slug": concept.get("slug", ""),
|
|||
|
|
"entity_count": len(entities),
|
|||
|
|
"depth": input_data.get("depth", "intermediate"),
|
|||
|
|
"language": input_data.get("language", "zh-CN"),
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> None:
|
|||
|
|
"""Entry point: read from stdin or file, output JSON."""
|
|||
|
|
import argparse
|
|||
|
|
|
|||
|
|
parser = argparse.ArgumentParser(description="Concept Graph → OpenMAIC Requirement 转换器")
|
|||
|
|
parser.add_argument("--file", "-f", help="输入 JSON 文件路径")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
if args.file:
|
|||
|
|
with open(args.file, "r", encoding="utf-8") as f:
|
|||
|
|
input_data = json.load(f)
|
|||
|
|
else:
|
|||
|
|
input_text = sys.stdin.read()
|
|||
|
|
if not input_text.strip():
|
|||
|
|
print(
|
|||
|
|
"错误: 未提供输入数据。用法:\n"
|
|||
|
|
' echo \'{"concept": {...}, "entities": [...]}\' | python concept-to-requirement.py\n'
|
|||
|
|
" python concept-to-requirement.py --file input.json",
|
|||
|
|
file=sys.stderr,
|
|||
|
|
)
|
|||
|
|
sys.exit(1)
|
|||
|
|
try:
|
|||
|
|
input_data = json.loads(input_text)
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f"错误: 输入 JSON 解析失败: {e}", file=sys.stderr)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
result = process(input_data)
|
|||
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|