Files
breakpilot-lehrer/backend-lehrer/worksheets_api.py
Benjamin Admin bd4b956e3c [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>
2026-04-25 09:41:42 +02:00

440 lines
14 KiB
Python

"""
Worksheets API - REST API für Arbeitsblatt-Generierung.
Integriert alle Content-Generatoren:
- Multiple Choice Questions
- Lückentexte (Cloze)
- Mindmaps
- Quizze (True/False, Matching, Sorting, Open)
Unterstützt:
- H5P-Export für interaktive Inhalte
- PDF-Export für Druckversionen
- JSON-Export für Frontend-Integration
"""
import logging
import uuid
from datetime import datetime
from typing import Dict
from fastapi import APIRouter, HTTPException
from generators import (
MultipleChoiceGenerator,
ClozeGenerator,
MindmapGenerator,
QuizGenerator
)
from worksheets_models import (
ContentType,
GenerateRequest,
MCGenerateRequest,
ClozeGenerateRequest,
MindmapGenerateRequest,
QuizGenerateRequest,
BatchGenerateRequest,
WorksheetContent,
GenerateResponse,
BatchGenerateResponse,
parse_difficulty,
parse_cloze_type,
parse_quiz_types,
)
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/worksheets",
tags=["worksheets"],
)
# ============================================================================
# In-Memory Storage (später durch DB ersetzen)
# ============================================================================
_generated_content: Dict[str, WorksheetContent] = {}
# ============================================================================
# Generator Instances
# ============================================================================
mc_generator = MultipleChoiceGenerator()
cloze_generator = ClozeGenerator()
mindmap_generator = MindmapGenerator()
quiz_generator = QuizGenerator()
def _store_content(content: WorksheetContent) -> None:
"""Speichert generierten Content."""
_generated_content[content.id] = content
# ============================================================================
# API Endpoints
# ============================================================================
@router.post("/generate/multiple-choice", response_model=GenerateResponse)
async def generate_multiple_choice(request: MCGenerateRequest):
"""Generiert Multiple-Choice-Fragen aus Quelltext."""
try:
difficulty = parse_difficulty(request.difficulty)
questions = mc_generator.generate(
source_text=request.source_text,
num_questions=request.num_questions,
difficulty=difficulty,
subject=request.subject,
grade_level=request.grade_level
)
if not questions:
return GenerateResponse(
success=False,
error="Keine Fragen generiert. Text möglicherweise zu kurz."
)
questions_dict = mc_generator.to_dict(questions)
h5p_format = mc_generator.to_h5p_format(questions)
content = WorksheetContent(
id=str(uuid.uuid4()),
content_type=ContentType.MULTIPLE_CHOICE.value,
data={"questions": questions_dict},
h5p_format=h5p_format,
created_at=datetime.utcnow(),
topic=request.topic,
difficulty=request.difficulty
)
_store_content(content)
return GenerateResponse(success=True, content=content)
except Exception as e:
logger.error(f"Error generating MC questions: {e}")
return GenerateResponse(success=False, error=str(e))
@router.post("/generate/cloze", response_model=GenerateResponse)
async def generate_cloze(request: ClozeGenerateRequest):
"""Generiert Lückentext aus Quelltext."""
try:
cloze_type = parse_cloze_type(request.cloze_type)
cloze = cloze_generator.generate(
source_text=request.source_text,
num_gaps=request.num_gaps,
difficulty=request.difficulty,
cloze_type=cloze_type,
topic=request.topic
)
if not cloze.gaps:
return GenerateResponse(
success=False,
error="Keine Lücken generiert. Text möglicherweise zu kurz."
)
cloze_dict = cloze_generator.to_dict(cloze)
h5p_format = cloze_generator.to_h5p_format(cloze)
content = WorksheetContent(
id=str(uuid.uuid4()),
content_type=ContentType.CLOZE.value,
data=cloze_dict,
h5p_format=h5p_format,
created_at=datetime.utcnow(),
topic=request.topic,
difficulty=request.difficulty
)
_store_content(content)
return GenerateResponse(success=True, content=content)
except Exception as e:
logger.error(f"Error generating cloze: {e}")
return GenerateResponse(success=False, error=str(e))
@router.post("/generate/mindmap", response_model=GenerateResponse)
async def generate_mindmap(request: MindmapGenerateRequest):
"""Generiert Mindmap aus Quelltext."""
try:
mindmap = mindmap_generator.generate(
source_text=request.source_text,
title=request.topic,
max_depth=request.max_depth,
topic=request.topic
)
if mindmap.total_nodes <= 1:
return GenerateResponse(
success=False,
error="Mindmap konnte nicht generiert werden. Text möglicherweise zu kurz."
)
mindmap_dict = mindmap_generator.to_dict(mindmap)
mermaid = mindmap_generator.to_mermaid(mindmap)
json_tree = mindmap_generator.to_json_tree(mindmap)
content = WorksheetContent(
id=str(uuid.uuid4()),
content_type=ContentType.MINDMAP.value,
data={
"mindmap": mindmap_dict,
"mermaid": mermaid,
"json_tree": json_tree
},
h5p_format=None,
created_at=datetime.utcnow(),
topic=request.topic,
difficulty=None
)
_store_content(content)
return GenerateResponse(success=True, content=content)
except Exception as e:
logger.error(f"Error generating mindmap: {e}")
return GenerateResponse(success=False, error=str(e))
@router.post("/generate/quiz", response_model=GenerateResponse)
async def generate_quiz(request: QuizGenerateRequest):
"""Generiert Quiz mit verschiedenen Fragetypen."""
try:
quiz_types = parse_quiz_types(request.quiz_types)
all_questions = []
quizzes = []
for quiz_type in quiz_types:
quiz = quiz_generator.generate(
source_text=request.source_text,
quiz_type=quiz_type,
num_questions=request.num_items,
difficulty=request.difficulty,
topic=request.topic
)
quizzes.append(quiz)
all_questions.extend(quiz.questions)
if len(all_questions) == 0:
return GenerateResponse(
success=False,
error="Quiz konnte nicht generiert werden. Text möglicherweise zu kurz."
)
combined_quiz_dict = {
"quiz_types": [qt.value for qt in quiz_types],
"title": f"Combined Quiz - {request.topic or 'Various Topics'}",
"topic": request.topic,
"difficulty": request.difficulty,
"questions": []
}
for quiz in quizzes:
quiz_dict = quiz_generator.to_dict(quiz)
combined_quiz_dict["questions"].extend(quiz_dict.get("questions", []))
h5p_format = quiz_generator.to_h5p_format(quizzes[0]) if quizzes else {}
content = WorksheetContent(
id=str(uuid.uuid4()),
content_type=ContentType.QUIZ.value,
data=combined_quiz_dict,
h5p_format=h5p_format,
created_at=datetime.utcnow(),
topic=request.topic,
difficulty=request.difficulty
)
_store_content(content)
return GenerateResponse(success=True, content=content)
except Exception as e:
logger.error(f"Error generating quiz: {e}")
return GenerateResponse(success=False, error=str(e))
@router.post("/generate/batch", response_model=BatchGenerateResponse)
async def generate_batch(request: BatchGenerateRequest):
"""Generiert mehrere Content-Typen aus einem Quelltext."""
contents = []
errors = []
for content_type in request.content_types:
try:
if content_type == "multiple_choice":
mc_req = MCGenerateRequest(
source_text=request.source_text,
topic=request.topic,
subject=request.subject,
grade_level=request.grade_level,
difficulty=request.difficulty
)
result = await generate_multiple_choice(mc_req)
elif content_type == "cloze":
cloze_req = ClozeGenerateRequest(
source_text=request.source_text,
topic=request.topic,
subject=request.subject,
grade_level=request.grade_level,
difficulty=request.difficulty
)
result = await generate_cloze(cloze_req)
elif content_type == "mindmap":
mindmap_req = MindmapGenerateRequest(
source_text=request.source_text,
topic=request.topic,
subject=request.subject,
grade_level=request.grade_level
)
result = await generate_mindmap(mindmap_req)
elif content_type == "quiz":
quiz_req = QuizGenerateRequest(
source_text=request.source_text,
topic=request.topic,
subject=request.subject,
grade_level=request.grade_level,
difficulty=request.difficulty
)
result = await generate_quiz(quiz_req)
else:
errors.append(f"Unbekannter Content-Typ: {content_type}")
continue
if result.success and result.content:
contents.append(result.content)
elif result.error:
errors.append(f"{content_type}: {result.error}")
except Exception as e:
errors.append(f"{content_type}: {str(e)}")
return BatchGenerateResponse(
success=len(contents) > 0,
contents=contents,
errors=errors
)
@router.get("/content/{content_id}", response_model=GenerateResponse)
async def get_content(content_id: str):
"""Ruft gespeicherten Content ab."""
content = _generated_content.get(content_id)
if not content:
raise HTTPException(status_code=404, detail="Content nicht gefunden")
return GenerateResponse(success=True, content=content)
@router.get("/content/{content_id}/h5p")
async def get_content_h5p(content_id: str):
"""Gibt H5P-Format für Content zurück."""
content = _generated_content.get(content_id)
if not content:
raise HTTPException(status_code=404, detail="Content nicht gefunden")
if not content.h5p_format:
raise HTTPException(
status_code=400,
detail="H5P-Format für diesen Content-Typ nicht verfügbar"
)
return content.h5p_format
@router.delete("/content/{content_id}")
async def delete_content(content_id: str):
"""Löscht gespeicherten Content."""
if content_id not in _generated_content:
raise HTTPException(status_code=404, detail="Content nicht gefunden")
del _generated_content[content_id]
return {"status": "deleted", "id": content_id}
@router.get("/types")
async def list_content_types():
"""Listet verfügbare Content-Typen und deren Optionen."""
return {
"content_types": [
{
"type": "multiple_choice",
"name": "Multiple Choice",
"description": "Fragen mit 4 Antwortmöglichkeiten",
"options": {
"num_questions": {"min": 1, "max": 20, "default": 5},
"difficulty": ["easy", "medium", "hard"]
},
"h5p_supported": True
},
{
"type": "cloze",
"name": "Lückentext",
"description": "Text mit ausgeblendeten Schlüsselwörtern",
"options": {
"num_gaps": {"min": 1, "max": 15, "default": 5},
"difficulty": ["easy", "medium", "hard"],
"cloze_type": ["fill_in", "drag_drop", "dropdown"]
},
"h5p_supported": True
},
{
"type": "mindmap",
"name": "Mindmap",
"description": "Hierarchische Struktur aus Hauptthema und Unterthemen",
"options": {
"max_depth": {"min": 2, "max": 5, "default": 3}
},
"h5p_supported": False,
"export_formats": ["mermaid", "json_tree"]
},
{
"type": "quiz",
"name": "Quiz",
"description": "Verschiedene Fragetypen kombiniert",
"options": {
"quiz_types": ["true_false", "matching", "sorting", "open_ended"],
"num_items": {"min": 1, "max": 10, "default": 5},
"difficulty": ["easy", "medium", "hard"]
},
"h5p_supported": True
}
]
}
@router.get("/history")
async def get_generation_history(limit: int = 10):
"""Gibt die letzten generierten Contents zurück."""
sorted_contents = sorted(
_generated_content.values(),
key=lambda x: x.created_at,
reverse=True
)
return {
"total": len(_generated_content),
"contents": [
{
"id": c.id,
"content_type": c.content_type,
"topic": c.topic,
"difficulty": c.difficulty,
"created_at": c.created_at.isoformat()
}
for c in sorted_contents[:limit]
]
}