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
« 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
6from anthropic.types import MessageParam
7from app.settings import ANTHROPIC_API_KEY
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()
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 """
23 if not questions or not isinstance(questions, dict):
24 return {"success": False, "message": "No questions provided"}
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"}
34 # Check for Claude API key
35 if not ANTHROPIC_API_KEY:
36 return {"success": False, "message":f"Anthropic key missing."}
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 )
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 )
80 # Build parts
81 parts = [
82 {
83 "role":"user",
84 "content":[
85 {"type":"text", "text": prompt_text.strip()}
86 ]
87 }
88 ]
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 )
99 if resp.content:
100 text = resp.content[0].text
101 else:
102 return {"success": False, "message": "Empty response"}
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()
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]}"}
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
132 return {"success":True, "variants":parsed}
133 except Exception as exc:
134 return {"success": False, "message":f"Persona generation failed: {exc}"}
135 #