[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:
@@ -32,7 +32,8 @@ from .models import (
|
||||
)
|
||||
from .fsm import LessonStateMachine
|
||||
from .timer import PhaseTimer
|
||||
from .suggestions import SuggestionEngine, PHASE_SUGGESTIONS, SUBJECT_SUGGESTIONS
|
||||
from .suggestions import SuggestionEngine
|
||||
from .suggestion_data import PHASE_SUGGESTIONS, SUBJECT_SUGGESTIONS
|
||||
from .context_models import (
|
||||
MacroPhaseEnum,
|
||||
EventTypeEnum,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
205
backend-lehrer/classroom_engine/analytics_models.py
Normal file
205
backend-lehrer/classroom_engine/analytics_models.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Analytics Models - Datenstrukturen fuer Classroom Analytics.
|
||||
|
||||
Enthaelt PhaseStatistics, SessionSummary, TeacherAnalytics, LessonReflection.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
@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:
|
||||
prefix = "+" if self.difference_seconds >= 0 else ""
|
||||
return f"{prefix}{self._format_seconds(abs(self.difference_seconds))}"
|
||||
|
||||
def _format_seconds(self, seconds: int) -> str:
|
||||
mins = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{mins:02d}:{secs:02d}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionSummary:
|
||||
"""Zusammenfassung einer Unterrichtsstunde."""
|
||||
session_id: str
|
||||
teacher_id: str
|
||||
class_id: str
|
||||
subject: str
|
||||
topic: Optional[str]
|
||||
date: datetime
|
||||
|
||||
total_duration_seconds: int
|
||||
planned_duration_seconds: int
|
||||
|
||||
phases_completed: int
|
||||
total_phases: int = 5
|
||||
phase_statistics: List[PhaseStatistics] = field(default_factory=list)
|
||||
|
||||
total_overtime_seconds: int = 0
|
||||
phases_with_overtime: int = 0
|
||||
|
||||
total_pause_count: int = 0
|
||||
total_pause_seconds: int = 0
|
||||
|
||||
reflection_notes: str = ""
|
||||
reflection_rating: Optional[int] = None
|
||||
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."""
|
||||
teacher_id: str
|
||||
period_start: datetime
|
||||
period_end: datetime
|
||||
|
||||
total_sessions: int = 0
|
||||
completed_sessions: int = 0
|
||||
total_teaching_minutes: int = 0
|
||||
|
||||
avg_phase_durations: Dict[str, float] = field(default_factory=dict)
|
||||
|
||||
sessions_with_overtime: int = 0
|
||||
avg_overtime_seconds: float = 0
|
||||
most_overtime_phase: Optional[str] = None
|
||||
|
||||
avg_pause_count: float = 0
|
||||
avg_pause_duration_seconds: float = 0
|
||||
|
||||
subjects_taught: Dict[str, int] = field(default_factory=dict)
|
||||
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}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LessonReflection:
|
||||
"""Post-Lesson Reflection (Feature)."""
|
||||
reflection_id: str
|
||||
session_id: str
|
||||
teacher_id: str
|
||||
|
||||
notes: str = ""
|
||||
overall_rating: Optional[int] = None
|
||||
what_worked: List[str] = field(default_factory=list)
|
||||
improvements: List[str] = field(default_factory=list)
|
||||
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,
|
||||
}
|
||||
494
backend-lehrer/classroom_engine/suggestion_data.py
Normal file
494
backend-lehrer/classroom_engine/suggestion_data.py
Normal file
@@ -0,0 +1,494 @@
|
||||
"""
|
||||
Phasenspezifische und fachspezifische Vorschlags-Daten (Feature f18).
|
||||
|
||||
Enthaelt die vordefinierten Vorschlaege fuer allgemeine Phasen
|
||||
und fachspezifische Aktivitaeten.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from .models import LessonPhase
|
||||
|
||||
|
||||
# Unterstuetzte Faecher fuer fachspezifische Vorschlaege
|
||||
SUPPORTED_SUBJECTS = [
|
||||
"mathematik", "mathe", "math",
|
||||
"deutsch",
|
||||
"englisch", "english",
|
||||
"biologie", "bio",
|
||||
"physik",
|
||||
"chemie",
|
||||
"geschichte",
|
||||
"geografie", "erdkunde",
|
||||
"kunst",
|
||||
"musik",
|
||||
"sport",
|
||||
"informatik",
|
||||
]
|
||||
|
||||
|
||||
# Fachspezifische Vorschlaege (Feature f18)
|
||||
SUBJECT_SUGGESTIONS: Dict[str, Dict[LessonPhase, List[Dict[str, Any]]]] = {
|
||||
"mathematik": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "math_warm_up",
|
||||
"title": "Kopfrechnen-Challenge",
|
||||
"description": "5 schnelle Kopfrechenaufgaben zum Aufwaermen",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "calculate",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
{
|
||||
"id": "math_puzzle",
|
||||
"title": "Mathematisches Raetsel",
|
||||
"description": "Ein kniffliges Zahlenraetsel als Einstieg",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "extension",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "math_geogebra",
|
||||
"title": "GeoGebra-Exploration",
|
||||
"description": "Interaktive Visualisierung mit GeoGebra",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "functions",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
{
|
||||
"id": "math_peer_explain",
|
||||
"title": "Rechenweg erklaeren",
|
||||
"description": "Schueler erklaeren sich gegenseitig ihre Loesungswege",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "groups",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
],
|
||||
LessonPhase.SICHERUNG: [
|
||||
{
|
||||
"id": "math_formula_card",
|
||||
"title": "Formelkarte erstellen",
|
||||
"description": "Wichtigste Formeln auf einer Karte festhalten",
|
||||
"activity_type": "documentation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "note_alt",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"deutsch": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "deutsch_wordle",
|
||||
"title": "Wordle-Variante",
|
||||
"description": "Wort des Tages erraten",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "abc",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
{
|
||||
"id": "deutsch_zitat",
|
||||
"title": "Zitat-Interpretation",
|
||||
"description": "Ein literarisches Zitat gemeinsam deuten",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "format_quote",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "deutsch_textarbeit",
|
||||
"title": "Textanalyse in Gruppen",
|
||||
"description": "Gruppenarbeit zu verschiedenen Textabschnitten",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "menu_book",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
{
|
||||
"id": "deutsch_schreibworkshop",
|
||||
"title": "Schreibwerkstatt",
|
||||
"description": "Kreatives Schreiben mit Peer-Feedback",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "edit_note",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
],
|
||||
LessonPhase.SICHERUNG: [
|
||||
{
|
||||
"id": "deutsch_zusammenfassung",
|
||||
"title": "Text-Zusammenfassung",
|
||||
"description": "Die wichtigsten Punkte in 3 Saetzen formulieren",
|
||||
"activity_type": "summary",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "summarize",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"englisch": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "english_smalltalk",
|
||||
"title": "Small Talk Warm-Up",
|
||||
"description": "2-Minuten Gespraeche zu einem Alltagsthema",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "chat",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
{
|
||||
"id": "english_video",
|
||||
"title": "Authentic Video Clip",
|
||||
"description": "Kurzer Clip aus einer englischen Serie oder Nachricht",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "movie",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "english_role_play",
|
||||
"title": "Role Play Activity",
|
||||
"description": "Dialoguebung in authentischen Situationen",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 12,
|
||||
"icon": "theater_comedy",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
{
|
||||
"id": "english_reading_circle",
|
||||
"title": "Reading Circle",
|
||||
"description": "Gemeinsames Lesen mit verteilten Rollen",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "auto_stories",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"biologie": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "bio_nature_question",
|
||||
"title": "Naturfrage",
|
||||
"description": "Eine spannende Frage aus der Natur diskutieren",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "eco",
|
||||
"subjects": ["biologie", "bio"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "bio_experiment",
|
||||
"title": "Mini-Experiment",
|
||||
"description": "Einfaches Experiment zum Thema durchfuehren",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "science",
|
||||
"subjects": ["biologie", "bio"],
|
||||
},
|
||||
{
|
||||
"id": "bio_diagram",
|
||||
"title": "Biologische Zeichnung",
|
||||
"description": "Beschriftete Zeichnung eines Organismus",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "draw",
|
||||
"subjects": ["biologie", "bio"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"physik": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "physik_demo",
|
||||
"title": "Phaenomen-Demo",
|
||||
"description": "Ein physikalisches Phaenomen vorfuehren",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "bolt",
|
||||
"subjects": ["physik"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "physik_simulation",
|
||||
"title": "PhET-Simulation",
|
||||
"description": "Interaktive Simulation von phet.colorado.edu",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "smart_toy",
|
||||
"subjects": ["physik"],
|
||||
},
|
||||
{
|
||||
"id": "physik_rechnung",
|
||||
"title": "Physikalische Rechnung",
|
||||
"description": "Rechenaufgabe mit physikalischem Kontext",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 12,
|
||||
"icon": "calculate",
|
||||
"subjects": ["physik"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"informatik": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "info_code_puzzle",
|
||||
"title": "Code-Puzzle",
|
||||
"description": "Kurzen Code-Schnipsel analysieren - was macht er?",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "code",
|
||||
"subjects": ["informatik"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "info_live_coding",
|
||||
"title": "Live Coding",
|
||||
"description": "Gemeinsam Code entwickeln mit Erklaerungen",
|
||||
"activity_type": "instruction",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "terminal",
|
||||
"subjects": ["informatik"],
|
||||
},
|
||||
{
|
||||
"id": "info_pair_programming",
|
||||
"title": "Pair Programming",
|
||||
"description": "Zu zweit programmieren - Driver und Navigator",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "computer",
|
||||
"subjects": ["informatik"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Vordefinierte allgemeine Vorschlaege pro Phase
|
||||
PHASE_SUGGESTIONS: Dict[LessonPhase, List[Dict[str, Any]]] = {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "warmup_quiz",
|
||||
"title": "Kurzes Quiz zum Einstieg",
|
||||
"description": "Aktivieren Sie das Vorwissen der Schueler mit 3-5 Fragen zum Thema",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "quiz"
|
||||
},
|
||||
{
|
||||
"id": "problem_story",
|
||||
"title": "Problemgeschichte erzaehlen",
|
||||
"description": "Stellen Sie ein alltagsnahes Problem vor, das zum Thema fuehrt",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "auto_stories"
|
||||
},
|
||||
{
|
||||
"id": "video_intro",
|
||||
"title": "Kurzes Erklaervideo",
|
||||
"description": "Zeigen Sie ein 2-3 Minuten Video zur Einfuehrung ins Thema",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "play_circle"
|
||||
},
|
||||
{
|
||||
"id": "brainstorming",
|
||||
"title": "Brainstorming",
|
||||
"description": "Sammeln Sie Ideen und Vorkenntnisse der Schueler an der Tafel",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "psychology"
|
||||
},
|
||||
{
|
||||
"id": "daily_challenge",
|
||||
"title": "Tagesaufgabe vorstellen",
|
||||
"description": "Praesentieren Sie die zentrale Frage oder Aufgabe der Stunde",
|
||||
"activity_type": "problem_introduction",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "flag"
|
||||
}
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "think_pair_share",
|
||||
"title": "Think-Pair-Share",
|
||||
"description": "Schueler denken erst einzeln nach, tauschen sich dann zu zweit aus und praesentieren im Plenum",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "groups"
|
||||
},
|
||||
{
|
||||
"id": "worksheet_digital",
|
||||
"title": "Digitales Arbeitsblatt",
|
||||
"description": "Schueler bearbeiten ein interaktives Arbeitsblatt am Tablet oder Computer",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "description"
|
||||
},
|
||||
{
|
||||
"id": "station_learning",
|
||||
"title": "Stationenlernen",
|
||||
"description": "Verschiedene Stationen mit unterschiedlichen Aufgaben und Materialien",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "hub"
|
||||
},
|
||||
{
|
||||
"id": "expert_puzzle",
|
||||
"title": "Expertenrunde (Jigsaw)",
|
||||
"description": "Schueler werden Experten fuer ein Teilthema und lehren es anderen",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "extension"
|
||||
},
|
||||
{
|
||||
"id": "guided_instruction",
|
||||
"title": "Geleitete Instruktion",
|
||||
"description": "Schrittweise Erklaerung mit Uebungsphasen zwischendurch",
|
||||
"activity_type": "instruction",
|
||||
"estimated_minutes": 12,
|
||||
"icon": "school"
|
||||
},
|
||||
{
|
||||
"id": "pair_programming",
|
||||
"title": "Partnerarbeit",
|
||||
"description": "Zwei Schueler loesen gemeinsam eine Aufgabe",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "people"
|
||||
}
|
||||
],
|
||||
LessonPhase.SICHERUNG: [
|
||||
{
|
||||
"id": "mindmap_class",
|
||||
"title": "Gemeinsame Mindmap",
|
||||
"description": "Ergebnisse als Mindmap an der Tafel oder digital sammeln und strukturieren",
|
||||
"activity_type": "visualization",
|
||||
"estimated_minutes": 8,
|
||||
"icon": "account_tree"
|
||||
},
|
||||
{
|
||||
"id": "exit_ticket",
|
||||
"title": "Exit Ticket",
|
||||
"description": "Schueler notieren 3 Dinge die sie gelernt haben und 1 offene Frage",
|
||||
"activity_type": "summary",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "sticky_note_2"
|
||||
},
|
||||
{
|
||||
"id": "gallery_walk",
|
||||
"title": "Galerie-Rundgang",
|
||||
"description": "Schueler praesentieren ihre Ergebnisse und geben sich Feedback",
|
||||
"activity_type": "presentation",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "photo_library"
|
||||
},
|
||||
{
|
||||
"id": "key_points",
|
||||
"title": "Kernpunkte zusammenfassen",
|
||||
"description": "Gemeinsam die wichtigsten Erkenntnisse der Stunde formulieren",
|
||||
"activity_type": "summary",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "format_list_bulleted"
|
||||
},
|
||||
{
|
||||
"id": "quick_check",
|
||||
"title": "Schneller Wissenscheck",
|
||||
"description": "5 kurze Fragen zur Ueberpruefung des Verstaendnisses",
|
||||
"activity_type": "documentation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "fact_check"
|
||||
}
|
||||
],
|
||||
LessonPhase.TRANSFER: [
|
||||
{
|
||||
"id": "real_world_example",
|
||||
"title": "Alltagsbeispiele finden",
|
||||
"description": "Schueler suchen Beispiele aus ihrem Alltag, wo das Gelernte vorkommt",
|
||||
"activity_type": "application",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "public"
|
||||
},
|
||||
{
|
||||
"id": "challenge_task",
|
||||
"title": "Knobelaufgabe",
|
||||
"description": "Eine anspruchsvollere Aufgabe fuer schnelle Schueler oder als Bonus",
|
||||
"activity_type": "differentiation",
|
||||
"estimated_minutes": 7,
|
||||
"icon": "psychology"
|
||||
},
|
||||
{
|
||||
"id": "creative_application",
|
||||
"title": "Kreative Anwendung",
|
||||
"description": "Schueler wenden das Gelernte in einem kreativen Projekt an",
|
||||
"activity_type": "application",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "palette"
|
||||
},
|
||||
{
|
||||
"id": "peer_teaching",
|
||||
"title": "Peer-Teaching",
|
||||
"description": "Schueler erklaeren sich gegenseitig das Gelernte",
|
||||
"activity_type": "real_world_connection",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "supervisor_account"
|
||||
}
|
||||
],
|
||||
LessonPhase.REFLEXION: [
|
||||
{
|
||||
"id": "thumbs_feedback",
|
||||
"title": "Daumen-Feedback",
|
||||
"description": "Schnelle Stimmungsabfrage: Daumen hoch/mitte/runter",
|
||||
"activity_type": "feedback",
|
||||
"estimated_minutes": 2,
|
||||
"icon": "thumb_up"
|
||||
},
|
||||
{
|
||||
"id": "homework_assign",
|
||||
"title": "Hausaufgabe vergeben",
|
||||
"description": "Passende Hausaufgabe zur Vertiefung des Gelernten",
|
||||
"activity_type": "homework",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "home_work"
|
||||
},
|
||||
{
|
||||
"id": "one_word",
|
||||
"title": "Ein-Wort-Reflexion",
|
||||
"description": "Jeder Schueler nennt ein Wort, das die Stunde beschreibt",
|
||||
"activity_type": "feedback",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "chat"
|
||||
},
|
||||
{
|
||||
"id": "preview_next",
|
||||
"title": "Ausblick naechste Stunde",
|
||||
"description": "Kurzer Ausblick auf das Thema der naechsten Stunde",
|
||||
"activity_type": "preview",
|
||||
"estimated_minutes": 2,
|
||||
"icon": "event"
|
||||
},
|
||||
{
|
||||
"id": "learning_log",
|
||||
"title": "Lerntagebuch",
|
||||
"description": "Schueler notieren ihre wichtigsten Erkenntnisse im Lerntagebuch",
|
||||
"activity_type": "feedback",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "menu_book"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,490 +8,11 @@ und optional dem Fach.
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from .models import LessonPhase, LessonSession, PhaseSuggestion
|
||||
|
||||
|
||||
# Unterstuetzte Faecher fuer fachspezifische Vorschlaege
|
||||
SUPPORTED_SUBJECTS = [
|
||||
"mathematik", "mathe", "math",
|
||||
"deutsch",
|
||||
"englisch", "english",
|
||||
"biologie", "bio",
|
||||
"physik",
|
||||
"chemie",
|
||||
"geschichte",
|
||||
"geografie", "erdkunde",
|
||||
"kunst",
|
||||
"musik",
|
||||
"sport",
|
||||
"informatik",
|
||||
]
|
||||
|
||||
|
||||
# Fachspezifische Vorschlaege (Feature f18)
|
||||
SUBJECT_SUGGESTIONS: Dict[str, Dict[LessonPhase, List[Dict[str, Any]]]] = {
|
||||
"mathematik": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "math_warm_up",
|
||||
"title": "Kopfrechnen-Challenge",
|
||||
"description": "5 schnelle Kopfrechenaufgaben zum Aufwaermen",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "calculate",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
{
|
||||
"id": "math_puzzle",
|
||||
"title": "Mathematisches Raetsel",
|
||||
"description": "Ein kniffliges Zahlenraetsel als Einstieg",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "extension",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "math_geogebra",
|
||||
"title": "GeoGebra-Exploration",
|
||||
"description": "Interaktive Visualisierung mit GeoGebra",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "functions",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
{
|
||||
"id": "math_peer_explain",
|
||||
"title": "Rechenweg erklaeren",
|
||||
"description": "Schueler erklaeren sich gegenseitig ihre Loesungswege",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "groups",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
],
|
||||
LessonPhase.SICHERUNG: [
|
||||
{
|
||||
"id": "math_formula_card",
|
||||
"title": "Formelkarte erstellen",
|
||||
"description": "Wichtigste Formeln auf einer Karte festhalten",
|
||||
"activity_type": "documentation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "note_alt",
|
||||
"subjects": ["mathematik", "mathe"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"deutsch": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "deutsch_wordle",
|
||||
"title": "Wordle-Variante",
|
||||
"description": "Wort des Tages erraten",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "abc",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
{
|
||||
"id": "deutsch_zitat",
|
||||
"title": "Zitat-Interpretation",
|
||||
"description": "Ein literarisches Zitat gemeinsam deuten",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "format_quote",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "deutsch_textarbeit",
|
||||
"title": "Textanalyse in Gruppen",
|
||||
"description": "Gruppenarbeit zu verschiedenen Textabschnitten",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "menu_book",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
{
|
||||
"id": "deutsch_schreibworkshop",
|
||||
"title": "Schreibwerkstatt",
|
||||
"description": "Kreatives Schreiben mit Peer-Feedback",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "edit_note",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
],
|
||||
LessonPhase.SICHERUNG: [
|
||||
{
|
||||
"id": "deutsch_zusammenfassung",
|
||||
"title": "Text-Zusammenfassung",
|
||||
"description": "Die wichtigsten Punkte in 3 Saetzen formulieren",
|
||||
"activity_type": "summary",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "summarize",
|
||||
"subjects": ["deutsch"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"englisch": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "english_smalltalk",
|
||||
"title": "Small Talk Warm-Up",
|
||||
"description": "2-Minuten Gespraeche zu einem Alltagsthema",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "chat",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
{
|
||||
"id": "english_video",
|
||||
"title": "Authentic Video Clip",
|
||||
"description": "Kurzer Clip aus einer englischen Serie oder Nachricht",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "movie",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "english_role_play",
|
||||
"title": "Role Play Activity",
|
||||
"description": "Dialoguebung in authentischen Situationen",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 12,
|
||||
"icon": "theater_comedy",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
{
|
||||
"id": "english_reading_circle",
|
||||
"title": "Reading Circle",
|
||||
"description": "Gemeinsames Lesen mit verteilten Rollen",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "auto_stories",
|
||||
"subjects": ["englisch", "english"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"biologie": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "bio_nature_question",
|
||||
"title": "Naturfrage",
|
||||
"description": "Eine spannende Frage aus der Natur diskutieren",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "eco",
|
||||
"subjects": ["biologie", "bio"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "bio_experiment",
|
||||
"title": "Mini-Experiment",
|
||||
"description": "Einfaches Experiment zum Thema durchfuehren",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "science",
|
||||
"subjects": ["biologie", "bio"],
|
||||
},
|
||||
{
|
||||
"id": "bio_diagram",
|
||||
"title": "Biologische Zeichnung",
|
||||
"description": "Beschriftete Zeichnung eines Organismus",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "draw",
|
||||
"subjects": ["biologie", "bio"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"physik": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "physik_demo",
|
||||
"title": "Phaenomen-Demo",
|
||||
"description": "Ein physikalisches Phaenomen vorfuehren",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "bolt",
|
||||
"subjects": ["physik"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "physik_simulation",
|
||||
"title": "PhET-Simulation",
|
||||
"description": "Interaktive Simulation von phet.colorado.edu",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "smart_toy",
|
||||
"subjects": ["physik"],
|
||||
},
|
||||
{
|
||||
"id": "physik_rechnung",
|
||||
"title": "Physikalische Rechnung",
|
||||
"description": "Rechenaufgabe mit physikalischem Kontext",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 12,
|
||||
"icon": "calculate",
|
||||
"subjects": ["physik"],
|
||||
},
|
||||
],
|
||||
},
|
||||
"informatik": {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "info_code_puzzle",
|
||||
"title": "Code-Puzzle",
|
||||
"description": "Kurzen Code-Schnipsel analysieren - was macht er?",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "code",
|
||||
"subjects": ["informatik"],
|
||||
},
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "info_live_coding",
|
||||
"title": "Live Coding",
|
||||
"description": "Gemeinsam Code entwickeln mit Erklaerungen",
|
||||
"activity_type": "instruction",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "terminal",
|
||||
"subjects": ["informatik"],
|
||||
},
|
||||
{
|
||||
"id": "info_pair_programming",
|
||||
"title": "Pair Programming",
|
||||
"description": "Zu zweit programmieren - Driver und Navigator",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "computer",
|
||||
"subjects": ["informatik"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Vordefinierte allgemeine Vorschlaege pro Phase
|
||||
PHASE_SUGGESTIONS: Dict[LessonPhase, List[Dict[str, Any]]] = {
|
||||
LessonPhase.EINSTIEG: [
|
||||
{
|
||||
"id": "warmup_quiz",
|
||||
"title": "Kurzes Quiz zum Einstieg",
|
||||
"description": "Aktivieren Sie das Vorwissen der Schueler mit 3-5 Fragen zum Thema",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "quiz"
|
||||
},
|
||||
{
|
||||
"id": "problem_story",
|
||||
"title": "Problemgeschichte erzaehlen",
|
||||
"description": "Stellen Sie ein alltagsnahes Problem vor, das zum Thema fuehrt",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "auto_stories"
|
||||
},
|
||||
{
|
||||
"id": "video_intro",
|
||||
"title": "Kurzes Erklaervideo",
|
||||
"description": "Zeigen Sie ein 2-3 Minuten Video zur Einfuehrung ins Thema",
|
||||
"activity_type": "motivation",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "play_circle"
|
||||
},
|
||||
{
|
||||
"id": "brainstorming",
|
||||
"title": "Brainstorming",
|
||||
"description": "Sammeln Sie Ideen und Vorkenntnisse der Schueler an der Tafel",
|
||||
"activity_type": "warmup",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "psychology"
|
||||
},
|
||||
{
|
||||
"id": "daily_challenge",
|
||||
"title": "Tagesaufgabe vorstellen",
|
||||
"description": "Praesentieren Sie die zentrale Frage oder Aufgabe der Stunde",
|
||||
"activity_type": "problem_introduction",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "flag"
|
||||
}
|
||||
],
|
||||
LessonPhase.ERARBEITUNG: [
|
||||
{
|
||||
"id": "think_pair_share",
|
||||
"title": "Think-Pair-Share",
|
||||
"description": "Schueler denken erst einzeln nach, tauschen sich dann zu zweit aus und praesentieren im Plenum",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "groups"
|
||||
},
|
||||
{
|
||||
"id": "worksheet_digital",
|
||||
"title": "Digitales Arbeitsblatt",
|
||||
"description": "Schueler bearbeiten ein interaktives Arbeitsblatt am Tablet oder Computer",
|
||||
"activity_type": "individual_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "description"
|
||||
},
|
||||
{
|
||||
"id": "station_learning",
|
||||
"title": "Stationenlernen",
|
||||
"description": "Verschiedene Stationen mit unterschiedlichen Aufgaben und Materialien",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 20,
|
||||
"icon": "hub"
|
||||
},
|
||||
{
|
||||
"id": "expert_puzzle",
|
||||
"title": "Expertenrunde (Jigsaw)",
|
||||
"description": "Schueler werden Experten fuer ein Teilthema und lehren es anderen",
|
||||
"activity_type": "group_work",
|
||||
"estimated_minutes": 15,
|
||||
"icon": "extension"
|
||||
},
|
||||
{
|
||||
"id": "guided_instruction",
|
||||
"title": "Geleitete Instruktion",
|
||||
"description": "Schrittweise Erklaerung mit Uebungsphasen zwischendurch",
|
||||
"activity_type": "instruction",
|
||||
"estimated_minutes": 12,
|
||||
"icon": "school"
|
||||
},
|
||||
{
|
||||
"id": "pair_programming",
|
||||
"title": "Partnerarbeit",
|
||||
"description": "Zwei Schueler loesen gemeinsam eine Aufgabe",
|
||||
"activity_type": "partner_work",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "people"
|
||||
}
|
||||
],
|
||||
LessonPhase.SICHERUNG: [
|
||||
{
|
||||
"id": "mindmap_class",
|
||||
"title": "Gemeinsame Mindmap",
|
||||
"description": "Ergebnisse als Mindmap an der Tafel oder digital sammeln und strukturieren",
|
||||
"activity_type": "visualization",
|
||||
"estimated_minutes": 8,
|
||||
"icon": "account_tree"
|
||||
},
|
||||
{
|
||||
"id": "exit_ticket",
|
||||
"title": "Exit Ticket",
|
||||
"description": "Schueler notieren 3 Dinge die sie gelernt haben und 1 offene Frage",
|
||||
"activity_type": "summary",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "sticky_note_2"
|
||||
},
|
||||
{
|
||||
"id": "gallery_walk",
|
||||
"title": "Galerie-Rundgang",
|
||||
"description": "Schueler praesentieren ihre Ergebnisse und geben sich Feedback",
|
||||
"activity_type": "presentation",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "photo_library"
|
||||
},
|
||||
{
|
||||
"id": "key_points",
|
||||
"title": "Kernpunkte zusammenfassen",
|
||||
"description": "Gemeinsam die wichtigsten Erkenntnisse der Stunde formulieren",
|
||||
"activity_type": "summary",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "format_list_bulleted"
|
||||
},
|
||||
{
|
||||
"id": "quick_check",
|
||||
"title": "Schneller Wissenscheck",
|
||||
"description": "5 kurze Fragen zur Ueberpruefung des Verstaendnisses",
|
||||
"activity_type": "documentation",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "fact_check"
|
||||
}
|
||||
],
|
||||
LessonPhase.TRANSFER: [
|
||||
{
|
||||
"id": "real_world_example",
|
||||
"title": "Alltagsbeispiele finden",
|
||||
"description": "Schueler suchen Beispiele aus ihrem Alltag, wo das Gelernte vorkommt",
|
||||
"activity_type": "application",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "public"
|
||||
},
|
||||
{
|
||||
"id": "challenge_task",
|
||||
"title": "Knobelaufgabe",
|
||||
"description": "Eine anspruchsvollere Aufgabe fuer schnelle Schueler oder als Bonus",
|
||||
"activity_type": "differentiation",
|
||||
"estimated_minutes": 7,
|
||||
"icon": "psychology"
|
||||
},
|
||||
{
|
||||
"id": "creative_application",
|
||||
"title": "Kreative Anwendung",
|
||||
"description": "Schueler wenden das Gelernte in einem kreativen Projekt an",
|
||||
"activity_type": "application",
|
||||
"estimated_minutes": 10,
|
||||
"icon": "palette"
|
||||
},
|
||||
{
|
||||
"id": "peer_teaching",
|
||||
"title": "Peer-Teaching",
|
||||
"description": "Schueler erklaeren sich gegenseitig das Gelernte",
|
||||
"activity_type": "real_world_connection",
|
||||
"estimated_minutes": 5,
|
||||
"icon": "supervisor_account"
|
||||
}
|
||||
],
|
||||
LessonPhase.REFLEXION: [
|
||||
{
|
||||
"id": "thumbs_feedback",
|
||||
"title": "Daumen-Feedback",
|
||||
"description": "Schnelle Stimmungsabfrage: Daumen hoch/mitte/runter",
|
||||
"activity_type": "feedback",
|
||||
"estimated_minutes": 2,
|
||||
"icon": "thumb_up"
|
||||
},
|
||||
{
|
||||
"id": "homework_assign",
|
||||
"title": "Hausaufgabe vergeben",
|
||||
"description": "Passende Hausaufgabe zur Vertiefung des Gelernten",
|
||||
"activity_type": "homework",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "home_work"
|
||||
},
|
||||
{
|
||||
"id": "one_word",
|
||||
"title": "Ein-Wort-Reflexion",
|
||||
"description": "Jeder Schueler nennt ein Wort, das die Stunde beschreibt",
|
||||
"activity_type": "feedback",
|
||||
"estimated_minutes": 3,
|
||||
"icon": "chat"
|
||||
},
|
||||
{
|
||||
"id": "preview_next",
|
||||
"title": "Ausblick naechste Stunde",
|
||||
"description": "Kurzer Ausblick auf das Thema der naechsten Stunde",
|
||||
"activity_type": "preview",
|
||||
"estimated_minutes": 2,
|
||||
"icon": "event"
|
||||
},
|
||||
{
|
||||
"id": "learning_log",
|
||||
"title": "Lerntagebuch",
|
||||
"description": "Schueler notieren ihre wichtigsten Erkenntnisse im Lerntagebuch",
|
||||
"activity_type": "feedback",
|
||||
"estimated_minutes": 4,
|
||||
"icon": "menu_book"
|
||||
}
|
||||
]
|
||||
}
|
||||
from .suggestion_data import (
|
||||
SUPPORTED_SUBJECTS,
|
||||
SUBJECT_SUGGESTIONS,
|
||||
PHASE_SUGGESTIONS,
|
||||
)
|
||||
|
||||
|
||||
class SuggestionEngine:
|
||||
|
||||
Reference in New Issue
Block a user