Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
485 lines
18 KiB
Python
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
|
|
]
|