[split-required] Split 500-850 LOC files (batch 2)
backend-lehrer (10 files): - game/database.py (785 → 5), correction_api.py (683 → 4) - classroom_engine/antizipation.py (676 → 5) - llm_gateway schools/edu_search already done in prior batch klausur-service (12 files): - orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4) - zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5) - eh_templates.py (658 → 5), mail/api.py (651 → 5) - qdrant_service.py (638 → 5), training_api.py (625 → 4) website (6 pages): - middleware (696 → 8), mail (733 → 6), consent (628 → 8) - compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7) studio-v2 (3 components): - B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2) - dashboard-experimental (739 → 2) admin-lehrer (4 files): - uebersetzungen (769 → 4), manager (670 → 2) - ChunkBrowserQA (675 → 6), dsfa/page (674 → 5) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
340
backend-lehrer/classroom_engine/antizipation_rules.py
Normal file
340
backend-lehrer/classroom_engine/antizipation_rules.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
Antizipation Engine - Rule definitions and RuleEngine.
|
||||
|
||||
Each rule evaluates signals and optionally produces a Suggestion.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from .antizipation_models import Signals, Suggestion, SuggestionTone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==================== Rule Base Class ====================
|
||||
|
||||
@dataclass
|
||||
class Rule:
|
||||
"""Eine Regel die Signale zu Vorschlaegen mappt."""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
"""Evaluiert die Regel und gibt einen Vorschlag zurueck oder None."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# ==================== Rule Implementations ====================
|
||||
|
||||
class R01_CreateClasses(Rule):
|
||||
"""Klassen anlegen wenn noch keine vorhanden."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R01",
|
||||
name="Klassen anlegen",
|
||||
description="Empfiehlt Klassen anzulegen bei neuem Lehrer"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.macro_phase == "onboarding" and not signals.has_classes:
|
||||
return Suggestion(
|
||||
id="suggest_create_classes",
|
||||
title="Klassen anlegen",
|
||||
description="Legen Sie Ihre Klassen an, um den vollen Funktionsumfang zu nutzen.",
|
||||
tone=SuggestionTone.HINT,
|
||||
priority=90,
|
||||
rule_id=self.id,
|
||||
icon="group_add",
|
||||
action_url="/classes/new",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R02_PrepareRubric(Rule):
|
||||
"""Erwartungshorizont erstellen wenn Klausur in 7 Tagen."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R02",
|
||||
name="Erwartungshorizont",
|
||||
description="Empfiehlt Erwartungshorizont vor Klausur"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.exams_in_7_days:
|
||||
exam = signals.exams_in_7_days[0]
|
||||
# Pruefen ob Vorbereitung noch nicht erledigt
|
||||
if not exam.get("preparation_done", False):
|
||||
days = 7 # Vereinfacht
|
||||
return Suggestion(
|
||||
id=f"suggest_rubric_{exam['id'][:8]}",
|
||||
title="Erwartungshorizont erstellen",
|
||||
description=f"Klausur '{exam['title']}' steht bevor.",
|
||||
tone=SuggestionTone.SUGGESTION,
|
||||
badge=f"in {days} Tagen",
|
||||
priority=80,
|
||||
rule_id=self.id,
|
||||
icon="assignment",
|
||||
action_url=f"/exams/{exam['id']}/rubric",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R03_StartCorrection(Rule):
|
||||
"""Korrektur starten nach Klausur."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R03",
|
||||
name="Korrektur starten",
|
||||
description="Empfiehlt Korrektur nach Klausur"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.exams_past_ungraded:
|
||||
exam = signals.exams_past_ungraded[0]
|
||||
return Suggestion(
|
||||
id=f"suggest_correction_{exam['id'][:8]}",
|
||||
title="Korrektur-Setup starten",
|
||||
description=f"Klausur '{exam['title']}' ist geschrieben.",
|
||||
tone=SuggestionTone.HINT,
|
||||
badge="bereit",
|
||||
priority=75,
|
||||
rule_id=self.id,
|
||||
icon="rate_review",
|
||||
action_url=f"/exams/{exam['id']}/correct",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R05_PrepareAgenda(Rule):
|
||||
"""Agenda vorbereiten wenn Konferenz heute."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R05",
|
||||
name="Konferenz-Agenda",
|
||||
description="Empfiehlt Agenda wenn Konferenz heute"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.has_conference_today:
|
||||
# Finde die Konferenz
|
||||
conf = next(
|
||||
(r for r in signals.routines_today
|
||||
if r.get("routine_type") in ("teacher_conference", "subject_conference")),
|
||||
None
|
||||
)
|
||||
if conf:
|
||||
return Suggestion(
|
||||
id="suggest_agenda",
|
||||
title="Konferenz-Agenda vorbereiten",
|
||||
description=f"{conf.get('title', 'Konferenz')} heute.",
|
||||
tone=SuggestionTone.SUGGESTION,
|
||||
badge="heute",
|
||||
priority=70,
|
||||
rule_id=self.id,
|
||||
icon="event_note",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R07_PlanFirstExam(Rule):
|
||||
"""Erste Klausur planen nach 4 Wochen."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R07",
|
||||
name="Erste Arbeit planen",
|
||||
description="Empfiehlt erste Klausur nach Anlaufphase"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if (signals.weeks_since_start >= 4 and
|
||||
signals.exams_scheduled_count == 0 and
|
||||
signals.has_classes):
|
||||
return Suggestion(
|
||||
id="suggest_first_exam",
|
||||
title="Erste Klassenarbeit planen",
|
||||
description="Nach 4 Wochen Unterricht ist ein guter Zeitpunkt fuer die erste Leistungsueberpruefung.",
|
||||
tone=SuggestionTone.SUGGESTION,
|
||||
priority=60,
|
||||
rule_id=self.id,
|
||||
icon="quiz",
|
||||
action_url="/exams/new",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R08_CorrectionMode(Rule):
|
||||
"""Korrekturmodus vor Ferien."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R08",
|
||||
name="Ferien-Korrekturmodus",
|
||||
description="Empfiehlt Korrekturen vor Ferien"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.is_before_holidays and signals.corrections_pending > 0:
|
||||
return Suggestion(
|
||||
id="suggest_correction_mode",
|
||||
title="Ferien-Korrekturmodus",
|
||||
description=f"{signals.corrections_pending} Korrekturen noch offen vor den Ferien.",
|
||||
tone=SuggestionTone.REMINDER,
|
||||
badge=f"{signals.days_until_holidays}d bis Ferien",
|
||||
priority=65,
|
||||
rule_id=self.id,
|
||||
icon="grading",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R09_TripChecklist(Rule):
|
||||
"""Klassenfahrt-Checkliste wenn Fahrt in 30 Tagen."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R09",
|
||||
name="Klassenfahrt-Checkliste",
|
||||
description="Empfiehlt Checkliste vor Klassenfahrt"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.trips_in_30_days:
|
||||
trip = signals.trips_in_30_days[0]
|
||||
return Suggestion(
|
||||
id=f"suggest_trip_{trip['id'][:8]}",
|
||||
title="Klassenfahrt-Checkliste",
|
||||
description=f"'{trip['title']}' steht bevor.",
|
||||
tone=SuggestionTone.SUGGESTION,
|
||||
badge="in 30 Tagen",
|
||||
priority=55,
|
||||
rule_id=self.id,
|
||||
icon="luggage",
|
||||
action_url=f"/trips/{trip['id']}/checklist",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R10_CompleteGrades(Rule):
|
||||
"""Noten vervollstaendigen vor Halbjahresende."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R10",
|
||||
name="Noten vervollstaendigen",
|
||||
description="Empfiehlt Noten vor Notenschluss"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if (signals.macro_phase in ("halbjahresabschluss", "jahresabschluss") and
|
||||
signals.grades_completion_ratio < 0.8):
|
||||
pct = int(signals.grades_completion_ratio * 100)
|
||||
return Suggestion(
|
||||
id="suggest_complete_grades",
|
||||
title="Noten vervollstaendigen",
|
||||
description=f"Nur {pct}% der Noten eingetragen. Notenschluss naht!",
|
||||
tone=SuggestionTone.REMINDER,
|
||||
priority=85,
|
||||
rule_id=self.id,
|
||||
icon="calculate",
|
||||
action_url="/grades",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R11_SetupSchedule(Rule):
|
||||
"""Stundenplan einrichten wenn noch nicht vorhanden."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R11",
|
||||
name="Stundenplan einrichten",
|
||||
description="Empfiehlt Stundenplan-Setup"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.macro_phase == "onboarding" and not signals.has_schedule:
|
||||
return Suggestion(
|
||||
id="suggest_setup_schedule",
|
||||
title="Stundenplan einrichten",
|
||||
description="Richten Sie Ihren Stundenplan ein fuer personalisierte Vorschlaege.",
|
||||
tone=SuggestionTone.HINT,
|
||||
priority=85,
|
||||
rule_id=self.id,
|
||||
icon="calendar_month",
|
||||
action_url="/schedule/setup",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class R12_ParentEvening(Rule):
|
||||
"""Elternabend vorbereiten."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="R12",
|
||||
name="Elternabend vorbereiten",
|
||||
description="Empfiehlt Vorbereitung vor Elternabend"
|
||||
)
|
||||
|
||||
def evaluate(self, signals: Signals) -> Optional[Suggestion]:
|
||||
if signals.parent_evenings_soon:
|
||||
event = signals.parent_evenings_soon[0]
|
||||
return Suggestion(
|
||||
id=f"suggest_parent_{event['id'][:8]}",
|
||||
title="Elternabend vorbereiten",
|
||||
description=f"'{event['title']}' steht bevor.",
|
||||
tone=SuggestionTone.SUGGESTION,
|
||||
badge="bald",
|
||||
priority=65,
|
||||
rule_id=self.id,
|
||||
icon="family_restroom",
|
||||
action_url=f"/events/{event['id']}/prepare",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Rule Engine ====================
|
||||
|
||||
class RuleEngine:
|
||||
"""
|
||||
Evaluiert alle Regeln gegen die gesammelten Signale.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.rules: List[Rule] = [
|
||||
R01_CreateClasses(),
|
||||
R02_PrepareRubric(),
|
||||
R03_StartCorrection(),
|
||||
R05_PrepareAgenda(),
|
||||
R07_PlanFirstExam(),
|
||||
R08_CorrectionMode(),
|
||||
R09_TripChecklist(),
|
||||
R10_CompleteGrades(),
|
||||
R11_SetupSchedule(),
|
||||
R12_ParentEvening(),
|
||||
]
|
||||
|
||||
def evaluate(self, signals: Signals) -> List[Suggestion]:
|
||||
"""Evaluiert alle Regeln und gibt passende Vorschlaege zurueck."""
|
||||
suggestions = []
|
||||
|
||||
for rule in self.rules:
|
||||
try:
|
||||
suggestion = rule.evaluate(signals)
|
||||
if suggestion:
|
||||
suggestions.append(suggestion)
|
||||
except Exception as e:
|
||||
logger.warning(f"Rule {rule.id} failed: {e}")
|
||||
|
||||
# Nach Prioritaet sortieren (hoechste zuerst)
|
||||
suggestions.sort(key=lambda s: s.priority, reverse=True)
|
||||
|
||||
return suggestions
|
||||
Reference in New Issue
Block a user