Spaces:
Sleeping
Sleeping
| # modules/mcq_generator.py | |
| """Enhanced MCQ Quiz Generator Module""" | |
| import random | |
| from typing import Dict, List, Tuple | |
| from modules.api_utils import ( | |
| fetch_wikipedia_summary, | |
| search_wikipedia, | |
| fetch_related_topics, | |
| fetch_wikipedia_categories, | |
| ) | |
| from config.settings import RANDOM_TOPICS | |
| from modules.hf_llm_generator import generate_quiz_set_with_llm as generate_quiz_set_from_llm | |
| def generate_smart_distractors( | |
| correct_answer: str, topic: str, context: str | |
| ) -> List[str]: | |
| """Generate intelligent distractor options""" | |
| distractors = set() | |
| # Get related topics | |
| related = fetch_related_topics(topic, 10) | |
| distractors.update(related) | |
| # Get categories and use them for distractors | |
| categories = fetch_wikipedia_categories(topic) | |
| if categories: | |
| # Search for other items in the same categories | |
| for category in categories[:2]: | |
| similar_items = search_wikipedia(category, 5) | |
| distractors.update(similar_items) | |
| # Remove the correct answer and topic | |
| distractors.discard(correct_answer) | |
| distractors.discard(topic) | |
| # Convert to list and shuffle | |
| distractor_list = list(distractors) | |
| random.shuffle(distractor_list) | |
| # If not enough distractors, add generic ones | |
| if len(distractor_list) < 3: | |
| generic_distractors = [ | |
| "Scientific Theory", | |
| "Historical Event", | |
| "Mathematical Concept", | |
| "Geographical Location", | |
| "Literary Work", | |
| "Technological Innovation", | |
| "Cultural Phenomenon", | |
| "Economic System", | |
| "Political Movement", | |
| ] | |
| distractor_list.extend(generic_distractors) | |
| return distractor_list[:3] | |
| def generate_question_types(topic: str, summary_data: Dict, difficulty: str) -> Dict: | |
| """Generate different types of questions based on difficulty""" | |
| title = summary_data.get("title", topic) | |
| extract = summary_data.get("extract", "") | |
| description = summary_data.get("description", "") | |
| # Prioritize question/answer extraction from the extract for better relevance | |
| sentences = [s.strip() for s in extract.split('.') if s.strip()] | |
| if sentences: | |
| # Try to find a sentence that defines the topic | |
| definition_sentences = [s for s in sentences if title.lower() in s.lower() and ("is a" in s.lower() or "are" in s.lower() or "defined as" in s.lower())] | |
| if definition_sentences: | |
| question = f"What is {title}?" | |
| correct_answer = definition_sentences[0] | |
| if len(correct_answer) > 100: # Truncate if too long | |
| correct_answer = correct_answer[:100] + "..." | |
| return {"question": question, "correct_answer": correct_answer, "context": extract} | |
| # Fallback to other sentences or description | |
| if difficulty == "Easy": | |
| question = f"What is {title}?" | |
| correct_answer = description if description else title | |
| elif difficulty == "Medium": | |
| question = f"Explain {title}." | |
| correct_answer = sentences[0] if sentences else title | |
| else: # Hard | |
| question = f"What is the significance of {title}?" | |
| correct_answer = sentences[-1] if sentences else title # Last sentence for more detail | |
| else: # If no sentences, fallback to title/description | |
| if difficulty == "Easy": | |
| question = f"What is {title}?" | |
| correct_answer = title | |
| elif difficulty == "Medium": | |
| question = f"How is {title} commonly defined?" | |
| correct_answer = description if description else title | |
| else: # Hard | |
| question = f"What is the key principle underlying {title}?" | |
| correct_answer = description if description else title | |
| return {"question": question, "correct_answer": correct_answer, "context": extract} | |
| def generate_mcq(topic: str, difficulty: str) -> Dict: | |
| """Generate an enhanced multiple choice question""" | |
| summary_data = fetch_wikipedia_summary(topic) | |
| if not summary_data: | |
| # Try searching for the topic | |
| search_results = search_wikipedia(topic, 3) | |
| if search_results: | |
| # Try to pick the most relevant search result | |
| found_topic = next((res for res in search_results if topic.lower() in res.lower()), search_results[0]) | |
| summary_data = fetch_wikipedia_summary(found_topic) | |
| if summary_data: | |
| topic = found_topic # Update topic if a better one was found | |
| if not summary_data: | |
| return { | |
| "error": "Topic not found on Wikipedia. Try a different topic or check spelling.", | |
| "status": False, | |
| "suggestions": search_results if search_results else [], | |
| } | |
| # Generate question based on difficulty | |
| question_data = generate_question_types(topic, summary_data, difficulty) | |
| # Generate smart distractors | |
| distractors = generate_smart_distractors( | |
| question_data["correct_answer"], topic, question_data["context"] | |
| ) | |
| # Create options | |
| options = [question_data["correct_answer"]] + distractors[:3] | |
| random.shuffle(options) | |
| # Create explanation | |
| extract = summary_data.get("extract", "") | |
| explanation = extract[:300] + "..." if len(extract) > 300 else extract | |
| return { | |
| "question": question_data["question"], | |
| "options": options, | |
| "correct_answer": question_data["correct_answer"], | |
| "explanation": explanation, | |
| "topic": summary_data.get("title", topic), | |
| "difficulty": difficulty, | |
| "status": True, | |
| } | |
| def generate_quiz_set( | |
| topic: str, difficulty: str, num_questions: int = 5, use_llm: bool = False | |
| ) -> List[Dict]: | |
| """Generate a set of questions for a complete quiz""" | |
| if use_llm: | |
| return generate_quiz_set_from_llm(topic, difficulty, num_questions) | |
| questions = [] | |
| used_topics = set() | |
| attempted_topics = set() # Keep track of all topics attempted in this call | |
| # Start with the main topic and add related topics for variety | |
| all_possible_topics = [topic] + fetch_related_topics(topic, 10) | |
| # Add some random topics from the global list to ensure diversity if related topics are exhausted | |
| all_possible_topics.extend(random.sample(RANDOM_TOPICS, min(5, len(RANDOM_TOPICS)))) | |
| random.shuffle(all_possible_topics) # Shuffle to randomize the order of attempt | |
| topic_idx = 0 | |
| while len(questions) < num_questions and topic_idx < len(all_possible_topics): | |
| current_topic = all_possible_topics[topic_idx] | |
| # Only try to generate a question if we haven't already used this topic in this quiz set | |
| # and it hasn't been attempted and failed too recently | |
| if current_topic not in used_topics and current_topic not in attempted_topics: | |
| question = generate_mcq(current_topic, difficulty) | |
| if question.get("status"): | |
| questions.append(question) | |
| used_topics.add(current_topic) | |
| # Reset attempted_topics for this branch if successful, to allow retrying later | |
| attempted_topics.clear() | |
| else: | |
| attempted_topics.add(current_topic) # Mark as attempted and failed for this pass | |
| topic_idx += 1 | |
| # If we've gone through all topics and still need questions, | |
| # reset and try again with a fresh set of potential topics | |
| if topic_idx >= len(all_possible_topics) and len(questions) < num_questions: | |
| remaining_needed = num_questions - len(questions) | |
| additional_random_topics = random.sample(RANDOM_TOPICS, min(remaining_needed + 5, len(RANDOM_TOPICS))) | |
| all_possible_topics.extend([t for t in additional_random_topics if t not in used_topics and t not in attempted_topics]) | |
| random.shuffle(all_possible_topics) | |
| topic_idx = 0 # Reset topic index for new pass | |
| # Ensure we have exactly num_questions, even if some are repeated | |
| # This loop is a fallback if the diverse topic generation wasn't enough | |
| while len(questions) < num_questions: | |
| fallback_topic = random.choice(list(used_topics) if used_topics else RANDOM_TOPICS) | |
| question = generate_mcq(fallback_topic, difficulty) | |
| if question.get("status"): | |
| questions.append(question) | |
| # Add a safeguard to prevent infinite loops if no questions can be generated | |
| if not question.get("status") and len(questions) == 0: | |
| break # Cannot generate any questions, break to prevent infinite loop | |
| random.shuffle(questions) # Shuffle the final set of questions | |
| return questions | |