[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

@@ -12,21 +12,29 @@ Endpoints:
import logging
import uuid
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from state_engine import (
AnticipationEngine,
PhaseService,
TeacherContext,
SchoolYearPhase,
ClassSummary,
Event,
TeacherStats,
get_phase_info,
PHASE_INFO
)
from state_engine_models import (
MilestoneRequest,
TransitionRequest,
ContextResponse,
SuggestionsResponse,
DashboardResponse,
_teacher_contexts,
_milestones,
get_or_create_context,
update_context_from_services,
get_phase_display_name,
)
logger = logging.getLogger(__name__)
@@ -41,157 +49,15 @@ _engine = AnticipationEngine()
_phase_service = PhaseService()
# ============================================================================
# In-Memory Storage (später durch DB ersetzen)
# ============================================================================
# Simulierter Lehrer-Kontext (in Produktion aus DB)
_teacher_contexts: Dict[str, TeacherContext] = {}
_milestones: Dict[str, List[str]] = {} # teacher_id -> milestones
# ============================================================================
# Pydantic Models
# ============================================================================
class MilestoneRequest(BaseModel):
"""Request zum Abschließen eines Meilensteins."""
milestone: str = Field(..., description="Name des Meilensteins")
class TransitionRequest(BaseModel):
"""Request für Phasen-Übergang."""
target_phase: str = Field(..., description="Zielphase")
class ContextResponse(BaseModel):
"""Response mit TeacherContext."""
context: Dict[str, Any]
phase_info: Dict[str, Any]
class SuggestionsResponse(BaseModel):
"""Response mit Vorschlägen."""
suggestions: List[Dict[str, Any]]
current_phase: str
phase_display_name: str
priority_counts: Dict[str, int]
class DashboardResponse(BaseModel):
"""Response mit Dashboard-Daten."""
context: Dict[str, Any]
suggestions: List[Dict[str, Any]]
stats: Dict[str, Any]
upcoming_events: List[Dict[str, Any]]
progress: Dict[str, Any]
phases: List[Dict[str, Any]]
# ============================================================================
# Helper Functions
# ============================================================================
def _get_or_create_context(teacher_id: str) -> TeacherContext:
"""
Holt oder erstellt TeacherContext.
In Produktion würde dies aus der Datenbank geladen.
"""
if teacher_id not in _teacher_contexts:
# Erstelle Demo-Kontext
now = datetime.now()
school_year_start = datetime(now.year if now.month >= 8 else now.year - 1, 8, 1)
weeks_since_start = (now - school_year_start).days // 7
# Bestimme Phase basierend auf Monat
month = now.month
if month in [8, 9]:
phase = SchoolYearPhase.SCHOOL_YEAR_START
elif month in [10, 11]:
phase = SchoolYearPhase.TEACHING_SETUP
elif month == 12:
phase = SchoolYearPhase.PERFORMANCE_1
elif month in [1, 2]:
phase = SchoolYearPhase.SEMESTER_END
elif month in [3, 4]:
phase = SchoolYearPhase.TEACHING_2
elif month in [5, 6]:
phase = SchoolYearPhase.PERFORMANCE_2
else:
phase = SchoolYearPhase.YEAR_END
_teacher_contexts[teacher_id] = TeacherContext(
teacher_id=teacher_id,
school_id=str(uuid.uuid4()),
school_year_id=str(uuid.uuid4()),
federal_state="niedersachsen",
school_type="gymnasium",
school_year_start=school_year_start,
current_phase=phase,
phase_entered_at=now - timedelta(days=7),
weeks_since_start=weeks_since_start,
days_in_phase=7,
classes=[],
total_students=0,
upcoming_events=[],
completed_milestones=_milestones.get(teacher_id, []),
pending_milestones=[],
stats=TeacherStats(),
)
return _teacher_contexts[teacher_id]
def _update_context_from_services(ctx: TeacherContext) -> TeacherContext:
"""
Aktualisiert Kontext mit Daten aus anderen Services.
In Produktion würde dies von school-service, gradebook etc. laden.
"""
# Simulierte Daten - in Produktion API-Calls
# Hier könnten wir den Kontext mit echten Daten anreichern
# Berechne days_in_phase
ctx.days_in_phase = (datetime.now() - ctx.phase_entered_at).days
# Lade abgeschlossene Meilensteine
ctx.completed_milestones = _milestones.get(ctx.teacher_id, [])
# Berechne pending milestones
phase_info = get_phase_info(ctx.current_phase)
ctx.pending_milestones = [
m for m in phase_info.required_actions
if m not in ctx.completed_milestones
]
return ctx
def _get_phase_display_name(phase: str) -> str:
"""Gibt Display-Name für Phase zurück."""
try:
return get_phase_info(SchoolYearPhase(phase)).display_name
except (ValueError, KeyError):
return phase
# ============================================================================
# API Endpoints
# ============================================================================
@router.get("/context", response_model=ContextResponse)
async def get_teacher_context(teacher_id: str = Query("demo-teacher")):
"""
Gibt den aggregierten TeacherContext zurück.
Enthält alle relevanten Informationen für:
- Phasen-Anzeige
- Antizipations-Engine
- Dashboard
"""
ctx = _get_or_create_context(teacher_id)
ctx = _update_context_from_services(ctx)
"""Gibt den aggregierten TeacherContext zurück."""
ctx = get_or_create_context(teacher_id)
ctx = update_context_from_services(ctx)
phase_info = get_phase_info(ctx.current_phase)
@@ -210,10 +76,8 @@ async def get_teacher_context(teacher_id: str = Query("demo-teacher")):
@router.get("/phase")
async def get_current_phase(teacher_id: str = Query("demo-teacher")):
"""
Gibt die aktuelle Phase mit Details zurück.
"""
ctx = _get_or_create_context(teacher_id)
"""Gibt die aktuelle Phase mit Details zurück."""
ctx = get_or_create_context(teacher_id)
phase_info = get_phase_info(ctx.current_phase)
return {
@@ -230,11 +94,7 @@ async def get_current_phase(teacher_id: str = Query("demo-teacher")):
@router.get("/phases")
async def get_all_phases():
"""
Gibt alle Phasen mit Metadaten zurück.
Nützlich für die Phasen-Anzeige im Dashboard.
"""
"""Gibt alle Phasen mit Metadaten zurück."""
return {
"phases": _phase_service.get_all_phases()
}
@@ -242,13 +102,9 @@ async def get_all_phases():
@router.get("/suggestions", response_model=SuggestionsResponse)
async def get_suggestions(teacher_id: str = Query("demo-teacher")):
"""
Gibt Vorschläge basierend auf dem aktuellen Kontext zurück.
Die Vorschläge sind priorisiert und auf max. 5 limitiert.
"""
ctx = _get_or_create_context(teacher_id)
ctx = _update_context_from_services(ctx)
"""Gibt Vorschläge basierend auf dem aktuellen Kontext zurück."""
ctx = get_or_create_context(teacher_id)
ctx = update_context_from_services(ctx)
suggestions = _engine.get_suggestions(ctx)
priority_counts = _engine.count_by_priority(ctx)
@@ -256,18 +112,16 @@ async def get_suggestions(teacher_id: str = Query("demo-teacher")):
return SuggestionsResponse(
suggestions=[s.to_dict() for s in suggestions],
current_phase=ctx.current_phase.value,
phase_display_name=_get_phase_display_name(ctx.current_phase.value),
phase_display_name=get_phase_display_name(ctx.current_phase.value),
priority_counts=priority_counts,
)
@router.get("/suggestions/top")
async def get_top_suggestion(teacher_id: str = Query("demo-teacher")):
"""
Gibt den wichtigsten einzelnen Vorschlag zurück.
"""
ctx = _get_or_create_context(teacher_id)
ctx = _update_context_from_services(ctx)
"""Gibt den wichtigsten einzelnen Vorschlag zurück."""
ctx = get_or_create_context(teacher_id)
ctx = update_context_from_services(ctx)
suggestion = _engine.get_top_suggestion(ctx)
@@ -284,28 +138,17 @@ async def get_top_suggestion(teacher_id: str = Query("demo-teacher")):
@router.get("/dashboard", response_model=DashboardResponse)
async def get_dashboard_data(teacher_id: str = Query("demo-teacher")):
"""
Gibt alle Daten für das Begleiter-Dashboard zurück.
Kombiniert:
- TeacherContext
- Vorschläge
- Statistiken
- Termine
- Fortschritt
"""
ctx = _get_or_create_context(teacher_id)
ctx = _update_context_from_services(ctx)
"""Gibt alle Daten für das Begleiter-Dashboard zurück."""
ctx = get_or_create_context(teacher_id)
ctx = update_context_from_services(ctx)
suggestions = _engine.get_suggestions(ctx)
phase_info = get_phase_info(ctx.current_phase)
# Berechne Fortschritt
required = set(phase_info.required_actions)
completed = set(ctx.completed_milestones)
completed_in_phase = len(required.intersection(completed))
# Alle Phasen für Anzeige
all_phases = []
phase_order = [
SchoolYearPhase.ONBOARDING,
@@ -376,14 +219,9 @@ async def complete_milestone(
request: MilestoneRequest,
teacher_id: str = Query("demo-teacher")
):
"""
Markiert einen Meilenstein als erledigt.
Prüft automatisch ob ein Phasen-Übergang möglich ist.
"""
"""Markiert einen Meilenstein als erledigt."""
milestone = request.milestone
# Speichere Meilenstein
if teacher_id not in _milestones:
_milestones[teacher_id] = []
@@ -391,12 +229,10 @@ async def complete_milestone(
_milestones[teacher_id].append(milestone)
logger.info(f"Milestone '{milestone}' completed for teacher {teacher_id}")
# Aktualisiere Kontext
ctx = _get_or_create_context(teacher_id)
ctx = get_or_create_context(teacher_id)
ctx.completed_milestones = _milestones[teacher_id]
_teacher_contexts[teacher_id] = ctx
# Prüfe automatischen Phasen-Übergang
new_phase = _phase_service.check_and_transition(ctx)
if new_phase:
@@ -420,9 +256,7 @@ async def transition_phase(
request: TransitionRequest,
teacher_id: str = Query("demo-teacher")
):
"""
Führt einen manuellen Phasen-Übergang durch.
"""
"""Führt einen manuellen Phasen-Übergang durch."""
try:
target_phase = SchoolYearPhase(request.target_phase)
except ValueError:
@@ -431,16 +265,14 @@ async def transition_phase(
detail=f"Ungültige Phase: {request.target_phase}"
)
ctx = _get_or_create_context(teacher_id)
ctx = get_or_create_context(teacher_id)
# Prüfe ob Übergang erlaubt
if not _phase_service.can_transition_to(ctx, target_phase):
raise HTTPException(
status_code=400,
detail=f"Übergang von {ctx.current_phase.value} zu {target_phase.value} nicht erlaubt"
)
# Führe Übergang durch
old_phase = ctx.current_phase
ctx.current_phase = target_phase
ctx.phase_entered_at = datetime.now()
@@ -459,10 +291,8 @@ async def transition_phase(
@router.get("/next-phase")
async def get_next_phase(teacher_id: str = Query("demo-teacher")):
"""
Gibt die nächste Phase und Anforderungen zurück.
"""
ctx = _get_or_create_context(teacher_id)
"""Gibt die nächste Phase und Anforderungen zurück."""
ctx = get_or_create_context(teacher_id)
next_phase = _phase_service.get_next_phase(ctx.current_phase)
if not next_phase:
@@ -475,7 +305,6 @@ async def get_next_phase(teacher_id: str = Query("demo-teacher")):
next_info = get_phase_info(next_phase)
current_info = get_phase_info(ctx.current_phase)
# Fehlende Anforderungen
missing = [
m for m in current_info.required_actions
if m not in ctx.completed_milestones
@@ -505,7 +334,7 @@ async def demo_add_class(
teacher_id: str = Query("demo-teacher")
):
"""Demo: Fügt eine Klasse zum Kontext hinzu."""
ctx = _get_or_create_context(teacher_id)
ctx = get_or_create_context(teacher_id)
ctx.classes.append(ClassSummary(
class_id=str(uuid.uuid4()),
@@ -515,7 +344,6 @@ async def demo_add_class(
subject="Deutsch"
))
ctx.total_students += student_count
_teacher_contexts[teacher_id] = ctx
return {"success": True, "classes": len(ctx.classes)}
@@ -529,7 +357,7 @@ async def demo_add_event(
teacher_id: str = Query("demo-teacher")
):
"""Demo: Fügt ein Event zum Kontext hinzu."""
ctx = _get_or_create_context(teacher_id)
ctx = get_or_create_context(teacher_id)
ctx.upcoming_events.append(Event(
type=event_type,
@@ -538,7 +366,6 @@ async def demo_add_event(
in_days=in_days,
priority="high" if in_days <= 3 else "medium"
))
_teacher_contexts[teacher_id] = ctx
return {"success": True, "events": len(ctx.upcoming_events)}
@@ -554,7 +381,7 @@ async def demo_update_stats(
teacher_id: str = Query("demo-teacher")
):
"""Demo: Aktualisiert Statistiken."""
ctx = _get_or_create_context(teacher_id)
ctx = get_or_create_context(teacher_id)
if learning_units:
ctx.stats.learning_units_created = learning_units