capradeepgujaran commited on
Commit
82ee3e0
1 Parent(s): f708398

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -105
app.py CHANGED
@@ -21,14 +21,11 @@ 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:
@@ -36,87 +33,57 @@ class Question:
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"""
@@ -134,18 +101,20 @@ class CacheableQuizGenerator:
134
 
135
  json_text = cleaned_text[start_idx:end_idx + 1]
136
 
137
- # Attempt to parse the JSON
138
  try:
139
- return json.loads(json_text)
 
 
 
140
  except json.JSONDecodeError as e:
141
- print(f"JSON Parse Error: {str(e)}")
142
- print(f"Attempted to parse: {json_text}")
143
  raise
144
 
145
  except Exception as e:
146
- print(f"Error parsing response: {str(e)}")
147
- print(f"Original response: {response_text}")
148
- raise ValueError(f"Failed to parse response: {str(e)}")
149
 
150
  def _validate_questions(self, questions: List[Dict], num_questions: int) -> List[Question]:
151
  """Validate questions with improved error checking"""
@@ -154,7 +123,7 @@ class CacheableQuizGenerator:
154
  for q in questions:
155
  try:
156
  if not self._is_valid_question(q):
157
- print(f"Invalid question format: {q}")
158
  continue
159
 
160
  validated.append(Question(
@@ -163,7 +132,7 @@ class CacheableQuizGenerator:
163
  correct_answer=int(q["correct_answer"]) % 4
164
  ))
165
  except Exception as e:
166
- print(f"Error validating question: {str(e)}")
167
  continue
168
 
169
  if not validated:
@@ -455,61 +424,57 @@ class CertificateGenerator:
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]:
515
  """
@@ -562,6 +527,8 @@ class QuizApp:
562
  gr.update(selected=1)
563
  )
564
 
 
 
565
  def submit_quiz(self, q1: Optional[str], q2: Optional[str], q3: Optional[str],
566
  q4: Optional[str], q5: Optional[str], questions: List[Question]
567
  ) -> Tuple[gr.update, List[gr.update], float, str, gr.update]:
 
21
 
22
  @dataclass(frozen=True)
23
  class Question:
 
24
  question: str
25
+ options: List[str]
26
  correct_answer: int
27
 
28
  def __post_init__(self):
 
 
29
  if not (0 <= self.correct_answer < len(self.options)):
30
  raise ValueError(f"Correct answer index {self.correct_answer} out of range")
31
  if len(self.options) != 4:
 
33
 
34
  @dataclass(frozen=True)
35
  class QuizFeedback:
 
36
  is_correct: bool
37
  selected: Optional[str]
38
  correct_answer: str
39
 
40
+ class QuizGenerator:
 
41
  def __init__(self, api_key: str):
42
  self.client = Groq(api_key=api_key)
43
+ self._question_cache = lru_cache(maxsize=100)(self._generate_questions_internal)
44
 
 
 
 
 
 
 
 
 
45
  def generate_questions(self, text: str, num_questions: int) -> List[Question]:
 
 
 
46
  try:
47
+ # Use text hash as cache key
48
+ text_hash = hash(text)
49
+ return self._question_cache(text_hash, num_questions)
50
  except Exception as e:
51
  logger.error(f"Error generating questions: {e}")
52
  raise QuizGenerationError(f"Failed to generate questions: {e}")
53
 
54
+ def _generate_questions_internal(self, text_hash: str, num_questions: int) -> List[Question]:
55
+ """Internal question generation with retry logic"""
 
 
 
 
 
 
 
 
 
 
56
  max_retries = 3
57
+ last_error = None
58
+
59
  for attempt in range(max_retries):
60
  try:
61
  response = self.client.chat.completions.create(
62
+ messages=[
63
+ {
64
+ "role": "system",
65
+ "content": """You are a quiz generator. Create clear questions with concise answer options.
66
+ Always return valid JSON array of questions. Format answers as short, clear phrases."""
67
+ },
68
+ {
69
+ "role": "user",
70
+ "content": self._create_prompt(text_hash, num_questions)
71
+ }
72
+ ],
73
  model="llama-3.2-3b-preview",
74
  temperature=0,
75
  max_tokens=2048
76
  )
