1 line
35 KiB
JSON
1 line
35 KiB
JSON
{"repo": "adrianasiam19/smarttrack", "files": {"smarttrack-backend/app/assessment/deepseek_service.py": {"8f17344d0076ad888f76081209d384cdb0c24078": {"t": 1785745459.3443394, "n": 16725, "h": "04fe2cde104735ee", "c": "\"\"\"\ndeepseek_service.py — DeepSeek AI integration for dynamic challenge generation\n\nThis module handles all DeepSeek API interactions for generating\nSHS-standard challenge questions dynamically using the OpenAI-compatible API.\n\"\"\"\nimport json\nimport logging\nimport re\nfrom typing import Optional, Dict, Any\n\nimport httpx\n\nfrom app.config import settings\n\n# Configure logging\nlogger = logging.getLogger(__name__)\n\n# DeepSeek API configuration\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_CHAT_URL = f\"{DEEPSEEK_BASE_URL}/v1/chat/completions\"\n\n\nclass DeepSeekChallengeGenerator:\n \"\"\"Service for generating SHS challenge questions using DeepSeek AI.\"\"\"\n\n # Supported categories and programmes\n VALID_CATEGORIES = [\n \"Logic\",\n \"Quantitative Thinking\",\n \"Scientific Thinking\",\n \"Verbal Reasoning\",\n \"Critical Thinking\",\n ]\n\n VALID_PROGRAMMES = [\n \"General Science\",\n \"General Arts\",\n ]\n\n VALID_DIFFICULTY = [\n \"Beginner\",\n \"Intermediate\",\n \"Advanced\",\n ]\n\n # System prompt for consistent, high-quality question generation\n SYSTEM_PROMPT = \"\"\"You are an expert SHS (Senior High School) educational assessment creator\nspecializing in Ghanaian curriculum standards.\n\nYour task is to generate engaging, intellectually challenging questions that:\n- Follow WAEC/SHS examination standards\n- Are suitable for Ghanaian SHS students\n- Focus on reasoning and deeper understanding, not memorization\n- Are interactive and engaging\n- Provide valuable learning through explanation\n\nImportant guidelines:\n1. Create questions that develop critical thinking skills\n2. Avoid simple factual recall questions — always test understanding, application, or analysis\n3. Include distractors that reveal common misconceptions\n4. Ensure explanations help students understand WHY the answer is correct\n5. Make questions culturally relevant to Ghana context where appropriate\n6. Use clear, unambiguous language\n7. Keep explanations concise — maximum 3 short sentences\n\nSCIENCE DOMAIN INSTRUCTIONS (when generating Scientific Thinking questions):\nRotate across ALL branches of science for diversity. Never repeat the same scientific concept twice in a row.\n- Physics: mechanics, thermodynamics, optics, waves, electricity, magnetism, nuclear physics, quantum basics\n- Chemistry: atomic structure, bonding, reactions, stoichiometry, organic chemistry, acids/bases, electrochemistry\n- Biology: cell biology, genetics, evolution, ecology, human physiology, plant biology, microbiology\n- Earth Science: geology, meteorology, oceanography, natural disasters, rock cycle, plate tectonics\n- Astronomy: solar system, stellar evolution, cosmology basics, planetary science, space exploration\n- Environmental Science: ecosystems, pollution, climate change, renewable energy, conservation\n- Health Science: nutrition, disease, public health, pharmacology basics\n\nEnsure each science question tests a real scientific principle with accurate data/facts. Include applied scenarios like experiments, observations, or real-world phenomena.\"\"\"\n\n def __init__(self):\n \"\"\"Initialize DeepSeek service.\"\"\"\n if not settings.DEEPSEEK_API_KEY:\n logger.warning(\"DEEPSEEK_API_KEY not configured. DeepSeek integration will not work.\")\n\n def validate_parameters(\n self,\n category: str,\n difficulty: str,\n programme: str,\n ) -> tuple[bool, Optional[str]]:\n \"\"\"\n Validate request parameters.\n\n Returns:\n Tuple of (is_valid, error_message)\n \"\"\"\n if category not in self.VALID_CATEGORIES:\n return False, f\"Invalid category. Valid options: {', '.join(self.VALID_CATEGORIES)}\"\n\n if difficulty not in self.VALID_DIFFICULTY:\n return False, f\"Invalid difficulty. Valid options: {', '.join(self.VALID_DIFFICULTY)}\"\n\n if programme not in self.VALID_PROGRAMMES:\n return False, f\"Invalid programme. Valid options: {', '.join(self.VALID_PROGRAMMES)}\"\n\n return True, None\n\n def _create_prompt(\n self,\n category: str,\n difficulty: str,\n programme: str,\n concept: Optional[str] = None,\n ) -> str:\n \"\"\"\n Create the prompt for DeepSeek to generate a question.\n\n Args:\n category: Question category\n difficulty: Difficulty level\n programme: SHS programme (General Science or General Arts)\n concept: Optional specific concept to focus on\n\n Returns:\n Formatted prompt for DeepSeek\n \"\"\"\n concept_text = f\"\\nFocus on this concept: {concept}\" if concept else \"\"\n\n prompt = f\"\"\"Generate ONE SHS-level challenge question in JSON format with the following specifications:\n\nCategory: {category}\nDifficulty: {difficulty}\nProgramme: {programme}\n{concept_text}\n\nFor SCIENTIFIC THINKING category questions specifically:\n- Include real scientific data, phenomena, or principles\n- Use applied scenarios: experiments, observations, calculations, case studies\n- Cover diverse branches of science: physics, chemistry, biology, earth science, astronomy, environmental science, health science\n- Feature a specific real scientific concept as the `concept` field\n\nReturn ONLY valid JSON (no markdown, no code blocks) matching this structure:\n{{\n \"category\": \"{category}\",\n \"difficulty\": \"{difficulty}\",\n \"concept\": \"specific scientific concept being tested (e.g., 'Newton's Second Law' or 'Photosynthesis')\",\n \"question\": \"The actual question text\",\n \"options\": [\n \"Option A\",\n \"Option B\",\n \"Option C\",\n \"Option D\"\n ],\n \"correct_answer\": \"Option A\",\n \"explanation\": \"Detailed explanation of why this answer is correct and what students should understand\"\n}}\n\nImportant:\n- Ensure the question tests reasoning, not just memorization\n- Make options realistic but distinguishable by understanding, not guessing\n- The explanation should be educational and help students learn\n- Correct answer must be one of the four options\n- All text should be clear and appropriate for SHS students\"\"\"\n\n return prompt\n\n async def generate_challenge(\n self,\n category: str,\n difficulty: str,\n programme: str,\n concept: Optional[str] = None,\n ) -> Dict[str, Any]:\n \"\"\"\n Generate a single SHS challenge question using DeepSeek AI.\n\n Args:\n category: Question category (Logic, Quantitative Thinking, etc.)\n difficulty: Difficulty level (Beginner, Intermediate, Advanced)\n programme: SHS programme (General Science, General Arts)\n concept: Optional specific concept to focus on\n\n Returns:\n Dictionary with generated question data or error information\n \"\"\"\n # Validate parameters\n is_valid, error_msg = self.validate_parameters(category, difficulty, programme)\n if not is_valid:\n return {\n \"success\": False,\n \"error\": error_msg,\n }\n\n # Check API key configuration\n if not settings.NVIDIA_API_KEY and not settings.DEEPSEEK_API_KEY:\n return {\n \"success\": False,\n \"error\": \"Neither NVIDIA_API_KEY nor DEEPSEEK_API_KEY is configured. Please set them in your .env file.\",\n }\n\n prompt = self._create_prompt(category, difficulty, programme, concept)\n response_text = \"\"\n\n # 1. Try NVIDIA if API key is provided\n if settings.NVIDIA_API_KEY:\n try:\n logger.info(f\"Generating challenge via NVIDIA API using model: {settings.NVIDIA_MODEL}\")\n headers = {\n \"Authorization\": f\"Bearer {settings.NVIDIA_API_KEY}\",\n \"Content-Type\": \"application/json\"\n }\n payload = {\n \"model\": settings.NVIDIA_MODEL,\n \"messages\": [\n {\"role\": \"system\", \"content\": self.SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": prompt}\n ],\n \"temperature\": 0.5,\n \"top_p\": 1.0,\n \"max_tokens\": 2048\n }\n async with httpx.AsyncClient(timeout=120.0) as client:\n response = await client.post(\n \"https://integrate.api.nvidia.com/v1/chat/completions\",\n headers=headers,\n json=payload\n )\n response.raise_for_status()\n result_data = response.json()\n response_text = result_data[\"choices\"][0][\"message\"][\"content\"]\n logger.info(\"Successfully received response from NVIDIA API.\")\n except httpx.ReadTimeout:\n err_msg = \"NVIDIA API request timed out after 120s. The model may be overloaded — please retry.\"\n logger.error(err_msg)\n if not settings.DEEPSEEK_API_KEY:\n return {\"success\": False, \"error\": err_msg}\n logger.info(\"Falling back to DeepSeek after timeout...\")\n except httpx.HTTPStatusError as e:\n err_msg = f\"NVIDIA API HTTP error {e.response.status_code}: {e.response.text}\"\n logger.error(err_msg)\n if not settings.DEEPSEEK_API_KEY:\n return {\"success\": False, \"error\": err_msg}\n logger.info(\"Falling back to DeepSeek after HTTP error...\")\n except Exception as e:\n err_msg = f\"NVIDIA API error ({type(e).__name__}): {e!r}\"\n logger.error(err_msg)\n # If NVIDIA fails, fall back to DeepSeek if possible\n if not settings.DEEPSEEK_API_KEY:\n return {\"success\": False, \"error\": err_msg}\n logger.info(\"Falling back to DeepSeek...\")\n\n # 2. Fall back or use DeepSeek\n if not response_text and settings.DEEPSEEK_API_KEY:\n try:\n logger.info(f\"Generating challenge via DeepSeek API using model: {settings.DEEPSEEK_MODEL}\")\n headers = {\n \"Authorization\": f\"Bearer {settings.DEEPSEEK_API_KEY}\",\n \"Content-Type\": \"application/json\"\n }\n payload = {\n \"model\": settings.DEEPSEEK_MODEL,\n \"messages\": [\n {\"role\": \"system\", \"content\": self.SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": prompt}\n ],\n \"temperature\": 0.5,\n \"top_p\": 1.0,\n \"max_tokens\": 2048,\n \"stream\": False,\n }\n async with httpx.AsyncClient(timeout=120.0) as client:\n response = await client.post(\n DEEPSEEK_CHAT_URL,\n headers=headers,\n json=payload\n )\n response.raise_for_status()\n result_data = response.json()\n response_text = result_data[\"choices\"][0][\"message\"][\"content\"]\n logger.info(\"Successfully received response from DeepSeek API.\")\n\n except httpx.HTTPStatusError as e:\n err_msg = f\"DeepSeek API HTTP error {e.response.status_code}: {e.response.text}\"\n logger.error(err_msg)\n return {\"success\": False, \"error\": err_msg}\n except httpx.TimeoutException:\n err_msg = \"DeepSeek API request timed out after 120s.\"\n logger.error(err_msg)\n return {\"success\": False, \"error\": err_msg}\n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return {\n \"success\": False,\n \"error\": f\"DeepSeek API error: {str(e)}\",\n }\n\n if not response_text:\n return {\n \"success\": False,\n \"error\": \"Failed to get response from AI engines.\",\n }\n\n try:\n # Parse the response\n response_text = response_text.strip()\n\n # Remove markdown code blocks if present\n if response_text.startswith(\"```json\"):\n response_text = response_text[7:] # Remove ```json\n if response_text.startswith(\"```\"):\n response_text = response_text[3:] # Remove ```\n if response_text.endswith(\"```\"):\n response_text = response_text[:-3] # Remove trailing ```\n\n response_text = response_text.strip()\n\n # Parse JSON\n question_data = json.loads(response_text)\n\n # Validate the structure\n required_fields = [\n \"category\",\n \"difficulty\",\n \"concept\",\n \"question\",\n \"options\",\n \"correct_answer\",\n \"explanation\",\n ]\n\n missing_fields = [f for f in required_fields if f not in question_data]\n if missing_fields:\n return {\n \"success\": False,\n \"error\": f\"AI response missing required fields: {', '.join(missing_fields)}\",\n }\n\n # Validate options\n if not isinstance(question_data[\"options\"], list) or len(question_data[\"options\"]) != 4:\n return {\n \"success\": False,\n \"error\": f\"Question must have exactly 4 options. Got: {question_data.get('options')}\",\n }\n\n # Normalize correct_answer: models may return:\n # - A letter index (\"A\", \"B\", \"C\", \"D\")\n # - \"Option X\" format (\"Option B\")\n # - The full option text\n # - A numeric index (0-3)\n # Resolve to the matching option text.\n answer = question_data[\"correct_answer\"]\n options = question_data[\"options\"]\n\n # Helper: find option by exact match, prefix match, or fuzzy match\n def _resolve_answer(ans: str, opts: list) -> str | None:\n # 1. Exact match\n if ans in opts:\n return ans\n # 2. Single uppercase letter → index\n LETTER_MAP = {\"A\": 0, \"B\": 1, \"C\": 2, \"D\": 3}\n if ans in LETTER_MAP:\n idx = LETTER_MAP[ans]\n if idx < len(opts):\n return opts[idx]\n # 3. \"Option X\" format → extract letter\n m = re.match(r\"^[Oo]ption\\s*([A-Da-d])\\s*$\", ans.strip())\n if m:\n idx = LETTER_MAP[m.group(1).upper()]\n if idx < len(opts):\n return opts[idx]\n # 4. Numeric string → index\n if ans.isdigit():\n idx = int(ans)\n if 0 <= idx < len(opts):\n return opts[idx]\n return None\n\n resolved = _resolve_answer(answer, options)\n if resolved:\n question_data[\"correct_answer\"] = resolved\n else:\n return {\n \"success\": False,\n \"error\": f\"Correct answer '{answer}' does not match any option in {options}.\",\n }\n\n # Return success response\n return {\n \"success\": True,\n \"data\": question_data,\n }\n\n except json.JSONDecodeError as e:\n logger.error(f\"Failed to parse AI JSON response: {e}. Raw response: {response_text}\")\n return {\n \"success\": False,\n \"error\": f\"Failed to parse AI response as JSON: {str(e)}\",\n }\n\n except Exception as e:\n logger.error(f\"AI Generation post-processing error: {e}\")\n return {\n \"success\": False,\n \"error\": f\"AI Generation post-processing error: {str(e)}\",\n }\n\n\n# Create a singleton instance\ndeepseek_generator = DeepSeekChallengeGenerator()\n\n\nasync def generate_challenge_question(\n category: str,\n difficulty: str,\n programme: str,\n concept: Optional[str] = None,\n) -> Dict[str, Any]:\n \"\"\"\n Async wrapper for challenge generation.\n\n This function can be called from FastAPI routes.\n \"\"\"\n return await deepseek_generator.generate_challenge(\n category=category,\n difficulty=difficulty,\n programme=programme,\n concept=concept,\n )\n"}, "e055fe5a1a14cf1068f527b613afb700921ba14f": {"t": 1785753487.867421, "n": 16725, "h": "04fe2cde104735ee", "c": "\"\"\"\ndeepseek_service.py — DeepSeek AI integration for dynamic challenge generation\n\nThis module handles all DeepSeek API interactions for generating\nSHS-standard challenge questions dynamically using the OpenAI-compatible API.\n\"\"\"\nimport json\nimport logging\nimport re\nfrom typing import Optional, Dict, Any\n\nimport httpx\n\nfrom app.config import settings\n\n# Configure logging\nlogger = logging.getLogger(__name__)\n\n# DeepSeek API configuration\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_CHAT_URL = f\"{DEEPSEEK_BASE_URL}/v1/chat/completions\"\n\n\nclass DeepSeekChallengeGenerator:\n \"\"\"Service for generating SHS challenge questions using DeepSeek AI.\"\"\"\n\n # Supported categories and programmes\n VALID_CATEGORIES = [\n \"Logic\",\n \"Quantitative Thinking\",\n \"Scientific Thinking\",\n \"Verbal Reasoning\",\n \"Critical Thinking\",\n ]\n\n VALID_PROGRAMMES = [\n \"General Science\",\n \"General Arts\",\n ]\n\n VALID_DIFFICULTY = [\n \"Beginner\",\n \"Intermediate\",\n \"Advanced\",\n ]\n\n # System prompt for consistent, high-quality question generation\n SYSTEM_PROMPT = \"\"\"You are an expert SHS (Senior High School) educational assessment creator\nspecializing in Ghanaian curriculum standards.\n\nYour task is to generate engaging, intellectually challenging questions that:\n- Follow WAEC/SHS examination standards\n- Are suitable for Ghanaian SHS students\n- Focus on reasoning and deeper understanding, not memorization\n- Are interactive and engaging\n- Provide valuable learning through explanation\n\nImportant guidelines:\n1. Create questions that develop critical thinking skills\n2. Avoid simple factual recall questions — always test understanding, application, or analysis\n3. Include distractors that reveal common misconceptions\n4. Ensure explanations help students understand WHY the answer is correct\n5. Make questions culturally relevant to Ghana context where appropriate\n6. Use clear, unambiguous language\n7. Keep explanations concise — maximum 3 short sentences\n\nSCIENCE DOMAIN INSTRUCTIONS (when generating Scientific Thinking questions):\nRotate across ALL branches of science for diversity. Never repeat the same scientific concept twice in a row.\n- Physics: mechanics, thermodynamics, optics, waves, electricity, magnetism, nuclear physics, quantum basics\n- Chemistry: atomic structure, bonding, reactions, stoichiometry, organic chemistry, acids/bases, electrochemistry\n- Biology: cell biology, genetics, evolution, ecology, human physiology, plant biology, microbiology\n- Earth Science: geology, meteorology, oceanography, natural disasters, rock cycle, plate tectonics\n- Astronomy: solar system, stellar evolution, cosmology basics, planetary science, space exploration\n- Environmental Science: ecosystems, pollution, climate change, renewable energy, conservation\n- Health Science: nutrition, disease, public health, pharmacology basics\n\nEnsure each science question tests a real scientific principle with accurate data/facts. Include applied scenarios like experiments, observations, or real-world phenomena.\"\"\"\n\n def __init__(self):\n \"\"\"Initialize DeepSeek service.\"\"\"\n if not settings.DEEPSEEK_API_KEY:\n logger.warning(\"DEEPSEEK_API_KEY not configured. DeepSeek integration will not work.\")\n\n def validate_parameters(\n self,\n category: str,\n difficulty: str,\n programme: str,\n ) -> tuple[bool, Optional[str]]:\n \"\"\"\n Validate request parameters.\n\n Returns:\n Tuple of (is_valid, error_message)\n \"\"\"\n if category not in self.VALID_CATEGORIES:\n return False, f\"Invalid category. Valid options: {', '.join(self.VALID_CATEGORIES)}\"\n\n if difficulty not in self.VALID_DIFFICULTY:\n return False, f\"Invalid difficulty. Valid options: {', '.join(self.VALID_DIFFICULTY)}\"\n\n if programme not in self.VALID_PROGRAMMES:\n return False, f\"Invalid programme. Valid options: {', '.join(self.VALID_PROGRAMMES)}\"\n\n return True, None\n\n def _create_prompt(\n self,\n category: str,\n difficulty: str,\n programme: str,\n concept: Optional[str] = None,\n ) -> str:\n \"\"\"\n Create the prompt for DeepSeek to generate a question.\n\n Args:\n category: Question category\n difficulty: Difficulty level\n programme: SHS programme (General Science or General Arts)\n concept: Optional specific concept to focus on\n\n Returns:\n Formatted prompt for DeepSeek\n \"\"\"\n concept_text = f\"\\nFocus on this concept: {concept}\" if concept else \"\"\n\n prompt = f\"\"\"Generate ONE SHS-level challenge question in JSON format with the following specifications:\n\nCategory: {category}\nDifficulty: {difficulty}\nProgramme: {programme}\n{concept_text}\n\nFor SCIENTIFIC THINKING category questions specifically:\n- Include real scientific data, phenomena, or principles\n- Use applied scenarios: experiments, observations, calculations, case studies\n- Cover diverse branches of science: physics, chemistry, biology, earth science, astronomy, environmental science, health science\n- Feature a specific real scientific concept as the `concept` field\n\nReturn ONLY valid JSON (no markdown, no code blocks) matching this structure:\n{{\n \"category\": \"{category}\",\n \"difficulty\": \"{difficulty}\",\n \"concept\": \"specific scientific concept being tested (e.g., 'Newton's Second Law' or 'Photosynthesis')\",\n \"question\": \"The actual question text\",\n \"options\": [\n \"Option A\",\n \"Option B\",\n \"Option C\",\n \"Option D\"\n ],\n \"correct_answer\": \"Option A\",\n \"explanation\": \"Detailed explanation of why this answer is correct and what students should understand\"\n}}\n\nImportant:\n- Ensure the question tests reasoning, not just memorization\n- Make options realistic but distinguishable by understanding, not guessing\n- The explanation should be educational and help students learn\n- Correct answer must be one of the four options\n- All text should be clear and appropriate for SHS students\"\"\"\n\n return prompt\n\n async def generate_challenge(\n self,\n category: str,\n difficulty: str,\n programme: str,\n concept: Optional[str] = None,\n ) -> Dict[str, Any]:\n \"\"\"\n Generate a single SHS challenge question using DeepSeek AI.\n\n Args:\n category: Question category (Logic, Quantitative Thinking, etc.)\n difficulty: Difficulty level (Beginner, Intermediate, Advanced)\n programme: SHS programme (General Science, General Arts)\n concept: Optional specific concept to focus on\n\n Returns:\n Dictionary with generated question data or error information\n \"\"\"\n # Validate parameters\n is_valid, error_msg = self.validate_parameters(category, difficulty, programme)\n if not is_valid:\n return {\n \"success\": False,\n \"error\": error_msg,\n }\n\n # Check API key configuration\n if not settings.NVIDIA_API_KEY and not settings.DEEPSEEK_API_KEY:\n return {\n \"success\": False,\n \"error\": \"Neither NVIDIA_API_KEY nor DEEPSEEK_API_KEY is configured. Please set them in your .env file.\",\n }\n\n prompt = self._create_prompt(category, difficulty, programme, concept)\n response_text = \"\"\n\n # 1. Try NVIDIA if API key is provided\n if settings.NVIDIA_API_KEY:\n try:\n logger.info(f\"Generating challenge via NVIDIA API using model: {settings.NVIDIA_MODEL}\")\n headers = {\n \"Authorization\": f\"Bearer {settings.NVIDIA_API_KEY}\",\n \"Content-Type\": \"application/json\"\n }\n payload = {\n \"model\": settings.NVIDIA_MODEL,\n \"messages\": [\n {\"role\": \"system\", \"content\": self.SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": prompt}\n ],\n \"temperature\": 0.5,\n \"top_p\": 1.0,\n \"max_tokens\": 2048\n }\n async with httpx.AsyncClient(timeout=120.0) as client:\n response = await client.post(\n \"https://integrate.api.nvidia.com/v1/chat/completions\",\n headers=headers,\n json=payload\n )\n response.raise_for_status()\n result_data = response.json()\n response_text = result_data[\"choices\"][0][\"message\"][\"content\"]\n logger.info(\"Successfully received response from NVIDIA API.\")\n except httpx.ReadTimeout:\n err_msg = \"NVIDIA API request timed out after 120s. The model may be overloaded — please retry.\"\n logger.error(err_msg)\n if not settings.DEEPSEEK_API_KEY:\n return {\"success\": False, \"error\": err_msg}\n logger.info(\"Falling back to DeepSeek after timeout...\")\n except httpx.HTTPStatusError as e:\n err_msg = f\"NVIDIA API HTTP error {e.response.status_code}: {e.response.text}\"\n logger.error(err_msg)\n if not settings.DEEPSEEK_API_KEY:\n return {\"success\": False, \"error\": err_msg}\n logger.info(\"Falling back to DeepSeek after HTTP error...\")\n except Exception as e:\n err_msg = f\"NVIDIA API error ({type(e).__name__}): {e!r}\"\n logger.error(err_msg)\n # If NVIDIA fails, fall back to DeepSeek if possible\n if not settings.DEEPSEEK_API_KEY:\n return {\"success\": False, \"error\": err_msg}\n logger.info(\"Falling back to DeepSeek...\")\n\n # 2. Fall back or use DeepSeek\n if not response_text and settings.DEEPSEEK_API_KEY:\n try:\n logger.info(f\"Generating challenge via DeepSeek API using model: {settings.DEEPSEEK_MODEL}\")\n headers = {\n \"Authorization\": f\"Bearer {settings.DEEPSEEK_API_KEY}\",\n \"Content-Type\": \"application/json\"\n }\n payload = {\n \"model\": settings.DEEPSEEK_MODEL,\n \"messages\": [\n {\"role\": \"system\", \"content\": self.SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": prompt}\n ],\n \"temperature\": 0.5,\n \"top_p\": 1.0,\n \"max_tokens\": 2048,\n \"stream\": False,\n }\n async with httpx.AsyncClient(timeout=120.0) as client:\n response = await client.post(\n DEEPSEEK_CHAT_URL,\n headers=headers,\n json=payload\n )\n response.raise_for_status()\n result_data = response.json()\n response_text = result_data[\"choices\"][0][\"message\"][\"content\"]\n logger.info(\"Successfully received response from DeepSeek API.\")\n\n except httpx.HTTPStatusError as e:\n err_msg = f\"DeepSeek API HTTP error {e.response.status_code}: {e.response.text}\"\n logger.error(err_msg)\n return {\"success\": False, \"error\": err_msg}\n except httpx.TimeoutException:\n err_msg = \"DeepSeek API request timed out after 120s.\"\n logger.error(err_msg)\n return {\"success\": False, \"error\": err_msg}\n except Exception as e:\n logger.error(f\"DeepSeek API error: {e}\")\n return {\n \"success\": False,\n \"error\": f\"DeepSeek API error: {str(e)}\",\n }\n\n if not response_text:\n return {\n \"success\": False,\n \"error\": \"Failed to get response from AI engines.\",\n }\n\n try:\n # Parse the response\n response_text = response_text.strip()\n\n # Remove markdown code blocks if present\n if response_text.startswith(\"```json\"):\n response_text = response_text[7:] # Remove ```json\n if response_text.startswith(\"```\"):\n response_text = response_text[3:] # Remove ```\n if response_text.endswith(\"```\"):\n response_text = response_text[:-3] # Remove trailing ```\n\n response_text = response_text.strip()\n\n # Parse JSON\n question_data = json.loads(response_text)\n\n # Validate the structure\n required_fields = [\n \"category\",\n \"difficulty\",\n \"concept\",\n \"question\",\n \"options\",\n \"correct_answer\",\n \"explanation\",\n ]\n\n missing_fields = [f for f in required_fields if f not in question_data]\n if missing_fields:\n return {\n \"success\": False,\n \"error\": f\"AI response missing required fields: {', '.join(missing_fields)}\",\n }\n\n # Validate options\n if not isinstance(question_data[\"options\"], list) or len(question_data[\"options\"]) != 4:\n return {\n \"success\": False,\n \"error\": f\"Question must have exactly 4 options. Got: {question_data.get('options')}\",\n }\n\n # Normalize correct_answer: models may return:\n # - A letter index (\"A\", \"B\", \"C\", \"D\")\n # - \"Option X\" format (\"Option B\")\n # - The full option text\n # - A numeric index (0-3)\n # Resolve to the matching option text.\n answer = question_data[\"correct_answer\"]\n options = question_data[\"options\"]\n\n # Helper: find option by exact match, prefix match, or fuzzy match\n def _resolve_answer(ans: str, opts: list) -> str | None:\n # 1. Exact match\n if ans in opts:\n return ans\n # 2. Single uppercase letter → index\n LETTER_MAP = {\"A\": 0, \"B\": 1, \"C\": 2, \"D\": 3}\n if ans in LETTER_MAP:\n idx = LETTER_MAP[ans]\n if idx < len(opts):\n return opts[idx]\n # 3. \"Option X\" format → extract letter\n m = re.match(r\"^[Oo]ption\\s*([A-Da-d])\\s*$\", ans.strip())\n if m:\n idx = LETTER_MAP[m.group(1).upper()]\n if idx < len(opts):\n return opts[idx]\n # 4. Numeric string → index\n if ans.isdigit():\n idx = int(ans)\n if 0 <= idx < len(opts):\n return opts[idx]\n return None\n\n resolved = _resolve_answer(answer, options)\n if resolved:\n question_data[\"correct_answer\"] = resolved\n else:\n return {\n \"success\": False,\n \"error\": f\"Correct answer '{answer}' does not match any option in {options}.\",\n }\n\n # Return success response\n return {\n \"success\": True,\n \"data\": question_data,\n }\n\n except json.JSONDecodeError as e:\n logger.error(f\"Failed to parse AI JSON response: {e}. Raw response: {response_text}\")\n return {\n \"success\": False,\n \"error\": f\"Failed to parse AI response as JSON: {str(e)}\",\n }\n\n except Exception as e:\n logger.error(f\"AI Generation post-processing error: {e}\")\n return {\n \"success\": False,\n \"error\": f\"AI Generation post-processing error: {str(e)}\",\n }\n\n\n# Create a singleton instance\ndeepseek_generator = DeepSeekChallengeGenerator()\n\n\nasync def generate_challenge_question(\n category: str,\n difficulty: str,\n programme: str,\n concept: Optional[str] = None,\n) -> Dict[str, Any]:\n \"\"\"\n Async wrapper for challenge generation.\n\n This function can be called from FastAPI routes.\n \"\"\"\n return await deepseek_generator.generate_challenge(\n category=category,\n difficulty=difficulty,\n programme=programme,\n concept=concept,\n )\n"}}}} |