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>
206 lines
7.3 KiB
Python
206 lines
7.3 KiB
Python
"""
|
|
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,
|
|
}
|