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>
174 lines
5.6 KiB
Python
174 lines
5.6 KiB
Python
"""
|
|
Classroom API - Session Actions Routes
|
|
|
|
Quick actions (pause, extend, timer), suggestions, utility endpoints.
|
|
"""
|
|
|
|
from typing import Dict, Optional, Any
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from sqlalchemy import text
|
|
|
|
from classroom_engine import (
|
|
LessonPhase,
|
|
LessonStateMachine,
|
|
PhaseTimer,
|
|
SuggestionEngine,
|
|
LESSON_PHASES,
|
|
)
|
|
|
|
from ..models import (
|
|
ExtendTimeRequest,
|
|
TimerStatus,
|
|
SuggestionItem,
|
|
SuggestionsResponse,
|
|
PhasesListResponse,
|
|
ActiveSessionsResponse,
|
|
)
|
|
from ..services.persistence import (
|
|
sessions,
|
|
persist_session,
|
|
get_session_or_404,
|
|
DB_ENABLED,
|
|
SessionLocal,
|
|
)
|
|
from .sessions_core import build_session_response, SessionResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=["Sessions"])
|
|
|
|
|
|
# === Quick Actions (Feature f26/f27/f28) ===
|
|
|
|
@router.post("/sessions/{session_id}/pause", response_model=SessionResponse)
|
|
async def toggle_pause(session_id: str) -> SessionResponse:
|
|
"""Pausiert oder setzt die laufende Stunde fort (Feature f27)."""
|
|
session = get_session_or_404(session_id)
|
|
|
|
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
|
|
raise HTTPException(status_code=400, detail="Stunde ist nicht aktiv")
|
|
|
|
if session.is_paused:
|
|
if session.pause_started_at:
|
|
pause_duration = (datetime.utcnow() - session.pause_started_at).total_seconds()
|
|
session.total_paused_seconds += int(pause_duration)
|
|
session.is_paused = False
|
|
session.pause_started_at = None
|
|
else:
|
|
session.is_paused = True
|
|
session.pause_started_at = datetime.utcnow()
|
|
|
|
persist_session(session)
|
|
return build_session_response(session)
|
|
|
|
|
|
@router.post("/sessions/{session_id}/extend", response_model=SessionResponse)
|
|
async def extend_phase(session_id: str, request: ExtendTimeRequest) -> SessionResponse:
|
|
"""Verlaengert die aktuelle Phase um zusaetzliche Minuten (Feature f28)."""
|
|
session = get_session_or_404(session_id)
|
|
|
|
if session.current_phase in [LessonPhase.NOT_STARTED, LessonPhase.ENDED]:
|
|
raise HTTPException(status_code=400, detail="Stunde ist nicht aktiv")
|
|
|
|
phase_id = session.current_phase.value
|
|
current_duration = session.phase_durations.get(phase_id, 10)
|
|
session.phase_durations[phase_id] = current_duration + request.minutes
|
|
|
|
persist_session(session)
|
|
return build_session_response(session)
|
|
|
|
|
|
@router.get("/sessions/{session_id}/timer", response_model=TimerStatus)
|
|
async def get_timer(session_id: str) -> TimerStatus:
|
|
"""Ruft den Timer-Status der aktuellen Phase ab."""
|
|
session = get_session_or_404(session_id)
|
|
timer = PhaseTimer()
|
|
status = timer.get_phase_status(session)
|
|
return TimerStatus(**status)
|
|
|
|
|
|
@router.get("/sessions/{session_id}/suggestions", response_model=SuggestionsResponse)
|
|
async def get_suggestions(
|
|
session_id: str,
|
|
limit: int = Query(3, ge=1, le=10)
|
|
) -> SuggestionsResponse:
|
|
"""Ruft phasenspezifische Aktivitaets-Vorschlaege ab."""
|
|
session = get_session_or_404(session_id)
|
|
engine = SuggestionEngine()
|
|
response = engine.get_suggestions_response(session, limit)
|
|
|
|
return SuggestionsResponse(
|
|
suggestions=[SuggestionItem(**s) for s in response["suggestions"]],
|
|
current_phase=response["current_phase"],
|
|
phase_display_name=response["phase_display_name"],
|
|
total_available=response["total_available"],
|
|
)
|
|
|
|
|
|
# === Utility Endpoints ===
|
|
|
|
@router.get("/phases", response_model=PhasesListResponse)
|
|
async def list_phases() -> PhasesListResponse:
|
|
"""Listet alle verfuegbaren Unterrichtsphasen mit Metadaten."""
|
|
phases = []
|
|
for phase_id, config in LESSON_PHASES.items():
|
|
phases.append({
|
|
"phase": phase_id,
|
|
"display_name": config["display_name"],
|
|
"default_duration_minutes": config["default_duration_minutes"],
|
|
"activities": config["activities"],
|
|
"icon": config["icon"],
|
|
"description": config.get("description", ""),
|
|
})
|
|
return PhasesListResponse(phases=phases)
|
|
|
|
|
|
@router.get("/sessions", response_model=ActiveSessionsResponse)
|
|
async def list_active_sessions(
|
|
teacher_id: Optional[str] = Query(None)
|
|
) -> ActiveSessionsResponse:
|
|
"""Listet alle (optionally gefilterten) Sessions."""
|
|
sessions_list = []
|
|
for session in sessions.values():
|
|
if teacher_id and session.teacher_id != teacher_id:
|
|
continue
|
|
|
|
fsm = LessonStateMachine()
|
|
sessions_list.append({
|
|
"session_id": session.session_id,
|
|
"teacher_id": session.teacher_id,
|
|
"class_id": session.class_id,
|
|
"subject": session.subject,
|
|
"current_phase": session.current_phase.value,
|
|
"is_active": fsm.is_lesson_active(session),
|
|
"lesson_started_at": session.lesson_started_at.isoformat() if session.lesson_started_at else None,
|
|
})
|
|
|
|
return ActiveSessionsResponse(sessions=sessions_list, count=len(sessions_list))
|
|
|
|
|
|
@router.get("/health")
|
|
async def health_check() -> Dict[str, Any]:
|
|
"""Health-Check fuer den Classroom Service."""
|
|
db_status = "disabled"
|
|
if DB_ENABLED:
|
|
try:
|
|
db = SessionLocal()
|
|
db.execute(text("SELECT 1"))
|
|
db.close()
|
|
db_status = "connected"
|
|
except Exception as e:
|
|
db_status = f"error: {str(e)}"
|
|
|
|
return {
|
|
"status": "healthy",
|
|
"service": "classroom-engine",
|
|
"active_sessions": len(sessions),
|
|
"db_enabled": DB_ENABLED,
|
|
"db_status": db_status,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
}
|