Restructure: Move final 16 root files into packages (backend-lehrer)
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 35s
CI / test-python-klausur (push) Failing after 2m41s
CI / test-python-agent-core (push) Successful in 30s
CI / test-nodejs-website (push) Successful in 38s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 35s
CI / test-python-klausur (push) Failing after 2m41s
CI / test-python-agent-core (push) Successful in 30s
CI / test-nodejs-website (push) Successful in 38s
classroom/ (+2): state_engine_api, state_engine_models vocabulary/ (2): api, db worksheets/ (2): api, models services/ (+6): audio, email, translation, claude_vision, ai_processor, story_generator api/ (4): school, klausur_proxy, progress, user_language Only main.py + config.py remain at root. 16 shims added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,410 +1,4 @@
|
||||
"""
|
||||
State Engine API - REST API für Begleiter-Modus.
|
||||
|
||||
Endpoints:
|
||||
- GET /api/state/context - TeacherContext abrufen
|
||||
- GET /api/state/suggestions - Vorschläge abrufen
|
||||
- GET /api/state/dashboard - Dashboard-Daten
|
||||
- POST /api/state/milestone - Meilenstein abschließen
|
||||
- POST /api/state/transition - Phasen-Übergang
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from state_engine import (
|
||||
AnticipationEngine,
|
||||
PhaseService,
|
||||
SchoolYearPhase,
|
||||
ClassSummary,
|
||||
Event,
|
||||
get_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__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/state",
|
||||
tags=["state-engine"],
|
||||
)
|
||||
|
||||
# Singleton instances
|
||||
_engine = AnticipationEngine()
|
||||
_phase_service = PhaseService()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 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."""
|
||||
ctx = get_or_create_context(teacher_id)
|
||||
ctx = update_context_from_services(ctx)
|
||||
|
||||
phase_info = get_phase_info(ctx.current_phase)
|
||||
|
||||
return ContextResponse(
|
||||
context=ctx.to_dict(),
|
||||
phase_info={
|
||||
"phase": phase_info.phase.value,
|
||||
"display_name": phase_info.display_name,
|
||||
"description": phase_info.description,
|
||||
"typical_months": phase_info.typical_months,
|
||||
"required_actions": phase_info.required_actions,
|
||||
"optional_actions": phase_info.optional_actions,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
phase_info = get_phase_info(ctx.current_phase)
|
||||
|
||||
return {
|
||||
"current_phase": ctx.current_phase.value,
|
||||
"phase_info": {
|
||||
"display_name": phase_info.display_name,
|
||||
"description": phase_info.description,
|
||||
"expected_duration_weeks": phase_info.expected_duration_weeks,
|
||||
},
|
||||
"days_in_phase": ctx.days_in_phase,
|
||||
"progress": _phase_service.get_progress_percentage(ctx),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/phases")
|
||||
async def get_all_phases():
|
||||
"""Gibt alle Phasen mit Metadaten zurück."""
|
||||
return {
|
||||
"phases": _phase_service.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."""
|
||||
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)
|
||||
|
||||
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),
|
||||
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)
|
||||
|
||||
suggestion = _engine.get_top_suggestion(ctx)
|
||||
|
||||
if not suggestion:
|
||||
return {
|
||||
"suggestion": None,
|
||||
"message": "Alles erledigt! Keine offenen Aufgaben."
|
||||
}
|
||||
|
||||
return {
|
||||
"suggestion": suggestion.to_dict()
|
||||
}
|
||||
|
||||
|
||||
@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."""
|
||||
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)
|
||||
|
||||
required = set(phase_info.required_actions)
|
||||
completed = set(ctx.completed_milestones)
|
||||
completed_in_phase = len(required.intersection(completed))
|
||||
|
||||
all_phases = []
|
||||
phase_order = [
|
||||
SchoolYearPhase.ONBOARDING,
|
||||
SchoolYearPhase.SCHOOL_YEAR_START,
|
||||
SchoolYearPhase.TEACHING_SETUP,
|
||||
SchoolYearPhase.PERFORMANCE_1,
|
||||
SchoolYearPhase.SEMESTER_END,
|
||||
SchoolYearPhase.TEACHING_2,
|
||||
SchoolYearPhase.PERFORMANCE_2,
|
||||
SchoolYearPhase.YEAR_END,
|
||||
]
|
||||
|
||||
current_idx = phase_order.index(ctx.current_phase) if ctx.current_phase in phase_order else 0
|
||||
|
||||
for i, phase in enumerate(phase_order):
|
||||
info = get_phase_info(phase)
|
||||
all_phases.append({
|
||||
"phase": phase.value,
|
||||
"display_name": info.display_name,
|
||||
"short_name": info.display_name[:10],
|
||||
"is_current": phase == ctx.current_phase,
|
||||
"is_completed": i < current_idx,
|
||||
"is_future": i > current_idx,
|
||||
})
|
||||
|
||||
return DashboardResponse(
|
||||
context={
|
||||
"current_phase": ctx.current_phase.value,
|
||||
"phase_display_name": phase_info.display_name,
|
||||
"phase_description": phase_info.description,
|
||||
"weeks_since_start": ctx.weeks_since_start,
|
||||
"days_in_phase": ctx.days_in_phase,
|
||||
"federal_state": ctx.federal_state,
|
||||
"school_type": ctx.school_type,
|
||||
},
|
||||
suggestions=[s.to_dict() for s in suggestions],
|
||||
stats={
|
||||
"learning_units_created": ctx.stats.learning_units_created,
|
||||
"exams_scheduled": ctx.stats.exams_scheduled,
|
||||
"exams_graded": ctx.stats.exams_graded,
|
||||
"grades_entered": ctx.stats.grades_entered,
|
||||
"classes_count": len(ctx.classes),
|
||||
"students_count": ctx.total_students,
|
||||
},
|
||||
upcoming_events=[
|
||||
{
|
||||
"type": e.type,
|
||||
"title": e.title,
|
||||
"date": e.date.isoformat(),
|
||||
"in_days": e.in_days,
|
||||
"priority": e.priority,
|
||||
}
|
||||
for e in ctx.upcoming_events[:5]
|
||||
],
|
||||
progress={
|
||||
"completed": completed_in_phase,
|
||||
"total": len(required),
|
||||
"percentage": (completed_in_phase / len(required) * 100) if required else 100,
|
||||
"milestones_completed": list(completed.intersection(required)),
|
||||
"milestones_pending": list(required - completed),
|
||||
},
|
||||
phases=all_phases,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/milestone")
|
||||
async def complete_milestone(
|
||||
request: MilestoneRequest,
|
||||
teacher_id: str = Query("demo-teacher")
|
||||
):
|
||||
"""Markiert einen Meilenstein als erledigt."""
|
||||
milestone = request.milestone
|
||||
|
||||
if teacher_id not in _milestones:
|
||||
_milestones[teacher_id] = []
|
||||
|
||||
if milestone not in _milestones[teacher_id]:
|
||||
_milestones[teacher_id].append(milestone)
|
||||
logger.info(f"Milestone '{milestone}' completed for teacher {teacher_id}")
|
||||
|
||||
ctx = get_or_create_context(teacher_id)
|
||||
ctx.completed_milestones = _milestones[teacher_id]
|
||||
_teacher_contexts[teacher_id] = ctx
|
||||
|
||||
new_phase = _phase_service.check_and_transition(ctx)
|
||||
|
||||
if new_phase:
|
||||
ctx.current_phase = new_phase
|
||||
ctx.phase_entered_at = datetime.now()
|
||||
ctx.days_in_phase = 0
|
||||
_teacher_contexts[teacher_id] = ctx
|
||||
logger.info(f"Auto-transitioned to {new_phase} for teacher {teacher_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"milestone": milestone,
|
||||
"new_phase": new_phase.value if new_phase else None,
|
||||
"current_phase": ctx.current_phase.value,
|
||||
"completed_milestones": ctx.completed_milestones,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/transition")
|
||||
async def transition_phase(
|
||||
request: TransitionRequest,
|
||||
teacher_id: str = Query("demo-teacher")
|
||||
):
|
||||
"""Führt einen manuellen Phasen-Übergang durch."""
|
||||
try:
|
||||
target_phase = SchoolYearPhase(request.target_phase)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ungültige Phase: {request.target_phase}"
|
||||
)
|
||||
|
||||
ctx = get_or_create_context(teacher_id)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
old_phase = ctx.current_phase
|
||||
ctx.current_phase = target_phase
|
||||
ctx.phase_entered_at = datetime.now()
|
||||
ctx.days_in_phase = 0
|
||||
_teacher_contexts[teacher_id] = ctx
|
||||
|
||||
logger.info(f"Manual transition from {old_phase} to {target_phase} for teacher {teacher_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"old_phase": old_phase.value,
|
||||
"new_phase": target_phase.value,
|
||||
"phase_info": get_phase_info(target_phase).__dict__,
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
next_phase = _phase_service.get_next_phase(ctx.current_phase)
|
||||
|
||||
if not next_phase:
|
||||
return {
|
||||
"next_phase": None,
|
||||
"message": "Letzte Phase erreicht"
|
||||
}
|
||||
|
||||
can_transition = _phase_service.can_transition_to(ctx, next_phase)
|
||||
next_info = get_phase_info(next_phase)
|
||||
current_info = get_phase_info(ctx.current_phase)
|
||||
|
||||
missing = [
|
||||
m for m in current_info.required_actions
|
||||
if m not in ctx.completed_milestones
|
||||
]
|
||||
|
||||
return {
|
||||
"current_phase": ctx.current_phase.value,
|
||||
"next_phase": next_phase.value,
|
||||
"next_phase_info": {
|
||||
"display_name": next_info.display_name,
|
||||
"description": next_info.description,
|
||||
},
|
||||
"can_transition": can_transition,
|
||||
"missing_requirements": missing,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Demo Data Endpoints (nur für Entwicklung)
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/demo/add-class")
|
||||
async def demo_add_class(
|
||||
name: str = Query(...),
|
||||
grade_level: int = Query(...),
|
||||
student_count: int = Query(25),
|
||||
teacher_id: str = Query("demo-teacher")
|
||||
):
|
||||
"""Demo: Fügt eine Klasse zum Kontext hinzu."""
|
||||
ctx = get_or_create_context(teacher_id)
|
||||
|
||||
ctx.classes.append(ClassSummary(
|
||||
class_id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
grade_level=grade_level,
|
||||
student_count=student_count,
|
||||
subject="Deutsch"
|
||||
))
|
||||
ctx.total_students += student_count
|
||||
_teacher_contexts[teacher_id] = ctx
|
||||
|
||||
return {"success": True, "classes": len(ctx.classes)}
|
||||
|
||||
|
||||
@router.post("/demo/add-event")
|
||||
async def demo_add_event(
|
||||
event_type: str = Query(...),
|
||||
title: str = Query(...),
|
||||
in_days: int = Query(...),
|
||||
teacher_id: str = Query("demo-teacher")
|
||||
):
|
||||
"""Demo: Fügt ein Event zum Kontext hinzu."""
|
||||
ctx = get_or_create_context(teacher_id)
|
||||
|
||||
ctx.upcoming_events.append(Event(
|
||||
type=event_type,
|
||||
title=title,
|
||||
date=datetime.now() + timedelta(days=in_days),
|
||||
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)}
|
||||
|
||||
|
||||
@router.post("/demo/update-stats")
|
||||
async def demo_update_stats(
|
||||
learning_units: int = Query(0),
|
||||
exams_scheduled: int = Query(0),
|
||||
exams_graded: int = Query(0),
|
||||
grades_entered: int = Query(0),
|
||||
unanswered_messages: int = Query(0),
|
||||
teacher_id: str = Query("demo-teacher")
|
||||
):
|
||||
"""Demo: Aktualisiert Statistiken."""
|
||||
ctx = get_or_create_context(teacher_id)
|
||||
|
||||
if learning_units:
|
||||
ctx.stats.learning_units_created = learning_units
|
||||
if exams_scheduled:
|
||||
ctx.stats.exams_scheduled = exams_scheduled
|
||||
if exams_graded:
|
||||
ctx.stats.exams_graded = exams_graded
|
||||
if grades_entered:
|
||||
ctx.stats.grades_entered = grades_entered
|
||||
if unanswered_messages:
|
||||
ctx.stats.unanswered_messages = unanswered_messages
|
||||
|
||||
_teacher_contexts[teacher_id] = ctx
|
||||
|
||||
return {"success": True, "stats": ctx.stats.__dict__}
|
||||
|
||||
|
||||
@router.post("/demo/reset")
|
||||
async def demo_reset(teacher_id: str = Query("demo-teacher")):
|
||||
"""Demo: Setzt den Kontext zurück."""
|
||||
if teacher_id in _teacher_contexts:
|
||||
del _teacher_contexts[teacher_id]
|
||||
if teacher_id in _milestones:
|
||||
del _milestones[teacher_id]
|
||||
|
||||
return {"success": True, "message": "Kontext zurückgesetzt"}
|
||||
# Backward-compat shim -- module moved to classroom/state_engine_api.py
|
||||
import importlib as _importlib
|
||||
import sys as _sys
|
||||
_sys.modules[__name__] = _importlib.import_module("classroom.state_engine_api")
|
||||
|
||||
Reference in New Issue
Block a user