""" Quiz Generator - Erstellt verschiedene Quiz-Typen aus Quelltexten. Generiert: - True/False Fragen - Zuordnungsaufgaben (Matching) - Sortieraufgaben - Offene Fragen mit Musterlösungen """ import logging import json import re from typing import List, Dict, Any, Optional, Tuple from dataclasses import dataclass from enum import Enum logger = logging.getLogger(__name__) class QuizType(str, Enum): """Typen von Quiz-Aufgaben.""" TRUE_FALSE = "true_false" MATCHING = "matching" SORTING = "sorting" OPEN_ENDED = "open_ended" @dataclass class TrueFalseQuestion: """Eine Wahr/Falsch-Frage.""" statement: str is_true: bool explanation: str source_reference: Optional[str] = None @dataclass class MatchingPair: """Ein Zuordnungspaar.""" left: str right: str hint: Optional[str] = None @dataclass class SortingItem: """Ein Element zum Sortieren.""" text: str correct_position: int category: Optional[str] = None @dataclass class OpenQuestion: """Eine offene Frage.""" question: str model_answer: str keywords: List[str] points: int = 1 @dataclass class Quiz: """Ein komplettes Quiz.""" quiz_type: QuizType title: str questions: List[Any] # Je nach Typ unterschiedlich topic: Optional[str] = None difficulty: str = "medium" class QuizGenerator: """ Generiert verschiedene Quiz-Typen aus Quelltexten. """ def __init__(self, llm_client=None): """ Initialisiert den Generator. Args: llm_client: Optional - LLM-Client für intelligente Generierung """ self.llm_client = llm_client logger.info("QuizGenerator initialized") def generate( self, source_text: str, quiz_type: QuizType, num_questions: int = 5, title: Optional[str] = None, topic: Optional[str] = None, difficulty: str = "medium" ) -> Quiz: """ Generiert ein Quiz aus einem Quelltext. Args: source_text: Der Ausgangstext quiz_type: Art des Quiz num_questions: Anzahl der Fragen/Aufgaben title: Optionaler Titel topic: Optionales Thema difficulty: Schwierigkeitsgrad Returns: Quiz-Objekt """ logger.info(f"Generating {quiz_type} quiz with {num_questions} questions") if not source_text or len(source_text.strip()) < 50: logger.warning("Source text too short") return self._empty_quiz(quiz_type, title or "Quiz") generators = { QuizType.TRUE_FALSE: self._generate_true_false, QuizType.MATCHING: self._generate_matching, QuizType.SORTING: self._generate_sorting, QuizType.OPEN_ENDED: self._generate_open_ended, } generator = generators.get(quiz_type) if not generator: raise ValueError(f"Unbekannter Quiz-Typ: {quiz_type}") questions = generator(source_text, num_questions, difficulty) return Quiz( quiz_type=quiz_type, title=title or f"{quiz_type.value.replace('_', ' ').title()} Quiz", questions=questions, topic=topic, difficulty=difficulty ) def _generate_true_false( self, source_text: str, num_questions: int, difficulty: str ) -> List[TrueFalseQuestion]: """Generiert Wahr/Falsch-Fragen.""" if self.llm_client: return self._generate_true_false_llm(source_text, num_questions, difficulty) # Automatische Generierung sentences = self._extract_factual_sentences(source_text) questions = [] for i, sentence in enumerate(sentences[:num_questions]): # Abwechselnd wahre und falsche Aussagen if i % 2 == 0: # Wahre Aussage questions.append(TrueFalseQuestion( statement=sentence, is_true=True, explanation="Diese Aussage entspricht dem Text.", source_reference=sentence[:50] )) else: # Falsche Aussage (Negation) false_statement = self._negate_sentence(sentence) questions.append(TrueFalseQuestion( statement=false_statement, is_true=False, explanation=f"Richtig wäre: {sentence}", source_reference=sentence[:50] )) return questions def _generate_true_false_llm( self, source_text: str, num_questions: int, difficulty: str ) -> List[TrueFalseQuestion]: """Generiert Wahr/Falsch-Fragen mit LLM.""" prompt = f""" Erstelle {num_questions} Wahr/Falsch-Aussagen auf Deutsch basierend auf folgendem Text. Schwierigkeit: {difficulty} Erstelle etwa gleich viele wahre und falsche Aussagen. Text: {source_text} Antworte im JSON-Format: {{ "questions": [ {{ "statement": "Die Aussage...", "is_true": true, "explanation": "Erklärung warum wahr/falsch" }} ] }} """ try: response = self.llm_client.generate(prompt) data = json.loads(response) return [ TrueFalseQuestion( statement=q["statement"], is_true=q["is_true"], explanation=q["explanation"] ) for q in data.get("questions", []) ] except Exception as e: logger.error(f"LLM error: {e}") return self._generate_true_false(source_text, num_questions, difficulty) def _generate_matching( self, source_text: str, num_pairs: int, difficulty: str ) -> List[MatchingPair]: """Generiert Zuordnungsaufgaben.""" if self.llm_client: return self._generate_matching_llm(source_text, num_pairs, difficulty) # Automatische Generierung: Begriff -> Definition pairs = [] definitions = self._extract_definitions(source_text) for term, definition in definitions[:num_pairs]: pairs.append(MatchingPair( left=term, right=definition, hint=f"Beginnt mit '{definition[0]}'" )) return pairs def _generate_matching_llm( self, source_text: str, num_pairs: int, difficulty: str ) -> List[MatchingPair]: """Generiert Zuordnungen mit LLM.""" prompt = f""" Erstelle {num_pairs} Zuordnungspaare auf Deutsch basierend auf folgendem Text. Jedes Paar besteht aus einem Begriff und seiner Definition/Erklärung. Schwierigkeit: {difficulty} Text: {source_text} Antworte im JSON-Format: {{ "pairs": [ {{ "term": "Begriff", "definition": "Definition des Begriffs", "hint": "Optionaler Hinweis" }} ] }} """ try: response = self.llm_client.generate(prompt) data = json.loads(response) return [ MatchingPair( left=p["term"], right=p["definition"], hint=p.get("hint") ) for p in data.get("pairs", []) ] except Exception as e: logger.error(f"LLM error: {e}") return self._generate_matching(source_text, num_pairs, difficulty) def _generate_sorting( self, source_text: str, num_items: int, difficulty: str ) -> List[SortingItem]: """Generiert Sortieraufgaben.""" if self.llm_client: return self._generate_sorting_llm(source_text, num_items, difficulty) # Automatische Generierung: Chronologische Reihenfolge items = [] steps = self._extract_sequence(source_text) for i, step in enumerate(steps[:num_items]): items.append(SortingItem( text=step, correct_position=i + 1 )) return items def _generate_sorting_llm( self, source_text: str, num_items: int, difficulty: str ) -> List[SortingItem]: """Generiert Sortierung mit LLM.""" prompt = f""" Erstelle eine Sortieraufgabe auf Deutsch basierend auf folgendem Text. Finde {num_items} Elemente, die in eine logische Reihenfolge gebracht werden müssen. (z.B. chronologisch, nach Wichtigkeit, nach Größe, etc.) Schwierigkeit: {difficulty} Text: {source_text} Antworte im JSON-Format: {{ "category": "chronologisch/nach Größe/etc.", "items": [ {{"text": "Erstes Element", "position": 1}}, {{"text": "Zweites Element", "position": 2}} ] }} """ try: response = self.llm_client.generate(prompt) data = json.loads(response) category = data.get("category") return [ SortingItem( text=item["text"], correct_position=item["position"], category=category ) for item in data.get("items", []) ] except Exception as e: logger.error(f"LLM error: {e}") return self._generate_sorting(source_text, num_items, difficulty) def _generate_open_ended( self, source_text: str, num_questions: int, difficulty: str ) -> List[OpenQuestion]: """Generiert offene Fragen.""" if self.llm_client: return self._generate_open_ended_llm(source_text, num_questions, difficulty) # Automatische Generierung questions = [] sentences = self._extract_factual_sentences(source_text) question_starters = [ "Was bedeutet", "Erkläre", "Warum", "Wie funktioniert", "Nenne die Hauptmerkmale von" ] for i, sentence in enumerate(sentences[:num_questions]): # Extrahiere Schlüsselwort keywords = self._extract_keywords(sentence) if keywords: keyword = keywords[0] starter = question_starters[i % len(question_starters)] question = f"{starter} '{keyword}'?" questions.append(OpenQuestion( question=question, model_answer=sentence, keywords=keywords, points=1 )) return questions def _generate_open_ended_llm( self, source_text: str, num_questions: int, difficulty: str ) -> List[OpenQuestion]: """Generiert offene Fragen mit LLM.""" prompt = f""" Erstelle {num_questions} offene Fragen auf Deutsch basierend auf folgendem Text. Jede Frage sollte eine ausführliche Antwort erfordern. Schwierigkeit: {difficulty} Text: {source_text} Antworte im JSON-Format: {{ "questions": [ {{ "question": "Die Frage...", "model_answer": "Eine vollständige Musterantwort", "keywords": ["Schlüsselwort1", "Schlüsselwort2"], "points": 2 }} ] }} """ try: response = self.llm_client.generate(prompt) data = json.loads(response) return [ OpenQuestion( question=q["question"], model_answer=q["model_answer"], keywords=q.get("keywords", []), points=q.get("points", 1) ) for q in data.get("questions", []) ] except Exception as e: logger.error(f"LLM error: {e}") return self._generate_open_ended(source_text, num_questions, difficulty) # Hilfsmethoden def _extract_factual_sentences(self, text: str) -> List[str]: """Extrahiert Fakten-Sätze aus dem Text.""" sentences = re.split(r'[.!?]+', text) factual = [] for sentence in sentences: sentence = sentence.strip() # Filtere zu kurze oder fragende Sätze if len(sentence) > 20 and '?' not in sentence: factual.append(sentence) return factual def _negate_sentence(self, sentence: str) -> str: """Negiert eine Aussage einfach.""" # Einfache Negation durch Einfügen von "nicht" words = sentence.split() if len(words) > 2: # Nach erstem Verb "nicht" einfügen for i, word in enumerate(words): if word.endswith(('t', 'en', 'st')) and i > 0: words.insert(i + 1, 'nicht') break return ' '.join(words) def _extract_definitions(self, text: str) -> List[Tuple[str, str]]: """Extrahiert Begriff-Definition-Paare.""" definitions = [] # Suche nach Mustern wie "X ist Y" oder "X bezeichnet Y" patterns = [ r'(\w+)\s+ist\s+(.+?)[.]', r'(\w+)\s+bezeichnet\s+(.+?)[.]', r'(\w+)\s+bedeutet\s+(.+?)[.]', r'(\w+):\s+(.+?)[.]', ] for pattern in patterns: matches = re.findall(pattern, text) for term, definition in matches: if len(definition) > 10: definitions.append((term, definition.strip())) return definitions def _extract_sequence(self, text: str) -> List[str]: """Extrahiert eine Sequenz von Schritten.""" steps = [] # Suche nach nummerierten Schritten numbered = re.findall(r'\d+[.)]\s*([^.]+)', text) steps.extend(numbered) # Suche nach Signalwörtern signal_words = ['zuerst', 'dann', 'danach', 'anschließend', 'schließlich'] for word in signal_words: pattern = rf'{word}\s+([^.]+)' matches = re.findall(pattern, text, re.IGNORECASE) steps.extend(matches) return steps def _extract_keywords(self, text: str) -> List[str]: """Extrahiert Schlüsselwörter.""" # Längere Wörter mit Großbuchstaben (meist Substantive) words = re.findall(r'\b[A-ZÄÖÜ][a-zäöüß]+\b', text) return list(set(words))[:5] def _empty_quiz(self, quiz_type: QuizType, title: str) -> Quiz: """Erstellt leeres Quiz bei Fehler.""" return Quiz( quiz_type=quiz_type, title=title, questions=[], difficulty="medium" ) def to_dict(self, quiz: Quiz) -> Dict[str, Any]: """Konvertiert Quiz zu Dictionary-Format.""" questions_data = [] for q in quiz.questions: if isinstance(q, TrueFalseQuestion): questions_data.append({ "type": "true_false", "statement": q.statement, "is_true": q.is_true, "explanation": q.explanation }) elif isinstance(q, MatchingPair): questions_data.append({ "type": "matching", "left": q.left, "right": q.right, "hint": q.hint }) elif isinstance(q, SortingItem): questions_data.append({ "type": "sorting", "text": q.text, "correct_position": q.correct_position, "category": q.category }) elif isinstance(q, OpenQuestion): questions_data.append({ "type": "open_ended", "question": q.question, "model_answer": q.model_answer, "keywords": q.keywords, "points": q.points }) return { "quiz_type": quiz.quiz_type.value, "title": quiz.title, "topic": quiz.topic, "difficulty": quiz.difficulty, "questions": questions_data } def to_h5p_format(self, quiz: Quiz) -> Dict[str, Any]: """Konvertiert Quiz ins H5P-Format.""" if quiz.quiz_type == QuizType.TRUE_FALSE: return self._true_false_to_h5p(quiz) elif quiz.quiz_type == QuizType.MATCHING: return self._matching_to_h5p(quiz) # Weitere Typen... return {} def _true_false_to_h5p(self, quiz: Quiz) -> Dict[str, Any]: """Konvertiert True/False zu H5P.""" statements = [] for q in quiz.questions: statements.append({ "text": q.statement, "correct": q.is_true, "feedback": q.explanation }) return { "library": "H5P.TrueFalse", "params": { "statements": statements, "behaviour": { "enableRetry": True, "enableSolutionsButton": True } } } def _matching_to_h5p(self, quiz: Quiz) -> Dict[str, Any]: """Konvertiert Matching zu H5P.""" pairs = [] for q in quiz.questions: pairs.append({ "question": q.left, "answer": q.right }) return { "library": "H5P.DragText", "params": { "pairs": pairs, "behaviour": { "enableRetry": True, "enableSolutionsButton": True } } }