[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user