a6f05ab2d5
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd) - Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py) - Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks - Fix .gitignore to track tools/ and .githooks/ - Fix git remote URL (remove plaintext token) - New wiki pages: 504 pages, 34 raw sources
324 lines
11 KiB
Python
324 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
import sys
|
|
import io
|
|
import json
|
|
import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, List, Any, Optional
|
|
import random
|
|
|
|
if sys.platform == "win32":
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
|
|
|
|
class KnowledgeGraphUpdater:
|
|
def __init__(self, canvas_path: Path, extracted_data_path: Path):
|
|
self.canvas_path = canvas_path
|
|
self.data_path = extracted_data_path
|
|
self.canvas_data = self.load_canvas()
|
|
self.extracted_data = self.load_extracted_data()
|
|
|
|
def load_canvas(self) -> Dict[str, Any]:
|
|
try:
|
|
with open(self.canvas_path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
print(f"Error loading canvas: {str(e)}")
|
|
return {}
|
|
|
|
def load_extracted_data(self) -> Dict[str, Any]:
|
|
try:
|
|
with open(self.data_path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
print(f"Error loading extracted data: {str(e)}")
|
|
return {}
|
|
|
|
def get_existing_institutions(self) -> set:
|
|
institutions = set()
|
|
for node in self.canvas_data.get("nodes", []):
|
|
if node.get("type") == "text" and "机构档案" in node.get("id", ""):
|
|
institutions.add(node["id"])
|
|
return institutions
|
|
|
|
def generate_node_id(self, prefix: str, name: str) -> str:
|
|
safe_name = name.replace(" ", "_").replace("/", "_").replace("\\", "_")[:20]
|
|
return (
|
|
f"{prefix}_{safe_name}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
)
|
|
|
|
def create_institution_node(
|
|
self, inst: Dict[str, Any], x: int, y: int
|
|
) -> Dict[str, Any]:
|
|
inst_name = inst.get("name", "Unknown")
|
|
inst_type = inst.get("type", "Unknown")
|
|
quality_score = inst.get("quality_score", 0)
|
|
key_data = inst.get("key_data", "")
|
|
|
|
node_id = self.generate_node_id("node", inst_name)
|
|
|
|
node = {
|
|
"id": node_id,
|
|
"type": "text",
|
|
"x": x + random.randint(-50, 50),
|
|
"y": y + random.randint(-50, 50),
|
|
"width": 300,
|
|
"height": 120,
|
|
"color": "1" if quality_score >= 85 else "2",
|
|
"text": f"## {inst_name}\n**类型**: {inst_type}\n**质量分**: {quality_score:.1f}\n**核心数据**: {key_data[:50]}...",
|
|
}
|
|
|
|
return node_id, node
|
|
|
|
def create_edge(
|
|
self, source_id: str, target_id: str, label: str = ""
|
|
) -> Dict[str, Any]:
|
|
edge = {
|
|
"id": f"edge_{source_id}_{target_id}",
|
|
"from": source_id,
|
|
"to": target_id,
|
|
"label": label,
|
|
}
|
|
return edge
|
|
|
|
def add_new_institutions(self):
|
|
new_nodes = []
|
|
new_edges = []
|
|
|
|
institutions = self.extracted_data.get("reports", [])
|
|
existing_institutions = self.get_existing_institutions()
|
|
|
|
group_positions = {
|
|
"教师工具": {"x": 1500, "y": -850, "group": "group_edtech000001"},
|
|
"协作辅导": {"x": 1850, "y": -750, "group": "group_edtech000001"},
|
|
"K-12平台": {"x": 1500, "y": -650, "group": "group_edtech000001"},
|
|
}
|
|
|
|
for report in institutions:
|
|
for inst in report.get("institutions", []):
|
|
inst_name = inst.get("name", "")
|
|
inst_type = inst.get("type", "")
|
|
|
|
if inst_name and inst_name not in existing_institutions:
|
|
position = group_positions.get(
|
|
inst_type, {"x": 1200, "y": -500, "group": "group_reports00001"}
|
|
)
|
|
node_id, node = self.create_institution_node(
|
|
inst, position["x"], position["y"]
|
|
)
|
|
|
|
new_nodes.append(node)
|
|
existing_institutions.add(inst_name)
|
|
|
|
if position.get("group"):
|
|
edge = self.create_edge(
|
|
position["group"], node_id, f"属于{inst_type}"
|
|
)
|
|
new_edges.append(edge)
|
|
|
|
if new_nodes:
|
|
self.canvas_data["nodes"].extend(new_nodes)
|
|
print(f"✅ Added {len(new_nodes)} new institution nodes")
|
|
|
|
if new_edges:
|
|
existing_edges = self.canvas_data.get("edges", [])
|
|
existing_edges.extend(new_edges)
|
|
self.canvas_data["edges"] = existing_edges
|
|
print(f"✅ Added {len(new_edges)} new edges")
|
|
|
|
return len(new_nodes), len(new_edges)
|
|
|
|
def update_metadata(self):
|
|
today = datetime.date.today()
|
|
week_number = today.isocalendar()[1]
|
|
|
|
total_institutions = sum(
|
|
len(report.get("institutions", []))
|
|
for report in self.extracted_data.get("reports", [])
|
|
)
|
|
|
|
for node in self.canvas_data.get("nodes", []):
|
|
if node.get("id") == "title001canvasv12":
|
|
node["text"] = (
|
|
f"# 全球教育AI知识图谱\n更新日期:{today} | W{week_number} | 机构总计:{total_institutions}家"
|
|
)
|
|
print(f"✅ Updated metadata in title node")
|
|
break
|
|
|
|
def add_research_report_nodes(self):
|
|
new_nodes = []
|
|
reports = self.extracted_data.get("reports", [])
|
|
existing_reports = set()
|
|
|
|
for node in self.canvas_data.get("nodes", []):
|
|
if node.get("type") == "text" and "研究计划" in node.get("text", ""):
|
|
existing_reports.add(node["id"])
|
|
|
|
report_count = 0
|
|
base_y = -300
|
|
base_x = 1600
|
|
|
|
for report in reports:
|
|
theme = report.get("theme", "")
|
|
week_num = report.get("week_number", "")
|
|
|
|
if theme and f"W{week_num}" not in existing_reports and report_count < 5:
|
|
node_id = f"report_W{week_num}"
|
|
y_pos = base_y + (report_count * 150)
|
|
|
|
node = {
|
|
"id": node_id,
|
|
"type": "text",
|
|
"x": base_x + random.randint(-50, 50),
|
|
"y": y_pos,
|
|
"width": 400,
|
|
"height": 100,
|
|
"color": "6",
|
|
"text": f"**W{week_num}**: {theme[:40]}...",
|
|
}
|
|
|
|
new_nodes.append(node)
|
|
existing_reports.add(f"W{week_num}")
|
|
report_count += 1
|
|
|
|
if new_nodes:
|
|
self.canvas_data["nodes"].extend(new_nodes)
|
|
print(f"✅ Added {len(new_nodes)} research report nodes")
|
|
|
|
reports_group_id = "group_reports00001"
|
|
for node_id in [node["id"] for node in new_nodes]:
|
|
edge = self.create_edge(reports_group_id, node_id, "研究")
|
|
existing_edges = self.canvas_data.get("edges", [])
|
|
existing_edges.append(edge)
|
|
self.canvas_data["edges"] = existing_edges
|
|
|
|
print(f"✅ Added {len(new_nodes)} edges to research reports group")
|
|
|
|
return len(new_nodes)
|
|
|
|
def save_updated_canvas(self):
|
|
output_path = (
|
|
self.canvas_path.parent
|
|
/ f"全球教育AI机构关系图谱_v{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.canvas"
|
|
)
|
|
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
json.dump(self.canvas_data, f, ensure_ascii=False, indent=2)
|
|
|
|
print(f"✅ Updated canvas saved to: {output_path}")
|
|
return output_path
|
|
|
|
def generate_update_report(self) -> str:
|
|
update_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
report = f"""# 知识图谱更新报告
|
|
|
|
## 基本信息
|
|
|
|
- **更新时间**: {update_time}
|
|
- **源文件**: {self.canvas_path.name}
|
|
- **数据来源**: {self.data_path.name}
|
|
- **新版本**: v{datetime.datetime.now().strftime("%Y%m%d")}
|
|
|
|
## 更新内容
|
|
|
|
### 新增节点
|
|
|
|
- **机构档案节点**: 基于每周报告提取的新机构
|
|
- **研究报告节点**: 近期研究计划/报告主题
|
|
|
|
### 更新的元数据
|
|
|
|
- **标题节点**: 更新日期和机构总数
|
|
- **版本号**: 自动递增
|
|
|
|
## 图谱统计
|
|
|
|
- **总节点数**: {len(self.canvas_data.get("nodes", []))}
|
|
- **总边数**: {len(self.canvas_data.get("edges", []))}
|
|
- **分组数**: {len([n for n in self.canvas_data.get("nodes", []) if n.get("type") == "group"])}
|
|
|
|
## 下一步建议
|
|
|
|
1. 手动检查节点布局,调整重叠
|
|
2. 添加更多关系边以增强连接性
|
|
3. 定期更新以保持图谱时效性
|
|
|
|
---
|
|
|
|
*更新工具: Knowledge Graph Updater*
|
|
*自动生成时间: {update_time}*
|
|
"""
|
|
|
|
return report
|
|
|
|
def save_update_report(self, report_path: Path):
|
|
report_content = self.generate_update_report()
|
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(report_path, "w", encoding="utf-8") as f:
|
|
f.write(report_content)
|
|
|
|
print(f"✅ Update report saved to: {report_path}")
|
|
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print("Knowledge Graph Visualization Update Tool")
|
|
print("=" * 60)
|
|
print(f"Execution time: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
print()
|
|
|
|
project_dir = Path(__file__).parent.parent
|
|
canvas_path = project_dir / "全球教育AI机构关系图谱.canvas"
|
|
extracted_data_path = (
|
|
project_dir / "outputs" / "weekly_reports_extraction_2026-04-16.json"
|
|
)
|
|
|
|
if not canvas_path.exists():
|
|
print(f"❌ Canvas file not found: {canvas_path}")
|
|
return
|
|
|
|
if not extracted_data_path.exists():
|
|
print(f"❌ Extracted data file not found: {extracted_data_path}")
|
|
print("Please run extract_weekly_reports.py first")
|
|
return
|
|
|
|
updater = KnowledgeGraphUpdater(canvas_path, extracted_data_path)
|
|
|
|
print("Step 1: Adding new institution nodes...")
|
|
new_insts, new_edges = updater.add_new_institutions()
|
|
|
|
print("\nStep 2: Adding research report nodes...")
|
|
new_reports = updater.add_research_report_nodes()
|
|
|
|
print("\nStep 3: Updating metadata...")
|
|
updater.update_metadata()
|
|
|
|
print("\nStep 4: Saving updated canvas...")
|
|
output_canvas = updater.save_updated_canvas()
|
|
|
|
print("\nStep 5: Generating update report...")
|
|
report_path = (
|
|
output_canvas.parent
|
|
/ "outputs"
|
|
/ f"knowledge_graph_update_report_{datetime.date.today()}.md"
|
|
)
|
|
updater.save_update_report(report_path)
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("Update Complete")
|
|
print("=" * 60)
|
|
print(f"New institutions: {new_insts}")
|
|
print(f"New edges: {new_edges}")
|
|
print(f"New research reports: {new_reports}")
|
|
print(f"Output canvas: {output_canvas.name}")
|
|
print(f"Update report: {report_path.name}")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|