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:
439
backend-lehrer/worksheets/api.py
Normal file
439
backend-lehrer/worksheets/api.py
Normal file
@@ -0,0 +1,439 @@
|
||||
"""
|
||||
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 .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]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user