yusenthebot
Integrate advanced OCR, FlashcardGenerator, DifficultyScorer, and AI Quiz from language project
aa3fdef
| # -*- coding: utf-8 -*- | |
| """ | |
| Quiz Tools - AI-Powered Quiz Generation from Flashcards | |
| Supports multiple question types and uses OpenAI API for intelligent quiz creation | |
| """ | |
| import json | |
| import os | |
| import random | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Dict, List, Any, Optional | |
| from .config import get_user_dir | |
| from .flashcards_tools import load_deck, list_user_decks | |
| # Try to import OpenAI | |
| try: | |
| from openai import OpenAI | |
| HAS_OPENAI = True | |
| except ImportError: | |
| HAS_OPENAI = False | |
| class QuizGenerator: | |
| """Generate intelligent quizzes using OpenAI API""" | |
| QUESTION_TYPES = [ | |
| 'multiple_choice', | |
| 'fill_in_blank', | |
| 'true_false', | |
| 'matching', | |
| 'short_answer' | |
| ] | |
| def __init__(self, api_key: Optional[str] = None, model: str = "gpt-4o-mini"): | |
| """ | |
| Initialize the quiz generator | |
| Args: | |
| api_key: OpenAI API key (uses env var if not provided) | |
| model: Model to use for quiz generation | |
| """ | |
| self.api_key = api_key or os.environ.get("OPENAI_API_KEY") | |
| self.model = model | |
| self.client = None | |
| if HAS_OPENAI and self.api_key: | |
| try: | |
| self.client = OpenAI(api_key=self.api_key) | |
| except Exception as e: | |
| print(f"[QuizGenerator] OpenAI init failed: {e}") | |
| def _prepare_flashcard_context(self, flashcards: List[Dict], max_cards: int = 20) -> str: | |
| """Prepare flashcard data as context for AI""" | |
| selected_cards = flashcards[:max_cards] if len(flashcards) > max_cards else flashcards | |
| context_parts = [] | |
| for idx, card in enumerate(selected_cards, 1): | |
| card_info = ( | |
| f"{idx}. Word: {card.get('front', '')}\n" | |
| f" Translation: {card.get('back', '')}\n" | |
| f" Language: {card.get('language', 'unknown')}\n" | |
| f" Context: {card.get('context', 'N/A')}" | |
| ) | |
| context_parts.append(card_info) | |
| return "\n\n".join(context_parts) | |
| def _create_quiz_prompt(self, flashcards: List[Dict], num_questions: int = 30) -> str: | |
| """Create the prompt for AI quiz generation""" | |
| flashcard_context = self._prepare_flashcard_context(flashcards) | |
| prompt = f"""You are an expert language teacher creating a QUESTION BANK to test students' knowledge of vocabulary. | |
| Based on the following flashcards, generate exactly {num_questions} diverse quiz questions. | |
| FLASHCARDS: | |
| {flashcard_context} | |
| REQUIREMENTS: | |
| 1. Generate exactly {num_questions} questions | |
| 2. Use different question types: multiple_choice, fill_in_blank, true_false, matching, short_answer | |
| 3. Questions should test different aspects: vocabulary recall, context understanding, usage | |
| 4. Each question must include the correct answer | |
| 5. For multiple choice questions, provide 4 options with one correct answer | |
| 6. For matching questions, provide 4 word-translation pairs | |
| 7. Make questions challenging but fair | |
| 8. Vary difficulty levels across questions | |
| OUTPUT FORMAT (JSON): | |
| {{ | |
| "quiz_title": "Vocabulary Quiz", | |
| "total_questions": {num_questions}, | |
| "questions": [ | |
| {{ | |
| "question_number": 1, | |
| "type": "multiple_choice", | |
| "question": "What does 'word' mean?", | |
| "options": ["Option A", "Option B", "Option C", "Option D"], | |
| "correct_answer": "Option B", | |
| "explanation": "Brief explanation." | |
| }}, | |
| {{ | |
| "question_number": 2, | |
| "type": "fill_in_blank", | |
| "question": "Complete: The ___ ran quickly.", | |
| "correct_answer": "cat", | |
| "explanation": "Brief explanation." | |
| }}, | |
| {{ | |
| "question_number": 3, | |
| "type": "true_false", | |
| "question": "'Word' means 'definition' in English.", | |
| "correct_answer": false, | |
| "explanation": "Brief explanation." | |
| }}, | |
| {{ | |
| "question_number": 4, | |
| "type": "matching", | |
| "question": "Match the words to their correct translations", | |
| "pairs": [ | |
| {{"word": "word1", "translation": "translation1"}}, | |
| {{"word": "word2", "translation": "translation2"}}, | |
| {{"word": "word3", "translation": "translation3"}}, | |
| {{"word": "word4", "translation": "translation4"}} | |
| ], | |
| "correct_answer": "All pairs are correctly matched", | |
| "explanation": "Brief explanation." | |
| }}, | |
| {{ | |
| "question_number": 5, | |
| "type": "short_answer", | |
| "question": "Explain the usage of 'word'.", | |
| "correct_answer": "Model answer here.", | |
| "explanation": "Brief explanation." | |
| }} | |
| ] | |
| }} | |
| Generate the quiz now:""" | |
| return prompt | |
| def generate_quiz_with_ai(self, flashcards: List[Dict], num_questions: int = 30) -> Dict[str, Any]: | |
| """Generate quiz using OpenAI API""" | |
| if not self.client: | |
| raise ValueError("OpenAI client not initialized. Check API key.") | |
| if not flashcards: | |
| raise ValueError("No flashcards provided for quiz generation") | |
| prompt = self._create_quiz_prompt(flashcards, num_questions) | |
| try: | |
| response = self.client.chat.completions.create( | |
| model=self.model, | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": "You are an expert language teacher who creates engaging, educational quizzes. Always respond with valid JSON." | |
| }, | |
| { | |
| "role": "user", | |
| "content": prompt | |
| } | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.7, | |
| max_tokens=4000 | |
| ) | |
| quiz_content = response.choices[0].message.content | |
| quiz_data = json.loads(quiz_content) | |
| quiz_data['metadata'] = { | |
| 'generator': 'AI-Powered Quiz Generator', | |
| 'model': self.model, | |
| 'source_flashcards': len(flashcards), | |
| 'tokens_used': response.usage.total_tokens if response.usage else 0 | |
| } | |
| return quiz_data | |
| except Exception as e: | |
| print(f"[QuizGenerator] AI generation failed: {e}") | |
| raise | |
| def generate_simple_quiz(self, flashcards: List[Dict], num_questions: int = 5) -> Dict[str, Any]: | |
| """Generate a simple quiz without AI (fallback)""" | |
| if not flashcards: | |
| raise ValueError("No flashcards provided") | |
| questions = [] | |
| used_cards = random.sample(flashcards, min(num_questions * 2, len(flashcards))) | |
| for i, card in enumerate(used_cards[:num_questions]): | |
| q_type = random.choice(['multiple_choice', 'fill_in_blank', 'true_false']) | |
| if q_type == 'multiple_choice': | |
| # Create wrong options from other cards | |
| other_cards = [c for c in flashcards if c != card] | |
| wrong_options = random.sample( | |
| [c.get('back', 'Unknown') for c in other_cards], | |
| min(3, len(other_cards)) | |
| ) | |
| while len(wrong_options) < 3: | |
| wrong_options.append(f"Not {card.get('back', 'this')}") | |
| options = wrong_options + [card.get('back', '')] | |
| random.shuffle(options) | |
| questions.append({ | |
| "question_number": i + 1, | |
| "type": "multiple_choice", | |
| "question": f"What does '{card.get('front', '')}' mean?", | |
| "options": options, | |
| "correct_answer": card.get('back', ''), | |
| "explanation": f"'{card.get('front', '')}' translates to '{card.get('back', '')}'." | |
| }) | |
| elif q_type == 'fill_in_blank': | |
| questions.append({ | |
| "question_number": i + 1, | |
| "type": "fill_in_blank", | |
| "question": f"Translate: '{card.get('front', '')}' = _____", | |
| "correct_answer": card.get('back', ''), | |
| "explanation": f"The correct translation is '{card.get('back', '')}'." | |
| }) | |
| elif q_type == 'true_false': | |
| is_true = random.choice([True, False]) | |
| if is_true: | |
| shown_answer = card.get('back', '') | |
| else: | |
| other_cards = [c for c in flashcards if c != card] | |
| if other_cards: | |
| shown_answer = random.choice(other_cards).get('back', 'something else') | |
| else: | |
| shown_answer = f"Not {card.get('back', 'this')}" | |
| questions.append({ | |
| "question_number": i + 1, | |
| "type": "true_false", | |
| "question": f"'{card.get('front', '')}' means '{shown_answer}'.", | |
| "correct_answer": is_true, | |
| "explanation": f"'{card.get('front', '')}' actually means '{card.get('back', '')}'." | |
| }) | |
| return { | |
| "quiz_title": "Vocabulary Quiz", | |
| "total_questions": len(questions), | |
| "questions": questions, | |
| "metadata": { | |
| "generator": "Simple Quiz Generator", | |
| "source_flashcards": len(flashcards) | |
| } | |
| } | |
| def create_quiz_from_deck( | |
| username: str, | |
| deck_name: str, | |
| num_questions: int = 5, | |
| use_ai: bool = True, | |
| api_key: Optional[str] = None | |
| ) -> Dict[str, Any]: | |
| """ | |
| Create a quiz from a user's flashcard deck | |
| Args: | |
| username: User identifier | |
| deck_name: Name of the deck to create quiz from | |
| num_questions: Number of questions for the quiz session | |
| use_ai: Whether to use AI for quiz generation | |
| api_key: Optional OpenAI API key | |
| Returns: | |
| Quiz dictionary with questions | |
| """ | |
| decks = list_user_decks(username) | |
| if deck_name not in decks: | |
| raise ValueError(f"Deck '{deck_name}' not found") | |
| deck = load_deck(decks[deck_name]) | |
| flashcards = deck.get('cards', []) | |
| if not flashcards: | |
| raise ValueError(f"Deck '{deck_name}' has no cards") | |
| generator = QuizGenerator(api_key=api_key) | |
| try: | |
| if use_ai and generator.client: | |
| # Generate larger question bank with AI | |
| quiz = generator.generate_quiz_with_ai(flashcards, num_questions=30) | |
| else: | |
| # Use simple generator | |
| quiz = generator.generate_simple_quiz(flashcards, num_questions=num_questions) | |
| except Exception as e: | |
| print(f"[quiz_tools] AI quiz generation failed: {e}, using simple generator") | |
| quiz = generator.generate_simple_quiz(flashcards, num_questions=num_questions) | |
| # Add quiz metadata | |
| ts = datetime.utcnow().strftime("%Y-%m-%dT%H-%M-%SZ") | |
| quiz['id'] = f"quiz_{ts}" | |
| quiz['created_at'] = ts | |
| quiz['deck_name'] = deck_name | |
| quiz['questions_per_session'] = num_questions | |
| return quiz | |
| def create_semantic_quiz_for_user(username: str, topic: str, num_questions: int = 5) -> Dict[str, Any]: | |
| """ | |
| Create a semantic quiz based on a topic (for conversation practice) | |
| Args: | |
| username: User identifier | |
| topic: Topic for the quiz | |
| num_questions: Number of questions | |
| Returns: | |
| Quiz dictionary | |
| """ | |
| reading_passages = [ | |
| f"{topic.capitalize()} is important in daily life. Many people enjoy talking about it.", | |
| f"Here is a short story based on the topic '{topic}'.", | |
| f"In this short description, you will learn more about {topic}.", | |
| ] | |
| questions = [] | |
| for i in range(num_questions): | |
| passage = random.choice(reading_passages) | |
| q_type = random.choice(["translate_phrase", "summarize", "interpret"]) | |
| if q_type == "translate_phrase": | |
| questions.append({ | |
| "question_number": i + 1, | |
| "type": "semantic_translate_phrase", | |
| "prompt": f"Translate:\n\n'{passage}'", | |
| "answer": "(model evaluated)", | |
| "explanation": f"Checks ability to translate topic '{topic}'." | |
| }) | |
| elif q_type == "summarize": | |
| questions.append({ | |
| "question_number": i + 1, | |
| "type": "semantic_summarize", | |
| "prompt": f"Summarize:\n\n{passage}", | |
| "answer": "(model evaluated)", | |
| "explanation": f"Checks comprehension of topic '{topic}'." | |
| }) | |
| elif q_type == "interpret": | |
| questions.append({ | |
| "question_number": i + 1, | |
| "type": "semantic_interpret", | |
| "prompt": f"Interpret meaning:\n\n{passage}", | |
| "answer": "(model evaluated)", | |
| "explanation": f"Checks conceptual understanding of '{topic}'." | |
| }) | |
| ts = datetime.utcnow().strftime("%Y-%m-%dT%H-%M-%SZ") | |
| quiz_id = f"semantic_quiz_{ts}" | |
| return { | |
| "id": quiz_id, | |
| "created_at": ts, | |
| "topic": topic, | |
| "total_questions": len(questions), | |
| "questions": questions, | |
| } | |
| def save_quiz(username: str, quiz: Dict[str, Any]) -> Path: | |
| """Save a quiz to the user's directory""" | |
| user_dir = get_user_dir(username) | |
| quizzes_dir = user_dir / "quizzes" | |
| quizzes_dir.mkdir(parents=True, exist_ok=True) | |
| quiz_id = quiz.get('id', f"quiz_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}") | |
| quiz_path = quizzes_dir / f"{quiz_id}.json" | |
| with open(quiz_path, 'w', encoding='utf-8') as f: | |
| json.dump(quiz, f, ensure_ascii=False, indent=2) | |
| return quiz_path | |
| def load_quiz(username: str, quiz_id: str) -> Dict[str, Any]: | |
| """Load a saved quiz""" | |
| user_dir = get_user_dir(username) | |
| quiz_path = user_dir / "quizzes" / f"{quiz_id}.json" | |
| if not quiz_path.exists(): | |
| raise FileNotFoundError(f"Quiz '{quiz_id}' not found") | |
| with open(quiz_path, 'r', encoding='utf-8') as f: | |
| return json.load(f) | |
| def list_user_quizzes(username: str) -> List[Dict[str, Any]]: | |
| """List all quizzes for a user""" | |
| user_dir = get_user_dir(username) | |
| quizzes_dir = user_dir / "quizzes" | |
| if not quizzes_dir.exists(): | |
| return [] | |
| quizzes = [] | |
| for quiz_file in sorted(quizzes_dir.glob("*.json"), reverse=True): | |
| try: | |
| with open(quiz_file, 'r', encoding='utf-8') as f: | |
| quiz = json.load(f) | |
| quizzes.append({ | |
| 'id': quiz.get('id', quiz_file.stem), | |
| 'title': quiz.get('quiz_title', 'Untitled Quiz'), | |
| 'created_at': quiz.get('created_at', ''), | |
| 'total_questions': quiz.get('total_questions', 0), | |
| 'deck_name': quiz.get('deck_name', ''), | |
| }) | |
| except Exception: | |
| continue | |
| return quizzes | |