capradeepgujaran commited on
Commit
f708398
1 Parent(s): e537a5a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -99
app.py CHANGED
@@ -5,80 +5,118 @@ from PIL import Image, ImageDraw, ImageFont
5
  from datetime import datetime
6
  import json
7
  import tempfile
8
- from typing import List, Dict, Tuple, Optional
9
  from dataclasses import dataclass
10
  import subprocess
 
 
 
11
 
12
- @dataclass
 
 
 
 
 
 
 
13
  class Question:
 
14
  question: str
15
- options: List[str]
16
  correct_answer: int
17
 
18
- @dataclass
 
 
 
 
 
 
 
 
19
  class QuizFeedback:
 
20
  is_correct: bool
21
  selected: Optional[str]
22
  correct_answer: str
23
 
24
- class QuizGenerator:
 
25
  def __init__(self, api_key: str):
26
  self.client = Groq(api_key=api_key)
 
27
 
 
 
 
 
 
 
 
 
28
  def generate_questions(self, text: str, num_questions: int) -> List[Question]:
29
- prompt = self._create_prompt(text, num_questions)
30
-
 
31
  try:
32
- response = self.client.chat.completions.create(
33
- messages=[
34
- {
35
- "role": "system",
36
- "content": """You are a quiz generator. Create clear questions with concise answer options.
37
- Always return valid JSON array of questions. Format answers as short, clear phrases."""
38
- },
39
- {
40
- "role": "user",
41
- "content": prompt
42
- }
43
- ],
44
- model="llama-3.2-3b-preview",
45
- temperature=0,
46
- max_tokens=2048
47
- )
48
-
49
- questions = self._parse_response(response.choices[0].message.content)
50
  return self._validate_questions(questions, num_questions)
51
-
52
  except Exception as e:
