math-tutor/tutor/history_quiz.py

185 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
历史知识问答引擎 — 七年级中国历史背诵题库
从 data/历史背诵题库.json 加载题目,支持随机出题、答题判断、进度追踪
"""
import json
import random
import os
from pathlib import Path
from datetime import datetime
QUIZ_FILE = Path(os.environ.get(
"HISTORY_QUIZ_FILE",
os.path.join(os.path.dirname(__file__), "..", "data", "历史背诵题库.json")
))
# 单元别名映射(用于前端显示)
UNIT_ALIASES = {
"U1": "七上·第一单元 史前时期",
"U2": "七上·第二单元 夏商周时期",
"U3": "七上·第三单元 秦汉时期",
"U4": "七上·第四单元 三国两晋南北朝",
"U5": "七下·第一单元 隋唐时期",
"U6": "七下·第二单元 辽宋夏金元",
"U7": "七下·第三单元 明清时期",
}
class HistoryQuiz:
"""历史题库引擎"""
def __init__(self):
self.data = self._load_quiz()
self.units = {u["unit_id"]: u for u in self.data.get("units", [])}
self.all_questions = []
for unit in self.data.get("units", []):
for q in unit.get("questions", []):
q["unit_id"] = unit["unit_id"]
q["unit_title"] = unit.get("title", "")
self.all_questions.append(q)
def _load_quiz(self) -> dict:
if QUIZ_FILE.exists():
return json.loads(QUIZ_FILE.read_text(encoding="utf-8"))
# 降级:空题库
return {"metadata": {}, "units": []}
def get_units(self) -> list:
"""获取所有单元信息(含题目数量)"""
result = []
for unit in self.data.get("units", []):
uid = unit["unit_id"]
result.append({
"unit_id": uid,
"title": unit.get("title", ""),
"grade": unit.get("grade", ""),
"alias": UNIT_ALIASES.get(uid, uid),
"question_count": len(unit.get("questions", [])),
"lessons": unit.get("lessons", []),
})
return result
def get_random_question(self, unit_id: str = None, question_type: str = None) -> dict:
"""随机获取一道题,可选按单元/题型过滤"""
pool = self.all_questions
if unit_id:
pool = [q for q in pool if q.get("unit_id") == unit_id]
if question_type:
pool = [q for q in pool if q.get("type") == question_type]
if not pool:
return {"error": "该条件下没有题目"}
q = random.choice(pool)
return {
"id": q["id"],
"type": q["type"],
"question": q["question"],
"answer": q["answer"],
"unit_id": q["unit_id"],
"unit_title": q.get("unit_title", ""),
"unit_alias": UNIT_ALIASES.get(q["unit_id"], q["unit_id"]),
"source": q.get("source", ""),
}
def get_batch(self, count: int = 10, unit_id: str = None, question_type: str = None) -> list:
"""批量获取题目(不重复)"""
pool = list(self.all_questions)
if unit_id:
pool = [q for q in pool if q.get("unit_id") == unit_id]
if question_type:
pool = [q for q in pool if q.get("type") == question_type]
random.shuffle(pool)
selected = pool[:count]
return [{
"id": q["id"],
"type": q["type"],
"question": q["question"],
"answer": q["answer"],
"unit_id": q["unit_id"],
"unit_title": q.get("unit_title", ""),
"unit_alias": UNIT_ALIASES.get(q["unit_id"], q["unit_id"]),
} for q in selected]
def check_answer(self, question_id: str, student_answer: str) -> dict:
"""检查答案 — 模糊匹配(关键词匹配)"""
q = next((q for q in self.all_questions if q["id"] == question_id), None)
if not q:
return {"correct": False, "error": "题目不存在"}
correct = q["answer"].strip()
student = student_answer.strip()
# 精确匹配
if student == correct:
return {
"correct": True,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"match_type": "exact"
}
# 包含匹配(学生答案包含在标准答案中,或反之)
if len(student) >= 2 and (student in correct or correct in student):
return {
"correct": True,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"match_type": "partial"
}
# 关键词匹配(答案拆分为关键词,匹配度>60%算对)
keywords = set(correct.replace("", " ").replace("", " ").replace("", " ").split())
student_words = set(student.replace("", " ").replace("", " ").replace("", " ").split())
if keywords and student_words:
overlap = len(keywords & student_words) / len(keywords)
if overlap >= 0.6:
return {
"correct": True,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"match_type": "keyword",
"keyword_match": f"{overlap:.0%}"
}
return {
"correct": False,
"question_id": question_id,
"student_answer": student,
"correct_answer": correct,
"hint": self._generate_hint(correct)
}
def _generate_hint(self, answer: str) -> str:
"""生成提示(显示答案首字+长度)"""
if len(answer) <= 1:
return "答案很短,再想想?"
first_char = answer[0]
return f"提示:答案以「{first_char}」开头,共{len(answer)}个字"
def get_stats(self) -> dict:
"""题库统计"""
type_counts = {}
for q in self.all_questions:
t = q.get("type", "unknown")
type_counts[t] = type_counts.get(t, 0) + 1
return {
"total_questions": len(self.all_questions),
"total_units": len(self.units),
"by_type": type_counts,
"by_unit": {uid: len(u.get("questions", [])) for uid, u in self.units.items()},
}
# 全局单例
_quiz_instance = None
def get_quiz() -> HistoryQuiz:
global _quiz_instance
if _quiz_instance is None:
_quiz_instance = HistoryQuiz()
return _quiz_instance