77
+
78
+ questions = self._parse_response(response.choices[0].message.content)
79
+ return self._validate_questions(questions, num_questions)
80
+
81
  except Exception as e:
82
+ last_error = e
83
+ logger.warning(f"Attempt {attempt + 1}/{max_retries} failed: {e}")
84
  if attempt == max_retries - 1:
85
+ raise QuizGenerationError(f"Failed after {max_retries} attempts: {last_error}")
 
86
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  def _parse_response(self, response_text: str) -> List[Dict]:
89
  """Parse response with improved error handling"""
 
101
 
102
  json_text = cleaned_text[start_idx:end_idx + 1]
103
 
 
104
  try:
105
+ parsed = json.loads(json_text)
106
+ if not isinstance(parsed, list):
107
+ raise ValueError("Response is not a JSON array")
108
+ return parsed
109
  except json.JSONDecodeError as e:
110
+ logger.error(f"JSON Parse Error: {e}")
111
+ logger.debug(f"Attempted to parse: {json_text}")
112
  raise
113
 
114
  except Exception as e:
115
+ logger.error(f"Error parsing response: {e}")
116
+ logger.debug(f"Original response: {response_text}")
117
+ raise ValueError(f"Failed to parse response: {e}")
118
 
119
  def _validate_questions(self, questions: List[Dict], num_questions: int) -> List[Question]:
120
  """Validate questions with improved error checking"""
 
123
  for q in questions:
124
  try:
125
  if not self._is_valid_question(q):
126
+ logger.warning(f"Invalid question format: {q}")
127
  continue
128
 
129
  validated.append(Question(
 
132
  correct_answer=int(q["correct_answer"]) % 4
133
  ))
134
  except Exception as e:
135
+ logger.error(f"Error validating question: {e}")
136
  continue
137
 
138
  if not validated:
 
424
 
425
 
426
  class QuizApp:
 
427
  def __init__(self, api_key: str):
428
+ self.quiz_generator = QuizGenerator(api_key)
429
  self.certificate_generator = CertificateGenerator()
430
  self._current_questions: List[Question] = []
431
  self._user_answers: List[Optional[str]] = []
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
+ def generate_questions(self, text: str, num_questions: int) -> Tuple[bool, List[Question]]:
434
+ """Generate quiz questions with improved error handling"""
435
+ try:
436
+ questions = self.quiz_generator.generate_questions(text, num_questions)
437
+ self._current_questions = questions
438
+ self._user_answers = [None] * len(questions)
439
+ return True, questions
440
+ except Exception as e:
441
+ logger.error(f"Error generating questions: {e}")
442
+ return False, []
443
 
444
+ def calculate_score(self, answers: List[Optional[str]]) -> Tuple[float, bool, List[QuizFeedback]]:
445
+ """Calculate quiz score with improved validation"""
446
+ if not answers or not self._current_questions:
447
+ return 0, False, []
448
+
449
+ if len(answers) != len(self._current_questions):
450
+ logger.warning("Answer count mismatch with questions")
451
+ return 0, False, []
452
 
453
  feedback = []
454
+ correct = 0
455
+
456
+ for question, answer in zip(self._current_questions, answers):
457
  is_correct = False
458
+ correct_answer = question.options[question.correct_answer]
459
+
460
  if answer is not None:
461
  try:
462
  selected_index = question.options.index(answer)
463
  is_correct = selected_index == question.correct_answer
464
  if is_correct:
465
+ correct += 1
466
  except ValueError:
467
+ logger.warning(f"Invalid answer selected: {answer}")
468
+
469
  feedback.append(QuizFeedback(
470
  is_correct=is_correct,
471
  selected=answer,
472
+ correct_answer=correct_answer
473
  ))
474
+
475
+ score = (correct / len(self._current_questions)) * 100
476
  return score, score >= 80, feedback
477
+
478
 
479
  def update_questions(self, text: str, num_questions: int) -> Tuple[gr.update, gr.update, List[gr.update], List[Question], gr.update]:
480
  """
 
527
  gr.update(selected=1)
528
  )
529
 
530
+
531
+
532
  def submit_quiz(self, q1: Optional[str], q2: Optional[str], q3: Optional[str],
533
  q4: Optional[str], q5: Optional[str], questions: List[Question]
534
  ) -> Tuple[gr.update, List[gr.update], float, str, gr.update]: