Restructure: Move 43 files into 8 domain 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 27s
CI / test-go-edu-search (push) Successful in 40s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 28s
CI / test-nodejs-website (push) Successful in 20s
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 27s
CI / test-go-edu-search (push) Successful in 40s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 28s
CI / test-nodejs-website (push) Successful in 20s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,376 +1,4 @@
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from learning_units import (
|
||||
LearningUnit,
|
||||
LearningUnitCreate,
|
||||
LearningUnitUpdate,
|
||||
list_learning_units,
|
||||
get_learning_unit,
|
||||
create_learning_unit,
|
||||
update_learning_unit,
|
||||
delete_learning_unit,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/learning-units",
|
||||
tags=["learning-units"],
|
||||
)
|
||||
|
||||
|
||||
# ---------- Payload-Modelle für das Frontend ----------
|
||||
|
||||
|
||||
class LearningUnitCreatePayload(BaseModel):
|
||||
"""
|
||||
Payload so, wie er aus dem Frontend kommt:
|
||||
{
|
||||
"student": "...",
|
||||
"subject": "...",
|
||||
"title": "...",
|
||||
"grade": "7a"
|
||||
}
|
||||
"""
|
||||
student: Optional[str] = None
|
||||
subject: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
grade: Optional[str] = None
|
||||
|
||||
|
||||
class AttachWorksheetsPayload(BaseModel):
|
||||
worksheet_files: List[str]
|
||||
|
||||
|
||||
class RemoveWorksheetPayload(BaseModel):
|
||||
worksheet_file: str
|
||||
|
||||
|
||||
class GenerateFromAnalysisPayload(BaseModel):
|
||||
analysis_data: Dict[str, Any]
|
||||
num_questions: int = 8
|
||||
|
||||
|
||||
# ---------- Hilfsfunktion: Backend-Modell -> Frontend-Objekt ----------
|
||||
|
||||
|
||||
def unit_to_frontend_dict(lu: LearningUnit) -> Dict[str, Any]:
|
||||
"""
|
||||
Wandelt eine LearningUnit in das Format um, das das Frontend erwartet.
|
||||
Wichtig sind:
|
||||
- id
|
||||
- label (sichtbarer Name)
|
||||
- meta (Untertitelzeile)
|
||||
- worksheet_files (Liste von Dateinamen)
|
||||
"""
|
||||
label = lu.title or "Lerneinheit"
|
||||
|
||||
# Meta-Text: z.B. "Thema: Auge · Klasse: 7a · angelegt am 10.12.2025"
|
||||
meta_parts: List[str] = []
|
||||
if lu.topic:
|
||||
meta_parts.append(f"Thema: {lu.topic}")
|
||||
if lu.grade_level:
|
||||
meta_parts.append(f"Klasse: {lu.grade_level}")
|
||||
created_str = lu.created_at.strftime("%d.%m.%Y")
|
||||
meta_parts.append(f"angelegt am {created_str}")
|
||||
|
||||
meta = " · ".join(meta_parts)
|
||||
|
||||
return {
|
||||
"id": lu.id,
|
||||
"label": label,
|
||||
"meta": meta,
|
||||
"title": lu.title,
|
||||
"topic": lu.topic,
|
||||
"grade_level": lu.grade_level,
|
||||
"language": lu.language,
|
||||
"status": lu.status,
|
||||
"worksheet_files": lu.worksheet_files,
|
||||
"created_at": lu.created_at.isoformat(),
|
||||
"updated_at": lu.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# ---------- Endpunkte ----------
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Dict[str, Any]])
|
||||
def api_list_learning_units():
|
||||
"""Alle Lerneinheiten für das Frontend auflisten."""
|
||||
units = list_learning_units()
|
||||
return [unit_to_frontend_dict(u) for u in units]
|
||||
|
||||
|
||||
@router.post("/", response_model=Dict[str, Any])
|
||||
def api_create_learning_unit(payload: LearningUnitCreatePayload):
|
||||
"""
|
||||
Neue Lerneinheit anlegen.
|
||||
Mapped das Frontend-Payload (student/subject/title/grade)
|
||||
auf das generische LearningUnit-Modell.
|
||||
"""
|
||||
|
||||
# Mindestens eines der Felder muss gesetzt sein
|
||||
if not (payload.student or payload.subject or payload.title):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Bitte mindestens Schüler/in, Fach oder Thema angeben.",
|
||||
)
|
||||
|
||||
# Titel/Topic bestimmen
|
||||
# sichtbarer Titel: bevorzugt Thema (title), sonst Kombination
|
||||
if payload.title:
|
||||
title = payload.title
|
||||
else:
|
||||
parts = []
|
||||
if payload.subject:
|
||||
parts.append(payload.subject)
|
||||
if payload.student:
|
||||
parts.append(payload.student)
|
||||
title = " – ".join(parts) if parts else "Lerneinheit"
|
||||
|
||||
topic = payload.title or payload.subject or None
|
||||
grade_level = payload.grade or None
|
||||
|
||||
lu_create = LearningUnitCreate(
|
||||
title=title,
|
||||
description=None,
|
||||
topic=topic,
|
||||
grade_level=grade_level,
|
||||
language="de",
|
||||
worksheet_files=[],
|
||||
status="raw",
|
||||
)
|
||||
|
||||
lu = create_learning_unit(lu_create)
|
||||
return unit_to_frontend_dict(lu)
|
||||
|
||||
|
||||
@router.post("/{unit_id}/attach-worksheets", response_model=Dict[str, Any])
|
||||
def api_attach_worksheets(unit_id: str, payload: AttachWorksheetsPayload):
|
||||
"""
|
||||
Fügt der Lerneinheit eine oder mehrere Arbeitsblätter hinzu.
|
||||
"""
|
||||
lu = get_learning_unit(unit_id)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
|
||||
files_to_add = [f for f in payload.worksheet_files if f not in lu.worksheet_files]
|
||||
if files_to_add:
|
||||
new_list = lu.worksheet_files + files_to_add
|
||||
update = LearningUnitUpdate(worksheet_files=new_list)
|
||||
lu = update_learning_unit(unit_id, update)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=500, detail="Lerneinheit konnte nicht aktualisiert werden.")
|
||||
|
||||
return unit_to_frontend_dict(lu)
|
||||
|
||||
|
||||
@router.post("/{unit_id}/remove-worksheet", response_model=Dict[str, Any])
|
||||
def api_remove_worksheet(unit_id: str, payload: RemoveWorksheetPayload):
|
||||
"""
|
||||
Entfernt genau ein Arbeitsblatt aus der Lerneinheit.
|
||||
"""
|
||||
lu = get_learning_unit(unit_id)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
|
||||
if payload.worksheet_file not in lu.worksheet_files:
|
||||
# Nichts zu tun, aber kein Fehler – einfach unverändert zurückgeben
|
||||
return unit_to_frontend_dict(lu)
|
||||
|
||||
new_list = [f for f in lu.worksheet_files if f != payload.worksheet_file]
|
||||
update = LearningUnitUpdate(worksheet_files=new_list)
|
||||
lu = update_learning_unit(unit_id, update)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=500, detail="Lerneinheit konnte nicht aktualisiert werden.")
|
||||
|
||||
return unit_to_frontend_dict(lu)
|
||||
|
||||
|
||||
@router.delete("/{unit_id}")
|
||||
def api_delete_learning_unit(unit_id: str):
|
||||
"""
|
||||
Lerneinheit komplett löschen (aktuell vom Frontend noch nicht verwendet).
|
||||
"""
|
||||
ok = delete_learning_unit(unit_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
return {"status": "deleted", "id": unit_id}
|
||||
|
||||
|
||||
# ---------- Generator-Endpunkte ----------
|
||||
|
||||
LERNEINHEITEN_DIR = os.path.expanduser("~/Arbeitsblaetter/Lerneinheiten")
|
||||
|
||||
|
||||
def _save_analysis_and_get_path(unit_id: str, analysis_data: Dict[str, Any]) -> Path:
|
||||
"""Save analysis_data to disk and return the path."""
|
||||
os.makedirs(LERNEINHEITEN_DIR, exist_ok=True)
|
||||
path = Path(LERNEINHEITEN_DIR) / f"{unit_id}_analyse.json"
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(analysis_data, f, ensure_ascii=False, indent=2)
|
||||
return path
|
||||
|
||||
|
||||
@router.post("/{unit_id}/generate-qa")
|
||||
def api_generate_qa(unit_id: str, payload: GenerateFromAnalysisPayload):
|
||||
"""Generate Q&A items with Leitner fields from analysis data."""
|
||||
lu = get_learning_unit(unit_id)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
|
||||
analysis_path = _save_analysis_and_get_path(unit_id, payload.analysis_data)
|
||||
|
||||
try:
|
||||
from ai_processing.qa_generator import generate_qa_from_analysis
|
||||
qa_path = generate_qa_from_analysis(analysis_path, num_questions=payload.num_questions)
|
||||
with open(qa_path, "r", encoding="utf-8") as f:
|
||||
qa_data = json.load(f)
|
||||
|
||||
# Update unit status
|
||||
update_learning_unit(unit_id, LearningUnitUpdate(status="qa_generated"))
|
||||
logger.info(f"Generated QA for unit {unit_id}: {len(qa_data.get('qa_items', []))} items")
|
||||
return qa_data
|
||||
except Exception as e:
|
||||
logger.error(f"QA generation failed for {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"QA-Generierung fehlgeschlagen: {e}")
|
||||
|
||||
|
||||
@router.post("/{unit_id}/generate-mc")
|
||||
def api_generate_mc(unit_id: str, payload: GenerateFromAnalysisPayload):
|
||||
"""Generate multiple choice questions from analysis data."""
|
||||
lu = get_learning_unit(unit_id)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
|
||||
analysis_path = _save_analysis_and_get_path(unit_id, payload.analysis_data)
|
||||
|
||||
try:
|
||||
from ai_processing.mc_generator import generate_mc_from_analysis
|
||||
mc_path = generate_mc_from_analysis(analysis_path, num_questions=payload.num_questions)
|
||||
with open(mc_path, "r", encoding="utf-8") as f:
|
||||
mc_data = json.load(f)
|
||||
|
||||
update_learning_unit(unit_id, LearningUnitUpdate(status="mc_generated"))
|
||||
logger.info(f"Generated MC for unit {unit_id}: {len(mc_data.get('questions', []))} questions")
|
||||
return mc_data
|
||||
except Exception as e:
|
||||
logger.error(f"MC generation failed for {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"MC-Generierung fehlgeschlagen: {e}")
|
||||
|
||||
|
||||
@router.post("/{unit_id}/generate-cloze")
|
||||
def api_generate_cloze(unit_id: str, payload: GenerateFromAnalysisPayload):
|
||||
"""Generate cloze (fill-in-the-blank) items from analysis data."""
|
||||
lu = get_learning_unit(unit_id)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
|
||||
analysis_path = _save_analysis_and_get_path(unit_id, payload.analysis_data)
|
||||
|
||||
try:
|
||||
from ai_processing.cloze_generator import generate_cloze_from_analysis
|
||||
cloze_path = generate_cloze_from_analysis(analysis_path)
|
||||
with open(cloze_path, "r", encoding="utf-8") as f:
|
||||
cloze_data = json.load(f)
|
||||
|
||||
update_learning_unit(unit_id, LearningUnitUpdate(status="cloze_generated"))
|
||||
logger.info(f"Generated Cloze for unit {unit_id}: {len(cloze_data.get('cloze_items', []))} items")
|
||||
return cloze_data
|
||||
except Exception as e:
|
||||
logger.error(f"Cloze generation failed for {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Cloze-Generierung fehlgeschlagen: {e}")
|
||||
|
||||
|
||||
@router.get("/{unit_id}/qa")
|
||||
def api_get_qa(unit_id: str):
|
||||
"""Get generated QA items for a unit."""
|
||||
qa_path = Path(LERNEINHEITEN_DIR) / f"{unit_id}_qa.json"
|
||||
if not qa_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Keine QA-Daten gefunden.")
|
||||
with open(qa_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@router.get("/{unit_id}/mc")
|
||||
def api_get_mc(unit_id: str):
|
||||
"""Get generated MC questions for a unit."""
|
||||
mc_path = Path(LERNEINHEITEN_DIR) / f"{unit_id}_mc.json"
|
||||
if not mc_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Keine MC-Daten gefunden.")
|
||||
with open(mc_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@router.get("/{unit_id}/cloze")
|
||||
def api_get_cloze(unit_id: str):
|
||||
"""Get generated cloze items for a unit."""
|
||||
cloze_path = Path(LERNEINHEITEN_DIR) / f"{unit_id}_cloze.json"
|
||||
if not cloze_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Keine Cloze-Daten gefunden.")
|
||||
with open(cloze_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@router.post("/{unit_id}/leitner/update")
|
||||
def api_update_leitner(unit_id: str, item_id: str, correct: bool):
|
||||
"""Update Leitner progress for a QA item."""
|
||||
qa_path = Path(LERNEINHEITEN_DIR) / f"{unit_id}_qa.json"
|
||||
if not qa_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Keine QA-Daten gefunden.")
|
||||
try:
|
||||
from ai_processing.qa_generator import update_leitner_progress
|
||||
result = update_leitner_progress(qa_path, item_id, correct)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{unit_id}/leitner/next")
|
||||
def api_get_next_review(unit_id: str, limit: int = 5):
|
||||
"""Get next Leitner review items."""
|
||||
qa_path = Path(LERNEINHEITEN_DIR) / f"{unit_id}_qa.json"
|
||||
if not qa_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Keine QA-Daten gefunden.")
|
||||
try:
|
||||
from ai_processing.qa_generator import get_next_review_items
|
||||
items = get_next_review_items(qa_path, limit=limit)
|
||||
return {"items": items, "count": len(items)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class StoryGeneratePayload(BaseModel):
|
||||
vocabulary: List[Dict[str, Any]]
|
||||
language: str = "en"
|
||||
grade_level: str = "5-8"
|
||||
|
||||
|
||||
@router.post("/{unit_id}/generate-story")
|
||||
def api_generate_story(unit_id: str, payload: StoryGeneratePayload):
|
||||
"""Generate a short story using vocabulary words."""
|
||||
lu = get_learning_unit(unit_id)
|
||||
if not lu:
|
||||
raise HTTPException(status_code=404, detail="Lerneinheit nicht gefunden.")
|
||||
|
||||
try:
|
||||
from story_generator import generate_story
|
||||
result = generate_story(
|
||||
vocabulary=payload.vocabulary,
|
||||
language=payload.language,
|
||||
grade_level=payload.grade_level,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Story generation failed for {unit_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Story-Generierung fehlgeschlagen: {e}")
|
||||
|
||||
# Backward-compat shim -- module moved to units/learning_api.py
|
||||
import importlib as _importlib
|
||||
import sys as _sys
|
||||
_sys.modules[__name__] = _importlib.import_module("units.learning_api")
|
||||
|
||||
Reference in New Issue
Block a user