# import streamlit as st # import json # import re # import random # from typing import Dict, List, Optional, Set # from huggingface_hub import hf_hub_download # from llama_cpp import Llama # @st.cache_resource # def load_model(): # model_path = hf_hub_download( # repo_id="Nitish035/gguf_4", # filename="unsloth.Q4_K_M.gguf", # local_dir=".", # ) # model = Llama( # model_path=model_path, # n_ctx=4096, # n_gpu_layers=35, # verbose=True, # Set to True temporarily for debugging # ) # return model # def get_difficulty(dok): # difficulty_map = { # 1: "Easy", # 2: "Medium", # 3: "Hard", # 4: "Very Hard" # } # return difficulty_map.get(dok, "Medium") # def generate_response(prompt_dict, max_tokens=3024): # try: # st.info("Generating response with model...") # dok_descriptions = { # 1: "Basic recall of math facts, definitions, or simple calculations", # 2: "Application of math concepts requiring 1-2 step procedures", # 3: "Multi-step math problems requiring analysis and justification", # 4: "Complex math problems needing synthesis and creative approaches" # } # difficulty_levels = { # "Easy": "Multi-step problems requiring reasoning and understanding of concepts - easy; Straightforward problems with obvious approaches.", # "Medium": "Multi-step problems requiring moderate reasoning and understanding of concepts - medium; Requires careful analysis but standard methods apply.", # "Hard": "Complex multi-step problems with multiple variables and operations - hard; Demands innovative thinking and multiple concepts.", # "Very Hard": "Advanced problems requiring systems of equations, conditional logic, and optimization - very hard; Requires advanced reasoning, optimization strategies, and integration of multiple topics." # } # contexts = [ # "a school fundraiser", # "a community bake sale", # "a sports team's snack stand", # "a charity event", # "a classroom project", # "real world", # "", # "", # "man", # "women" # ] # operations = [ # "addition", # "multiplication", # "division", # "subtraction", # "use numbers", # "", # "" # ] # prompt = f"""<|im_start|>user # Generate a {prompt_dict['Difficulty']} difficulty math multiple-choice question with options and correct answer with these specifications: # * Grade Level: {prompt_dict['Grade']} # * Topic: {prompt_dict['Topic']} (align with appropriate CCSS standard) # * Depth of Knowledge (DOK): Level {prompt_dict['DOK']} ({dok_descriptions[prompt_dict['DOK']]}) # * Difficulty: {prompt_dict['Difficulty']} ({difficulty_levels[prompt_dict['Difficulty']]}) # * Context: {random.choice(contexts)} # * Math Operations: {random.choice(operations)} # 1. Create a unique word problem based on the context and operations # 2. Design a question that matches DOK level {prompt_dict['DOK']} # 3. Create four plausible options with one clearly correct answer # 4. Format as a clean multiple-choice question # # Requirements: # 1. The question must be unique and different from previous questions # 2. Make sure the final answer computed in the explanation is inserted into one of the 4 options # 3. The `correct_answer` key must match the option letter that holds the correct answer # 4. Options should reflect common student misconceptions # 5. Format the response as a JSON object with these keys: 'question', 'options', 'correct_answer', 'explanation' # IMPORTANT: Only return a valid JSON object with no additional text or formatting outside the JSON structure. # <|im_end|> # <|im_start|>assistant # """ # model = load_model() # response = model.create_completion( # prompt=prompt, # max_tokens=max_tokens, # temperature=0.1, # top_p=0.9, # echo=False # ) # st.success("Response generated!") # return response['choices'][0]['text'] # except Exception as e: # st.error(f"Error generating response: {str(e)}") # return "" # def try_parse_json(response_text): # try: # # First try to find a JSON object using regex # json_pattern = r'(\{[\s\S]*\})' # match = re.search(json_pattern, response_text) # if match: # potential_json = match.group(1) # return json.loads(potential_json) # # If no match using regex, try direct JSON parse # return json.loads(response_text) # except json.JSONDecodeError as e: # st.error(f"Failed to parse JSON response: {e}") # st.text("Raw response:") # st.text(response_text) # return None # def main(): # st.title("DOK-Based Math Question Generator") # st.write("Generate math questions based on a Depth of Knowledge level") # col1, col2 = st.columns(2) # with col1: # dok_level = st.selectbox("Select DOK Level:", [1, 2, 3, 4]) # topic = st.selectbox("Select Topic:", [ # "Number System", # "Statistics and Probability", # "Geometry", # "Expressions and Equations", # "Functions", # "Ratios and Proportional" # ]) # with col2: # no_of_questions = st.slider("Number of Questions:", 1, 2) # grade_level = st.selectbox("Select Grade Level:", [6, 7, 8]) # if st.button("Generate Questions"): # with st.spinner("Generating questions..."): # difficulty = get_difficulty(dok_level) # all_questions = [] # generated_questions = set() # for i in range(no_of_questions): # st.info(f"Generating question {i+1} of {no_of_questions}...") # attempts = 0 # while attempts < 3: # prompt_dict = { # "Grade": str(grade_level), # "Topic": topic, # "DOK": dok_level, # "Difficulty": difficulty # } # response_text = generate_response(prompt_dict) # parsed_json = try_parse_json(response_text) # if parsed_json and parsed_json.get('question') and parsed_json['question'] not in generated_questions: # generated_questions.add(parsed_json['question']) # all_questions.append(parsed_json) # break # attempts += 1 # st.warning(f"Attempt {attempts}: Failed to generate unique question or parse response. Retrying...") # st.subheader("Generated Questions:") # if all_questions: # for i, question in enumerate(all_questions): # with st.expander(f"Question {i+1}"): # st.write(f"**Question:** {question.get('question', 'No question found')}") # if 'options' in question: # st.write("**Options:**") # options = question['options'] # if isinstance(options, dict): # for key, value in options.items(): # st.write(f"{key}: {value}") # else: # st.write(options) # st.write(f"**Correct Answer:** {question.get('correct_answer', 'No answer found')}") # st.write(f"**Explanation:** {question.get('explanation', 'No explanation found')}") # st.download_button( # label="Download JSON", # data=json.dumps(all_questions, indent=2), # file_name="math_questions.json", # mime="application/json", # ) # else: # st.error("Failed to generate unique questions. Please try again.") # if __name__ == "__main__": # main() import streamlit as st import json import re import random import os import time from typing import Dict, List, Optional, Set from huggingface_hub import hf_hub_download from llama_cpp import Llama # Use /tmp directory which is writable in the Docker container MODEL_CACHE_DIR = "/tmp/model_cache" os.makedirs(MODEL_CACHE_DIR, exist_ok=True) # Global variable to track if model loading was attempted model_load_attempted = False @st.cache_resource def load_model(): """Load the model with proper error handling""" try: st.info("Loading model... This may take a few minutes on first run.") model_path = hf_hub_download( repo_id="Nitish035/qwen_gguf_adapter", filename="unsloth.Q4_K_M.gguf", local_dir=MODEL_CACHE_DIR, cache_dir=MODEL_CACHE_DIR, ) # Reduce n_gpu_layers if on CPU-only environment try: # First attempt with GPU layers model = Llama( model_path=model_path, n_ctx=2048, # Reduced context size for better performance n_gpu_layers=35, verbose=False, ) except Exception as gpu_error: st.warning(f"GPU acceleration failed: {str(gpu_error)}. Falling back to CPU.") # Fall back to CPU-only model = Llama( model_path=model_path, n_ctx=2048, n_gpu_layers=0, # CPU only verbose=False, ) st.success("Model loaded successfully!") return model except Exception as e: st.error(f"Error loading model: {str(e)}") return None def get_difficulty(dok): difficulty_map = { 1: "Easy", 2: "Medium", 3: "Hard", 4: "Very Hard" } return difficulty_map.get(dok, "Medium") def generate_response(prompt_dict, max_tokens=2048): """Generate response with timing and improved error handling""" try: # Use global to check if model loading was attempted global model_load_attempted if not model_load_attempted: with st.spinner("Loading the model (first-time only)..."): model = load_model() model_load_attempted = True else: model = load_model() # This will use cached version if available if model is None: st.error("Model failed to load. Please check logs or try again.") return "" dok_descriptions = { 1: "Basic recall of math facts, definitions, or simple calculations", 2: "Application of math concepts requiring 1-2 step procedures", 3: "Multi-step math problems requiring analysis and justification", 4: "Complex math problems needing synthesis and creative approaches" } difficulty_levels = { "Easy": "Multi-step problems requiring reasoning and understanding of concepts - easy", "Medium": "Multi-step problems requiring moderate reasoning - medium", "Hard": "Complex multi-step problems with multiple variables - hard", "Very Hard": "Advanced problems requiring systems of equations - very hard" } contexts = [ "a school fundraiser", "a community event", "a sports competition", "a classroom project", "real world situation" ] operations = [ "addition", "multiplication", "division", "subtraction", "basic calculation" ] # Simplified prompt for faster generation prompt = f"""<|im_start|>user Generate a {prompt_dict['Difficulty']} math multiple-choice question: * Grade: {prompt_dict['Grade']} * Topic: {prompt_dict['Topic']} * DOK: Level {prompt_dict['DOK']} ({dok_descriptions[prompt_dict['DOK']]}) * Context: {random.choice(contexts)} * Operations: {random.choice(operations)} Create a word problem with four options (A-D), one correct answer. Format as JSON with keys: 'question', 'options', 'correct_answer', 'explanation'. <|im_end|> <|im_start|>assistant """ # Time the generation start_time = time.time() response = model.create_completion( prompt=prompt, max_tokens=max_tokens, temperature=0.2, # Lower temperature for more deterministic output top_p=0.9, stop=["<|im_end|>", "<|im_start|>"], # Add explicit stop tokens echo=False ) end_time = time.time() generation_time = end_time - start_time result_text = response['choices'][0]['text'] st.session_state["last_generation_time"] = generation_time return result_text except Exception as e: st.error(f"Error generating response: {str(e)}") st.session_state["last_generation_time"] = 0 return "" def try_parse_json(response_text): """More robust JSON parsing""" try: # First try to clean up the response for better parsing # Remove any trailing commas in JSON objects (common error) cleaned_text = re.sub(r',\s*}', '}', response_text) cleaned_text = re.sub(r',\s*]', ']', cleaned_text) # Find JSON pattern json_pattern = r'(\{[\s\S]*\})' match = re.search(json_pattern, cleaned_text) if match: potential_json = match.group(1) return json.loads(potential_json) # If no match using regex, try direct JSON parse return json.loads(cleaned_text) except json.JSONDecodeError as e: st.error(f"Failed to parse JSON response. Error: {e}") return None def main(): st.title("DOK-Based Math Question Generator") st.write("Generate math questions based on a Depth of Knowledge level") # Initialize session state for results if "results" not in st.session_state: st.session_state["results"] = [] if "last_generation_time" not in st.session_state: st.session_state["last_generation_time"] = 0 col1, col2 = st.columns(2) with col1: dok_level = st.selectbox("Select DOK Level:", [1, 2, 3, 4], index=0) topic = st.selectbox("Select Topic:", [ "Number System", "Statistics and Probability", "Geometry", "Expressions and Equations", "Functions", "Ratios and Proportional" ]) with col2: no_of_questions = st.slider("Number of Questions:", 1, 2, value=1) grade_level = st.selectbox("Select Grade Level:", [6, 7, 8]) if st.button("Generate Questions"): # Clear previous results st.session_state["results"] = [] all_questions = [] generated_questions = set() time_stats = [] progress_bar = st.progress(0) question_status = st.empty() for i in range(no_of_questions): question_status.write(f"Generating question {i+1} of {no_of_questions}...") attempts = 0 success = False while attempts < 3 and not success: prompt_dict = { "Grade": str(grade_level), "Topic": topic, "DOK": dok_level, "Difficulty": get_difficulty(dok_level) } response_text = generate_response(prompt_dict) parsed_json = try_parse_json(response_text) if parsed_json and parsed_json.get('question') and parsed_json['question'] not in generated_questions: generated_questions.add(parsed_json['question']) all_questions.append(parsed_json) # Store generation time generation_time = st.session_state.get("last_generation_time", 0) time_stats.append({ "question_number": i+1, "generation_time": round(generation_time, 2), "attempts": attempts+1 }) success = True else: attempts += 1 st.warning(f"Attempt {attempts}: Failed to generate valid question. Retrying...") # Update progress progress_bar.progress((i+1)/no_of_questions) # Clear status question_status.empty() # Store results in session state st.session_state["results"] = all_questions st.session_state["time_stats"] = time_stats # Display results display_results(all_questions, time_stats) def display_results(questions, time_stats): """Display results and time statistics""" if not questions: st.error("Failed to generate any questions. Please try again.") return st.subheader("Generated Questions:") # Display each question in an expander for i, question in enumerate(questions): with st.expander(f"Question {i+1}", expanded=True): st.markdown(f"**Question:** {question.get('question', 'No question found')}") if 'options' in question: st.markdown("**Options:**") options = question['options'] if isinstance(options, dict): for key, value in options.items(): st.markdown(f"**{key}:** {value}") else: st.write(options) st.markdown(f"**Correct Answer:** {question.get('correct_answer', 'No answer found')}") st.markdown(f"**Explanation:** {question.get('explanation', 'No explanation found')}") # Display timing information st.subheader("Generation Time Statistics:") for stat in time_stats: st.write(f"Question {stat['question_number']}: Generated in {stat['generation_time']} seconds (after {stat['attempts']} attempt(s))") # Calculate and display average generation time if time_stats: avg_time = sum(stat['generation_time'] for stat in time_stats) / len(time_stats) st.write(f"Average generation time: {round(avg_time, 2)} seconds per question") # Add download button st.download_button( label="Download Questions as JSON", data=json.dumps(questions, indent=2), file_name="math_questions.json", mime="application/json", ) if __name__ == "__main__": main()