""" 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