""" Multiple Choice Generator - Erstellt MC-Fragen aus Quelltexten. Verwendet LLM (Claude/Ollama) zur Generierung von: - Multiple-Choice-Fragen mit 4 Antwortmöglichkeiten - Unterschiedliche Schwierigkeitsgrade - Erklärungen für falsche Antworten """ import logging import json from typing import List, Dict, Any, Optional from dataclasses import dataclass from enum import Enum logger = logging.getLogger(__name__) class Difficulty(str, Enum): """Schwierigkeitsgrade.""" EASY = "easy" MEDIUM = "medium" HARD = "hard" @dataclass class MCOption: """Eine Antwortmöglichkeit.""" text: str is_correct: bool explanation: Optional[str] = None @dataclass class MCQuestion: """Eine Multiple-Choice-Frage.""" question: str options: List[MCOption] difficulty: Difficulty topic: Optional[str] = None hint: Optional[str] = None class MultipleChoiceGenerator: """ Generiert Multiple-Choice-Fragen aus Quelltexten. Verwendet ein LLM zur intelligenten Fragengenerierung. """ def __init__(self, llm_client=None): """ Initialisiert den Generator. Args: llm_client: Optional - LLM-Client für Generierung. Falls nicht angegeben, wird ein Mock verwendet. """ self.llm_client = llm_client logger.info("MultipleChoiceGenerator initialized") def generate( self, source_text: str, num_questions: int = 5, difficulty: Difficulty = Difficulty.MEDIUM, subject: Optional[str] = None, grade_level: Optional[str] = None ) -> List[MCQuestion]: """ Generiert Multiple-Choice-Fragen aus einem Quelltext. Args: source_text: Der Text, aus dem Fragen generiert werden num_questions: Anzahl der zu generierenden Fragen difficulty: Schwierigkeitsgrad subject: Fach (z.B. "Biologie", "Geschichte") grade_level: Klassenstufe (z.B. "7") Returns: Liste von MCQuestion-Objekten """ logger.info(f"Generating {num_questions} MC questions (difficulty: {difficulty})") if not source_text or len(source_text.strip()) < 50: logger.warning("Source text too short for meaningful questions") return [] if self.llm_client: return self._generate_with_llm( source_text, num_questions, difficulty, subject, grade_level ) else: return self._generate_mock( source_text, num_questions, difficulty ) def _generate_with_llm( self, source_text: str, num_questions: int, difficulty: Difficulty, subject: Optional[str], grade_level: Optional[str] ) -> List[MCQuestion]: """Generiert Fragen mit einem LLM.""" difficulty_desc = { Difficulty.EASY: "einfach (Faktenwissen)", Difficulty.MEDIUM: "mittel (Verständnis)", Difficulty.HARD: "schwer (Anwendung und Analyse)" } prompt = f""" Erstelle {num_questions} Multiple-Choice-Fragen auf Deutsch basierend auf folgendem Text. Schwierigkeitsgrad: {difficulty_desc[difficulty]} {f'Fach: {subject}' if subject else ''} {f'Klassenstufe: {grade_level}' if grade_level else ''} Text: {source_text} Erstelle für jede Frage: - Eine klare Frage - 4 Antwortmöglichkeiten (genau eine richtig) - Eine kurze Erklärung, warum die richtigen Antwort richtig ist - Einen optionalen Hinweis Antworte im folgenden JSON-Format: {{ "questions": [ {{ "question": "Die Frage...", "options": [ {{"text": "Antwort A", "is_correct": false, "explanation": "Warum falsch"}}, {{"text": "Antwort B", "is_correct": true, "explanation": "Warum richtig"}}, {{"text": "Antwort C", "is_correct": false, "explanation": "Warum falsch"}}, {{"text": "Antwort D", "is_correct": false, "explanation": "Warum falsch"}} ], "topic": "Thema der Frage", "hint": "Optionaler Hinweis" }} ] }} """ try: response = self.llm_client.generate(prompt) data = json.loads(response) return self._parse_llm_response(data, difficulty) except Exception as e: logger.error(f"Error generating with LLM: {e}") return self._generate_mock(source_text, num_questions, difficulty) def _parse_llm_response( self, data: Dict[str, Any], difficulty: Difficulty ) -> List[MCQuestion]: """Parst die LLM-Antwort zu MCQuestion-Objekten.""" questions = [] for q_data in data.get("questions", []): options = [ MCOption( text=opt["text"], is_correct=opt.get("is_correct", False), explanation=opt.get("explanation") ) for opt in q_data.get("options", []) ] question = MCQuestion( question=q_data.get("question", ""), options=options, difficulty=difficulty, topic=q_data.get("topic"), hint=q_data.get("hint") ) questions.append(question) return questions def _generate_mock( self, source_text: str, num_questions: int, difficulty: Difficulty ) -> List[MCQuestion]: """Generiert Mock-Fragen für Tests/Demo.""" logger.info("Using mock generator (no LLM client)") # Extrahiere einige Schlüsselwörter aus dem Text words = source_text.split() keywords = [w for w in words if len(w) > 5][:10] questions = [] for i in range(min(num_questions, 5)): keyword = keywords[i] if i < len(keywords) else f"Begriff {i+1}" question = MCQuestion( question=f"Was beschreibt '{keyword}' im Kontext des Textes am besten?", options=[ MCOption(text=f"Definition A von {keyword}", is_correct=True, explanation="Dies ist die korrekte Definition."), MCOption(text=f"Falsche Definition B", is_correct=False, explanation="Diese Definition passt nicht."), MCOption(text=f"Falsche Definition C", is_correct=False, explanation="Diese Definition ist unvollständig."), MCOption(text=f"Falsche Definition D", is_correct=False, explanation="Diese Definition ist irreführend."), ], difficulty=difficulty, topic="Allgemein", hint=f"Denke an die Bedeutung von '{keyword}'." ) questions.append(question) return questions def to_h5p_format(self, questions: List[MCQuestion]) -> Dict[str, Any]: """ Konvertiert Fragen ins H5P-Format für Multiple Choice. Args: questions: Liste von MCQuestion-Objekten Returns: H5P-kompatibles Dict """ h5p_questions = [] for q in questions: answers = [] for opt in q.options: answers.append({ "text": opt.text, "correct": opt.is_correct, "tpiMessage": opt.explanation or "" }) h5p_questions.append({ "question": q.question, "answers": answers, "tip": q.hint or "" }) return { "library": "H5P.MultiChoice", "params": { "questions": h5p_questions, "behaviour": { "enableRetry": True, "enableSolutionsButton": True, "singleAnswer": True } } } def to_dict(self, questions: List[MCQuestion]) -> List[Dict[str, Any]]: """Konvertiert Fragen zu Dictionary-Format.""" return [ { "question": q.question, "options": [ { "text": opt.text, "is_correct": opt.is_correct, "explanation": opt.explanation } for opt in q.options ], "difficulty": q.difficulty.value, "topic": q.topic, "hint": q.hint } for q in questions ]