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>
This commit is contained in:
43
backend/state_engine/__init__.py
Normal file
43
backend/state_engine/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
State Engine - Herzstück des BreakPilot Begleiter-Modus.
|
||||
|
||||
Komponenten:
|
||||
1. Schuljahres-State-Machine (Phasen)
|
||||
2. Antizipations-Engine (Regeln + Vorschläge)
|
||||
3. TeacherContext (Aggregierter Kontext)
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
SchoolYearPhase,
|
||||
PhaseInfo,
|
||||
TeacherContext,
|
||||
ClassSummary,
|
||||
Event,
|
||||
TeacherStats,
|
||||
Milestone,
|
||||
PHASE_INFO,
|
||||
get_phase_info
|
||||
)
|
||||
from .rules import Rule, Suggestion, SuggestionPriority, RULES
|
||||
from .engine import AnticipationEngine, PhaseService
|
||||
|
||||
__all__ = [
|
||||
# Models
|
||||
"SchoolYearPhase",
|
||||
"PhaseInfo",
|
||||
"TeacherContext",
|
||||
"ClassSummary",
|
||||
"Event",
|
||||
"TeacherStats",
|
||||
"Milestone",
|
||||
"PHASE_INFO",
|
||||
"get_phase_info",
|
||||
# Rules
|
||||
"Rule",
|
||||
"Suggestion",
|
||||
"SuggestionPriority",
|
||||
"RULES",
|
||||
# Engine
|
||||
"AnticipationEngine",
|
||||
"PhaseService",
|
||||
]
|
||||
367
backend/state_engine/engine.py
Normal file
367
backend/state_engine/engine.py
Normal file
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
State Engine - Antizipations-Engine und Phasen-Service.
|
||||
|
||||
Komponenten:
|
||||
- AnticipationEngine: Evaluiert Regeln und generiert Vorschläge
|
||||
- PhaseService: Verwaltet Phasen-Übergänge
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .models import (
|
||||
SchoolYearPhase,
|
||||
TeacherContext,
|
||||
PhaseInfo,
|
||||
get_phase_info,
|
||||
PHASE_INFO
|
||||
)
|
||||
from .rules import Rule, Suggestion, SuggestionPriority, RULES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Phasen-Übergänge
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class PhaseTransition:
|
||||
"""Definition eines erlaubten Phasen-Übergangs."""
|
||||
from_phase: SchoolYearPhase
|
||||
to_phase: SchoolYearPhase
|
||||
condition: Callable[[TeacherContext], bool]
|
||||
auto_trigger: bool = False # Automatisch wenn Condition erfüllt
|
||||
|
||||
|
||||
# Vordefinierte Übergänge
|
||||
VALID_TRANSITIONS: List[PhaseTransition] = [
|
||||
# Onboarding → SchoolYearStart
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.ONBOARDING,
|
||||
to_phase=SchoolYearPhase.SCHOOL_YEAR_START,
|
||||
condition=lambda ctx: (
|
||||
ctx.has_completed_milestone("school_select") and
|
||||
ctx.has_completed_milestone("consent_accept") and
|
||||
ctx.has_completed_milestone("profile_complete")
|
||||
),
|
||||
auto_trigger=True,
|
||||
),
|
||||
|
||||
# SchoolYearStart → TeachingSetup
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.SCHOOL_YEAR_START,
|
||||
to_phase=SchoolYearPhase.TEACHING_SETUP,
|
||||
condition=lambda ctx: (
|
||||
len(ctx.classes) > 0 and
|
||||
ctx.has_completed_milestone("add_students")
|
||||
),
|
||||
auto_trigger=True,
|
||||
),
|
||||
|
||||
# TeachingSetup → Performance1
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.TEACHING_SETUP,
|
||||
to_phase=SchoolYearPhase.PERFORMANCE_1,
|
||||
condition=lambda ctx: (
|
||||
ctx.weeks_since_start >= 6 and
|
||||
ctx.has_learning_units()
|
||||
),
|
||||
auto_trigger=False, # Manueller Übergang
|
||||
),
|
||||
|
||||
# Performance1 → SemesterEnd
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.PERFORMANCE_1,
|
||||
to_phase=SchoolYearPhase.SEMESTER_END,
|
||||
condition=lambda ctx: (
|
||||
(ctx.is_in_month(1) or ctx.is_in_month(2)) and
|
||||
ctx.has_completed_milestone("enter_grades")
|
||||
),
|
||||
auto_trigger=True,
|
||||
),
|
||||
|
||||
# SemesterEnd → Teaching2
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.SEMESTER_END,
|
||||
to_phase=SchoolYearPhase.TEACHING_2,
|
||||
condition=lambda ctx: (
|
||||
ctx.has_completed_milestone("generate_certificates")
|
||||
),
|
||||
auto_trigger=True,
|
||||
),
|
||||
|
||||
# Teaching2 → Performance2
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.TEACHING_2,
|
||||
to_phase=SchoolYearPhase.PERFORMANCE_2,
|
||||
condition=lambda ctx: (
|
||||
ctx.weeks_since_start >= 30 or
|
||||
ctx.is_in_month(4) or ctx.is_in_month(5)
|
||||
),
|
||||
auto_trigger=False,
|
||||
),
|
||||
|
||||
# Performance2 → YearEnd
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.PERFORMANCE_2,
|
||||
to_phase=SchoolYearPhase.YEAR_END,
|
||||
condition=lambda ctx: (
|
||||
(ctx.is_in_month(6) or ctx.is_in_month(7)) and
|
||||
ctx.has_completed_milestone("enter_final_grades")
|
||||
),
|
||||
auto_trigger=True,
|
||||
),
|
||||
|
||||
# YearEnd → Archived
|
||||
PhaseTransition(
|
||||
from_phase=SchoolYearPhase.YEAR_END,
|
||||
to_phase=SchoolYearPhase.ARCHIVED,
|
||||
condition=lambda ctx: (
|
||||
ctx.has_completed_milestone("generate_final_certificates") and
|
||||
ctx.has_completed_milestone("archive_year")
|
||||
),
|
||||
auto_trigger=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PhaseService:
|
||||
"""
|
||||
Verwaltet Schuljahres-Phasen und deren Übergänge.
|
||||
|
||||
Funktionen:
|
||||
- Phasen-Status abrufen
|
||||
- Automatische Übergänge prüfen
|
||||
- Manuelle Übergänge durchführen
|
||||
"""
|
||||
|
||||
def __init__(self, transitions: List[PhaseTransition] = None):
|
||||
self.transitions = transitions or VALID_TRANSITIONS
|
||||
|
||||
def get_current_phase_info(self, phase: SchoolYearPhase) -> PhaseInfo:
|
||||
"""Gibt Metadaten zur aktuellen Phase zurück."""
|
||||
return get_phase_info(phase)
|
||||
|
||||
def get_all_phases(self) -> List[Dict[str, Any]]:
|
||||
"""Gibt alle Phasen mit Metadaten zurück."""
|
||||
return [
|
||||
{
|
||||
"phase": info.phase.value,
|
||||
"display_name": info.display_name,
|
||||
"description": info.description,
|
||||
"typical_months": info.typical_months,
|
||||
}
|
||||
for info in PHASE_INFO.values()
|
||||
]
|
||||
|
||||
def check_and_transition(self, ctx: TeacherContext) -> Optional[SchoolYearPhase]:
|
||||
"""
|
||||
Prüft ob ein automatischer Phasen-Übergang möglich ist.
|
||||
|
||||
Args:
|
||||
ctx: Aktueller TeacherContext
|
||||
|
||||
Returns:
|
||||
Neue Phase wenn Übergang stattfand, sonst None
|
||||
"""
|
||||
current_phase = ctx.current_phase
|
||||
|
||||
for transition in self.transitions:
|
||||
if (transition.from_phase == current_phase and
|
||||
transition.auto_trigger and
|
||||
transition.condition(ctx)):
|
||||
|
||||
logger.info(
|
||||
f"Auto-transition from {current_phase} to {transition.to_phase} "
|
||||
f"for teacher {ctx.teacher_id}"
|
||||
)
|
||||
return transition.to_phase
|
||||
|
||||
return None
|
||||
|
||||
def can_transition_to(self, ctx: TeacherContext, target_phase: SchoolYearPhase) -> bool:
|
||||
"""
|
||||
Prüft ob ein Übergang zu einer bestimmten Phase möglich ist.
|
||||
|
||||
Args:
|
||||
ctx: Aktueller TeacherContext
|
||||
target_phase: Zielphase
|
||||
|
||||
Returns:
|
||||
True wenn Übergang erlaubt
|
||||
"""
|
||||
for transition in self.transitions:
|
||||
if (transition.from_phase == ctx.current_phase and
|
||||
transition.to_phase == target_phase):
|
||||
return transition.condition(ctx)
|
||||
return False
|
||||
|
||||
def get_next_phase(self, current: SchoolYearPhase) -> Optional[SchoolYearPhase]:
|
||||
"""Gibt die nächste Phase in der Sequenz zurück."""
|
||||
phase_order = [
|
||||
SchoolYearPhase.ONBOARDING,
|
||||
SchoolYearPhase.SCHOOL_YEAR_START,
|
||||
SchoolYearPhase.TEACHING_SETUP,
|
||||
SchoolYearPhase.PERFORMANCE_1,
|
||||
SchoolYearPhase.SEMESTER_END,
|
||||
SchoolYearPhase.TEACHING_2,
|
||||
SchoolYearPhase.PERFORMANCE_2,
|
||||
SchoolYearPhase.YEAR_END,
|
||||
SchoolYearPhase.ARCHIVED,
|
||||
]
|
||||
|
||||
try:
|
||||
idx = phase_order.index(current)
|
||||
if idx < len(phase_order) - 1:
|
||||
return phase_order[idx + 1]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_progress_percentage(self, ctx: TeacherContext) -> float:
|
||||
"""
|
||||
Berechnet Fortschritt in der aktuellen Phase.
|
||||
|
||||
Returns:
|
||||
Prozent (0-100)
|
||||
"""
|
||||
phase_info = get_phase_info(ctx.current_phase)
|
||||
required = set(phase_info.required_actions)
|
||||
completed = set(ctx.completed_milestones)
|
||||
|
||||
if not required:
|
||||
return 100.0
|
||||
|
||||
done = len(required.intersection(completed))
|
||||
return (done / len(required)) * 100
|
||||
|
||||
|
||||
class AnticipationEngine:
|
||||
"""
|
||||
Evaluiert Antizipations-Regeln und generiert priorisierte Vorschläge.
|
||||
|
||||
Die Engine:
|
||||
- Wendet alle aktiven Regeln auf den TeacherContext an
|
||||
- Priorisiert Vorschläge nach Dringlichkeit
|
||||
- Limitiert auf max. 5 Vorschläge
|
||||
"""
|
||||
|
||||
def __init__(self, rules: List[Rule] = None, max_suggestions: int = 5):
|
||||
self.rules = rules or RULES
|
||||
self.max_suggestions = max_suggestions
|
||||
|
||||
def get_suggestions(self, ctx: TeacherContext) -> List[Suggestion]:
|
||||
"""
|
||||
Evaluiert alle Regeln und gibt priorisierte Vorschläge zurück.
|
||||
|
||||
Args:
|
||||
ctx: TeacherContext mit allen relevanten Informationen
|
||||
|
||||
Returns:
|
||||
Liste von max. 5 Vorschlägen, sortiert nach Priorität
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
for rule in self.rules:
|
||||
try:
|
||||
suggestion = rule.evaluate(ctx)
|
||||
if suggestion:
|
||||
suggestions.append(suggestion)
|
||||
except Exception as e:
|
||||
logger.warning(f"Rule {rule.id} evaluation failed: {e}")
|
||||
|
||||
return self._prioritize(suggestions)
|
||||
|
||||
def _prioritize(self, suggestions: List[Suggestion]) -> List[Suggestion]:
|
||||
"""
|
||||
Sortiert Vorschläge nach Priorität und limitiert.
|
||||
|
||||
Reihenfolge:
|
||||
1. URGENT (1)
|
||||
2. HIGH (2)
|
||||
3. MEDIUM (3)
|
||||
4. LOW (4)
|
||||
"""
|
||||
sorted_suggestions = sorted(
|
||||
suggestions,
|
||||
key=lambda s: s.priority.value
|
||||
)
|
||||
return sorted_suggestions[:self.max_suggestions]
|
||||
|
||||
def get_top_suggestion(self, ctx: TeacherContext) -> Optional[Suggestion]:
|
||||
"""
|
||||
Gibt den wichtigsten Vorschlag zurück.
|
||||
|
||||
Args:
|
||||
ctx: TeacherContext
|
||||
|
||||
Returns:
|
||||
Wichtigster Vorschlag oder None
|
||||
"""
|
||||
suggestions = self.get_suggestions(ctx)
|
||||
return suggestions[0] if suggestions else None
|
||||
|
||||
def get_suggestions_by_category(
|
||||
self,
|
||||
ctx: TeacherContext
|
||||
) -> Dict[str, List[Suggestion]]:
|
||||
"""
|
||||
Gruppiert Vorschläge nach Kategorie.
|
||||
|
||||
Returns:
|
||||
Dict mit Kategorien als Keys und Listen von Vorschlägen
|
||||
"""
|
||||
suggestions = self.get_suggestions(ctx)
|
||||
by_category: Dict[str, List[Suggestion]] = {}
|
||||
|
||||
for suggestion in suggestions:
|
||||
if suggestion.category not in by_category:
|
||||
by_category[suggestion.category] = []
|
||||
by_category[suggestion.category].append(suggestion)
|
||||
|
||||
return by_category
|
||||
|
||||
def count_by_priority(self, ctx: TeacherContext) -> Dict[str, int]:
|
||||
"""
|
||||
Zählt Vorschläge nach Priorität.
|
||||
|
||||
Returns:
|
||||
Dict mit Prioritäten und Anzahlen
|
||||
"""
|
||||
suggestions = self.get_suggestions(ctx)
|
||||
counts = {
|
||||
"urgent": 0,
|
||||
"high": 0,
|
||||
"medium": 0,
|
||||
"low": 0,
|
||||
}
|
||||
|
||||
for s in suggestions:
|
||||
if s.priority == SuggestionPriority.URGENT:
|
||||
counts["urgent"] += 1
|
||||
elif s.priority == SuggestionPriority.HIGH:
|
||||
counts["high"] += 1
|
||||
elif s.priority == SuggestionPriority.MEDIUM:
|
||||
counts["medium"] += 1
|
||||
else:
|
||||
counts["low"] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Factory Functions
|
||||
# ============================================================================
|
||||
|
||||
def create_anticipation_engine(custom_rules: List[Rule] = None) -> AnticipationEngine:
|
||||
"""Erstellt eine neue AnticipationEngine."""
|
||||
return AnticipationEngine(rules=custom_rules)
|
||||
|
||||
|
||||
def create_phase_service(custom_transitions: List[PhaseTransition] = None) -> PhaseService:
|
||||
"""Erstellt einen neuen PhaseService."""
|
||||
return PhaseService(transitions=custom_transitions)
|
||||
317
backend/state_engine/models.py
Normal file
317
backend/state_engine/models.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
State Engine Models - Datenstrukturen für das Phasen-Management.
|
||||
|
||||
Definiert:
|
||||
- SchoolYearPhase: Die 9 Phasen des Schuljahres
|
||||
- TeacherContext: Aggregierter Kontext für Antizipation
|
||||
- Event, Milestone, Stats: Unterstützende Modelle
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Dict, Any
|
||||
import uuid
|
||||
|
||||
|
||||
class SchoolYearPhase(str, Enum):
|
||||
"""Die 9 Phasen eines Schuljahres."""
|
||||
|
||||
# Phase 1: Schuljahresbeginn (Aug/Sep)
|
||||
ONBOARDING = "onboarding"
|
||||
# Neue Lehrer, Schulsuche, Grundkonfiguration
|
||||
|
||||
# Phase 2: Schuljahresstart (Sep/Okt)
|
||||
SCHOOL_YEAR_START = "school_year_start"
|
||||
# Klassen anlegen, Stundenplan, erste Einheiten
|
||||
|
||||
# Phase 3: Unterrichtsaufbau (Okt/Nov)
|
||||
TEACHING_SETUP = "teaching_setup"
|
||||
# Lerneinheiten, Materialien, Elternkommunikation
|
||||
|
||||
# Phase 4: Leistungsphase 1 (Nov/Dez)
|
||||
PERFORMANCE_1 = "performance_1"
|
||||
# Klausuren, Korrektur, erste Noten
|
||||
|
||||
# Phase 5: Halbjahresabschluss (Jan/Feb)
|
||||
SEMESTER_END = "semester_end"
|
||||
# Halbjahreszeugnisse, Konferenzen, Elterngespräche
|
||||
|
||||
# Phase 6: 2. Halbjahr Unterricht (Feb/Apr)
|
||||
TEACHING_2 = "teaching_2"
|
||||
# Wiederholung von Phase 3
|
||||
|
||||
# Phase 7: Leistungsphase 2 (Apr/Jun)
|
||||
PERFORMANCE_2 = "performance_2"
|
||||
# Klausuren, Korrektur, finale Noten
|
||||
|
||||
# Phase 8: Jahresabschluss (Jun/Jul)
|
||||
YEAR_END = "year_end"
|
||||
# Abschlusszeugnisse, Versetzung, Archivierung
|
||||
|
||||
# Phase 9: Archiviert
|
||||
ARCHIVED = "archived"
|
||||
# Schuljahr abgeschlossen
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseInfo:
|
||||
"""Metadaten zu einer Phase."""
|
||||
phase: SchoolYearPhase
|
||||
display_name: str
|
||||
description: str
|
||||
typical_months: List[int] # 1-12
|
||||
expected_duration_weeks: int
|
||||
required_actions: List[str]
|
||||
optional_actions: List[str]
|
||||
|
||||
|
||||
# Phasen-Definitionen mit Metadaten
|
||||
PHASE_INFO: Dict[SchoolYearPhase, PhaseInfo] = {
|
||||
SchoolYearPhase.ONBOARDING: PhaseInfo(
|
||||
phase=SchoolYearPhase.ONBOARDING,
|
||||
display_name="Onboarding",
|
||||
description="Willkommen bei BreakPilot! Richte dein Schuljahr ein.",
|
||||
typical_months=[8, 9],
|
||||
expected_duration_weeks=2,
|
||||
required_actions=["school_select", "consent_accept", "profile_complete"],
|
||||
optional_actions=["import_previous_year"],
|
||||
),
|
||||
SchoolYearPhase.SCHOOL_YEAR_START: PhaseInfo(
|
||||
phase=SchoolYearPhase.SCHOOL_YEAR_START,
|
||||
display_name="Schuljahresstart",
|
||||
description="Lege deine Klassen und den Stundenplan an.",
|
||||
typical_months=[9, 10],
|
||||
expected_duration_weeks=3,
|
||||
required_actions=["create_classes", "add_students", "create_timetable"],
|
||||
optional_actions=["import_students_csv", "invite_parents"],
|
||||
),
|
||||
SchoolYearPhase.TEACHING_SETUP: PhaseInfo(
|
||||
phase=SchoolYearPhase.TEACHING_SETUP,
|
||||
display_name="Unterrichtsaufbau",
|
||||
description="Erstelle Lerneinheiten und Materialien.",
|
||||
typical_months=[10, 11],
|
||||
expected_duration_weeks=4,
|
||||
required_actions=["create_learning_units"],
|
||||
optional_actions=["generate_worksheets", "prepare_parent_meeting"],
|
||||
),
|
||||
SchoolYearPhase.PERFORMANCE_1: PhaseInfo(
|
||||
phase=SchoolYearPhase.PERFORMANCE_1,
|
||||
display_name="Leistungsphase 1",
|
||||
description="Erste Klausuren und Bewertungen.",
|
||||
typical_months=[11, 12],
|
||||
expected_duration_weeks=6,
|
||||
required_actions=["schedule_exams", "enter_grades"],
|
||||
optional_actions=["use_correction_module", "generate_feedback"],
|
||||
),
|
||||
SchoolYearPhase.SEMESTER_END: PhaseInfo(
|
||||
phase=SchoolYearPhase.SEMESTER_END,
|
||||
display_name="Halbjahresabschluss",
|
||||
description="Halbjahreszeugnisse und Konferenzen.",
|
||||
typical_months=[1, 2],
|
||||
expected_duration_weeks=3,
|
||||
required_actions=["complete_grades", "generate_certificates"],
|
||||
optional_actions=["parent_conferences", "archive_semester"],
|
||||
),
|
||||
SchoolYearPhase.TEACHING_2: PhaseInfo(
|
||||
phase=SchoolYearPhase.TEACHING_2,
|
||||
display_name="2. Halbjahr",
|
||||
description="Weiterführender Unterricht im 2. Halbjahr.",
|
||||
typical_months=[2, 3, 4],
|
||||
expected_duration_weeks=8,
|
||||
required_actions=["update_learning_units"],
|
||||
optional_actions=["generate_worksheets"],
|
||||
),
|
||||
SchoolYearPhase.PERFORMANCE_2: PhaseInfo(
|
||||
phase=SchoolYearPhase.PERFORMANCE_2,
|
||||
display_name="Leistungsphase 2",
|
||||
description="Finale Klausuren und Bewertungen.",
|
||||
typical_months=[4, 5, 6],
|
||||
expected_duration_weeks=8,
|
||||
required_actions=["schedule_exams", "enter_final_grades"],
|
||||
optional_actions=["use_correction_module"],
|
||||
),
|
||||
SchoolYearPhase.YEAR_END: PhaseInfo(
|
||||
phase=SchoolYearPhase.YEAR_END,
|
||||
display_name="Jahresabschluss",
|
||||
description="Abschlusszeugnisse und Versetzung.",
|
||||
typical_months=[6, 7],
|
||||
expected_duration_weeks=3,
|
||||
required_actions=["complete_all_grades", "generate_final_certificates"],
|
||||
optional_actions=["archive_year", "export_data"],
|
||||
),
|
||||
SchoolYearPhase.ARCHIVED: PhaseInfo(
|
||||
phase=SchoolYearPhase.ARCHIVED,
|
||||
display_name="Archiviert",
|
||||
description="Das Schuljahr ist abgeschlossen.",
|
||||
typical_months=[7, 8],
|
||||
expected_duration_weeks=0,
|
||||
required_actions=[],
|
||||
optional_actions=["view_archive"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_phase_info(phase: SchoolYearPhase) -> PhaseInfo:
|
||||
"""Gibt Metadaten für eine Phase zurück."""
|
||||
return PHASE_INFO.get(phase, PHASE_INFO[SchoolYearPhase.ONBOARDING])
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClassSummary:
|
||||
"""Zusammenfassung einer Klasse."""
|
||||
class_id: str
|
||||
name: str
|
||||
grade_level: int
|
||||
student_count: int
|
||||
subject: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
"""Ein anstehendes Ereignis."""
|
||||
type: str # "exam", "parent_meeting", "deadline"
|
||||
title: str
|
||||
date: datetime
|
||||
in_days: int
|
||||
class_id: Optional[str] = None
|
||||
priority: str = "medium" # "high", "medium", "low"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Milestone:
|
||||
"""Ein erreichter Meilenstein."""
|
||||
milestone: str
|
||||
completed_at: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeacherStats:
|
||||
"""Statistiken eines Lehrers."""
|
||||
learning_units_created: int = 0
|
||||
exams_scheduled: int = 0
|
||||
exams_graded: int = 0
|
||||
grades_entered: int = 0
|
||||
parent_messages_count: int = 0
|
||||
avg_response_time_hours: float = 0.0
|
||||
unanswered_messages: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeacherContext:
|
||||
"""
|
||||
Aggregierter Kontext für einen Lehrer.
|
||||
|
||||
Enthält alle relevanten Informationen für die Antizipations-Engine:
|
||||
- Identifikation
|
||||
- Schulkontext
|
||||
- Zeitlicher Kontext
|
||||
- Klassen und Schüler
|
||||
- Termine und Events
|
||||
- Fortschritt
|
||||
- Statistiken
|
||||
"""
|
||||
# Identifikation
|
||||
teacher_id: str
|
||||
school_id: str
|
||||
school_year_id: str
|
||||
|
||||
# Schulkontext
|
||||
federal_state: str = "niedersachsen" # Bundesland
|
||||
school_type: str = "gymnasium" # Schulform
|
||||
|
||||
# Zeitlicher Kontext
|
||||
school_year_start: datetime = field(default_factory=datetime.now)
|
||||
current_phase: SchoolYearPhase = SchoolYearPhase.ONBOARDING
|
||||
phase_entered_at: datetime = field(default_factory=datetime.now)
|
||||
weeks_since_start: int = 0
|
||||
days_in_phase: int = 0
|
||||
|
||||
# Klassen und Schüler
|
||||
classes: List[ClassSummary] = field(default_factory=list)
|
||||
total_students: int = 0
|
||||
|
||||
# Termine und Events
|
||||
upcoming_events: List[Event] = field(default_factory=list)
|
||||
overdue_actions: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
# Fortschritt
|
||||
completed_milestones: List[str] = field(default_factory=list)
|
||||
pending_milestones: List[str] = field(default_factory=list)
|
||||
|
||||
# Statistiken
|
||||
stats: TeacherStats = field(default_factory=TeacherStats)
|
||||
|
||||
def has_completed_milestone(self, milestone: str) -> bool:
|
||||
"""Prüft ob ein Meilenstein erreicht wurde."""
|
||||
return milestone in self.completed_milestones
|
||||
|
||||
def has_learning_units(self) -> bool:
|
||||
"""Prüft ob Lerneinheiten erstellt wurden."""
|
||||
return self.stats.learning_units_created > 0
|
||||
|
||||
def is_in_month(self, month: int) -> bool:
|
||||
"""Prüft ob aktueller Monat übereinstimmt."""
|
||||
return datetime.now().month == month
|
||||
|
||||
def get_next_deadline(self) -> Optional[Event]:
|
||||
"""Gibt die nächste Deadline zurück."""
|
||||
for e in self.upcoming_events:
|
||||
if e.type == "deadline":
|
||||
return e
|
||||
return None
|
||||
|
||||
def get_next_exam(self) -> Optional[Event]:
|
||||
"""Gibt die nächste Klausur zurück."""
|
||||
for e in self.upcoming_events:
|
||||
if e.type == "exam" and e.in_days > 0:
|
||||
return e
|
||||
return None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert zu Dictionary."""
|
||||
return {
|
||||
"teacher_id": self.teacher_id,
|
||||
"school_id": self.school_id,
|
||||
"school_year_id": self.school_year_id,
|
||||
"federal_state": self.federal_state,
|
||||
"school_type": self.school_type,
|
||||
"school_year_start": self.school_year_start.isoformat(),
|
||||
"current_phase": self.current_phase.value,
|
||||
"phase_entered_at": self.phase_entered_at.isoformat(),
|
||||
"weeks_since_start": self.weeks_since_start,
|
||||
"days_in_phase": self.days_in_phase,
|
||||
"classes": [
|
||||
{
|
||||
"class_id": c.class_id,
|
||||
"name": c.name,
|
||||
"grade_level": c.grade_level,
|
||||
"student_count": c.student_count,
|
||||
"subject": c.subject,
|
||||
}
|
||||
for c in self.classes
|
||||
],
|
||||
"total_students": self.total_students,
|
||||
"upcoming_events": [
|
||||
{
|
||||
"type": e.type,
|
||||
"title": e.title,
|
||||
"date": e.date.isoformat(),
|
||||
"in_days": e.in_days,
|
||||
"class_id": e.class_id,
|
||||
"priority": e.priority,
|
||||
}
|
||||
for e in self.upcoming_events
|
||||
],
|
||||
"completed_milestones": self.completed_milestones,
|
||||
"pending_milestones": self.pending_milestones,
|
||||
"stats": {
|
||||
"learning_units_created": self.stats.learning_units_created,
|
||||
"exams_scheduled": self.stats.exams_scheduled,
|
||||
"exams_graded": self.stats.exams_graded,
|
||||
"grades_entered": self.stats.grades_entered,
|
||||
"parent_messages_count": self.stats.parent_messages_count,
|
||||
"avg_response_time_hours": self.stats.avg_response_time_hours,
|
||||
"unanswered_messages": self.stats.unanswered_messages,
|
||||
},
|
||||
}
|
||||
484
backend/state_engine/rules.py
Normal file
484
backend/state_engine/rules.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
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
|
||||
]
|
||||
Reference in New Issue
Block a user