53
- print(f"Error in generate_questions: {str(e)}")
54
- print(f"Response content: {response.choices[0].message.content if response else 'No response'}")
55
- raise QuizGenerationError(f"Failed to generate questions: {str(e)}")
56
-
57
- def _create_prompt(self, text: str, num_questions: int) -> str:
58
- return f"""Generate exactly {num_questions} multiple choice questions based on the following text. Follow these rules strictly:
59
-
60
- 1. Each question must be clear and focused
61
- 2. Provide exactly 4 options for each question
62
- 3. Mark the correct answer with index (0-3)
63
- 4. Keep all options concise (under 10 words)
64
- 5. Return ONLY a JSON array with this exact format:
65
- [
66
- {{
67
- "question": "Clear question text here?",
68
- "options": [
69
- "Brief option 1",
70
- "Brief option 2",
71
- "Brief option 3",
72
- "Brief option 4"
73
- ],
74
- "correct_answer": 0
75
- }}
76
- ]
77
-
78
- Text to generate questions from:
79
- {text}
80
-
81
- Important: Return only the JSON array, no other text or formatting."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  def _parse_response(self, response_text: str) -> List[Dict]:
84
  """Parse response with improved error handling"""
@@ -417,54 +455,60 @@ class CertificateGenerator:
417
 
418
 
419
  class QuizApp:
 
420
  def __init__(self, api_key: str):
421
- self.quiz_generator = QuizGenerator(api_key)
422
  self.certificate_generator = CertificateGenerator()
423
- self.current_questions: List[Question] = []
424
-
425
- def generate_questions(self, text: str, num_questions: int) -> Tuple[bool, List[Question]]:
426
- """
427
- Generate quiz questions using the QuizGenerator
428
- Returns (success, questions) tuple
429
- """
430
- try:
431
- questions = self.quiz_generator.generate_questions(text, num_questions)
432
- self.current_questions = questions
433
- return True, questions
434
- except Exception as e:
435
- print(f"Error generating questions: {e}")
436
- return False, []
437
 
438
- def calculate_score(self, answers: List[Optional[str]]) -> Tuple[float, bool, List[QuizFeedback]]:
439
- """
440
- Calculate the quiz score and generate feedback
441
- Returns (score, passed, feedback) tuple
442
- """
443
- if not answers or not self.current_questions:
444
- return 0, False, []
445
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  feedback = []
447
- correct = 0
448
-
449
- for question, answer in zip(self.current_questions, answers):
450
- if answer is None:
451
- feedback.append(QuizFeedback(False, None, question.options[question.correct_answer]))
452
- continue
453
-
454
- try:
455
- selected_index = question.options.index(answer)
456
- is_correct = selected_index == question.correct_answer
457
- if is_correct:
458
- correct += 1
459
- feedback.append(QuizFeedback(
460
- is_correct,
461
- answer,
462
- question.options[question.correct_answer]
463
- ))
464
- except ValueError:
465
- feedback.append(QuizFeedback(False, answer, question.options[question.correct_answer]))
466
-
467
- score = (correct / len(self.current_questions)) * 100
468
  return score, score >= 80, feedback
469
 
470
  def update_questions(self, text: str, num_questions: int) -> Tuple[gr.update, gr.update, List[gr.update], List[Question], gr.update]:
 
5
  from datetime import datetime
6
  import json
7
  import tempfile
8
+ from typing import List, Dict, Tuple, Optional, Any
9
  from dataclasses import dataclass
10
  import subprocess
11
+ from pathlib import Path
12
+ import logging
13
+ from functools import lru_cache
14
 
15
+ # Configure logging
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ @dataclass(frozen=True)
23
  class Question:
24
+ """Immutable question data structure with validation"""
25
  question: str
26
+ options: Tuple[str, ...] # Using tuple for immutability
27
  correct_answer: int
28
 
29
+ def __post_init__(self):
30
+ if not isinstance(self.options, tuple):
31
+ object.__setattr__(self, 'options', tuple(self.options))
32
+ if not (0 <= self.correct_answer < len(self.options)):
33
+ raise ValueError(f"Correct answer index {self.correct_answer} out of range")
34
+ if len(self.options) != 4:
35
+ raise ValueError(f"Must have exactly 4 options, got {len(self.options)}")
36
+
37
+ @dataclass(frozen=True)
38
  class QuizFeedback:
39
+ """Immutable feedback data structure"""
40
  is_correct: bool
41
  selected: Optional[str]
42
  correct_answer: str
43
 
44
+ class CacheableQuizGenerator:
45
+ """Quiz generator with caching capabilities"""
46
  def __init__(self, api_key: str):
47
  self.client = Groq(api_key=api_key)
48
+ self._cache = {}
49
 
50
+ @lru_cache(maxsize=100)
51
+ def _generate_cached_questions(self, text_hash: str, num_questions: int) -> List[Question]:
52
+ """Cache questions based on content hash"""
53
+ text = self._cache.get(text_hash)
54
+ if not text:
55
+ raise KeyError("Text not found in cache")
56
+ return self._generate_questions_internal(text, num_questions)
57
+
58
  def generate_questions(self, text: str, num_questions: int) -> List[Question]:
59
+ """Generate questions with caching"""
60
+ text_hash = hash(text)
61
+ self._cache[text_hash] = text
62
  try:
63
+ return self._generate_cached_questions(text_hash, num_questions)
64
+ except Exception as e:
65
+ logger.error(f"Error generating questions: {e}")
66
+ raise QuizGenerationError(f"Failed to generate questions: {e}")
67
+
68
+ def _generate_questions_internal(self, text: str, num_questions: int) -> List[Question]:
69
+ """Internal question generation logic"""
70
+ try:
71
+ response = self._get_llm_response(text, num_questions)
72
+ questions = self._parse_response(response)
 
 
 
 
 
 
 
 
73
  return self._validate_questions(questions, num_questions)
 
74
  except Exception as e:
75
+ logger.error(f"Error in question generation: {e}")
76
+ raise
77
+
78
+ def _get_llm_response(self, text: str, num_questions: int) -> str:
79
+ """Get response from LLM with retry logic"""
80
+ max_retries = 3
81
+ for attempt in range(max_retries):
82
+ try:
83
+ response = self.client.chat.completions.create(
84
+ messages=self._create_messages(text, num_questions),
85
+ model="llama-3.2-3b-preview",
86
+ temperature=0,
87
+ max_tokens=2048
88
+ )
89
+ return response.choices[0].message.content
90
+ except Exception as e:
91
+ if attempt == max_retries - 1:
92
+ raise
93
+ logger.warning(f"Retry {attempt + 1}/{max_retries} after error: {e}")
94
+ continue
95
+
96
+ @staticmethod
97
+ def _create_messages(text: str, num_questions: int) -> List[Dict[str, str]]:
98
+ """Create messages for LLM prompt"""
99
+ return [
100
+ {
101
+ "role": "system",
102
+ "content": "You are a quiz generator. Create clear questions with concise answer options."
103
+ },
104
+ {
105
+ "role": "user",
106
+ "content": f"""Generate exactly {num_questions} multiple choice questions based on this text:
107
+
108
+ {text}
109
+
110
+ Return only a JSON array with this format:
111
+ [
112
+ {{
113
+ "question": "Question text?",
114
+ "options": ["Option 1", "Option 2", "Option 3", "Option 4"],
115
+ "correct_answer": 0
116
+ }}
117
+ ]"""
118
+ }
119
+ ]
120
 
121
  def _parse_response(self, response_text: str) -> List[Dict]:
122
  """Parse response with improved error handling"""
 
455
 
456
 
457
  class QuizApp:
458
+ """Enhanced quiz application with proper state management"""
459
  def __init__(self, api_key: str):
460
+ self.quiz_generator = CacheableQuizGenerator(api_key)
461
  self.certificate_generator = CertificateGenerator()
462
+ self._current_questions: List[Question] = []
463
+ self._user_answers: List[Optional[str]] = []
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
+ @property
466
+ def current_questions(self) -> List[Question]:
467
+ """Get current questions with defensive copy"""
468
+ return self._current_questions.copy()
469
+
470
+ def set_questions(self, questions: List[Question]) -> None:
471
+ """Set current questions with validation"""
472
+ if not all(isinstance(q, Question) for q in questions):
473
+ raise ValueError("All elements must be Question instances")
474
+ self._current_questions = questions.copy()
475
+ self._user_answers = [None] * len(questions)
476
+
477
+ def submit_answer(self, question_idx: int, answer: Optional[str]) -> None:
478
+ """Submit an answer with validation"""
479
+ if not 0 <= question_idx < len(self._current_questions):
480
+ raise ValueError(f"Invalid question index: {question_idx}")
481
+ self._user_answers[question_idx] = answer
482
+
483
+ def calculate_score(self) -> Tuple[float, bool, List[QuizFeedback]]:
484
+ """Calculate quiz score with proper validation"""
485
+ if not self._current_questions or not self._user_answers:
486
+ return 0.0, False, []
487
+
488
+ if len(self._current_questions) != len(self._user_answers):
489
+ raise ValueError("Questions and answers length mismatch")
490
+
491
  feedback = []
492
+ correct_count = 0
493
+
494
+ for question, answer in zip(self._current_questions, self._user_answers):
495
+ is_correct = False
496
+ if answer is not None:
497
+ try:
498
+ selected_index = question.options.index(answer)
499
+ is_correct = selected_index == question.correct_answer
500
+ if is_correct:
501
+ correct_count += 1
502
+ except ValueError:
503
+ pass
504
+
505
+ feedback.append(QuizFeedback(
506
+ is_correct=is_correct,
507
+ selected=answer,
508
+ correct_answer=question.options[question.correct_answer]
509
+ ))
510
+
511
+ score = (correct_count / len(self._current_questions)) * 100
 
512
  return score, score >= 80, feedback
513
 
514
  def update_questions(self, text: str, num_questions: int) -> Tuple[gr.update, gr.update, List[gr.update], List[Question], gr.update]: