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

368 lines
11 KiB
Python

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