Coverage for app \ services \ personalize_quiz_service.py: 20%

49 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 20:58 -0400

1import anthropic 

2import json 

3import os 

4from typing import Dict, Any, Optional 

5 

6from anthropic.types import MessageParam 

7from app.settings import ANTHROPIC_API_KEY 

8 

9# Claude parameters 

10model = os.getenv("ANTHROPIC_MODEL", "claude-haiku-4-5-20251001") 

11MAX_TOKENS = 1024 

12#creates the client object that connects to Claude API 

13client = anthropic.Anthropic() 

14 

15def generate_persona_variants(questions: Dict[str, Any], best_question_text: Optional[str] = None 

16) -> Dict[str, Any]: 

17 """ 

18 Take AI questions {type: {q, a, ...}} and rephrase them into 3 child-friendly 

19 personas: pig (friend), bunny (older sibling), alligator (parent/grandparent). 

20 Returns {"success": bool, "variants": {bunny: {type: {q, a}}, ...}}. 

21 """ 

22 

23 if not questions or not isinstance(questions, dict): 

24 return {"success": False, "message": "No questions provided"} 

25 

26 questions_text = "\n".join( 

27 f"- Type: {qtype.upper()}, Question: {data.get('q', '')}, Answer: {data.get('a', '')}" 

28 for qtype, data in questions.items() 

29 if isinstance(data, dict) and data.get("q") 

30 ) 

31 if not questions_text.strip(): 

32 return {"success": False, "message": "No valid questions to rephrase"} 

33 

34 # Check for Claude API key 

35 if not ANTHROPIC_API_KEY: 

36 return {"success": False, "message":f"Anthropic key missing."} 

37 

38 system_message = ( 

39 "You are a safe, child-focused educational assistant. " 

40 "The content is a children's video. " 

41 "Follow all safety policies and avoid disallowed content. " 

42 "Provide age-appropriate, neutral, factual responses only." 

43 ) 

44 

45 # Load prompt 

46 prompt_text = (""" 

47 You are helping rephrase comprehension questions for young children 

48 into different personas. Keep the meaning and correct answers exactly 

49 the same. Only change the wording and tone of the QUESTIONS.\n\n 

50 PERSONAS:\n\n 

51 PIGGY:  

52 A friendly, encouraging personality meant to emulate a childhood friend. 

53 Uses easy-to-understand language for children, but does not 'dumb down' the 

54 question. Instead, uses enthusiastic words and expressions such as 'Ooh!'  

55 or 'Wow!' to show mutual investment in the content.\n 

56 BUNNY: 

57 A cool, inspiring personality meant to act as an older sibling. 

58 Uses comprehensible wording for children, but might add slightly advanced  

59 vocabulary to keep the child sharp. Uses acknowledgement as a motivator by 

60 using phrases that indicate belief in the child.\n 

61 ALLIGATOR: 

62 A gentle, endearing personality designed for sensitive children and modeled after  

63 a parent or grandparent. Avoids using pet names, however. Uses vocabulary that caters towards children with  

64 weaker vocabularies. Uses pride in the child to invite them to answer questions.\n 

65 """ 

66 + 

67 f"Original questions:\n{questions_text}\n\n" 

68 + 

69 """ 

70 Return ONLY a valid JSON object with this exact structure:\n 

71 {\n 

72 "pig": {"TYPE": {"q": "rephrased question", "a": "same original answer"}, ...},\n 

73 "bunny": {"TYPE": {"q": "rephrased question", "a": "same original answer"}, ...},\n 

74 "alligator": {"TYPE": {"q": "rephrased question", "a": "same original answer"}, ...},\n 

75 }\n 

76 Use lowercase keys for question types. Return only the JSON, no explanation. 

77 """ 

78 ) 

79 

80 # Build parts 

81 parts = [ 

82 { 

83 "role":"user", 

84 "content":[ 

85 {"type":"text", "text": prompt_text.strip()} 

86 ] 

87 } 

88 ] 

89 

90 # Get response 

91 try: 

92 resp = client.messages.create( 

93 model=model, 

94 max_tokens=3000, 

95 system=system_message, 

96 messages=parts 

97 ) 

98 

99 if resp.content: 

100 text = resp.content[0].text 

101 else: 

102 return {"success": False, "message": "Empty response"} 

103 

104 # Strip markdown fences Claude may wrap around JSON 

105 cleaned = text.strip() 

106 if cleaned.startswith("```"): 

107 cleaned = cleaned[3:].lstrip() 

108 if cleaned.lower().startswith("json"): 

109 cleaned = cleaned[4:].lstrip() 

110 if cleaned.endswith("```"): 

111 cleaned = cleaned[:-3].rstrip() 

112 

113 # Parse the JSON string into a dict 

114 try: 

115 parsed = json.loads(cleaned) 

116 except json.JSONDecodeError: 

117 return {"success": False, "message": f"Invalid JSON from Claude: {cleaned[:300]}"} 

118 

119 

120 # Mark the best question in each persona variant 

121 if best_question_text: 

122 for persona_key, persona_qs in parsed.items(): 

123 if not isinstance(persona_qs, dict): 

124 continue 

125 for qtype, data in persona_qs.items(): 

126 if not isinstance(data, dict): 

127 continue 

128 orig = questions.get(qtype) 

129 if isinstance(orig, dict) and orig.get("q") == best_question_text: 

130 data["is_best"] = True 

131 

132 return {"success":True, "variants":parsed} 

133 except Exception as exc: 

134 return {"success": False, "message":f"Persona generation failed: {exc}"} 

135 #  

136