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>
225 lines
8.3 KiB
Python
225 lines
8.3 KiB
Python
"""
|
|
Analytics-Modul fuer Classroom Engine (Phase 5).
|
|
|
|
Bietet Statistiken und Auswertungen fuer Unterrichtsstunden:
|
|
- Phasen-Dauer Statistiken
|
|
- Overtime-Analyse
|
|
- Lehrer-Dashboard Daten
|
|
- Post-Lesson Reflection
|
|
|
|
WICHTIG: Keine wertenden Metriken (z.B. "Sie haben 70% geredet").
|
|
Fokus auf neutrale, hilfreiche Statistiken.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from .analytics_models import (
|
|
PhaseStatistics,
|
|
SessionSummary,
|
|
TeacherAnalytics,
|
|
LessonReflection,
|
|
)
|
|
|
|
# Re-export models for backward compatibility
|
|
__all__ = [
|
|
"PhaseStatistics",
|
|
"SessionSummary",
|
|
"TeacherAnalytics",
|
|
"LessonReflection",
|
|
"AnalyticsCalculator",
|
|
]
|
|
|
|
|
|
class AnalyticsCalculator:
|
|
"""Berechnet Analytics aus Session-Daten."""
|
|
|
|
PHASE_DISPLAY_NAMES = {
|
|
"einstieg": "Einstieg",
|
|
"erarbeitung": "Erarbeitung",
|
|
"sicherung": "Sicherung",
|
|
"transfer": "Transfer",
|
|
"reflexion": "Reflexion",
|
|
}
|
|
|
|
@classmethod
|
|
def calculate_session_summary(
|
|
cls,
|
|
session_data: Dict[str, Any],
|
|
phase_history: List[Dict[str, Any]]
|
|
) -> SessionSummary:
|
|
"""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")
|
|
|
|
lesson_started = session_data.get("lesson_started_at")
|
|
lesson_ended = session_data.get("lesson_ended_at")
|
|
|
|
if isinstance(lesson_started, str):
|
|
lesson_started = datetime.fromisoformat(lesson_started.replace("Z", "+00:00"))
|
|
if isinstance(lesson_ended, str):
|
|
lesson_ended = datetime.fromisoformat(lesson_ended.replace("Z", "+00:00"))
|
|
|
|
total_duration = 0
|
|
if lesson_started and lesson_ended:
|
|
total_duration = int((lesson_ended - lesson_started).total_seconds())
|
|
|
|
phase_durations = session_data.get("phase_durations", {})
|
|
planned_duration = sum(phase_durations.values()) * 60
|
|
|
|
phase_stats = []
|
|
total_overtime = 0
|
|
phases_with_overtime = 0
|
|
total_pause_count = 0
|
|
total_pause_seconds = 0
|
|
phases_completed = 0
|
|
|
|
for entry in phase_history:
|
|
phase = entry.get("phase", "")
|
|
if phase in ["not_started", "ended"]:
|
|
continue
|
|
|
|
planned_seconds = phase_durations.get(phase, 0) * 60
|
|
actual_seconds = entry.get("duration_seconds", 0) or 0
|
|
difference = actual_seconds - planned_seconds
|
|
|
|
had_overtime = difference > 0
|
|
overtime_seconds = max(0, difference)
|
|
|
|
if had_overtime:
|
|
total_overtime += overtime_seconds
|
|
phases_with_overtime += 1
|
|
|
|
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
|
|
|
|
if entry.get("ended_at"):
|
|
phases_completed += 1
|
|
|
|
phase_stats.append(PhaseStatistics(
|
|
phase=phase,
|
|
display_name=cls.PHASE_DISPLAY_NAMES.get(phase, phase),
|
|
planned_duration_seconds=planned_seconds,
|
|
actual_duration_seconds=actual_seconds,
|
|
difference_seconds=difference,
|
|
had_overtime=had_overtime,
|
|
overtime_seconds=overtime_seconds,
|
|
was_extended=entry.get("was_extended", False),
|
|
extension_minutes=entry.get("extension_minutes", 0) or 0,
|
|
pause_count=pause_count,
|
|
total_pause_seconds=pause_seconds,
|
|
))
|
|
|
|
return SessionSummary(
|
|
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,
|
|
phase_statistics=phase_stats,
|
|
total_overtime_seconds=total_overtime,
|
|
phases_with_overtime=phases_with_overtime,
|
|
total_pause_count=total_pause_count,
|
|
total_pause_seconds=total_pause_seconds,
|
|
)
|
|
|
|
@classmethod
|
|
def calculate_teacher_analytics(
|
|
cls,
|
|
sessions: List[Dict[str, Any]],
|
|
period_start: datetime,
|
|
period_end: datetime
|
|
) -> TeacherAnalytics:
|
|
"""Berechnet aggregierte Statistiken fuer einen Lehrer."""
|
|
if not sessions:
|
|
return TeacherAnalytics(teacher_id="", period_start=period_start, period_end=period_end)
|
|
|
|
teacher_id = sessions[0].get("teacher_id", "")
|
|
|
|
total_sessions = len(sessions)
|
|
completed_sessions = sum(1 for s in sessions if s.get("lesson_ended_at"))
|
|
|
|
total_minutes = 0
|
|
for session in sessions:
|
|
started = session.get("lesson_started_at")
|
|
ended = session.get("lesson_ended_at")
|
|
if started and ended:
|
|
if isinstance(started, str):
|
|
started = datetime.fromisoformat(started.replace("Z", "+00:00"))
|
|
if isinstance(ended, str):
|
|
ended = datetime.fromisoformat(ended.replace("Z", "+00:00"))
|
|
total_minutes += (ended - started).total_seconds() / 60
|
|
|
|
phase_durations_sum: Dict[str, List[int]] = {
|
|
"einstieg": [], "erarbeitung": [], "sicherung": [],
|
|
"transfer": [], "reflexion": [],
|
|
}
|
|
|
|
overtime_count = 0
|
|
overtime_seconds_total = 0
|
|
phase_overtime: Dict[str, int] = {}
|
|
pause_counts = []
|
|
pause_durations = []
|
|
subjects: Dict[str, int] = {}
|
|
classes: Dict[str, int] = {}
|
|
|
|
for session in sessions:
|
|
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
|
|
|
|
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:
|
|
phase = entry.get("phase", "")
|
|
if phase in phase_durations_sum:
|
|
duration = entry.get("duration_seconds", 0) or 0
|
|
phase_durations_sum[phase].append(duration)
|
|
|
|
planned = phase_durations_dict.get(phase, 0) * 60
|
|
if duration > planned:
|
|
overtime = duration - planned
|
|
overtime_seconds_total += overtime
|
|
session_has_overtime = True
|
|
phase_overtime[phase] = phase_overtime.get(phase, 0) + overtime
|
|
|
|
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)
|
|
|
|
avg_durations = {}
|
|
for phase, durations in phase_durations_sum.items():
|
|
avg_durations[phase] = round(sum(durations) / len(durations)) if durations else 0
|
|
|
|
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,
|
|
total_teaching_minutes=int(total_minutes),
|
|
avg_phase_durations=avg_durations,
|
|
sessions_with_overtime=overtime_count,
|
|
avg_overtime_seconds=overtime_seconds_total / max(total_sessions, 1),
|
|
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,
|
|
)
|