This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/state_engine/rules.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

485 lines
18 KiB
Python

"""
State Engine Rules - Antizipations-Regeln für proaktive Vorschläge.
Definiert:
- Suggestion: Ein Vorschlag mit Priorität und Aktion
- Rule: Eine Regel die auf TeacherContext angewendet wird
- RULES: Vordefinierte Regeln (15+)
"""
from dataclasses import dataclass
from enum import Enum
from typing import Callable, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .models import TeacherContext
class SuggestionPriority(Enum):
"""Priorität eines Vorschlags."""
URGENT = 1 # Sofort erforderlich
HIGH = 2 # Heute/Diese Woche
MEDIUM = 3 # Diese Woche/Bald
LOW = 4 # Irgendwann
@dataclass
class Suggestion:
"""Ein Vorschlag für den Lehrer."""
id: str
title: str
description: str
action_type: str # "navigate", "create", "remind"
action_target: str # URL oder Action-ID
priority: SuggestionPriority
category: str # "classes", "grades", "communication", etc.
icon: str # Material Icon Name
estimated_time: int # Minuten
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"action_type": self.action_type,
"action_target": self.action_target,
"priority": self.priority.name,
"priority_value": self.priority.value,
"category": self.category,
"icon": self.icon,
"estimated_time": self.estimated_time,
}
@dataclass
class Rule:
"""Eine Antizipations-Regel."""
id: str
name: str
condition: Callable[['TeacherContext'], bool]
suggestion_generator: Callable[['TeacherContext'], Suggestion]
applies_to_phases: List[str] # Leere Liste = alle Phasen
def evaluate(self, ctx: 'TeacherContext') -> Optional[Suggestion]:
"""
Evaluiert die Regel gegen den Kontext.
Returns:
Suggestion wenn Regel zutrifft, sonst None
"""
# Prüfe Phasen-Einschränkung
if self.applies_to_phases:
if ctx.current_phase.value not in self.applies_to_phases:
return None
# Prüfe Condition
try:
if not self.condition(ctx):
return None
except Exception:
return None
return self.suggestion_generator(ctx)
# ============================================================================
# Helper Functions für Regeln
# ============================================================================
def _get_parent_meeting_days(ctx: 'TeacherContext') -> int:
"""Gibt Tage bis zum nächsten Elternabend zurück."""
for e in ctx.upcoming_events:
if e.type == "parent_meeting":
return e.in_days
return 999
def _get_next_exam_days(ctx: 'TeacherContext') -> int:
"""Gibt Tage bis zur nächsten Klausur zurück."""
for e in ctx.upcoming_events:
if e.type == "exam" and e.in_days > 0:
return e.in_days
return 999
def _get_uncorrected_exams(ctx: 'TeacherContext') -> int:
"""Gibt Anzahl unkorrigierter Klausuren zurück."""
return ctx.stats.exams_scheduled - ctx.stats.exams_graded
# ============================================================================
# Vordefinierte Regeln (15+)
# ============================================================================
RULES: List[Rule] = [
# ═══════════════════════════════════════════════════════════════════════
# ONBOARDING / SCHULJAHRESSTART
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="no_classes",
name="Keine Klassen angelegt",
condition=lambda ctx: len(ctx.classes) == 0,
suggestion_generator=lambda ctx: Suggestion(
id="create_first_class",
title="Erste Klasse anlegen",
description="Lege deine erste Klasse an, um loszulegen.",
action_type="navigate",
action_target="/studio/school",
priority=SuggestionPriority.URGENT,
category="classes",
icon="group_add",
estimated_time=5,
),
applies_to_phases=["onboarding", "school_year_start"],
),
Rule(
id="no_students",
name="Klassen ohne Schüler",
condition=lambda ctx: len(ctx.classes) > 0 and ctx.total_students == 0,
suggestion_generator=lambda ctx: Suggestion(
id="add_students",
title="Schüler hinzufügen",
description=f"Deine {len(ctx.classes)} Klasse(n) haben noch keine Schüler.",
action_type="navigate",
action_target="/studio/school",
priority=SuggestionPriority.HIGH,
category="classes",
icon="person_add",
estimated_time=10,
),
applies_to_phases=["school_year_start"],
),
Rule(
id="consent_missing",
name="Einwilligung ausstehend",
condition=lambda ctx: not ctx.has_completed_milestone("consent_accept"),
suggestion_generator=lambda ctx: Suggestion(
id="accept_consent",
title="Datenschutz-Einwilligung",
description="Bitte akzeptiere die Datenschutzbestimmungen.",
action_type="navigate",
action_target="/studio/consent",
priority=SuggestionPriority.URGENT,
category="settings",
icon="security",
estimated_time=2,
),
applies_to_phases=["onboarding"],
),
Rule(
id="profile_incomplete",
name="Profil unvollständig",
condition=lambda ctx: not ctx.has_completed_milestone("profile_complete"),
suggestion_generator=lambda ctx: Suggestion(
id="complete_profile",
title="Profil vervollständigen",
description="Vervollständige dein Profil für bessere Personalisierung.",
action_type="navigate",
action_target="/studio/profile",
priority=SuggestionPriority.HIGH,
category="settings",
icon="account_circle",
estimated_time=5,
),
applies_to_phases=["onboarding", "school_year_start"],
),
# ═══════════════════════════════════════════════════════════════════════
# UNTERRICHT
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="no_learning_units",
name="Keine Lerneinheiten",
condition=lambda ctx: (
ctx.current_phase.value in ["teaching_setup", "teaching_2"] and
ctx.stats.learning_units_created == 0
),
suggestion_generator=lambda ctx: Suggestion(
id="create_learning_unit",
title="Erste Lerneinheit erstellen",
description="Erstelle Lerneinheiten für deine Klassen.",
action_type="navigate",
action_target="/studio/worksheets",
priority=SuggestionPriority.HIGH,
category="teaching",
icon="auto_stories",
estimated_time=15,
),
applies_to_phases=["teaching_setup", "teaching_2"],
),
Rule(
id="few_learning_units",
name="Wenige Lerneinheiten",
condition=lambda ctx: (
ctx.current_phase.value in ["teaching_setup", "teaching_2"] and
0 < ctx.stats.learning_units_created < 3
),
suggestion_generator=lambda ctx: Suggestion(
id="create_more_units",
title="Weitere Lerneinheiten erstellen",
description=f"Du hast {ctx.stats.learning_units_created} Lerneinheit(en). Erstelle mehr für abwechslungsreichen Unterricht.",
action_type="navigate",
action_target="/studio/worksheets",
priority=SuggestionPriority.MEDIUM,
category="teaching",
icon="library_add",
estimated_time=15,
),
applies_to_phases=["teaching_setup", "teaching_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# ELTERNKOMMUNIKATION
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="parent_meeting_upcoming",
name="Elternabend steht bevor",
condition=lambda ctx: any(
e.type == "parent_meeting" and e.in_days <= 14
for e in ctx.upcoming_events
),
suggestion_generator=lambda ctx: Suggestion(
id="prepare_parent_meeting",
title="Elternabend vorbereiten",
description=f"In {_get_parent_meeting_days(ctx)} Tagen ist Elternabend.",
action_type="navigate",
action_target="/studio/letters",
priority=SuggestionPriority.HIGH,
category="communication",
icon="groups",
estimated_time=30,
),
applies_to_phases=[], # Alle Phasen
),
Rule(
id="unanswered_parent_messages",
name="Unbeantwortete Elternnachrichten",
condition=lambda ctx: ctx.stats.unanswered_messages > 0,
suggestion_generator=lambda ctx: Suggestion(
id="answer_messages",
title="Elternnachrichten beantworten",
description=f"{ctx.stats.unanswered_messages} Nachricht(en) warten auf Antwort.",
action_type="navigate",
action_target="/studio/messenger",
priority=SuggestionPriority.HIGH if ctx.stats.unanswered_messages > 3 else SuggestionPriority.MEDIUM,
category="communication",
icon="mail",
estimated_time=15,
),
applies_to_phases=[], # Alle Phasen
),
Rule(
id="no_parent_contact",
name="Keine Elternkontakte",
condition=lambda ctx: (
ctx.total_students > 0 and
ctx.stats.parent_messages_count == 0 and
ctx.weeks_since_start >= 4
),
suggestion_generator=lambda ctx: Suggestion(
id="start_parent_communication",
title="Elternkommunikation starten",
description="Nimm Kontakt mit den Eltern auf.",
action_type="navigate",
action_target="/studio/letters",
priority=SuggestionPriority.MEDIUM,
category="communication",
icon="family_restroom",
estimated_time=20,
),
applies_to_phases=["teaching_setup", "teaching_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# LEISTUNG / KLAUSUREN
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="exam_in_7_days",
name="Klausur in 7 Tagen",
condition=lambda ctx: any(
e.type == "exam" and 0 < e.in_days <= 7
for e in ctx.upcoming_events
),
suggestion_generator=lambda ctx: Suggestion(
id="prepare_exam",
title="Klausur vorbereiten",
description=f"Klausur in {_get_next_exam_days(ctx)} Tagen.",
action_type="navigate",
action_target="/studio/worksheets",
priority=SuggestionPriority.URGENT,
category="exams",
icon="quiz",
estimated_time=60,
),
applies_to_phases=["performance_1", "performance_2"],
),
Rule(
id="exam_needs_correction",
name="Klausur wartet auf Korrektur",
condition=lambda ctx: _get_uncorrected_exams(ctx) > 0,
suggestion_generator=lambda ctx: Suggestion(
id="correct_exam",
title="Klausur korrigieren",
description=f"{_get_uncorrected_exams(ctx)} Klausur(en) warten auf Korrektur.",
action_type="navigate",
action_target="/studio/correction",
priority=SuggestionPriority.URGENT,
category="exams",
icon="grading",
estimated_time=120,
),
applies_to_phases=["performance_1", "performance_2"],
),
Rule(
id="no_exams_scheduled",
name="Keine Klausuren geplant",
condition=lambda ctx: (
ctx.current_phase.value in ["performance_1", "performance_2"] and
ctx.stats.exams_scheduled == 0
),
suggestion_generator=lambda ctx: Suggestion(
id="schedule_exams",
title="Klausuren planen",
description="Plane die Klausuren für diese Leistungsphase.",
action_type="navigate",
action_target="/studio/school",
priority=SuggestionPriority.HIGH,
category="exams",
icon="event",
estimated_time=15,
),
applies_to_phases=["performance_1", "performance_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# NOTEN / ZEUGNISSE
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="grades_missing",
name="Noten fehlen",
condition=lambda ctx: (
ctx.current_phase.value in ["semester_end", "year_end"] and
not ctx.has_completed_milestone("complete_grades")
),
suggestion_generator=lambda ctx: Suggestion(
id="enter_grades",
title="Noten eintragen",
description="Trage alle Noten für die Zeugnisse ein.",
action_type="navigate",
action_target="/studio/gradebook",
priority=SuggestionPriority.URGENT,
category="grades",
icon="calculate",
estimated_time=30,
),
applies_to_phases=["semester_end", "year_end"],
),
Rule(
id="certificates_due",
name="Zeugnisse erstellen",
condition=lambda ctx: (
ctx.current_phase.value in ["semester_end", "year_end"] and
ctx.has_completed_milestone("complete_grades") and
not ctx.has_completed_milestone("generate_certificates")
),
suggestion_generator=lambda ctx: Suggestion(
id="generate_certificates",
title="Zeugnisse erstellen",
description="Alle Noten sind eingetragen. Erstelle jetzt die Zeugnisse.",
action_type="navigate",
action_target="/studio/certificates",
priority=SuggestionPriority.HIGH,
category="certificates",
icon="description",
estimated_time=45,
),
applies_to_phases=["semester_end", "year_end"],
),
Rule(
id="gradebook_empty",
name="Notenbuch leer",
condition=lambda ctx: (
ctx.stats.grades_entered == 0 and
ctx.weeks_since_start >= 6
),
suggestion_generator=lambda ctx: Suggestion(
id="start_gradebook",
title="Notenbuch nutzen",
description="Beginne mit der Notenverwaltung im Notenbuch.",
action_type="navigate",
action_target="/studio/gradebook",
priority=SuggestionPriority.MEDIUM,
category="grades",
icon="grade",
estimated_time=10,
),
applies_to_phases=["teaching_setup", "performance_1", "teaching_2", "performance_2"],
),
# ═══════════════════════════════════════════════════════════════════════
# ALLGEMEINE ERINNERUNGEN
# ═══════════════════════════════════════════════════════════════════════
Rule(
id="long_time_inactive",
name="Lange inaktiv",
condition=lambda ctx: ctx.days_in_phase > 14 and len(ctx.completed_milestones) == 0,
suggestion_generator=lambda ctx: Suggestion(
id="get_started",
title="Loslegen",
description="Du bist schon eine Weile in dieser Phase. Lass uns starten!",
action_type="navigate",
action_target="/studio",
priority=SuggestionPriority.HIGH,
category="general",
icon="rocket_launch",
estimated_time=5,
),
applies_to_phases=[], # Alle Phasen
),
Rule(
id="deadline_approaching",
name="Deadline naht",
condition=lambda ctx: any(
e.type == "deadline" and 0 < e.in_days <= 3
for e in ctx.upcoming_events
),
suggestion_generator=lambda ctx: Suggestion(
id="check_deadline",
title="Deadline beachten",
description=f"Eine Deadline in {ctx.get_next_deadline().in_days if ctx.get_next_deadline() else 0} Tagen.",
action_type="navigate",
action_target="/studio",
priority=SuggestionPriority.URGENT,
category="general",
icon="alarm",
estimated_time=5,
),
applies_to_phases=[],
),
]
def get_rules_for_phase(phase: str) -> List[Rule]:
"""Gibt alle Regeln für eine bestimmte Phase zurück."""
return [
rule for rule in RULES
if not rule.applies_to_phases or phase in rule.applies_to_phases
]