[split-required] Split final 43 files (500-668 LOC) to complete refactoring

klausur-service (11 files):
- cv_gutter_repair, ocr_pipeline_regression, upload_api
- ocr_pipeline_sessions, smart_spell, nru_worksheet_generator
- ocr_pipeline_overlays, mail/aggregator, zeugnis_api
- cv_syllable_detect, self_rag

backend-lehrer (17 files):
- classroom_engine/suggestions, generators/quiz_generator
- worksheets_api, llm_gateway/comparison, state_engine_api
- classroom/models (→ 4 submodules), services/file_processor
- alerts_agent/api/wizard+digests+routes, content_generators/pdf
- classroom/routes/sessions, llm_gateway/inference
- classroom_engine/analytics, auth/keycloak_auth
- alerts_agent/processing/rule_engine, ai_processor/print_versions

agent-core (5 files):
- brain/memory_store, brain/knowledge_graph, brain/context_manager
- orchestrator/supervisor, sessions/session_manager

admin-lehrer (5 components):
- GridOverlay, StepGridReview, DevOpsPipelineSidebar
- DataFlowDiagram, sbom/wizard/page

website (2 files):
- DependencyMap, lehrer/abitur-archiv

Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 09:41:42 +02:00
parent 451365a312
commit bd4b956e3c
113 changed files with 13790 additions and 14148 deletions

View File

