Spaces:
Sleeping
Sleeping
import gradio as gr | |
import os | |
import torch | |
from transformers import AutoModelForCausalLM, AutoTokenizer | |
from accelerate import Accelerator | |
import re | |
import time | |
import PyPDF2 | |
import sqlite3 | |
import json # Import json for pretty printing parsed data | |
# --- Global Constants --- | |
MAX_QUIZ_QUESTIONS_UI = 20 # Define this globally so UI can access it | |
WEAK_THRESHOLD_PERCENTAGE = 65 # Percentage below which a topic is considered a weak area | |
quiz_context = {} | |
# --- Database Setup --- | |
DB_NAME = 'edututor.db' | |
def init_db(): | |
conn = sqlite3.connect(DB_NAME) | |
cursor = conn.cursor() | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS quiz_results ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
timestamp TEXT NOT NULL, | |
topic TEXT NOT NULL, | |
difficulty TEXT NOT NULL, | |
score INTEGER, | |
num_questions INTEGER | |
) | |
''') | |
conn.commit() | |
conn.close() | |
print(f"Database '{DB_NAME}' initialized successfully.") | |
def record_quiz_result(topic, difficulty, score, num_questions): | |
conn = sqlite3.connect(DB_NAME) | |
cursor = conn.cursor() | |
timestamp = time.strftime('%Y-%m-%d %H:%M:%S') | |
cursor.execute(''' | |
INSERT INTO quiz_results (timestamp, topic, difficulty, score, num_questions) | |
VALUES (?, ?, ?, ?, ?) | |
''', (timestamp, topic, difficulty, score, num_questions)) | |
conn.commit() | |
conn.close() | |
print(f"Recorded quiz result: Topic='{topic}', Difficulty='{difficulty}', Score={score}/{num_questions}") | |
def get_performance_data(): | |
"""Fetches raw performance data from the database.""" | |
conn = sqlite3.connect(DB_NAME) | |
cursor = conn.cursor() | |
cursor.execute('SELECT timestamp, topic, difficulty, score, num_questions FROM quiz_results ORDER BY timestamp DESC') | |
data = cursor.fetchall() | |
conn.close() | |
return data | |
def get_performance_insights(): | |
""" | |
Analyzes quiz performance data and generates markdown for weak areas | |
and suggested next steps, along with the dataframe for history. | |
""" | |
performance_data = get_performance_data() # Fetch raw data | |
# We return the actual data for the DataFrame, Gradio will handle updating its value | |
df_data_for_update = performance_data | |
if not performance_data: | |
# Return empty data for DataFrame, and messages for markdown | |
return [], "No quiz data available for analysis. Please take some quizzes first!", "No specific suggestions at this time." | |
topic_scores = {} # {topic: [score_percentage, ...]} | |
for timestamp, topic, difficulty, score, num_questions in performance_data: | |
if num_questions > 0: | |
percentage = (score / num_questions) * 100 | |
if topic not in topic_scores: | |
topic_scores[topic] = [] | |
topic_scores[topic].append(percentage) | |
weak_areas_list = [] | |
for topic, percentages in topic_scores.items(): | |
avg_percentage = sum(percentages) / len(percentages) | |
num_quizzes = len(percentages) | |
if avg_percentage < WEAK_THRESHOLD_PERCENTAGE: | |
weak_areas_list.append({ | |
"topic": topic, | |
"avg_score_percentage": avg_percentage, | |
"num_quizzes": num_quizzes | |
}) | |
# Sort weak areas by average score (lowest first) | |
weak_areas_list.sort(key=lambda x: x['avg_score_percentage']) | |
# --- Weak Areas Markdown --- | |
weak_areas_markdown = "" | |
if not weak_areas_list: | |
weak_areas_markdown = "π **Excellent work!** No significant weak areas detected based on your quiz performance. Keep up the great work!" | |
else: | |
weak_areas_markdown = "### Based on your performance, here are some areas where you might need more practice:\n\n" | |
for area in weak_areas_list: | |
weak_areas_markdown += f"- **{area['topic']}**: Average score of **{area['avg_score_percentage']:.1f}%** across {area['num_quizzes']} quiz(es).\n" | |
# --- Suggested Steps Markdown --- | |
suggested_steps_markdown = "" | |
if weak_areas_list: | |
suggested_steps_markdown += "### Suggested Next Steps:\n\n" | |
suggested_steps_markdown += "π **Focus on these topics:**\n" | |
for area in weak_areas_list: | |
suggested_steps_markdown += f"- Try generating a new quiz specifically on **{area['topic']}** (e.g., using the 'Generate Quiz' tab).\n" | |
suggested_steps_markdown += "\nπ‘ Practice makes perfect! Revisit relevant materials and re-take quizzes on these subjects." | |
else: | |
suggested_steps_markdown = "### Suggested Next Steps:\n\n" | |
suggested_steps_markdown += "Keep exploring new topics or challenge yourself with a 'hard' difficulty quiz! You're doing great!" | |
return df_data_for_update, weak_areas_markdown, suggested_steps_markdown | |
# --- Model Loading (Local Inference) --- | |
GRANITE_MODEL_NAME = "ibm-granite/granite-3.3-2b-instruct" | |
tokenizer = None | |
model = None | |
device = "cuda" if torch.cuda.is_available() else "cpu" | |
def load_granite_model(): | |
global tokenizer, model | |
if model is not None and tokenizer is not None: | |
return tokenizer, model | |
print(f"Loading {GRANITE_MODEL_NAME} on {device}...") | |
try: | |
tokenizer = AutoTokenizer.from_pretrained(GRANITE_MODEL_NAME) | |
model = AutoModelForCausalLM.from_pretrained( | |
GRANITE_MODEL_NAME, | |
device_map="auto", | |
torch_dtype=torch.float16 | |
) | |
model.eval() | |
print(f"Successfully loaded model: {GRANITE_MODEL_NAME} locally!") | |
print(f"Model is running on: {device}") | |
return tokenizer, model | |
except Exception as e: | |
print(f"Error loading model {GRANITE_MODEL_NAME}: {e}") | |
print("Possible reasons: Insufficient GPU RAM, model not found, or network issues during download.") | |
print("Consider trying a smaller model like 'ibm-granite/granite-3.0-2b-instruct' or upgrading Colab runtime.") | |
return None, None | |
# Try loading the actual model, if it fails, fallback to mock objects. | |
try: | |
tokenizer, model = load_granite_model() | |
if tokenizer is None or model is None: | |
raise Exception("Model or tokenizer failed to load, falling back to mock.") | |
except Exception as e: | |
print(f"INFO: Failed to load actual model ({e}). Using mock model for demonstration.") | |
class MockModel: | |
def generate(self, input_ids, max_new_tokens, temperature, top_p, do_sample, pad_token_id, eos_token_id): | |
text = self.tokenizer.decode(input_ids[0], skip_special_tokens=True) | |
if "summarize" in text.lower(): | |
return self.tokenizer.encode("This is a mock summary of your text.") | |
elif "refine" in text.lower(): | |
return self.tokenizer.encode("This is your mock refined text.") | |
elif "meaning of" in text.lower(): | |
return self.tokenizer.encode("Mock meaning: A mock object is a simulated object.") | |
elif "translate" in text.lower(): | |
return self.tokenizer.encode("Mock translation.") | |
elif "quiz" in text.lower(): | |
mock_quiz_output = """ | |
1. Question: What is the capital of France? | |
A. Berlin | |
B. Madrid | |
C. Paris | |
D. Rome | |
Correct Answer: C | |
Explanation: Paris is the capital and most populous city of France. | |
2. Question: Which planet is known as the Red Planet? | |
A. Earth | |
B. Mars | |
C. Jupiter | |
D. Venus | |
Correct Answer: B | |
Explanation: Mars is often referred to as the Red Planet because of its reddish appearance. | |
3. Question: What is the largest ocean on Earth? | |
A. Atlantic Ocean | |
B. Indian Ocean | |
C. Arctic Ocean | |
D. Pacific Ocean | |
Correct Answer: D | |
Explanation: The Pacific Ocean is the largest and deepest of Earth's five oceanic divisions. | |
""" | |
return self.tokenizer.encode(mock_quiz_output) | |
return self.tokenizer.encode("Hello! This is a mock response from Edu-Tutor AI.") | |
tokenizer = AutoTokenizer.from_pretrained("gpt2") # Using a small, easily downloadable tokenizer for mock | |
model = MockModel() | |
model.tokenizer = tokenizer # Attach tokenizer to mock model for encoding/decoding | |
# Ensure accelerator is prepared even for mock model (if using torch operations) | |
accelerator = Accelerator() | |
# Note: For the MockModel, accelerator.prepare might not be strictly necessary if it doesn't use torch tensors internally | |
# but keeping it for consistency if you swap to a real model. | |
# If you get errors here with the mock model, you can remove this line for the mock setup. | |
# model, tokenizer = accelerator.prepare(model, tokenizer) | |
# --- Helper Functions for Model Inference --- | |
STREAM_DELAY = 0.005 # seconds per word | |
def generate_response(prompt, chat_history_for_model): | |
if model is None or tokenizer is None: | |
yield "Error: Model not loaded. Please check Colab runtime and GPU memory." | |
return | |
messages = [] | |
for human, ai in chat_history_for_model: | |
messages.append({"role": "user", "content": human if human is not None else ""}) | |
messages.append({"role": "assistant", "content": ai if ai is not None else ""}) | |
messages.append({"role": "user", "content": prompt}) | |
input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
input_ids = tokenizer(input_text, return_tensors="pt").to(device) | |
current_response = "" | |
try: | |
input_token_len = input_ids.input_ids.shape[1] | |
# Ensure pad_token_id and eos_token_id are set for generate | |
if tokenizer.pad_token_id is None: | |
tokenizer.pad_token_id = tokenizer.eos_token_id # Often EOS token is used as pad_token_id | |
if tokenizer.eos_token_id is None: # Fallback if tokenizer doesn't provide one | |
# For some models/tokenizers, you might manually set an arbitrary ID or handle it differently | |
# For gpt2, eos_token_id is typically 50256 | |
if hasattr(tokenizer, 'default_end_token_id'): | |
tokenizer.eos_token_id = tokenizer.default_end_token_id | |
elif tokenizer.vocab_size > 0: | |
tokenizer.eos_token_id = tokenizer.vocab_size - 1 # Last token ID as a fallback | |
full_output_tokens = model.generate( | |
**input_ids, | |
max_new_tokens=100, | |
temperature=0.7, | |
do_sample=True, | |
pad_token_id=tokenizer.pad_token_id, | |
eos_token_id=tokenizer.eos_token_id | |
) | |
full_response = tokenizer.decode(full_output_tokens[0][input_token_len:], skip_special_tokens=True).strip() | |
words = full_response.split(" ") | |
for i in range(len(words)): | |
current_response += words[i] + " " | |
yield current_response | |
time.sleep(STREAM_DELAY) | |
return | |
except Exception as e: | |
yield f"Error during inference: {e}" | |
# Function to read text from PDF | |
def read_pdf_text(pdf_file_path): | |
text = "" | |
if pdf_file_path is None: | |
return "" | |
try: | |
with open(pdf_file_path, 'rb') as file: | |
reader = PyPDF2.PdfReader(file) | |
for page_num in range(len(reader.pages)): | |
page = reader.pages[page_num] | |
text += page.extract_text() | |
return text | |
except Exception as e: | |
return f"Error reading PDF: {e}" | |
# --- Quiz Parsing Function --- | |
def parse_quiz_text(raw_quiz_text): | |
""" | |
Parses the raw quiz text generated by the LLM into a structured list of dictionaries. | |
Each dictionary represents a question. | |
""" | |
parsed_questions = [] | |
# Regex to find each question block | |
# It looks for: | |
# 1. An optional number (e.g., "1.") | |
# 2. " Question:" followed by the question text | |
# 3. Lines starting with A., B., C., D. for options | |
# 4. "Correct Answer: [Letter]" | |
# 5. "Explanation: [Text]" | |
question_pattern = re.compile( | |
r'(\d*\.?\s*Q(?:uestion)?:\s*(.*?)\n' # Optional number, "Question:", then question text (group 2) | |
r'\s*A\.\s*(.*?)\n' # Option A (group 3) | |
r'\s*B\.\s*(.*?)\n' # Option B (group 4) | |
r'\s*C\.\s*(.*?)\n' # Option C (group 5) | |
r'\s*D\.\s*(.*?)\n' # Option D (group 6) | |
r'\s*Correct Answer:\s*([A-D])\s*\n' # Correct Answer letter (group 7) | |
r'\s*Explanation:\s*(.*?)(?=\n\d*\.?\s*Q(?:uestion)?:|\Z))', # Explanation (group 8), lookahead for next question or end of string | |
re.DOTALL # . matches newlines | |
) | |
matches = question_pattern.findall(raw_quiz_text) | |
for match in matches: | |
question_text = match[1].strip() | |
options = { | |
'A': match[2].strip(), | |
'B': match[3].strip(), | |
'C': match[4].strip(), | |
'D': match[5].strip() | |
} | |
correct_answer = match[6].strip() | |
explanation = match[7].strip() | |
# Clean up question text if it has extra "Question:" or leading/trailing spaces | |
# Also remove the leading number if present, as it's just for display initially | |
question_text = re.sub(r'^\d+\.\s*Question:\s*', '', question_text, flags=re.IGNORECASE).strip() | |
parsed_questions.append({ | |
'question_text': question_text, | |
'options': options, | |
'correct_answer': correct_answer, | |
'explanation': explanation | |
}) | |
print(f"DEBUG: Parsed {len(parsed_questions)} questions.") | |
return parsed_questions | |
# --- Quiz Generation and Scoring Functions --- | |
quiz_context = { | |
"topic": "", | |
"difficulty": "", | |
"parsed_quiz": [] | |
} | |
def generate_quiz_for_display(quiz_topic, num_quiz_questions_str, difficulty="medium", pdf_file=None): | |
# Initialize a list of gr.update objects for all expected outputs | |
# The first element is for parsed_quiz_data_state, and should be gr.update() | |
updates = [ | |
gr.update(), # 0: parsed_quiz_data_state (will be updated with actual data) - CORRECTED | |
gr.update(visible=False, value=""), # 1: raw_quiz_output_display (clear and hide) | |
gr.update(visible=False), # 2: submit_quiz_button | |
gr.update(value="", visible=False), # 3: quiz_results_display | |
gr.update(visible=False), # 4: start_new_quiz_button | |
gr.update(visible=False) # 5: interactive_quiz_column - initially hidden for reset | |
] | |
# Add updates for all possible question/radio pairs, initially hidden | |
for i in range(MAX_QUIZ_QUESTIONS_UI): | |
updates.append(gr.update(value="", visible=False)) # Q_md | |
updates.append(gr.update(choices=[], value=None, visible=False)) # Q_radio | |
# Yield initial reset state (important for clearing previous quiz display) | |
yield tuple(updates) | |
if model is None or tokenizer is None: | |
print(f"DEBUG: Model or tokenizer not loaded at start of generate_quiz_for_display. Current time: {time.time()}") | |
updates[1] = gr.update(value="Error: Model not loaded. Please check Colab runtime and GPU memory.", visible=True) | |
yield tuple(updates) | |
return | |
try: | |
num_quiz_questions = int(num_quiz_questions_str) | |
if not (1 <= num_quiz_questions <= MAX_QUIZ_QUESTIONS_UI): | |
raise ValueError(f"Number of questions must be between 1 and {MAX_QUIZ_QUESTIONS_UI}.") | |
except ValueError as e: | |
print(f"DEBUG: Invalid number of questions: {e}. Current time: {time.time()}") | |
updates[1] = gr.update(value=f"Invalid number of questions: {e}. Please enter a whole number between 1 and {MAX_QUIZ_QUESTIONS_UI}.", visible=True) | |
yield tuple(updates) | |
return | |
context_text = "" | |
effective_topic = "" | |
if pdf_file: | |
context_text = read_pdf_text(pdf_file.name) | |
if "Error reading PDF" in context_text: | |
print(f"DEBUG: Error reading PDF: {context_text}. Current time: {time.time()}") | |
updates[1] = gr.update(value=context_text, visible=True) | |
yield tuple(updates) | |
return | |
if not context_text.strip(): | |
print(f"DEBUG: PDF file is empty or could not be read (empty context_text). Current time: {time.time()}") | |
updates[1] = gr.update(value="PDF file is empty or could not be read.", visible=True) | |
yield tuple(updates) | |
return | |
effective_topic = f"PDF: {os.path.basename(pdf_file.name)}" | |
elif quiz_topic and quiz_topic.strip(): | |
context_text = quiz_topic | |
effective_topic = quiz_topic | |
else: | |
print(f"DEBUG: No topic or PDF provided. Current time: {time.time()}") | |
updates[1] = gr.update(value="Please provide a topic for the quiz or upload a PDF.", visible=True) | |
yield tuple(updates) | |
return | |
if not context_text.strip(): | |
print(f"DEBUG: Final context_text is empty after processing. Current time: {time.time()}") | |
updates[1] = gr.update(value="A valid topic or non-empty PDF content is required to generate a quiz.", visible=True) | |
yield tuple(updates) | |
return | |
difficulty_instruction = "" | |
if difficulty == "easy": | |
difficulty_instruction = "Make the questions relatively easy and straightforward." | |
elif difficulty == "hard": | |
difficulty_instruction = "Make the questions challenging and detailed, requiring deeper understanding." | |
else: # medium | |
difficulty_instruction = "Make the questions of medium difficulty." | |
quiz_prompt = f"""Generate {num_quiz_questions} multiple-choice questions about the following text or topic: | |
{context_text} | |
{difficulty_instruction} | |
For each question, provide exactly 4 options (A, B, C, D), clearly state the single correct answer letter, and give an **EXTREMELY brief, 1-sentence explanation. Be very concise.** | |
Focus on **very short questions** and **short options**. No introductory or concluding remarks. Just the questions. | |
**IMPORTANT: Prepend each "Question:" with its number (e.g., "1. Question:", "2. Question:").** | |
Format each question STRICTLY like this, with no extra text or numbering outside this pattern: | |
[Number]. Question: [Short question text] | |
A. [Option A] | |
B. [Option B] | |
C. [Option C] | |
D. [Option D] | |
Correct Answer: [Letter] | |
Explanation: [Very brief explanation, 1 sentence max, no fluff] | |
Example: | |
1. Question: Capital of France? | |
A. Berlin | |
B. Madrid | |
C. Paris | |
D. Rome | |
Correct Answer: C | |
Explanation: Paris is the capital. | |
""" | |
print(f"DEBUG: Quiz prompt length: {len(quiz_prompt)}. First 200 chars: {quiz_prompt[:200]}. Current time: {time.time()}") | |
messages = [{"role": "user", "content": quiz_prompt}] | |
input_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
if not input_text.strip(): | |
print(f"DEBUG: Tokenizer applied chat template resulted in empty input_text. Current time: {time.time()}") | |
updates[1] = gr.update(value="Error: Could not create a valid prompt for the quiz. This might happen if the topic or PDF content is too short or invalid.", visible=True) | |
yield tuple(updates) | |
return | |
quiz_input_ids = None | |
try: | |
quiz_input_ids = tokenizer(input_text, return_tensors="pt").to(device) | |
print(f"DEBUG: Tokenized input_ids shape: {quiz_input_ids.input_ids.shape}. Current time: {time.time()}") | |
except Exception as e: | |
print(f"DEBUG: Error tokenizing input for quiz: {e}. Current time: {time.time()}") | |
updates[1] = gr.update(value=f"Error tokenizing input for quiz: {e}. Please ensure input text is valid.", visible=True) | |
yield tuple(updates) | |
return | |
try: | |
MAX_TOKENS_PER_QUIZ_QUESTION = 70 | |
quiz_max_new_tokens = num_quiz_questions * MAX_TOKENS_PER_QUIZ_QUESTION | |
print(f"DEBUG: Generating with max_new_tokens={quiz_max_new_tokens}. Current time: {time.time()}") | |
full_output_tokens = model.generate( | |
**quiz_input_ids, | |
max_new_tokens=quiz_max_new_tokens, | |
temperature=0.7, | |
do_sample=True, | |
pad_token_id=tokenizer.eos_token_id | |
) | |
raw_quiz_text = tokenizer.decode(full_output_tokens[0][quiz_input_ids.input_ids.shape[1]:], skip_special_tokens=True) | |
# --- Temporarily show raw quiz text for debugging --- | |
# You can set visible=False after testing | |
updates[1] = gr.update(value=raw_quiz_text, visible=True, label="Raw Quiz Output (for debugging)") | |
yield tuple(updates) # Yield this update so you can see the raw output while parsing happens | |
print(f"DEBUG: Raw quiz text generated. Length: {len(raw_quiz_text)}. First 500 chars:\n{raw_quiz_text[:500]}. Current time: {time.time()}") | |
parsed_quiz_data = parse_quiz_text(raw_quiz_text) | |
print("DEBUG: Parsed quiz data:") | |
print(json.dumps(parsed_quiz_data, indent=2)) | |
print(f"DEBUG: Number of questions parsed: {len(parsed_quiz_data)}. Current time: {time.time()}") | |
if not parsed_quiz_data: | |
print(f"DEBUG: No questions parsed from the generated text. Current time: {time.time()}") | |
updates[1] = gr.update(value="Could not generate a valid quiz with the requested format. Please try again with a different topic or fewer questions.", visible=True) | |
yield tuple(updates) | |
return | |
# Store for later use by scoring function | |
quiz_context["topic"] = effective_topic | |
quiz_context["difficulty"] = difficulty | |
quiz_context["parsed_quiz"] = parsed_quiz_data | |
# Update the initial components | |
updates[0] = gr.update(value=parsed_quiz_data) # Update gr.State with the parsed data | |
print(f"DEBUG: Value for parsed_quiz_data_state update: {len(parsed_quiz_data)} questions.") | |
updates[1] = gr.update(visible=False, value="") # Hide the raw text output after successful parsing | |
updates[2] = gr.update(visible=True) # Show the submit button | |
updates[3] = gr.update(value="", visible=False) # Clear and hide previous results | |
updates[4] = gr.update(visible=False) # Hide start new quiz button initially | |
updates[5] = gr.update(visible=True) # Make the interactive quiz column visible here | |
# Populate quiz questions and options. | |
output_idx = 6 # Start index for question/radio pairs after the initial 6 common components | |
for i, q in enumerate(parsed_quiz_data): | |
if i < MAX_QUIZ_QUESTIONS_UI: # Ensure we don't exceed placeholder count | |
question_label = f"{i+1}. {q['question_text']}" | |
choices = [f"A. {q['options']['A']}", f"B. {q['options']['B']}", f"C. {q['options']['C']}", f"D. {q['options']['D']}"] | |
# Update question Markdown and Radio button using gr.update | |
updates[output_idx] = gr.update(value=question_label, visible=True) | |
updates[output_idx + 1] = gr.update(choices=choices, value=None, label=f"Select your answer for Q{i+1}", visible=True) | |
else: | |
# This should ideally not happen if num_quiz_questions <= MAX_QUIZ_QUESTIONS_UI | |
# But for safety, ensure any remaining placeholders are hidden. | |
updates[output_idx] = gr.update(value="", visible=False) | |
updates[output_idx + 1] = gr.update(value=None, visible=False, choices=[]) | |
output_idx += 2 | |
# Hide any remaining unused placeholder quiz elements | |
for i in range(len(parsed_quiz_data), MAX_QUIZ_QUESTIONS_UI): | |
updates[output_idx] = gr.update(value="", visible=False) | |
updates[output_idx + 1] = gr.update(value=None, visible=False, choices=[]) | |
output_idx += 2 | |
print(f"DEBUG: Gradio UI updates prepared for interactive quiz. Current time: {time.time()}") | |
yield tuple(updates) # Yield the populated quiz UI | |
except Exception as e: | |
print(f"DEBUG: Error during quiz generation inference or parsing: {e}. Current time: {time.time()}") | |
updates[1] = gr.update(value=f"Error generating or parsing quiz: {e}. Please check Colab console for details.", visible=True) | |
yield tuple(updates) | |
# Make sure quiz_context is defined globally at the top of your script, e.g.: | |
# quiz_context = {} | |
def submit_quiz(*user_answers_raw): # Removed parsed_quiz_data as an input argument | |
""" | |
Scores the quiz based on user answers and provides detailed feedback. | |
*user_answers_raw will be a tuple where each element is the selected option string for a question. | |
""" | |
# --- DEBUGGING PRINTS: Access quiz_data from global quiz_context --- | |
print(f"DEBUG: submit_quiz called.") | |
# Get parsed quiz data directly from the global quiz_context | |
parsed_quiz_data = quiz_context.get("parsed_quiz", []) | |
print(f"DEBUG: Type of parsed_quiz_data (from quiz_context): {type(parsed_quiz_data)}") | |
print(f"DEBUG: Content of parsed_quiz_data (from quiz_context): {parsed_quiz_data}") | |
print(f"DEBUG: Length of parsed_quiz_data (from quiz_context): {len(parsed_quiz_data) if parsed_quiz_data is not None else 'None'}") | |
# --- END DEBUGGING PRINTS --- | |
# Prepare outputs list, matching the order of submit_quiz_outputs | |
outputs = [] | |
# First, handle the common UI elements: | |
outputs.append(gr.update(visible=False)) # submit_quiz_button (will be hidden) | |
outputs.append(gr.update(value="", visible=False)) # quiz_results_display (cleared and hidden, will be updated) | |
outputs.append(gr.update(visible=True)) # start_new_quiz_button (will be updated) - Always show this after submit | |
outputs.append(gr.update(visible=False)) # interactive_quiz_column (will be hidden) | |
# Initialize updates for all quiz question and radio button components | |
# These must be included in the outputs tuple, even if they are hidden | |
for _ in range(MAX_QUIZ_QUESTIONS_UI): | |
outputs.append(gr.update(value="", visible=False)) # quiz_question_md[i] - No 'choices' here | |
outputs.append(gr.update(value=None, visible=False, choices=[])) # quiz_options_radio[i] - important to clear choices too | |
# --- Now check if the list retrieved from quiz_context is empty --- | |
if not parsed_quiz_data: # Check if the list 'parsed_quiz_data' is empty | |
print("DEBUG: parsed_quiz_data (from quiz_context) is empty. Returning 'No quiz data found' message.") | |
outputs[1] = gr.update(value="No quiz data found. Please generate a quiz first.", visible=True) | |
outputs[2] = gr.update(visible=True) # Show start new quiz button | |
return tuple(outputs) | |
score = 0 | |
feedback_markdown = "## Quiz Results\n\n" | |
for i, question in enumerate(parsed_quiz_data): # Iterate directly over the list | |
user_answer_display = "No answer provided" | |
user_choice_letter = None | |
if i < len(user_answers_raw) and user_answers_raw[i] is not None: | |
user_answer_display = user_answers_raw[i] | |
# Extract just the letter (A, B, C, D) from user_answer_raw (e.g., "A. Option Text") | |
if isinstance(user_answers_raw[i], str) and len(user_answers_raw[i]) >= 1: | |
user_choice_letter = user_answers_raw[i][0] | |
correct_answer_letter = question['correct_answer'] | |
is_correct = (user_choice_letter == correct_answer_letter) | |
if is_correct: | |
score += 1 | |
feedback_markdown += f"β **Question {i+1}: Correct!**\n" | |
else: | |
feedback_markdown += f"β **Question {i+1}: Incorrect.**\n" | |
feedback_markdown += f"**Your Answer:** {user_answer_display}\n" | |
feedback_markdown += f"**Correct Answer:** {correct_answer_letter}. {question['options'].get(correct_answer_letter, 'Option not found')}\n" | |
feedback_markdown += f"**Explanation:** {question['explanation']}\n\n---\n\n" | |
total_questions = len(parsed_quiz_data) # Get length directly from the list | |
final_score_text = f"## Your Score: {score} out of {total_questions}\n\n" | |
feedback_markdown = final_score_text + feedback_markdown | |
# Record result to database - ensure score and total_questions are integers | |
record_quiz_result(quiz_context["topic"], quiz_context["difficulty"], int(score), int(total_questions)) | |
# Update the relevant output components in the 'outputs' list | |
outputs[0] = gr.update(visible=False) # submit_quiz_button | |
outputs[1] = gr.update(value=feedback_markdown, visible=True) # quiz_results_display | |
outputs[2] = gr.update(visible=True) # start_new_quiz_button | |
outputs[3] = gr.update(visible=False) # interactive_quiz_column (hide this after submission) | |
# All other question/radio updates are already set to hidden/cleared by the initial `outputs` list setup. | |
return tuple(outputs) | |
def start_new_quiz_ui_reset(): | |
"""Resets the quiz UI to its initial state, hiding quiz and results elements.""" | |
outputs = [ | |
gr.update(value="", visible=True), # quiz_topic_input | |
gr.update(value="3", visible=True), # num_questions_input | |
gr.update(value="medium", visible=True), # quiz_difficulty | |
gr.update(value=None, visible=True), # pdf_upload_input | |
gr.update(visible=True), # generate_quiz_button | |
gr.update(value="", visible=False), # raw_quiz_output_display (ensure hidden, now a Textbox) | |
gr.update(visible=False), # submit_quiz_button (hidden) | |
gr.update(value="", visible=False), # quiz_results_display (hidden) | |
gr.update(visible=False), # start_new_quiz_button (hidden) | |
gr.update(visible=False) # interactive_quiz_column (hide this too) | |
] | |
# Hide all question fields and reset their values | |
for i in range(MAX_QUIZ_QUESTIONS_UI): | |
outputs.extend([ | |
gr.update(value="", visible=False), # quiz_question_md[i] - No 'choices' here | |
gr.update(value=None, visible=False, choices=[]) # quiz_options_radio[i] - important to clear choices too | |
]) | |
return tuple(outputs) | |
# --- Summarizer, Refiner, Word, Translate Functions (Unchanged - ensure they use gr.update for outputs) --- | |
def summarize_text(input_text, summary_length="short"): | |
if model is None or tokenizer is None: | |
yield gr.update(value="Error: Model not loaded. Please check Colab runtime and GPU memory.") | |
return | |
if not input_text: | |
yield gr.update(value="Please provide text to summarize.") | |
return | |
if summary_length == "short": | |
prompt_instruction = "Summarize the following text concisely:" | |
max_output_tokens = 100 | |
elif summary_length == "detailed": | |
prompt_instruction = "Provide a detailed summary of the following text:" | |
max_output_tokens = 250 | |
else: # simple_explanation | |
prompt_instruction = "Explain the following text in simple terms, suitable for a beginner:" | |
max_output_tokens = 200 | |
summary_prompt = f"{prompt_instruction}\n\n{input_text}" | |
messages = [{"role": "user", "content": summary_prompt}] | |
input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
summary_input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) | |
current_summary = "" | |
try: | |
full_output_tokens = model.generate( | |
**summary_input_ids, | |
max_new_tokens=max_output_tokens, | |
temperature=0.7, | |
do_sample=True, | |
pad_token_id=tokenizer.eos_token_id | |
) | |
summary_raw = tokenizer.decode(full_output_tokens[0][summary_input_ids.input_ids.shape[1]:], skip_special_tokens=True) | |
words = summary_raw.split(" ") | |
for i in range(len(words)): | |
current_summary += words[i] + " " | |
yield gr.update(value=current_summary) # Ensure outputs are gr.update | |
time.sleep(STREAM_DELAY) | |
return | |
except Exception as e: | |
yield gr.update(value=f"Error during summarization: {e}") # Ensure outputs are gr.update | |
def refine_text(input_text): | |
if model is None or tokenizer is None: | |
yield gr.update(value="Error: Model not loaded. Please check Colab runtime and GPU memory.") | |
return | |
if not input_text: | |
yield gr.update(value="Please provide text to refine.") | |
return | |
refine_prompt = f"""Review the following text for grammar, spelling, punctuation, and clarity. Provide corrected sentences and suggest improvements for style and conciseness. Explain any major changes. | |
Original Text: | |
{input_text} | |
Refined Text: | |
""" | |
messages = [{"role": "user", "content": refine_prompt}] | |
input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
refine_input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) | |
current_refined_text = "" | |
try: | |
full_output_tokens = model.generate( | |
**refine_input_ids, | |
max_new_tokens=400, | |
temperature=0.7, | |
do_sample=True, | |
pad_token_id=tokenizer.eos_token_id | |
) | |
refined_text_raw = tokenizer.decode(full_output_tokens[0][refine_input_ids.input_ids.shape[1]:], skip_special_tokens=True) | |
words = refined_text_raw.split(" ") | |
for i in range(len(words)): | |
current_refined_text += words[i] + " " | |
yield gr.update(value=current_refined_text) # Ensure outputs are gr.update | |
time.sleep(STREAM_DELAY) | |
return | |
except Exception as e: | |
yield gr.update(value=f"Error refining text: {e}") # Ensure outputs are gr.update | |
# Function for Word Meaning & Usage | |
def get_word_meaning_and_usage(word): | |
if model is None or tokenizer is None: | |
yield gr.update(value="Error: Model not loaded.") | |
return | |
if not word: | |
yield gr.update(value="Please enter a word.") | |
return | |
prompt = f"""Provide the meaning of the word '{word}' and demonstrate its usage in two different example sentences. | |
Format your response clearly. | |
Word: {word} | |
Meaning: | |
Example 1: | |
Example 2: | |
""" | |
messages = [{"role": "user", "content": prompt}] | |
input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) | |
current_output = "" | |
try: | |
full_output_tokens = model.generate( | |
**input_ids, | |
max_new_tokens=150, | |
temperature=0.7, | |
do_sample=True, | |
pad_token_id=tokenizer.eos_token_id | |
) | |
raw_output = tokenizer.decode(full_output_tokens[0][input_ids.input_ids.shape[1]:], skip_special_tokens=True) | |
words = raw_output.split(" ") | |
for i in range(len(words)): | |
current_output += words[i] + " " | |
yield gr.update(value=current_output) # Ensure outputs are gr.update | |
time.sleep(STREAM_DELAY) | |
return | |
except Exception as e: | |
yield gr.update(value=f"Error getting word meaning: {e}") # Ensure outputs are gr.update | |
# Function for Sentence Translation | |
def translate_sentence(sentence, target_language="Hindi"): | |
if model is None or tokenizer is None: | |
yield gr.update(value="Error: Model not loaded.") | |
return | |
if not sentence: | |
yield gr.update(value="Please enter a sentence to translate.") | |
return | |
prompt = f"Translate the following English sentence into {target_language}:\n\nEnglish: {sentence}\n{target_language}:" | |
messages = [{"role": "user", "content": prompt}] | |
input_text_tokenized = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) | |
input_ids = tokenizer(input_text_tokenized, return_tensors="pt").to(device) | |
current_output = "" | |
try: | |
full_output_tokens = model.generate( | |
**input_ids, | |
max_new_tokens=100, | |
temperature=0.7, | |
do_sample=True, | |
pad_token_id=tokenizer.eos_token_id | |
) | |
raw_output = tokenizer.decode(full_output_tokens[0][input_ids.input_ids.shape[1]:], skip_special_tokens=True) | |
words = raw_output.split(" ") | |
for i in range(len(words)): | |
current_output += words[i] + " " | |
yield gr.update(value=current_output) # Ensure outputs are gr.update | |
time.sleep(STREAM_DELAY) | |
return | |
except Exception as e: | |
yield gr.update(value=f"Error translating sentence: {e}") # Ensure outputs are gr.update | |
# Initialize database | |
init_db() | |
# Define the chat functions OUTSIDE the Blocks for proper scoping | |
def user_message(user_message, history): | |
# This now yields a gr.update to set the textbox value to empty | |
# and updates the chatbot history to show the user's message immediately. | |
return gr.update(value=""), history + [[user_message, None]] | |
def bot_response(history): | |
if not history: | |
# Yield an update for the chatbot to display an error | |
yield gr.update(value=[["", "Error: Chat history is empty."]]) | |
return | |
user_message_text = history[-1][0] | |
# Generate the full response using the helper function | |
full_bot_response = "" | |
for chunk in generate_response(user_message_text, history[:-1]): | |
full_bot_response = chunk | |
history[-1][1] = full_bot_response | |
yield history | |
def clear_chat(): | |
return None, None | |
# Make sure quiz_context = {} is defined globally at the top of your script, e.g.: | |
# quiz_context = {} | |
# --- Gradio UI --- | |
with gr.Blocks(title="Edu-Tutor AI", theme=gr.themes.Soft()) as demo: | |
gr.Markdown("# <center>β¨ Edu-Tutor AI: Your Personal Learning Assistant β¨</center>") | |
gr.Markdown("Welcome! I'm Edu-Tutor, powered by IBM Granite. I can help you summarize text, refine your writing, understand new words, translate sentences, and even generate quizzes!") | |
with gr.Tab("Chat with Edu-Tutor"): | |
chatbot = gr.Chatbot(label="Edu-Tutor Chat", bubble_full_width=False, layout="panel", render_markdown=True, height=500) | |
msg = gr.Textbox(label="Your message", placeholder="Ask me anything...", lines=2) | |
with gr.Row(): | |
submit_button = gr.Button("Send Message") | |
clear_button = gr.ClearButton([msg, chatbot]) | |
submit_button.click(user_message, [msg, chatbot], [msg, chatbot], queue=False).then( | |
bot_response, chatbot, chatbot | |
) | |
msg.submit(user_message, [msg, chatbot], [msg, chatbot], queue=False).then( | |
bot_response, chatbot, chatbot | |
) | |
clear_button.click(clear_chat, [], [msg, chatbot]) | |
with gr.Tab("Text Summarizer & Explainer"): | |
with gr.Row(): | |
summary_input = gr.Textbox(label="Enter text to summarize/explain", lines=10, placeholder="Paste your text here...") | |
with gr.Row(): | |
summary_type = gr.Radio(["short", "detailed", "simple_explanation"], label="Summary Type", value="short") | |
with gr.Row(): | |
summarize_button = gr.Button("Generate Summary/Explanation") | |
summary_output = gr.Markdown(label="Output") | |
summarize_button.click(summarize_text, inputs=[summary_input, summary_type], outputs=summary_output) | |
with gr.Tab("Text Refiner"): | |
refine_input = gr.Textbox(label="Enter text to refine", lines=10, placeholder="Paste your text here...") | |
refine_button = gr.Button("Refine Text") | |
refine_output = gr.Markdown(label="Refined Text") | |
refine_button.click(refine_text, inputs=refine_input, outputs=refine_output) | |
with gr.Tab("Word Meaning & Usage"): | |
word_input = gr.Textbox(label="Enter a word", placeholder="e.g., 'ubiquitous'") | |
word_button = gr.Button("Get Meaning & Usage") | |
word_output = gr.Markdown(label="Meaning and Examples") | |
word_button.click(get_word_meaning_and_usage, inputs=word_input, outputs=word_output) | |
with gr.Tab("Sentence Translator"): | |
translate_input = gr.Textbox(label="Enter an English sentence", placeholder="e.g., 'Hello, how are you?'") | |
target_language_dropdown = gr.Dropdown(["Hindi", "Spanish", "French", "German", "Japanese", "Tamil"], label="Translate to", value="Hindi") | |
translate_button = gr.Button("Translate Sentence") | |
translate_output = gr.Markdown(label="Translated Sentence") | |
translate_button.click(translate_sentence, inputs=[translate_input, target_language_dropdown], outputs=translate_output) | |
with gr.Tab("Generate Quiz"): | |
gr.Markdown("## Generate a Multiple-Choice Quiz") | |
gr.Markdown("Provide a topic or upload a PDF, and I'll generate a quiz for you.") | |
with gr.Row(): | |
quiz_topic_input = gr.Textbox(label="Quiz Topic (e.g., 'Photosynthesis', 'World War II')", lines=1, placeholder="Enter a topic or upload a PDF") | |
num_questions_input = gr.Slider(minimum=1, maximum=MAX_QUIZ_QUESTIONS_UI, value=3, step=1, label="Number of Questions") | |
quiz_difficulty = gr.Radio(["easy", "medium", "hard"], label="Difficulty", value="medium") | |
pdf_upload_input = gr.File(type="filepath", label="Upload PDF (Optional - overrides topic if provided)", file_types=[".pdf"]) | |
generate_quiz_button = gr.Button("Generate Quiz") | |
# This Textbox is for debugging raw output. It will be hidden in production. | |
raw_quiz_output_display = gr.Textbox(label="Raw Quiz Output (for debugging)", lines=10, visible=False) | |
# State to store the parsed quiz data (This component is still needed, even if not directly an input to submit_quiz) | |
parsed_quiz_data_state = gr.State(value=[]) | |
# Column to hold the interactive quiz questions | |
with gr.Column(visible=False) as interactive_quiz_column: | |
quiz_question_md = [] | |
quiz_options_radio = [] | |
for i in range(MAX_QUIZ_QUESTIONS_UI): | |
quiz_question_md.append(gr.Markdown(value="", visible=False)) | |
quiz_options_radio.append(gr.Radio(choices=[], value=None, label="", visible=False)) | |
submit_quiz_button = gr.Button("Submit Quiz", visible=False) | |
quiz_results_display = gr.Markdown(value="", visible=False, label="Quiz Results") | |
start_new_quiz_button = gr.Button("Start a New Quiz", visible=False) | |
# Define the outputs for generate_quiz_for_display | |
# Order MUST match the `updates` list in the function. | |
generate_quiz_outputs = [ | |
parsed_quiz_data_state, # State for quiz data | |
raw_quiz_output_display, # Raw output for debugging | |
submit_quiz_button, # Submit button visibility | |
quiz_results_display, # Quiz results display (clear/hide) | |
start_new_quiz_button, # Start new quiz button (hide) | |
interactive_quiz_column # Interactive quiz column visibility | |
] | |
# Dynamically add all question/radio components to outputs | |
for i in range(MAX_QUIZ_QUESTIONS_UI): | |
generate_quiz_outputs.append(quiz_question_md[i]) | |
generate_quiz_outputs.append(quiz_options_radio[i]) | |
generate_quiz_button.click( | |
generate_quiz_for_display, | |
inputs=[quiz_topic_input, num_questions_input, quiz_difficulty, pdf_upload_input], | |
outputs=generate_quiz_outputs | |
) | |
# Define the outputs for submit_quiz | |
# Order MUST match the `outputs` list in the function. | |
submit_quiz_outputs = [ | |
submit_quiz_button, | |
quiz_results_display, | |
start_new_quiz_button, | |
interactive_quiz_column # Hide the quiz column after submission | |
] | |
# Dynamically add all question/radio components to outputs for hiding/clearing | |
for i in range(MAX_QUIZ_QUESTIONS_UI): | |
submit_quiz_outputs.append(quiz_question_md[i]) | |
submit_quiz_outputs.append(quiz_options_radio[i]) | |
submit_quiz_button.click( | |
submit_quiz, | |
inputs=list(quiz_options_radio), # Corrected: parsed_quiz_data_state removed | |
outputs=submit_quiz_outputs | |
) | |
# Start New Quiz Button | |
start_new_quiz_button.click( | |
start_new_quiz_ui_reset, | |
inputs=[], | |
outputs=[quiz_topic_input, num_questions_input, quiz_difficulty, pdf_upload_input, | |
generate_quiz_button, raw_quiz_output_display, submit_quiz_button, | |
quiz_results_display, start_new_quiz_button, interactive_quiz_column] + | |
[item for sublist in zip(quiz_question_md, quiz_options_radio) for item in sublist] | |
) | |
with gr.Tab("Performance Dashboard"): | |
gr.Markdown("## Your Learning Performance") | |
gr.Markdown("Review your quiz history and identify weak areas.") | |
with gr.Column(): | |
refresh_performance_button = gr.Button("Refresh Performance Data") | |
weak_areas_display = gr.Markdown("### Weak Areas:\n\nNo data yet.") | |
suggested_steps_display = gr.Markdown("### Suggested Next Steps:\n\nTake some quizzes to get personalized suggestions!") | |
quiz_history_dataframe = gr.DataFrame( | |
headers=["Timestamp", "Topic", "Difficulty", "Score", "Total Questions"], | |
row_count=5, # Show at least 5 rows | |
col_count=(5, "fixed"), # 5 fixed columns | |
wrap=True, | |
label="Quiz History", | |
visible=True | |
) | |
# Load initial data when the tab is selected (or when refreshed) | |
demo.load( | |
get_performance_insights, | |
inputs=[], | |
outputs=[quiz_history_dataframe, weak_areas_display, suggested_steps_display] | |
) | |
# Refresh button click | |
refresh_performance_button.click( | |
get_performance_insights, | |
inputs=[], | |
outputs=[quiz_history_dataframe, weak_areas_display, suggested_steps_display] | |
) | |
demo.launch(debug=True, share=True) |