Coverage for app/services/quiz_service.py: 16%

43 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-05-02 02:49 +0000

1import json 

2from app.controllers.database import client 

3from app.controllers.ai import gemini_client 

4from app.models.errors import DatabaseError 

5import re 

6 

7def build_prompt_from_suggestions(user_section_id: str) -> str: 

8 try: 

9 result = client.table("suggestions").select("prompt, suggestion_array")\ 

10 .eq("user_section_id", user_section_id).execute() 

11 

12 if not result.data: 

13 raise ValueError(f"No suggestions found for section {user_section_id}") 

14 

15 prompt_parts = [ 

16 f"Prompt: {item['prompt']}\nAnswer: {item['suggestion_array']}" for item in result.data 

17 ] 

18 

19 full_prompt = "\n\n".join(prompt_parts) 

20 

21 return f"""Generate a quiz based on the following context.  

22 Each question should be multiple choice (a, b, c, d) with only one correct answer. 

23 Include the correct answer index and a brief explanation. 

24 

25 Context: 

26 {full_prompt} 

27 """ 

28 except Exception as e: 

29 raise DatabaseError(f"Failed to build prompt: {str(e)}") 

30 

31def generate_quiz_from_prompt(prompt: str): 

32 full_prompt = f"""Generate a programming quiz with EXACTLY these specifications: 

33 

34 1. Create 10 multiple-choice questions about JavaScript functions 

35 2. Only use CORRECT code examples from this context: 

36 {prompt} 

37 

38 3. For each question provide: 

39 - A clear question 

40 - 4 answer choices (1 correct, 3 plausible incorrect) 

41 - answer_index (0-3) 

42 - A 1-sentence explanation 

43 

44 4. Format the response as VALID JSON array with this EXACT structure: 

45 [ 

46 { 

47 "question": "text", 

48 "choices": ["a", "b", "c", "d"], 

49 "answer_index": 0, 

50 "explanation": "text" 

51 } , 

52 // 9 more questions 

53 ] 

54 

55 5. Important rules: 

56 - Only show proper implementations in questions 

57 - Test knowledge of correct patterns 

58 - No markdown or backticks 

59 - Ensure JSON is valid (no trailing commas) 

60 

61 Example of ONE question (you must provide 10): 

62 { 

63 "question": "What is the correct implementation to square a number?", 

64 "choices": [ 

65 "function square(n) { return n + n; } ", 

66 "function square(n) { return n * n; } ",  

67 "function square(n) { return Math.pow(n, 3); } ", 

68 "function square(n) { return n / n; } " 

69 ], 

70 "answer_index": 1, 

71 "explanation": "Squaring requires multiplying the number by itself" 

72 } 

73 """ 

74 

75 response = gemini_client.chat_session.send_message(full_prompt) 

76 

77 try: 

78 try: 

79 quiz = json.loads(response.text.strip()) 

80 except json.JSONDecodeError as e: 

81 # Clean the response by removing trailing commas 

82 cleaned_text = re.sub(r',\s*([}\]])', r'\1', response.text) 

83 try: 

84 quiz = json.loads(cleaned_text.strip()) 

85 except json.JSONDecodeError: 

86 # Fallback: extract the array from a larger response 

87 match = re.search(r'\[\s*{.*?}\s*\]', cleaned_text, re.DOTALL) 

88 if not match: 

89 raise ValueError("No valid JSON array found in Gemini response.") 

90 quiz = json.loads(match.group(0)) 

91 

92 if not isinstance(quiz, list) or len(quiz) != 10: 

93 raise ValueError(f"Expected a list of 10 quiz questions, got {len(quiz)}") 

94 

95 # Validate structure of each question 

96 required_keys = {"question", "choices", "answer_index", "explanation"} 

97 for i, q in enumerate(quiz): 

98 if not all(k in q for k in required_keys): 

99 raise ValueError(f"Question {i+1} is missing required fields") 

100 if not isinstance(q["choices"], list) or len(q["choices"]) != 4: 

101 raise ValueError(f"Question {i+1} should have exactly 4 choices") 

102 if not isinstance(q["answer_index"], int) or not 0 <= q["answer_index"] < 4: 

103 raise ValueError(f"Question {i+1} has invalid answer_index") 

104 

105 return quiz 

106 except Exception as e: 

107 raise ValueError(f"Failed to parse Gemini quiz response: {e}") 

108