@@ -11,256 +11,28 @@ WICHTIG: Keine wertenden Metriken (z.B. "Sie haben 70% geredet").
Fokus auf neutrale, hilfreiche Statistiken.
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from datetime import datetime
from typing import Optional, List, Dict, Any
from enum import Enum
from .analytics_models import (
PhaseStatistics,
SessionSummary,
TeacherAnalytics,
LessonReflection,
)
# ==================== Analytics Models ====================
# Re-export models for backward compatibility
__all__ = [
"PhaseStatistics",
"SessionSummary",
"TeacherAnalytics",
"LessonReflection",
"AnalyticsCalculator",
]
@dataclass
class PhaseStatistics:
"""Statistik fuer eine einzelne Phase."""
phase: str
display_name: str
# Dauer-Metriken
planned_duration_seconds: int
actual_duration_seconds: int
difference_seconds: int # positiv = laenger als geplant
# Overtime
had_overtime: bool
overtime_seconds: int = 0
# Erweiterungen
was_extended: bool = False
extension_minutes: int = 0
# Pausen
pause_count: int = 0
total_pause_seconds: int = 0
def to_dict(self) -> Dict[str, Any]:
return {
"phase": self.phase,
"display_name": self.display_name,
"planned_duration_seconds": self.planned_duration_seconds,
"actual_duration_seconds": self.actual_duration_seconds,
"difference_seconds": self.difference_seconds,
"difference_formatted": self._format_difference(),
"had_overtime": self.had_overtime,
"overtime_seconds": self.overtime_seconds,
"overtime_formatted": self._format_seconds(self.overtime_seconds),
"was_extended": self.was_extended,
"extension_minutes": self.extension_minutes,
"pause_count": self.pause_count,
"total_pause_seconds": self.total_pause_seconds,
}
def _format_difference(self) -> str:
"""Formatiert die Differenz als +/-MM:SS."""
prefix = "+" if self.difference_seconds >= 0 else ""
return f"{prefix}{self._format_seconds(abs(self.difference_seconds))}"
def _format_seconds(self, seconds: int) -> str:
"""Formatiert Sekunden als MM:SS."""
mins = seconds // 60
secs = seconds % 60
return f"{mins:02d}:{secs:02d}"
@dataclass
class SessionSummary:
"""
Zusammenfassung einer Unterrichtsstunde.
Wird nach Stundenende generiert und fuer das Lehrer-Dashboard verwendet.
"""
session_id: str
teacher_id: str
class_id: str
subject: str
topic: Optional[str]
date: datetime
# Dauer
total_duration_seconds: int
planned_duration_seconds: int
# Phasen-Statistiken
phases_completed: int
total_phases: int = 5
phase_statistics: List[PhaseStatistics] = field(default_factory=list)
# Overtime-Zusammenfassung
total_overtime_seconds: int = 0
phases_with_overtime: int = 0
# Pausen-Zusammenfassung
total_pause_count: int = 0
total_pause_seconds: int = 0
# Post-Lesson Reflection
reflection_notes: str = ""
reflection_rating: Optional[int] = None # 1-5 Sterne (optional)
key_learnings: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"session_id": self.session_id,
"teacher_id": self.teacher_id,
"class_id": self.class_id,
"subject": self.subject,
"topic": self.topic,
"date": self.date.isoformat() if self.date else None,
"date_formatted": self._format_date(),
"total_duration_seconds": self.total_duration_seconds,
"total_duration_formatted": self._format_seconds(self.total_duration_seconds),
"planned_duration_seconds": self.planned_duration_seconds,
"planned_duration_formatted": self._format_seconds(self.planned_duration_seconds),
"phases_completed": self.phases_completed,
"total_phases": self.total_phases,
"completion_percentage": round(self.phases_completed / self.total_phases * 100),
"phase_statistics": [p.to_dict() for p in self.phase_statistics],
"total_overtime_seconds": self.total_overtime_seconds,
"total_overtime_formatted": self._format_seconds(self.total_overtime_seconds),
"phases_with_overtime": self.phases_with_overtime,
"total_pause_count": self.total_pause_count,
"total_pause_seconds": self.total_pause_seconds,
"reflection_notes": self.reflection_notes,
"reflection_rating": self.reflection_rating,
"key_learnings": self.key_learnings,
}
def _format_seconds(self, seconds: int) -> str:
mins = seconds // 60
secs = seconds % 60
return f"{mins:02d}:{secs:02d}"
def _format_date(self) -> str:
if not self.date:
return ""
return self.date.strftime("%d.%m.%Y %H:%M")
@dataclass
class TeacherAnalytics:
"""
Aggregierte Statistiken fuer einen Lehrer.
Zeigt Trends und Muster ueber mehrere Stunden.
"""
teacher_id: str
period_start: datetime
period_end: datetime
# Stunden-Uebersicht
total_sessions: int = 0
completed_sessions: int = 0
total_teaching_minutes: int = 0
# Durchschnittliche Phasendauern
avg_phase_durations: Dict[str, float] = field(default_factory=dict)
# Overtime-Trends
sessions_with_overtime: int = 0
avg_overtime_seconds: float = 0
most_overtime_phase: Optional[str] = None
# Pausen-Statistik
avg_pause_count: float = 0
avg_pause_duration_seconds: float = 0
# Faecher-Verteilung
subjects_taught: Dict[str, int] = field(default_factory=dict)
# Klassen-Verteilung
classes_taught: Dict[str, int] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return {
"teacher_id": self.teacher_id,
"period_start": self.period_start.isoformat() if self.period_start else None,
"period_end": self.period_end.isoformat() if self.period_end else None,
"total_sessions": self.total_sessions,
"completed_sessions": self.completed_sessions,
"total_teaching_minutes": self.total_teaching_minutes,
"total_teaching_hours": round(self.total_teaching_minutes / 60, 1),
"avg_phase_durations": self.avg_phase_durations,
"sessions_with_overtime": self.sessions_with_overtime,
"overtime_percentage": round(self.sessions_with_overtime / max(self.total_sessions, 1) * 100),
"avg_overtime_seconds": round(self.avg_overtime_seconds),
"avg_overtime_formatted": self._format_seconds(int(self.avg_overtime_seconds)),
"most_overtime_phase": self.most_overtime_phase,
"avg_pause_count": round(self.avg_pause_count, 1),
"avg_pause_duration_seconds": round(self.avg_pause_duration_seconds),
"subjects_taught": self.subjects_taught,
"classes_taught": self.classes_taught,
}
def _format_seconds(self, seconds: int) -> str:
mins = seconds // 60
secs = seconds % 60
return f"{mins:02d}:{secs:02d}"
# ==================== Reflection Model ====================
@dataclass
class LessonReflection:
"""
Post-Lesson Reflection (Feature).
Ermoeglicht Lehrern, nach der Stunde Notizen zu machen.
Keine Bewertung, nur Reflexion.
"""
reflection_id: str
session_id: str
teacher_id: str
# Reflexionsnotizen
notes: str = ""
# Optional: Sterne-Bewertung (selbst-eingeschaetzt)
overall_rating: Optional[int] = None # 1-5
# Was hat gut funktioniert?
what_worked: List[str] = field(default_factory=list)
# Was wuerde ich naechstes Mal anders machen?
improvements: List[str] = field(default_factory=list)
# Notizen fuer naechste Stunde
notes_for_next_lesson: str = ""
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
def to_dict(self) -> Dict[str, Any]:
return {
"reflection_id": self.reflection_id,
"session_id": self.session_id,
"teacher_id": self.teacher_id,
"notes": self.notes,
"overall_rating": self.overall_rating,
"what_worked": self.what_worked,
"improvements": self.improvements,
"notes_for_next_lesson": self.notes_for_next_lesson,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
# ==================== Analytics Calculator ====================
class AnalyticsCalculator:
"""
Berechnet Analytics aus Session-Daten.
Verwendet In-Memory-Daten oder DB-Daten.
"""
"""Berechnet Analytics aus Session-Daten."""
PHASE_DISPLAY_NAMES = {
"einstieg": "Einstieg",
@@ -276,24 +48,13 @@ class AnalyticsCalculator:
session_data: Dict[str, Any],
phase_history: List[Dict[str, Any]]
) -> SessionSummary:
"""
Berechnet die Zusammenfassung einer Session.
Args:
session_data: Session-Dictionary (aus LessonSession.to_dict())
phase_history: Liste der Phasen-History-Eintraege
Returns:
SessionSummary mit allen berechneten Statistiken
"""
# Basis-Daten
"""Berechnet die Zusammenfassung einer Session."""
session_id = session_data.get("session_id", "")
teacher_id = session_data.get("teacher_id", "")
class_id = session_data.get("class_id", "")
subject = session_data.get("subject", "")
topic = session_data.get("topic")
# Timestamps
lesson_started = session_data.get("lesson_started_at")
lesson_ended = session_data.get("lesson_ended_at")
@@ -302,16 +63,13 @@ class AnalyticsCalculator:
if isinstance(lesson_ended, str):
lesson_ended = datetime.fromisoformat(lesson_ended.replace("Z", "+00:00"))
# Dauer berechnen
total_duration = 0
if lesson_started and lesson_ended:
total_duration = int((lesson_ended - lesson_started).total_seconds())
# Geplante Dauer
phase_durations = session_data.get("phase_durations", {})
planned_duration = sum(phase_durations.values()) * 60 # Minuten zu Sekunden
planned_duration = sum(phase_durations.values()) * 60
# Phasen-Statistiken berechnen
phase_stats = []
total_overtime = 0
phases_with_overtime = 0
@@ -324,18 +82,10 @@ class AnalyticsCalculator:
if phase in ["not_started", "ended"]:
continue
# Geplante Dauer fuer diese Phase
planned_seconds = phase_durations.get(phase, 0) * 60
# Tatsaechliche Dauer
actual_seconds = entry.get("duration_seconds", 0)
if actual_seconds is None:
actual_seconds = 0
# Differenz
actual_seconds = entry.get("duration_seconds", 0) or 0
difference = actual_seconds - planned_seconds
# Overtime (nur positive Differenz zaehlt)
had_overtime = difference > 0
overtime_seconds = max(0, difference)
@@ -343,13 +93,11 @@ class AnalyticsCalculator:
total_overtime += overtime_seconds
phases_with_overtime += 1
# Pausen
pause_count = entry.get("pause_count", 0) or 0
pause_seconds = entry.get("total_pause_seconds", 0) or 0
total_pause_count += pause_count
total_pause_seconds += pause_seconds
# Phase als abgeschlossen zaehlen
if entry.get("ended_at"):
phases_completed += 1
@@ -368,16 +116,12 @@ class AnalyticsCalculator:
))
return SessionSummary(
session_id=session_id,
teacher_id=teacher_id,
class_id=class_id,
subject=subject,
topic=topic,
session_id=session_id, teacher_id=teacher_id,
class_id=class_id, subject=subject, topic=topic,
date=lesson_started or datetime.now(),
total_duration_seconds=total_duration,
planned_duration_seconds=planned_duration,
phases_completed=phases_completed,
total_phases=5,
phases_completed=phases_completed, total_phases=5,
phase_statistics=phase_stats,
total_overtime_seconds=total_overtime,
phases_with_overtime=phases_with_overtime,
@@ -392,31 +136,15 @@ class AnalyticsCalculator:
period_start: datetime,
period_end: datetime
) -> TeacherAnalytics:
"""
Berechnet aggregierte Statistiken fuer einen Lehrer.
Args:
sessions: Liste von Session-Dictionaries
period_start: Beginn des Zeitraums
period_end: Ende des Zeitraums
Returns:
TeacherAnalytics mit aggregierten Statistiken
"""
"""Berechnet aggregierte Statistiken fuer einen Lehrer."""
if not sessions:
return TeacherAnalytics(
teacher_id="",
period_start=period_start,
period_end=period_end,
)
return TeacherAnalytics(teacher_id="", period_start=period_start, period_end=period_end)
teacher_id = sessions[0].get("teacher_id", "")
# Basis-Zaehler
total_sessions = len(sessions)
completed_sessions = sum(1 for s in sessions if s.get("lesson_ended_at"))
# Gesamtdauer berechnen
total_minutes = 0
for session in sessions:
started = session.get("lesson_started_at")
@@ -428,41 +156,29 @@ class AnalyticsCalculator:
ended = datetime.fromisoformat(ended.replace("Z", "+00:00"))
total_minutes += (ended - started).total_seconds() / 60
# Durchschnittliche Phasendauern
phase_durations_sum: Dict[str, List[int]] = {
"einstieg": [],
"erarbeitung": [],
"sicherung": [],
"transfer": [],
"reflexion": [],
"einstieg": [], "erarbeitung": [], "sicherung": [],
"transfer": [], "reflexion": [],
}
# Overtime-Tracking
overtime_count = 0
overtime_seconds_total = 0
phase_overtime: Dict[str, int] = {}
# Pausen-Tracking
pause_counts = []
pause_durations = []
# Faecher und Klassen
subjects: Dict[str, int] = {}
classes: Dict[str, int] = {}
for session in sessions:
# Fach und Klasse zaehlen
subject = session.get("subject", "")
class_id = session.get("class_id", "")
subjects[subject] = subjects.get(subject, 0) + 1
classes[class_id] = classes.get(class_id, 0) + 1
# Phase History analysieren
history = session.get("phase_history", [])
session_has_overtime = False
session_pause_count = 0
session_pause_duration = 0
phase_durations_dict = session.get("phase_durations", {})
for entry in history:
@@ -471,7 +187,6 @@ class AnalyticsCalculator:
duration = entry.get("duration_seconds", 0) or 0
phase_durations_sum[phase].append(duration)
# Overtime berechnen
planned = phase_durations_dict.get(phase, 0) * 60
if duration > planned:
overtime = duration - planned
@@ -479,35 +194,25 @@ class AnalyticsCalculator:
session_has_overtime = True
phase_overtime[phase] = phase_overtime.get(phase, 0) + overtime
# Pausen zaehlen
session_pause_count += entry.get("pause_count", 0) or 0
session_pause_duration += entry.get("total_pause_seconds", 0) or 0
if session_has_overtime:
overtime_count += 1
pause_counts.append(session_pause_count)
pause_durations.append(session_pause_duration)
# Durchschnitte berechnen
avg_durations = {}
for phase, durations in phase_durations_sum.items():
if durations:
avg_durations[phase] = round(sum(durations) / len(durations))
else:
avg_durations[phase] = 0
avg_durations[phase] = round(sum(durations) / len(durations)) if durations else 0
# Phase mit meistem Overtime finden
most_overtime_phase = None
if phase_overtime:
most_overtime_phase = max(phase_overtime, key=phase_overtime.get)
return TeacherAnalytics(
teacher_id=teacher_id,
period_start=period_start,
period_end=period_end,
total_sessions=total_sessions,
completed_sessions=completed_sessions,
teacher_id=teacher_id, period_start=period_start, period_end=period_end,
total_sessions=total_sessions, completed_sessions=completed_sessions,
total_teaching_minutes=int(total_minutes),
avg_phase_durations=avg_durations,
sessions_with_overtime=overtime_count,
@@ -515,6 +220,5 @@ class AnalyticsCalculator:
most_overtime_phase=most_overtime_phase,
avg_pause_count=sum(pause_counts) / max(len(pause_counts), 1),
avg_pause_duration_seconds=sum(pause_durations) / max(len(pause_durations), 1),
subjects_taught=subjects,
classes_taught=classes,
subjects_taught=subjects, classes_taught=classes,
)