""" 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] ] }