""" AI Processing - Leitner System. Spaced Repetition System für Q&A-Paare. """ from pathlib import Path from datetime import datetime, timedelta import json import logging logger = logging.getLogger(__name__) def update_leitner_progress(qa_path: Path, item_id: str, correct: bool) -> dict: """ Aktualisiert den Lernfortschritt eines Q&A-Items nach dem Leitner-System. Leitner-Boxen: - Box 0: Neu (noch nicht gelernt) - Box 1: Gelernt (bei Fehler → zurück zu Box 0) - Box 2: Gefestigt (bei Fehler → zurück zu Box 1) Bei korrekter Antwort: Box erhöhen (max 2) Bei falscher Antwort: Box verringern (min 0) Args: qa_path: Pfad zur *_qa.json Datei item_id: ID des Q&A-Items correct: True wenn korrekt beantwortet Returns: Dict mit aktualisiertem Item und Status """ if not qa_path.exists(): raise FileNotFoundError(f"Q&A-Datei nicht gefunden: {qa_path}") qa_data = json.loads(qa_path.read_text(encoding="utf-8")) # Finde das Item item = None for qa_item in qa_data.get("qa_items", []): if qa_item.get("id") == item_id: item = qa_item break if not item: return {"status": "NOT_FOUND", "message": f"Item {item_id} nicht gefunden"} # Aktualisiere Statistiken now = datetime.now().isoformat() item["last_seen"] = now if correct: item["correct_count"] = item.get("correct_count", 0) + 1 # Box erhöhen (max 2) current_box = item.get("leitner_box", 0) if current_box < 2: item["leitner_box"] = current_box + 1 # Nächste Wiederholung basierend auf Box # Box 0→1: Nach 1 Tag, Box 1→2: Nach 3 Tagen, Box 2: Nach 7 Tagen days = [1, 3, 7][item["leitner_box"]] item["next_review"] = (datetime.now() + timedelta(days=days)).isoformat() else: item["incorrect_count"] = item.get("incorrect_count", 0) + 1 # Box verringern (min 0) current_box = item.get("leitner_box", 0) if current_box > 0: item["leitner_box"] = current_box - 1 # Bei Fehler: Bald wiederholen item["next_review"] = (datetime.now() + timedelta(hours=4)).isoformat() # Speichere aktualisierte Daten qa_path.write_text(json.dumps(qa_data, ensure_ascii=False, indent=2), encoding="utf-8") box_names = ["Neu", "Gelernt", "Gefestigt"] return { "status": "OK", "item_id": item_id, "correct": correct, "new_box": item["leitner_box"], "box_name": box_names[item["leitner_box"]], "correct_count": item["correct_count"], "incorrect_count": item["incorrect_count"], "next_review": item["next_review"] } def get_next_review_items(qa_path: Path, limit: int = 5) -> list: """ Gibt die nächsten zu wiederholenden Items zurück. Priorisierung: 1. Falsch beantwortete Items (Box 0) - häufiger 2. Gelernte Items (Box 1) deren Review fällig ist 3. Gefestigte Items (Box 2) zur gelegentlichen Auffrischung Args: qa_path: Pfad zur *_qa.json Datei limit: Maximale Anzahl Items Returns: Liste der zu wiederholenden Items (sortiert nach Priorität) """ if not qa_path.exists(): return [] qa_data = json.loads(qa_path.read_text(encoding="utf-8")) items = qa_data.get("qa_items", []) now = datetime.now() review_items = [] for item in items: box = item.get("leitner_box", 0) next_review = item.get("next_review") incorrect = item.get("incorrect_count", 0) # Priorität berechnen (niedriger = wichtiger) priority = box * 10 # Box 0 hat höchste Priorität # Bonus für häufig falsch beantwortete priority -= incorrect * 2 # Prüfe ob Review fällig is_due = True if next_review: try: review_time = datetime.fromisoformat(next_review) is_due = now >= review_time # Überfällige Items bekommen höhere Priorität if is_due: overdue_hours = (now - review_time).total_seconds() / 3600 priority -= overdue_hours except (ValueError, TypeError): is_due = True # Neue Items (Box 0) immer einschließen if box == 0 or is_due: review_items.append({ **item, "_priority": priority, "_is_due": is_due }) # Sortiere nach Priorität (niedrigste zuerst) review_items.sort(key=lambda x: x["_priority"]) # Entferne interne Felder und limitiere result = [] for item in review_items[:limit]: clean_item = {k: v for k, v in item.items() if not k.startswith("_")} result.append(clean_item) return result