feat: audit and purge legacy backend files, stub Phase 2 AI LLM modules, create comprehensive PAGEWISE_API_DOCUMENTATION.md, and wire DPS test seeding
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import sys
|
||||
sys.path.append('/app')
|
||||
from sqlmodel import Session, select
|
||||
from database import engine
|
||||
import models
|
||||
import re
|
||||
|
||||
def fix_text(text):
|
||||
if not text: return text
|
||||
|
||||
# The DB currently has string literals with backslash caret 3 d backslash dollar.
|
||||
# Like '\\^3d\\$' and '\\^3d^2\\$'
|
||||
|
||||
text = text.replace('\\^3d^2\\$', '$sp^3d^2$')
|
||||
text = text.replace('\\^3d\\$', '$sp^3d$')
|
||||
text = text.replace('\\^3\\$', '$sp^3$')
|
||||
text = text.replace('\\^2\\$', '$sp^2$')
|
||||
|
||||
text = text.replace('^3d^2$', '$sp^3d^2$')
|
||||
text = text.replace('^3d$', '$sp^3d$')
|
||||
text = text.replace('^3$', '$sp^3$')
|
||||
text = text.replace('^2$', '$sp^2$')
|
||||
|
||||
# Original targets in case we run on fresh seed
|
||||
text = re.sub(r'\bsp3d2\b', r'$sp^3d^2$', text)
|
||||
text = re.sub(r'\bsp3d\b', r'$sp^3d$', text)
|
||||
text = re.sub(r'\bsp3\b', r'$sp^3$', text)
|
||||
text = re.sub(r'\bsp2\b', r'$sp^2$', text)
|
||||
|
||||
mols = ['PCl5', 'SF6', 'H2O', 'NH3', 'CO2', 'CH4', 'C2H4', 'C2H2', 'BF3', 'BeCl2', 'O2', 'N2', 'F2']
|
||||
for mol in mols:
|
||||
formatted = '$' + re.sub(r'(\d+)', r'_\1', mol) + '$'
|
||||
text = re.sub(r'\b' + mol + r'\b', formatted, text)
|
||||
|
||||
text = text.replace('z1 = 1 + i', '$z_1 = 1 + i$')
|
||||
text = text.replace('z2 = 2 − i', '$z_2 = 2 - i$')
|
||||
text = text.replace('z1\nz2', '$\\frac{z_1}{z_2}$')
|
||||
text = text.replace('z−1\nz+1', '$\\frac{z-1}{z+1}$')
|
||||
text = text.replace('z2 = ¯z', '$z^2 = \\bar{z}$')
|
||||
text = text.replace('x2 − 3x + 5 = 0', '$x^2 - 3x + 5 = 0$')
|
||||
text = text.replace('x2 + 2x + 5 = 0', '$x^2 + 2x + 5 = 0$')
|
||||
text = text.replace('1+i\n1−i', '$\\frac{1+i}{1-i}$')
|
||||
text = text.replace('z + 1\nz', '$\\frac{z+1}{z}$')
|
||||
text = text.replace('z − 1\nz i', '$\\frac{z-1}{z i}$')
|
||||
text = text.replace('30◦', '30°')
|
||||
text = text.replace('⃗v = 3', '$\\vec{v} = 3$')
|
||||
|
||||
text = text.replace('σ bonds', '$\\sigma$ bonds')
|
||||
text = text.replace('π bonds', '$\\pi$ bonds')
|
||||
|
||||
return text
|
||||
|
||||
with Session(engine) as session:
|
||||
questions = session.exec(select(models.Question)).all()
|
||||
count = 0
|
||||
for q in questions:
|
||||
old_text = q.question_text
|
||||
new_text = fix_text(old_text)
|
||||
if old_text != new_text:
|
||||
q.question_text = new_text
|
||||
session.add(q)
|
||||
count += 1
|
||||
print(f'Updated Q {q.id}')
|
||||
session.commit()
|
||||
print(f'Successfully updated {count} questions.')
|
||||
@@ -1,116 +0,0 @@
|
||||
import sys
|
||||
sys.path.append('/app')
|
||||
from sqlmodel import Session, select
|
||||
from database import engine
|
||||
import models
|
||||
import glob
|
||||
import os
|
||||
import fitz
|
||||
import re
|
||||
|
||||
def parse_pdf(filepath):
|
||||
doc = fitz.open(filepath)
|
||||
text = ""
|
||||
for page in doc:
|
||||
text += page.get_text() + "\n"
|
||||
|
||||
q_block = text.split("Questions\n")[-1].split("Answer Key\n")[0]
|
||||
q_pattern = re.compile(r'^(\d+)\.\s+(.*?)(?=^\d+\.\s+|\Z)', re.DOTALL | re.MULTILINE)
|
||||
|
||||
questions_dict = {}
|
||||
for match in q_pattern.finditer(q_block):
|
||||
q_num = int(match.group(1))
|
||||
if 1 <= q_num <= 15:
|
||||
questions_dict[q_num] = match.group(2).strip()
|
||||
|
||||
answers_dict = {}
|
||||
if "Answer Key\n" in text:
|
||||
a_block = text.split("Answer Key\n")[-1]
|
||||
a_pattern = re.compile(r'^(\d+)\s+(.*?)(?=^\d+\s+|\Z)', re.DOTALL | re.MULTILINE)
|
||||
for match in a_pattern.finditer(a_block):
|
||||
q_num = int(match.group(1))
|
||||
if 1 <= q_num <= 15:
|
||||
if q_num not in answers_dict:
|
||||
answers_dict[q_num] = match.group(2).strip()
|
||||
|
||||
return questions_dict, answers_dict
|
||||
|
||||
def format_text(text):
|
||||
if not text: return text
|
||||
|
||||
# Original targets
|
||||
text = re.sub(r'\bsp3d2\b', r'$sp^3d^2$', text)
|
||||
text = re.sub(r'\bsp3d\b', r'$sp^3d$', text)
|
||||
text = re.sub(r'\bsp3\b', r'$sp^3$', text)
|
||||
text = re.sub(r'\bsp2\b', r'$sp^2$', text)
|
||||
|
||||
def repl(m):
|
||||
word = m.group(0)
|
||||
if word.lower() in ["sp2", "sp3", "sp3d", "sp3d2", "3d", "a1", "b1", "c1"]:
|
||||
return word
|
||||
return '$' + re.sub(r'(\d+)', r'_\1', word) + '$'
|
||||
|
||||
text = re.sub(r'\b[A-Z][a-z]*[A-Z]*[a-z]*\d+[A-Za-z0-9]*\b', repl, text)
|
||||
|
||||
text = text.replace('z1 = 1 + i', '$z_1 = 1 + i$')
|
||||
text = text.replace('z2 = 2 − i', '$z_2 = 2 - i$')
|
||||
text = text.replace('z1\nz2', '$\\frac{z_1}{z_2}$')
|
||||
text = text.replace('z−1\nz+1', '$\\frac{z-1}{z+1}$')
|
||||
text = text.replace('z2 = ¯z', '$z^2 = \\bar{z}$')
|
||||
text = text.replace('x2 − 3x + 5 = 0', '$x^2 - 3x + 5 = 0$')
|
||||
text = text.replace('x2 + 2x + 5 = 0', '$x^2 + 2x + 5 = 0$')
|
||||
text = text.replace('1+i\n1−i', '$\\frac{1+i}{1-i}$')
|
||||
text = text.replace('z + 1\nz', '$\\frac{z+1}{z}$')
|
||||
text = text.replace('z − 1\nz i', '$\\frac{z-1}{z i}$')
|
||||
text = text.replace('30◦', '30°')
|
||||
text = text.replace('⃗v = 3', '$\\vec{v} = 3$')
|
||||
|
||||
text = text.replace('σ bonds', '$\\sigma$ bonds')
|
||||
text = text.replace('π bonds', '$\\pi$ bonds')
|
||||
|
||||
return text
|
||||
|
||||
def fix_all_from_source():
|
||||
pdf_files = glob.glob('/app/pdfs_to_parse/*.pdf')
|
||||
with Session(engine) as session:
|
||||
for pdf_path in pdf_files:
|
||||
filename = os.path.basename(pdf_path)
|
||||
subject = "Physics"
|
||||
topic = "General"
|
||||
if "phy" in filename.lower():
|
||||
subject = "Physics"
|
||||
topic = "Laws of Motion"
|
||||
elif "che" in filename.lower():
|
||||
subject = "Chemistry"
|
||||
topic = "Chemical Bonding"
|
||||
elif "mth" in filename.lower():
|
||||
subject = "Mathematics"
|
||||
topic = "Calculus"
|
||||
elif "bio" in filename.lower():
|
||||
subject = "Biology"
|
||||
topic = "Genetics"
|
||||
|
||||
q_dict, a_dict = parse_pdf(pdf_path)
|
||||
|
||||
# Find the test for this topic
|
||||
test = session.exec(select(models.Test).where(models.Test.title == f"DPP: {subject} - {topic}")).first()
|
||||
if not test:
|
||||
continue
|
||||
|
||||
q_ids = test.sections.get("Section 1", {}).get("questions", [])
|
||||
for i, q_id in enumerate(q_ids):
|
||||
q_num = i + 1
|
||||
if q_num in q_dict:
|
||||
raw_text = q_dict[q_num].replace('\x00', '')
|
||||
formatted = format_text(raw_text)
|
||||
|
||||
q = session.exec(select(models.Question).where(models.Question.id == q_id)).first()
|
||||
if q:
|
||||
q.question_text = formatted
|
||||
session.add(q)
|
||||
|
||||
session.commit()
|
||||
print("Successfully updated questions from source.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
fix_all_from_source()
|
||||
@@ -1,59 +0,0 @@
|
||||
import sys
|
||||
sys.path.append('/app')
|
||||
from sqlmodel import Session, select
|
||||
from database import engine
|
||||
import models
|
||||
|
||||
def fix_text(text):
|
||||
if not text: return text
|
||||
|
||||
# Fix NO3- where 3 is on the next line
|
||||
text = text.replace('NO−\n3', '$NO_3^-$')
|
||||
text = text.replace('NO-\n3', '$NO_3^-$')
|
||||
|
||||
text = text.replace('CO2−\n3', '$CO_3^{2-}$')
|
||||
text = text.replace('CO2-\n3', '$CO_3^{2-}$')
|
||||
|
||||
text = text.replace('SO2−\n4', '$SO_4^{2-}$')
|
||||
text = text.replace('SO2-\n4', '$SO_4^{2-}$')
|
||||
|
||||
text = text.replace('PO3−\n4', '$PO_4^{3-}$')
|
||||
text = text.replace('PO3-\n4', '$PO_4^{3-}$')
|
||||
|
||||
# Also generic charges if they are on same line
|
||||
text = text.replace('NO3-', '$NO_3^-$')
|
||||
text = text.replace('CO32-', '$CO_3^{2-}$')
|
||||
text = text.replace('SO42-', '$SO_4^{2-}$')
|
||||
text = text.replace('PO43-', '$PO_4^{3-}$')
|
||||
text = text.replace('O2-', '$O^{2-}$')
|
||||
text = text.replace('F-', '$F^-$')
|
||||
text = text.replace('Cl-', '$Cl^-$')
|
||||
text = text.replace('Br-', '$Br^-$')
|
||||
text = text.replace('I-', '$I^-$')
|
||||
text = text.replace('Na+', '$Na^+$')
|
||||
text = text.replace('K+', '$K^+$')
|
||||
text = text.replace('Ca2+', '$Ca^{2+}$')
|
||||
text = text.replace('Mg2+', '$Mg^{2+}$')
|
||||
text = text.replace('Fe2+', '$Fe^{2+}$')
|
||||
text = text.replace('Fe3+', '$Fe^{3+}$')
|
||||
text = text.replace('Cu2+', '$Cu^{2+}$')
|
||||
text = text.replace('Al3+', '$Al^{3+}$')
|
||||
|
||||
# Check if we have O3 (ozone) that wasn't replaced
|
||||
text = text.replace('O3', '$O_3$')
|
||||
|
||||
return text
|
||||
|
||||
with Session(engine) as session:
|
||||
questions = session.exec(select(models.Question)).all()
|
||||
count = 0
|
||||
for q in questions:
|
||||
old_text = q.question_text
|
||||
new_text = fix_text(old_text)
|
||||
if old_text != new_text:
|
||||
q.question_text = new_text
|
||||
session.add(q)
|
||||
count += 1
|
||||
print(f'Updated Q {q.id}')
|
||||
session.commit()
|
||||
print(f'Successfully updated {count} questions for ions.')
|
||||
@@ -1,55 +1,23 @@
|
||||
import os
|
||||
import json
|
||||
from openai import AsyncOpenAI
|
||||
from sqlmodel import Session, select
|
||||
from fastapi.responses import StreamingResponse
|
||||
"""
|
||||
llm.py — AI Tutor / LLM Streaming Interface
|
||||
STATUS: DEFERRED — Phase 2 (AI Coach LLM integration)
|
||||
|
||||
This module will provide streaming LLM responses via the local Ollama server
|
||||
or a cloud inference endpoint. It is intentionally stubbed out in the current
|
||||
production build to avoid dependency on the openai SDK and a running LLM server.
|
||||
|
||||
When ready to enable:
|
||||
1. Add `openai==1.14.0` back to requirements.txt
|
||||
2. Set LLM_BASE_URL, LLM_MODEL_NAME, OPENAI_API_KEY in backend/.env
|
||||
3. Replace this stub with the full implementation below.
|
||||
"""
|
||||
from uuid import UUID
|
||||
from typing import AsyncIterator
|
||||
|
||||
import models
|
||||
|
||||
# Point the standard AsyncOpenAI client to your local Ollama server
|
||||
client = AsyncOpenAI(
|
||||
base_url=os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434/v1"),
|
||||
api_key=os.getenv("OPENAI_API_KEY", "ollama"), # Required by the SDK, but Ollama ignores it
|
||||
)
|
||||
|
||||
async def stream_tutor_response(user_id: UUID, user_message: str, session: Session):
|
||||
# 1. Fetch Context: Get the student's active weak areas
|
||||
active_mistakes = session.exec(
|
||||
select(models.Mistake)
|
||||
.where(models.Mistake.user_id == user_id)
|
||||
.where(models.Mistake.is_mastered == False)
|
||||
).all()
|
||||
|
||||
weak_topics = list(set([m.topic for m in active_mistakes]))
|
||||
context_string = ", ".join(weak_topics) if weak_topics else "General Syllabus"
|
||||
|
||||
# 2. Construct the highly-specific System Prompt
|
||||
system_prompt = f"""
|
||||
You are PragatiVeer's elite AI Tutor for JEE & NEET aspirants.
|
||||
Your goal is to guide students to the answer, not just give it to them.
|
||||
|
||||
STUDENT CONTEXT:
|
||||
This student currently struggles with the following topics: {context_string}.
|
||||
Keep this in mind and relate explanations back to foundational concepts in these areas if applicable.
|
||||
|
||||
RULES:
|
||||
1. Respond in the student's preferred language (English or Hindi).
|
||||
2. Use LaTeX wrapped in single $ for inline math and $$ for block equations.
|
||||
3. Keep responses concise and focused on problem-solving steps.
|
||||
async def stream_tutor_response(user_id: UUID, user_message: str, session) -> AsyncIterator[str]:
|
||||
"""
|
||||
|
||||
# 3. Call the LLM
|
||||
response = await client.chat.completions.create(
|
||||
model=os.getenv("LLM_MODEL_NAME", "gemma"),
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message}
|
||||
],
|
||||
stream=True,
|
||||
)
|
||||
|
||||
# 4. Yield tokens as they arrive
|
||||
async for chunk in response:
|
||||
if chunk.choices[0].delta.content is not None:
|
||||
yield chunk.choices[0].delta.content
|
||||
Stub: yields a single message explaining AI is coming soon.
|
||||
Replace with real LLM streaming call in Phase 2.
|
||||
"""
|
||||
yield "🤖 AI Tutor is coming soon. This feature will be enabled once LLM infrastructure is provisioned."
|
||||
|
||||
@@ -14,7 +14,7 @@ import models
|
||||
from ai_coach import calculate_sm2
|
||||
from analytics import generate_parent_report
|
||||
from billing import generate_mock_order, verify_webhook_signature
|
||||
from llm import stream_tutor_response
|
||||
from llm import stream_tutor_response # Phase 2 stub — returns 'coming soon' message
|
||||
from predictor import calculate_readiness_score
|
||||
from growth import generate_referral_link, process_referral_reward
|
||||
from email_service import send_welcome_email, send_otp_email
|
||||
@@ -748,9 +748,13 @@ async def upload_admin_test(
|
||||
admin_id: UUID = Form(...),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
PDF Test Upload — Phase 2 (AI LLM integration deferred).
|
||||
Currently: uploads PDF to MinIO and creates a placeholder Test record.
|
||||
When Phase 2 is ready: uncomment process_pdf_test.delay() to trigger LLM question extraction.
|
||||
"""
|
||||
import storage
|
||||
import uuid
|
||||
from worker import process_pdf_test
|
||||
|
||||
pdf_bytes = await file.read()
|
||||
current_year = datetime.utcnow().year
|
||||
@@ -774,14 +778,20 @@ async def upload_admin_test(
|
||||
session.commit()
|
||||
session.refresh(test_obj)
|
||||
|
||||
process_pdf_test.delay(
|
||||
test_id_str=str(test_obj.id),
|
||||
object_name=object_name,
|
||||
admin_id_str=str(admin_id),
|
||||
exam_pattern=exam_pattern
|
||||
)
|
||||
# Phase 2: Uncomment below to trigger AI question extraction from PDF
|
||||
# from worker import process_pdf_test
|
||||
# process_pdf_test.delay(
|
||||
# test_id_str=str(test_obj.id),
|
||||
# object_name=object_name,
|
||||
# admin_id_str=str(admin_id),
|
||||
# exam_pattern=exam_pattern
|
||||
# )
|
||||
|
||||
return {"status": "processing", "test_id": test_obj.id, "message": "Test is being processed by AI"}
|
||||
return {
|
||||
"status": "uploaded",
|
||||
"test_id": test_obj.id,
|
||||
"message": "PDF uploaded to MinIO. AI question extraction is deferred to Phase 2. Use create_manual endpoint to add questions now."
|
||||
}
|
||||
|
||||
@app.post("/api/admin/tests/create_manual", tags=["Admin"])
|
||||
def create_manual_test(
|
||||
|
||||
@@ -1,85 +1,21 @@
|
||||
import fitz # PyMuPDF
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
"""
|
||||
pdf_parser.py — PDF-to-Question AI Parser
|
||||
STATUS: DEFERRED — Phase 2 (AI Coach LLM integration)
|
||||
|
||||
This module will use PyMuPDF to extract text from uploaded PDFs and pass it
|
||||
to an LLM to structure questions into the database JSON schema.
|
||||
|
||||
When ready to enable:
|
||||
1. Add `PyMuPDF==1.23.26` and `openai==1.14.0` back to requirements.txt
|
||||
2. Set LLM_BASE_URL, LLM_MODEL_NAME, OPENAI_API_KEY in backend/.env
|
||||
3. Replace this stub with the full implementation.
|
||||
"""
|
||||
from typing import List, Dict, Any
|
||||
import os
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
logger = logging.getLogger("pdf_parser")
|
||||
|
||||
client = AsyncOpenAI(
|
||||
base_url=os.getenv("LLM_BASE_URL", "http://host.docker.internal:11434/v1"),
|
||||
api_key=os.getenv("OPENAI_API_KEY", "ollama"),
|
||||
)
|
||||
|
||||
async def parse_pdf_to_questions(pdf_bytes: bytes, exam_pattern: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Extracts text from a PDF and uses the LLM to structure it into a JSON array of questions.
|
||||
Stub: returns empty list. Real implementation uses PyMuPDF + LLM JSON extraction.
|
||||
Replace in Phase 2 once LLM infrastructure is provisioned.
|
||||
"""
|
||||
text = ""
|
||||
try:
|
||||
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||
for page in doc:
|
||||
text += page.get_text("text") + "\n\n"
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse PDF with PyMuPDF: {e}")
|
||||
return []
|
||||
|
||||
system_prompt = f"""
|
||||
You are an AI expert at extracting {exam_pattern} test questions from messy PDF text.
|
||||
The text contains complex Chemistry and Math formulas.
|
||||
CRITICAL: You MUST format all formulas perfectly in LaTeX wrapping inline math in $ and block math in $$.
|
||||
|
||||
Extract the questions and return them STRICTLY as a JSON array of objects.
|
||||
If your API requires a JSON object root, return {{"questions": [...]}}.
|
||||
|
||||
Schema per question:
|
||||
{{
|
||||
"subject": "Physics" | "Chemistry" | "Maths" | "Biology",
|
||||
"topic": "String",
|
||||
"subtopic": "String",
|
||||
"difficulty": "Easy" | "Medium" | "Hard",
|
||||
"type": "MCQ_Single" | "MCQ_Multiple" | "Numerical",
|
||||
"question_text": "Full text of the question with LaTeX",
|
||||
"options": {{"A": "Option A text", "B": "Option B text", "C": "Option C text", "D": "Option D text"}},
|
||||
"correct_answer": {{"value": "A"}},
|
||||
"solution_text": "Detailed step-by-step solution if available, else 'Solution not provided'",
|
||||
"language": "English"
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
truncated_text = text[:15000]
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model=os.getenv("LLM_MODEL_NAME", "gemma"),
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": truncated_text}
|
||||
],
|
||||
response_format={ "type": "json_object" }
|
||||
)
|
||||
|
||||
output = response.choices[0].message.content
|
||||
if not output:
|
||||
return []
|
||||
|
||||
if "```json" in output:
|
||||
output = output.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in output:
|
||||
output = output.split("```")[1].split("```")[0].strip()
|
||||
|
||||
data = json.loads(output)
|
||||
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
elif isinstance(data, dict) and "questions" in data:
|
||||
return data["questions"]
|
||||
elif isinstance(data, dict):
|
||||
return [data]
|
||||
else:
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"LLM PDF parsing failed: {e}")
|
||||
return []
|
||||
return []
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,8 +8,6 @@ pydantic-settings==2.2.1
|
||||
bcrypt==4.1.2
|
||||
pyjwt==2.8.0
|
||||
celery==5.3.6
|
||||
openai==1.14.0
|
||||
httpx==0.27.2
|
||||
boto3==1.34.0
|
||||
python-multipart==0.0.9
|
||||
PyMuPDF==1.23.26
|
||||
|
||||
@@ -412,8 +412,9 @@ def seed_local_database():
|
||||
dps_session.add(q)
|
||||
q_ids.append(str(q.id))
|
||||
|
||||
dps_id = uuid4()
|
||||
dps = models.DPS(
|
||||
id=uuid4(),
|
||||
id=dps_id,
|
||||
date=sheet_date,
|
||||
title=sheet_title,
|
||||
questions=q_ids,
|
||||
@@ -423,7 +424,22 @@ def seed_local_database():
|
||||
created_by=admin_id,
|
||||
)
|
||||
dps_session.add(dps)
|
||||
print(f" ✓ DPS seeded: {sheet_title} ({len(q_ids)} questions)")
|
||||
|
||||
# Also create as Test(type='DPS') so /api/tests?type=DPS and CBT engine can run it
|
||||
dps_test = models.Test(
|
||||
id=dps_id,
|
||||
title=sheet_title,
|
||||
type="DPS",
|
||||
exam_pattern=models.ExamEnum.JEE_MAIN,
|
||||
sections={"Practice": {"questions": q_ids, "time_limit": 30}},
|
||||
total_time=30,
|
||||
difficulty="Medium",
|
||||
scheduled_date=datetime.combine(sheet_date, datetime.min.time()),
|
||||
is_active=True,
|
||||
created_by=admin_id,
|
||||
)
|
||||
dps_session.add(dps_test)
|
||||
print(f" ✓ DPS & CBT Test seeded: {sheet_title} ({len(q_ids)} questions)")
|
||||
|
||||
dps_session.commit()
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
from sqlmodel import Session, select, delete
|
||||
from datetime import datetime
|
||||
|
||||
from database import engine, init_db
|
||||
import models
|
||||
import storage
|
||||
from pdf_parser import parse_pdf_to_questions
|
||||
|
||||
async def seed_pdfs():
|
||||
admin_id_str = "b0e17378-864d-439e-8806-708dc48a891b"
|
||||
|
||||
with Session(engine) as session:
|
||||
admin = session.exec(select(models.User).limit(1)).first()
|
||||
admin_id = admin.id if admin else uuid.UUID(admin_id_str)
|
||||
|
||||
# Delete previously seeded AI DPPs to prevent duplicates
|
||||
statement = select(models.Test).where(models.Test.title.like("AI DPP:%"))
|
||||
old_tests = session.exec(statement).all()
|
||||
for t in old_tests:
|
||||
session.delete(t)
|
||||
session.commit()
|
||||
|
||||
# Realistic Mock Data
|
||||
mock_data = [
|
||||
{
|
||||
"topic": "Rotational Dynamics",
|
||||
"subject": "Physics",
|
||||
"difficulty": "Hard",
|
||||
"questions": [
|
||||
{
|
||||
"question_text": r"A solid sphere of mass $M$ and radius $R$ is rolling without slipping on a horizontal surface. Its center of mass is moving with velocity $v$. What is its total kinetic energy?",
|
||||
"options": {"A": r"$\frac{1}{2}Mv^2$", "B": r"$\frac{7}{10}Mv^2$", "C": r"$\frac{5}{7}Mv^2$", "D": r"$\frac{3}{4}Mv^2$"},
|
||||
"correct_answer": {"value": "B"},
|
||||
"solution_text": r"Total KE = Translational KE + Rotational KE. $KE_{tot} = \frac{1}{2}Mv^2 + \frac{1}{2}I\omega^2$. For solid sphere, $I = \frac{2}{5}MR^2$, and since it rolls without slipping, $\omega = v/R$. Thus, $KE_{tot} = \frac{1}{2}Mv^2 + \frac{1}{2}(\frac{2}{5}MR^2)(v/R)^2 = \frac{7}{10}Mv^2$."
|
||||
},
|
||||
{
|
||||
"question_text": r"A thin uniform rod of length $l$ and mass $m$ is suspended freely from one end. It is pulled aside through an angle $\theta$ and released. What is the angular velocity when it passes through the lowest point?",
|
||||
"options": {"A": r"$\sqrt{\frac{3g}{l}(1-\cos\theta)}$", "B": r"$\sqrt{\frac{g}{l}(1-\cos\theta)}$", "C": r"$\sqrt{\frac{2g}{l}(1-\cos\theta)}$", "D": r"$\sqrt{\frac{6g}{l}(1-\cos\theta)}$"},
|
||||
"correct_answer": {"value": "A"},
|
||||
"solution_text": r"By conservation of energy: $mgh_{cm} = \frac{1}{2}I\omega^2$. $h_{cm} = \frac{l}{2}(1-\cos\theta)$. $I = \frac{ml^2}{3}$. Therefore, $mg\frac{l}{2}(1-\cos\theta) = \frac{1}{2}(\frac{ml^2}{3})\omega^2 \implies \omega = \sqrt{\frac{3g}{l}(1-\cos\theta)}$."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"topic": "Chemical Kinetics",
|
||||
"subject": "Chemistry",
|
||||
"difficulty": "Medium",
|
||||
"questions": [
|
||||
{
|
||||
"question_text": r"For a first-order reaction $A \rightarrow P$, the rate constant is $k = 2.303 \times 10^{-3} s^{-1}$. What is the half-life ($t_{1/2}$) of this reaction?",
|
||||
"options": {"A": r"$300 s$", "B": r"$301 s$", "C": r"$69.3 s$", "D": r"$0.693 s$"},
|
||||
"correct_answer": {"value": "B"},
|
||||
"solution_text": r"For first-order reaction, $t_{1/2} = \frac{0.693}{k}$. Substituting $k$, $t_{1/2} = \frac{0.693}{2.303 \times 10^{-3}} \approx 301 s$."
|
||||
},
|
||||
{
|
||||
"question_text": r"If the rate of a reaction increases by a factor of $8$ when the concentration of reactant $X$ is doubled, what is the order of the reaction with respect to $X$?",
|
||||
"options": {"A": "1", "B": "2", "C": "3", "D": "0"},
|
||||
"correct_answer": {"value": "C"},
|
||||
"solution_text": r"Rate $R \propto [X]^n$. If $[X]$ is doubled, new rate $R' = 2^n \cdot R$. Given $R' = 8R$, so $2^n = 8 \implies n = 3$."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"topic": "Genetics",
|
||||
"subject": "Biology",
|
||||
"difficulty": "Easy",
|
||||
"questions": [
|
||||
{
|
||||
"question_text": r"In a Mendelian dihybrid cross, what is the phenotypic ratio of the F2 generation?",
|
||||
"options": {"A": "9:3:3:1", "B": "1:2:1", "C": "3:1", "D": "1:1:1:1"},
|
||||
"correct_answer": {"value": "A"},
|
||||
"solution_text": r"A standard dihybrid cross between two heterozygotes ($AaBb \times AaBb$) yields offspring with phenotypes in a 9:3:3:1 ratio."
|
||||
},
|
||||
{
|
||||
"question_text": r"Which of the following describes the condition where both alleles of a gene pair in a heterozygote are fully expressed?",
|
||||
"options": {"A": "Incomplete Dominance", "B": "Codominance", "C": "Pleiotropy", "D": "Epistasis"},
|
||||
"correct_answer": {"value": "B"},
|
||||
"solution_text": r"Codominance is the phenomenon where both alleles are simultaneously expressed in the heterozygote, such as in AB blood type."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"topic": "Calculus",
|
||||
"subject": "Mathematics",
|
||||
"difficulty": "Hard",
|
||||
"questions": [
|
||||
{
|
||||
"question_text": r"Evaluate the integral $\int_0^{\pi/2} \frac{\sqrt{\sin x}}{\sqrt{\sin x} + \sqrt{\cos x}} dx$.",
|
||||
"options": {"A": r"$\pi/4$", "B": r"$\pi/2$", "C": r"$\pi$", "D": r"$0$"},
|
||||
"correct_answer": {"value": "A"},
|
||||
"solution_text": r"Let $I = \int_0^{\pi/2} \frac{\sqrt{\sin x}}{\sqrt{\sin x} + \sqrt{\cos x}} dx$. Using the property $\int_0^a f(x) dx = \int_0^a f(a-x) dx$, we get $I = \int_0^{\pi/2} \frac{\sqrt{\cos x}}{\sqrt{\cos x} + \sqrt{\sin x}} dx$. Adding both, $2I = \int_0^{\pi/2} 1 dx = \pi/2 \implies I = \pi/4$."
|
||||
},
|
||||
{
|
||||
"question_text": r"Find the limit $\lim_{x \to 0} \frac{e^x - 1 - x}{x^2}$.",
|
||||
"options": {"A": "1", "B": r"$1/2$", "C": "0", "D": "Does not exist"},
|
||||
"correct_answer": {"value": "B"},
|
||||
"solution_text": r"Using L'Hôpital's Rule twice: $\lim_{x \to 0} \frac{e^x - 1}{2x} = \lim_{x \to 0} \frac{e^x}{2} = \frac{1}{2}$."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
for idx, mock in enumerate(mock_data):
|
||||
question_ids = []
|
||||
for q_data in mock["questions"]:
|
||||
new_q = models.Question(
|
||||
subject=mock["subject"],
|
||||
topic=mock["topic"],
|
||||
subtopic="General",
|
||||
difficulty=mock["difficulty"],
|
||||
type=models.QuestionTypeEnum.MCQ_SINGLE,
|
||||
question_text=q_data["question_text"],
|
||||
options=q_data["options"],
|
||||
correct_answer=q_data["correct_answer"],
|
||||
solution_text=q_data["solution_text"],
|
||||
language=models.MediumEnum.ENGLISH,
|
||||
exam=models.ExamEnum.JEE_MAIN,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(new_q)
|
||||
session.flush()
|
||||
question_ids.append(str(new_q.id))
|
||||
|
||||
test_obj = models.Test(
|
||||
title=f"AI DPP: {mock['topic']}",
|
||||
type="DPS",
|
||||
exam_pattern=models.ExamEnum.JEE_MAIN,
|
||||
sections={
|
||||
"Section 1": {
|
||||
"questions": question_ids,
|
||||
"time_limit": 60
|
||||
}
|
||||
},
|
||||
total_time=60,
|
||||
difficulty=mock["difficulty"],
|
||||
scheduled_date=datetime.utcnow(),
|
||||
is_active=True,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(test_obj)
|
||||
session.commit()
|
||||
print(f"Successfully created DPS Test: {test_obj.title} with {len(question_ids)} questions.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_pdfs())
|
||||
@@ -1,83 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
import random
|
||||
from sqlmodel import Session, select, delete
|
||||
from datetime import datetime
|
||||
|
||||
from database import engine
|
||||
import models
|
||||
|
||||
def seed_tests():
|
||||
admin_id_str = "b0e17378-864d-439e-8806-708dc48a891b"
|
||||
|
||||
with Session(engine) as session:
|
||||
admin = session.exec(select(models.User).limit(1)).first()
|
||||
admin_id = admin.id if admin else uuid.UUID(admin_id_str)
|
||||
|
||||
# Clear existing DPS tests
|
||||
statement = select(models.Test).where(models.Test.type == "DPS")
|
||||
old_tests = session.exec(statement).all()
|
||||
for t in old_tests:
|
||||
session.delete(t)
|
||||
session.commit()
|
||||
|
||||
subjects = [
|
||||
("Physics", "Laws of Motion", "dps-phy-ch4-20260707.pdf"),
|
||||
("Chemistry", "Chemical Thermodynamics", "dps-che-ch4-20260707.pdf"),
|
||||
("Mathematics", "Calculus", "dps-mth-ch4-20260707.pdf"),
|
||||
("Biology", "Genetics", "dps-bio-ch4-20260707.pdf")
|
||||
]
|
||||
|
||||
for exam in [models.ExamEnum.JEE_MAIN, models.ExamEnum.JEE_ADVANCED, models.ExamEnum.NEET]:
|
||||
for subject, topic, pdf in subjects:
|
||||
# Math for NEET? Biology for JEE? Let's just create them anyway so the UI is populated
|
||||
question_ids = []
|
||||
for i in range(1, 16):
|
||||
new_q = models.Question(
|
||||
subject=subject,
|
||||
topic=topic,
|
||||
subtopic="General",
|
||||
difficulty="Medium",
|
||||
type=models.QuestionTypeEnum.MCQ_SINGLE,
|
||||
question_text=f"Sample mock question {i} for {subject} - {topic}",
|
||||
options={
|
||||
"A": "Option A",
|
||||
"B": "Option B",
|
||||
"C": "Option C",
|
||||
"D": "Option D"
|
||||
},
|
||||
correct_answer={"value": "A"},
|
||||
solution_text="Solution provided here.",
|
||||
language=models.MediumEnum.ENGLISH,
|
||||
exam=exam,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(new_q)
|
||||
session.flush()
|
||||
question_ids.append(str(new_q.id))
|
||||
|
||||
test_obj = models.Test(
|
||||
title=f"{subject} Chapter 4 DPS ({exam})",
|
||||
type="DPS",
|
||||
exam_pattern=exam,
|
||||
sections={
|
||||
"Section 1": {
|
||||
"questions": question_ids,
|
||||
"time_limit": 30
|
||||
}
|
||||
},
|
||||
total_time=30,
|
||||
difficulty="Medium",
|
||||
scheduled_date=datetime.utcnow(),
|
||||
is_active=True,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(test_obj)
|
||||
print(f"Created Test: {test_obj.title}")
|
||||
|
||||
session.commit()
|
||||
print("Database seeded with mock CBT DPPs for all exams.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_tests()
|
||||
@@ -1,450 +0,0 @@
|
||||
"""
|
||||
Real DPP Question Seeder - Seeds Physics, Chemistry, Math, Biology DPPs
|
||||
with actual JEE/NEET standard questions as type=DPS (PrepPilot AI)
|
||||
and a separate Mock CBT test as type=CBT (Mock Tests section)
|
||||
"""
|
||||
import uuid
|
||||
from sqlmodel import Session, select
|
||||
from datetime import datetime
|
||||
from database import engine
|
||||
import models
|
||||
|
||||
PHYSICS_QUESTIONS = [
|
||||
{
|
||||
"text": "A body of mass 5 kg is acted upon by two perpendicular forces 8 N and 6 N. The magnitude and direction of the resultant with respect to the 6 N force is:",
|
||||
"options": {"A": "10 N at 53°", "B": "10 N at 37°", "C": "8 N at 45°", "D": "14 N at 0°"},
|
||||
"answer": "A",
|
||||
"solution": "Resultant R = √(8² + 6²) = √(64+36) = √100 = 10 N. tan θ = 8/6 = 4/3 → θ = 53° from the 6N force."
|
||||
},
|
||||
{
|
||||
"text": "A ball is thrown vertically upward with velocity 20 m/s. The maximum height reached is: (g = 10 m/s²)",
|
||||
"options": {"A": "10 m", "B": "20 m", "C": "40 m", "D": "5 m"},
|
||||
"answer": "B",
|
||||
"solution": "Using v² = u² - 2gh. At max height v=0. h = u²/2g = (20)²/(2×10) = 400/20 = 20 m."
|
||||
},
|
||||
{
|
||||
"text": "The ratio of radii of gyration of a circular disc to that of a circular ring, each of the same mass and radius, about their axes passing through their centres and perpendicular to their planes is:",
|
||||
"options": {"A": "√2 : 1", "B": "1 : √2", "C": "1 : 2", "D": "2 : 1"},
|
||||
"answer": "B",
|
||||
"solution": "I_disc = MR²/2, I_ring = MR². k_disc = R/√2, k_ring = R. Ratio = (1/√2) : 1 = 1 : √2."
|
||||
},
|
||||
{
|
||||
"text": "Two charges of +4μC and -4μC are placed at (3, 0) and (-3, 0) cm. The electric field at origin due to this dipole is:",
|
||||
"options": {"A": "4 × 10⁸ N/C along +x", "B": "4 × 10⁸ N/C along -x", "C": "8 × 10⁸ N/C along +x", "D": "Zero"},
|
||||
"answer": "A",
|
||||
"solution": "Each charge contributes E = kq/r² = (9×10⁹ × 4×10⁻⁶)/(0.03)² = 4×10⁷ N/C. Both fields point in +x direction. Total = 8×10⁷ N/C along +x."
|
||||
},
|
||||
{
|
||||
"text": "A wire of length L and resistance R is uniformly stretched to double its length. New resistance is:",
|
||||
"options": {"A": "R/2", "B": "R", "C": "2R", "D": "4R"},
|
||||
"answer": "D",
|
||||
"solution": "ρ is constant. Volume = AL = constant. New length L' = 2L → new area A' = A/2. R' = ρ(2L)/(A/2) = 4ρL/A = 4R."
|
||||
},
|
||||
{
|
||||
"text": "In Young's double slit experiment, the slits are 0.5 mm apart and interference fringes are observed on a screen 1.0 m away. It is found that the 9th bright fringe is at a distance of 8.835 mm from the 2nd dark fringe. Find the wavelength of light:",
|
||||
"options": {"A": "600 nm", "B": "589 nm", "C": "550 nm", "D": "500 nm"},
|
||||
"answer": "B",
|
||||
"solution": "Distance = (9+0.5-2+0.5)β = 8β = 8 × λD/d. 8.835 mm = 8λ(1)/(0.5×10⁻³). λ = 8.835×10⁻³ × 0.5×10⁻³/8 = 589 nm."
|
||||
},
|
||||
{
|
||||
"text": "The half life of a radioactive element is 3 hours. After 12 hours the fraction of initial amount remaining will be:",
|
||||
"options": {"A": "1/16", "B": "1/8", "C": "1/4", "D": "1/2"},
|
||||
"answer": "A",
|
||||
"solution": "Number of half-lives = 12/3 = 4. Fraction remaining = (1/2)^4 = 1/16."
|
||||
},
|
||||
{
|
||||
"text": "A solid sphere of radius R has a charge Q uniformly distributed over its volume. The magnitude of electric field at distance r from the center (r < R) is:",
|
||||
"options": {"A": "Q/(4πε₀R²)", "B": "Qr/(4πε₀R³)", "C": "Q/(4πε₀r²)", "D": "Zero"},
|
||||
"answer": "B",
|
||||
"solution": "By Gauss's law, E(4πr²) = Q_enc/ε₀ = (Q/R³)r³/ε₀. E = Qr/(4πε₀R³)."
|
||||
},
|
||||
{
|
||||
"text": "The kinetic energy of a satellite orbiting at height h above Earth's surface is (mass of satellite m, radius R, mass of Earth M):",
|
||||
"options": {"A": "GMm/(2(R+h))", "B": "GMm/(R+h)", "C": "-GMm/(2(R+h))", "D": "GMm/R"},
|
||||
"answer": "A",
|
||||
"solution": "For circular orbit: mv²/(R+h) = GMm/(R+h)². KE = mv²/2 = GMm/(2(R+h))."
|
||||
},
|
||||
{
|
||||
"text": "A particle executes SHM with amplitude A. The displacement at which kinetic energy equals potential energy is:",
|
||||
"options": {"A": "A/2", "B": "A/√2", "C": "A√2", "D": "2A"},
|
||||
"answer": "B",
|
||||
"solution": "KE = PE → (1/2)mω²(A²-x²) = (1/2)mω²x². A²-x² = x². x² = A²/2. x = A/√2."
|
||||
},
|
||||
{
|
||||
"text": "In Compton scattering, the change in wavelength is maximum when the photon is scattered through angle:",
|
||||
"options": {"A": "0°", "B": "45°", "C": "90°", "D": "180°"},
|
||||
"answer": "D",
|
||||
"solution": "Compton shift Δλ = (h/mₑc)(1-cosθ). Maximum when cosθ = -1 i.e., θ = 180°. Max Δλ = 2h/mₑc = 0.0485 Å."
|
||||
},
|
||||
{
|
||||
"text": "A capacitor of capacitance C is connected to a battery of EMF E through a resistance R. The time constant of the circuit is:",
|
||||
"options": {"A": "RC", "B": "R/C", "C": "C/R", "D": "1/RC"},
|
||||
"answer": "A",
|
||||
"solution": "The time constant τ = RC for a series RC circuit. It represents the time for the capacitor to charge to 63.2% of the final value."
|
||||
},
|
||||
{
|
||||
"text": "Which electromagnetic radiation has the highest penetrating power?",
|
||||
"options": {"A": "UV rays", "B": "X-rays", "C": "Gamma rays", "D": "Infrared rays"},
|
||||
"answer": "C",
|
||||
"solution": "Gamma rays have the highest penetrating power due to their extremely short wavelength and high energy. They can penetrate through several cm of lead."
|
||||
},
|
||||
{
|
||||
"text": "A bar magnet of magnetic moment M is placed in a uniform magnetic field B. The torque on the magnet when it makes angle 30° with the field is:",
|
||||
"options": {"A": "MB/2", "B": "MB√3/2", "C": "MB", "D": "MB√3"},
|
||||
"answer": "A",
|
||||
"solution": "τ = MBsinθ = MBsin30° = MB × (1/2) = MB/2."
|
||||
},
|
||||
{
|
||||
"text": "The de Broglie wavelength associated with a proton moving with velocity v is λ. For an alpha particle moving with the same speed, the de Broglie wavelength will be:",
|
||||
"options": {"A": "λ/4", "B": "λ/2", "C": "2λ", "D": "4λ"},
|
||||
"answer": "A",
|
||||
"solution": "λ = h/mv. α-particle mass = 4mₚ. λ_α = h/(4mₚv) = λ_p/4."
|
||||
},
|
||||
]
|
||||
|
||||
CHEMISTRY_QUESTIONS = [
|
||||
{
|
||||
"text": "The hybridization of carbon in diamond and graphite respectively is:",
|
||||
"options": {"A": "sp², sp³", "B": "sp³, sp²", "C": "sp, sp²", "D": "sp³, sp"},
|
||||
"answer": "B",
|
||||
"solution": "Diamond: each C is bonded to 4 others in tetrahedral arrangement → sp³. Graphite: each C is bonded to 3 others in trigonal planar → sp²."
|
||||
},
|
||||
{
|
||||
"text": "The equilibrium constant Kp for the reaction N₂(g) + 3H₂(g) ⇌ 2NH₃(g) is related to Kc by:",
|
||||
"options": {"A": "Kp = Kc(RT)²", "B": "Kp = Kc(RT)⁻²", "C": "Kp = Kc(RT)²", "D": "Kp = Kc"},
|
||||
"answer": "B",
|
||||
"solution": "Δn = 2 - (1+3) = -2. Kp = Kc(RT)^Δn = Kc(RT)^(-2)."
|
||||
},
|
||||
{
|
||||
"text": "The IUPAC name of the compound CH₃-CH(Cl)-CH₂-CH₃ is:",
|
||||
"options": {"A": "1-chlorobutane", "B": "2-chlorobutane", "C": "3-chlorobutane", "D": "Sec-butyl chloride"},
|
||||
"answer": "B",
|
||||
"solution": "The longest chain has 4 carbons (butane). Cl is on carbon 2. IUPAC name: 2-chlorobutane."
|
||||
},
|
||||
{
|
||||
"text": "Which of the following has the highest lattice energy?",
|
||||
"options": {"A": "NaCl", "B": "KCl", "C": "MgO", "D": "NaF"},
|
||||
"answer": "C",
|
||||
"solution": "Lattice energy ∝ (charge of ions)²/distance. MgO has doubly charged ions (Mg²⁺ and O²⁻) giving it much higher lattice energy than singly-charged ion compounds."
|
||||
},
|
||||
{
|
||||
"text": "The pH of 0.001 M H₂SO₄ solution is:",
|
||||
"options": {"A": "3", "B": "2.7", "C": "2", "D": "1"},
|
||||
"answer": "B",
|
||||
"solution": "H₂SO₄ is a strong diprotic acid. [H⁺] = 2 × 0.001 = 0.002 M. pH = -log(0.002) = -log(2×10⁻³) = 3 - log2 = 3 - 0.301 ≈ 2.7."
|
||||
},
|
||||
{
|
||||
"text": "Which of the following undergoes SN1 reaction most readily?",
|
||||
"options": {"A": "CH₃Cl", "B": "CH₃CH₂Cl", "C": "(CH₃)₂CHCl", "D": "(CH₃)₃CCl"},
|
||||
"answer": "D",
|
||||
"solution": "SN1 rate depends on carbocation stability. Tertiary carbocations are most stable due to hyperconjugation and inductive effect of alkyl groups. (CH₃)₃C⁺ is tertiary."
|
||||
},
|
||||
{
|
||||
"text": "The oxidation state of Cr in K₂Cr₂O₇ is:",
|
||||
"options": {"A": "+3", "B": "+4", "C": "+6", "D": "+7"},
|
||||
"answer": "C",
|
||||
"solution": "Let Cr be x. 2(+1) + 2x + 7(-2) = 0 → 2 + 2x - 14 = 0 → 2x = 12 → x = +6."
|
||||
},
|
||||
{
|
||||
"text": "Which quantum number determines the orientation of an orbital in space?",
|
||||
"options": {"A": "Principal (n)", "B": "Azimuthal (l)", "C": "Magnetic (mₗ)", "D": "Spin (mₛ)"},
|
||||
"answer": "C",
|
||||
"solution": "The magnetic quantum number (mₗ) determines the orientation of the orbital in three-dimensional space, ranging from -l to +l."
|
||||
},
|
||||
{
|
||||
"text": "The correct order of boiling points of hydrides of group 16 elements is:",
|
||||
"options": {"A": "H₂O > H₂S > H₂Se > H₂Te", "B": "H₂Te > H₂Se > H₂S > H₂O", "C": "H₂O > H₂Te > H₂Se > H₂S", "D": "H₂S > H₂O > H₂Se > H₂Te"},
|
||||
"answer": "C",
|
||||
"solution": "H₂O has highest BP due to hydrogen bonding. Among H₂S, H₂Se, H₂Te: BP increases with molar mass (van der Waals forces). So H₂O > H₂Te > H₂Se > H₂S."
|
||||
},
|
||||
{
|
||||
"text": "Identify the product formed when acetaldehyde reacts with excess HCN followed by hydrolysis:",
|
||||
"options": {"A": "Acetic acid", "B": "Lactic acid", "C": "Pyruvic acid", "D": "Glycolic acid"},
|
||||
"answer": "B",
|
||||
"solution": "CH₃CHO + HCN → CH₃CH(OH)CN (cyanohydrin). On hydrolysis: CH₃CH(OH)COOH = lactic acid (2-hydroxypropanoic acid)."
|
||||
},
|
||||
{
|
||||
"text": "The van't Hoff factor 'i' for a 0.01 M solution of K₂SO₄ that is 50% dissociated is:",
|
||||
"options": {"A": "1.5", "B": "2.0", "C": "2.5", "D": "3.0"},
|
||||
"answer": "B",
|
||||
"solution": "K₂SO₄ → 2K⁺ + SO₄²⁻ (3 ions). For 50% dissociation: i = 1 + α(n-1) = 1 + 0.5(3-1) = 1 + 1 = 2.0."
|
||||
},
|
||||
{
|
||||
"text": "Which of the following is NOT a property of transition metals?",
|
||||
"options": {"A": "They show variable oxidation states", "B": "They form colored compounds", "C": "They have low melting points", "D": "They act as catalysts"},
|
||||
"answer": "C",
|
||||
"solution": "Transition metals generally have high melting points due to strong metallic bonds involving d electrons. Options A, B, and D are characteristic properties of transition metals."
|
||||
},
|
||||
{
|
||||
"text": "The reaction of glucose with excess acetic anhydride gives:",
|
||||
"options": {"A": "Glucose diacetate", "B": "Glucose triacetate", "C": "Glucose pentaacetate", "D": "Glucose hexaacetate"},
|
||||
"answer": "C",
|
||||
"solution": "Glucose has 5 -OH groups (1 anomeric + 4 secondary). All react with acetic anhydride giving glucose pentaacetate."
|
||||
},
|
||||
{
|
||||
"text": "The number of σ and π bonds in benzene molecule are:",
|
||||
"options": {"A": "6σ, 3π", "B": "12σ, 3π", "C": "6σ, 6π", "D": "9σ, 3π"},
|
||||
"answer": "B",
|
||||
"solution": "C₆H₆: 6 C-C bonds (σ) + 6 C-H bonds (σ) = 12 σ bonds. 3 π bonds in alternate double bonds. Total: 12σ and 3π."
|
||||
},
|
||||
{
|
||||
"text": "Le Chatelier's principle predicts that increasing pressure will favor formation of products in:",
|
||||
"options": {"A": "N₂ + 3H₂ ⇌ 2NH₃", "B": "2SO₃ ⇌ 2SO₂ + O₂", "C": "H₂ + I₂ ⇌ 2HI", "D": "N₂ + O₂ ⇌ 2NO"},
|
||||
"answer": "A",
|
||||
"solution": "Increasing pressure favors the reaction with fewer moles of gas. N₂+3H₂→2NH₃: reactants have 4 mol gas, products have 2. Pressure increase shifts equilibrium right."
|
||||
},
|
||||
]
|
||||
|
||||
MATH_QUESTIONS = [
|
||||
{
|
||||
"text": "If f(x) = x² - 3x + 2, then f(f(x)) = 0 has how many solutions?",
|
||||
"options": {"A": "2", "B": "3", "C": "4", "D": "6"},
|
||||
"answer": "C",
|
||||
"solution": "f(x) = (x-1)(x-2). f(f(x)) = 0 → f(x) = 1 or f(x) = 2. f(x)=1: x²-3x+1=0, D=5>0 → 2 roots. f(x)=2: x²-3x=0, x(x-3)=0 → 2 roots. Total 4 roots."
|
||||
},
|
||||
{
|
||||
"text": "The sum of the series 1 + 2/3 + 4/9 + 8/27 + ... to infinity is:",
|
||||
"options": {"A": "1/3", "B": "2/3", "C": "3", "D": "3/2"},
|
||||
"answer": "C",
|
||||
"solution": "GP with a=1, r=2/3. Sum = a/(1-r) = 1/(1-2/3) = 1/(1/3) = 3."
|
||||
},
|
||||
{
|
||||
"text": "If A = [[1,2],[3,4]], then det(2A) = ?",
|
||||
"options": {"A": "-4", "B": "-8", "C": "-16", "D": "-2"},
|
||||
"answer": "C",
|
||||
"solution": "det(2A) = 2ⁿ × det(A) where n=2. det(A) = 4-6 = -2. det(2A) = 4 × (-2) = -8. Wait: det(2A) = 2² × det(A) = 4 × (-2) = -8. Correction: answer is B."
|
||||
},
|
||||
{
|
||||
"text": "The derivative of sin(x²) with respect to x is:",
|
||||
"options": {"A": "cos(x²)", "B": "2x·cos(x²)", "C": "2cos(x)", "D": "x·cos(x²)"},
|
||||
"answer": "B",
|
||||
"solution": "Using chain rule: d/dx[sin(x²)] = cos(x²) · d/dx(x²) = cos(x²) · 2x = 2x·cos(x²)."
|
||||
},
|
||||
{
|
||||
"text": "The area enclosed between y = x² and y = x is:",
|
||||
"options": {"A": "1/6", "B": "1/3", "C": "1/2", "D": "1"},
|
||||
"answer": "A",
|
||||
"solution": "Intersection at x=0 and x=1. Area = ∫₀¹(x - x²)dx = [x²/2 - x³/3]₀¹ = 1/2 - 1/3 = 1/6."
|
||||
},
|
||||
{
|
||||
"text": "The probability of getting exactly 2 heads in 5 tosses of a fair coin is:",
|
||||
"options": {"A": "5/16", "B": "1/4", "C": "5/32", "D": "10/32"},
|
||||
"answer": "D",
|
||||
"solution": "P = C(5,2) × (1/2)² × (1/2)³ = 10 × 1/32 = 10/32 = 5/16. Answer matches both A and D. Simplified: 5/16."
|
||||
},
|
||||
{
|
||||
"text": "The locus of a point equidistant from the points (1, 2) and (3, 4) is:",
|
||||
"options": {"A": "x + y = 5", "B": "x + y - 5 = 0", "C": "x - y = 0", "D": "2x + 2y - 9 = 0"},
|
||||
"answer": "B",
|
||||
"solution": "Midpoint = (2, 3). Slope of line joining = (4-2)/(3-1) = 1. Perpendicular bisector slope = -1. Equation: y-3 = -1(x-2) → x+y = 5."
|
||||
},
|
||||
{
|
||||
"text": "Value of lim(x→0) (sin 3x)/(5x) is:",
|
||||
"options": {"A": "3/5", "B": "5/3", "C": "1", "D": "0"},
|
||||
"answer": "A",
|
||||
"solution": "lim(x→0) sin(3x)/5x = lim(x→0) sin(3x)/(3x) × (3x/5x) = 1 × 3/5 = 3/5."
|
||||
},
|
||||
{
|
||||
"text": "The general solution of dy/dx = e^(x+y) is:",
|
||||
"options": {"A": "e^x + e^(-y) = C", "B": "e^(-y) = -e^x + C", "C": "e^x + e^y = C", "D": "e^(-x) + e^y = C"},
|
||||
"answer": "B",
|
||||
"solution": "Separating: e^(-y)dy = e^x dx. Integrating: -e^(-y) = e^x + C₁. So e^(-y) = -e^x + C where C = -C₁."
|
||||
},
|
||||
{
|
||||
"text": "If the vectors a = 2i + 3j - k and b = i - 2j + 3k are perpendicular, then what is the value of the dot product?",
|
||||
"options": {"A": "0", "B": "Not perpendicular; a·b = -7", "C": "5", "D": "11"},
|
||||
"answer": "B",
|
||||
"solution": "a·b = (2)(1) + (3)(-2) + (-1)(3) = 2 - 6 - 3 = -7 ≠ 0. So vectors are not perpendicular. The dot product is -7."
|
||||
},
|
||||
{
|
||||
"text": "The number of ways 6 books can be arranged on a shelf if 2 specific books must always be together:",
|
||||
"options": {"A": "120", "B": "240", "C": "480", "D": "720"},
|
||||
"answer": "B",
|
||||
"solution": "Treat 2 books as 1 unit: 5 units to arrange = 5! = 120 ways. The 2 books within unit can arrange in 2! = 2 ways. Total = 120 × 2 = 240."
|
||||
},
|
||||
{
|
||||
"text": "The equation of the tangent to the circle x² + y² = 25 at point (3, 4) is:",
|
||||
"options": {"A": "3x + 4y = 25", "B": "4x + 3y = 25", "C": "3x - 4y = 25", "D": "x + y = 7"},
|
||||
"answer": "A",
|
||||
"solution": "Tangent to circle x²+y²=r² at (x₁,y₁) is xx₁+yy₁=r². So tangent at (3,4) is 3x+4y=25."
|
||||
},
|
||||
{
|
||||
"text": "Evaluate ∫ (2x + 3)/(x² + 3x + 2) dx:",
|
||||
"options": {"A": "ln|x+1| + ln|x+2| + C", "B": "ln|x² + 3x + 2| + C", "C": "2ln|x+1| + C", "D": "ln|(x+1)(x+2)| + C"},
|
||||
"answer": "B",
|
||||
"solution": "Note numerator 2x+3 = d/dx(x²+3x+2). So integral = ln|x²+3x+2| + C. This equals ln|(x+1)(x+2)| + C, same as D."
|
||||
},
|
||||
{
|
||||
"text": "The value of ∑(k=1 to 10) k³ is:",
|
||||
"options": {"A": "3025", "B": "385", "C": "55", "D": "1210"},
|
||||
"answer": "A",
|
||||
"solution": "∑k³ = [n(n+1)/2]² = [10×11/2]² = 55² = 3025."
|
||||
},
|
||||
{
|
||||
"text": "If z = 1 + i, then |z²| is:",
|
||||
"options": {"A": "1", "B": "√2", "C": "2", "D": "4"},
|
||||
"answer": "C",
|
||||
"solution": "|z| = √(1²+1²) = √2. |z²| = |z|² = (√2)² = 2."
|
||||
},
|
||||
]
|
||||
|
||||
BIOLOGY_QUESTIONS = [
|
||||
{
|
||||
"text": "Which organelle is called the 'powerhouse of the cell'?",
|
||||
"options": {"A": "Ribosome", "B": "Mitochondria", "C": "Golgi apparatus", "D": "Lysosome"},
|
||||
"answer": "B",
|
||||
"solution": "Mitochondria produce ATP through cellular respiration (oxidative phosphorylation), providing energy for cellular activities. Hence called the powerhouse."
|
||||
},
|
||||
{
|
||||
"text": "The process by which organisms maintain internal equilibrium is called:",
|
||||
"options": {"A": "Osmoregulation", "B": "Homeostasis", "C": "Thermoregulation", "D": "Excretion"},
|
||||
"answer": "B",
|
||||
"solution": "Homeostasis is the ability of an organism to maintain a stable internal environment (temperature, pH, glucose levels) despite external changes."
|
||||
},
|
||||
{
|
||||
"text": "The full form of DNA is:",
|
||||
"options": {"A": "Deoxyribonucleic Acid", "B": "Dioxyribonucleic Acid", "C": "Diribonucleic Acid", "D": "Deoxyribose Nucleic Acid"},
|
||||
"answer": "A",
|
||||
"solution": "DNA = Deoxyribonucleic Acid. It contains deoxyribose sugar, phosphate groups, and nitrogenous bases (A, T, G, C)."
|
||||
},
|
||||
{
|
||||
"text": "Which blood group is called the universal donor?",
|
||||
"options": {"A": "A+", "B": "B+", "C": "AB+", "D": "O-"},
|
||||
"answer": "D",
|
||||
"solution": "O- blood type has no A, B antigens and no Rh factor. So it can be donated to any blood group without immune reaction."
|
||||
},
|
||||
{
|
||||
"text": "Meiosis is essential for:",
|
||||
"options": {"A": "Growth and repair", "B": "Asexual reproduction", "C": "Maintaining chromosome number across generations", "D": "Cell differentiation"},
|
||||
"answer": "C",
|
||||
"solution": "Meiosis reduces chromosome number by half during gamete formation. When gametes fuse at fertilization, the diploid number is restored."
|
||||
},
|
||||
{
|
||||
"text": "The site of photosynthesis in plant cells is:",
|
||||
"options": {"A": "Nucleus", "B": "Mitochondria", "C": "Chloroplast", "D": "Ribosome"},
|
||||
"answer": "C",
|
||||
"solution": "Chloroplasts contain chlorophyll and are the sites of photosynthesis. They have thylakoid membranes for light reactions and stroma for Calvin cycle."
|
||||
},
|
||||
{
|
||||
"text": "Which of the following is an example of a vestigial organ in humans?",
|
||||
"options": {"A": "Liver", "B": "Appendix", "C": "Pancreas", "D": "Spleen"},
|
||||
"answer": "B",
|
||||
"solution": "The vermiform appendix is a vestigial organ in humans - it was functional in ancestors who digested cellulose but has reduced to a non-functional remnant."
|
||||
},
|
||||
{
|
||||
"text": "The Krebs cycle occurs in the:",
|
||||
"options": {"A": "Cytoplasm", "B": "Nucleus", "C": "Mitochondrial matrix", "D": "Chloroplast stroma"},
|
||||
"answer": "C",
|
||||
"solution": "The Krebs (citric acid) cycle occurs in the matrix of mitochondria. It oxidizes acetyl-CoA and generates NADH, FADH₂, and ATP."
|
||||
},
|
||||
{
|
||||
"text": "Darwin's theory of natural selection states that organisms with favorable variations:",
|
||||
"options": {"A": "Produce more offspring", "B": "Have better survival and reproduction", "C": "Are immune to diseases", "D": "Live longer"},
|
||||
"answer": "B",
|
||||
"solution": "Darwin proposed that organisms with traits better suited to their environment have higher fitness - better survival and reproductive success - passing traits to offspring."
|
||||
},
|
||||
{
|
||||
"text": "The enzyme responsible for DNA replication is:",
|
||||
"options": {"A": "RNA polymerase", "B": "DNA polymerase", "C": "Ligase", "D": "Helicase"},
|
||||
"answer": "B",
|
||||
"solution": "DNA polymerase synthesizes new DNA strands using the original strand as template. It adds nucleotides in 5'→3' direction."
|
||||
},
|
||||
{
|
||||
"text": "Which of these hormones is secreted by the adrenal medulla?",
|
||||
"options": {"A": "Cortisol", "B": "Aldosterone", "C": "Adrenaline", "D": "Testosterone"},
|
||||
"answer": "C",
|
||||
"solution": "Adrenal medulla secretes catecholamines: adrenaline (epinephrine) and noradrenaline. Cortisol and aldosterone are from adrenal cortex."
|
||||
},
|
||||
{
|
||||
"text": "The Rh factor is named after which organism?",
|
||||
"options": {"A": "Rattus norvegicus", "B": "Rhesus monkey", "C": "Rhinoceros", "D": "Rabbit"},
|
||||
"answer": "B",
|
||||
"solution": "The Rh (Rhesus) factor antigen was first discovered in Rhesus monkeys and was found to be present on human red blood cells."
|
||||
},
|
||||
{
|
||||
"text": "Which of the following is a genetic disease?",
|
||||
"options": {"A": "Malaria", "B": "Tuberculosis", "C": "Sickle cell anemia", "D": "Cholera"},
|
||||
"answer": "C",
|
||||
"solution": "Sickle cell anemia is caused by a mutation in the HBB gene encoding the β-globin protein, inherited in autosomal recessive pattern."
|
||||
},
|
||||
{
|
||||
"text": "The basic unit of heredity is:",
|
||||
"options": {"A": "Chromosome", "B": "Nucleotide", "C": "Gene", "D": "Codon"},
|
||||
"answer": "C",
|
||||
"solution": "A gene is the functional unit of heredity - a specific sequence of DNA that encodes for a protein or functional RNA molecule."
|
||||
},
|
||||
{
|
||||
"text": "Transpiration in plants occurs mainly through:",
|
||||
"options": {"A": "Roots", "B": "Stomata", "C": "Lenticels", "D": "Xylem"},
|
||||
"answer": "B",
|
||||
"solution": "About 90-95% of transpiration occurs through stomata (stomatal transpiration). Lenticels and cuticle account for remaining 5-10%."
|
||||
},
|
||||
]
|
||||
|
||||
def seed_real_dpps():
|
||||
with Session(engine) as session:
|
||||
# Get or use system admin
|
||||
admin = session.exec(select(models.User).limit(1)).first()
|
||||
admin_id = admin.id if admin else uuid.UUID("11111111-1111-1111-1111-111111111111")
|
||||
|
||||
# Remove existing DPS tests
|
||||
existing = session.exec(select(models.Test).where(models.Test.type == "DPS")).all()
|
||||
for t in existing:
|
||||
session.delete(t)
|
||||
session.commit()
|
||||
print(f"Cleared {len(existing)} old DPS tests.")
|
||||
|
||||
subjects_data = [
|
||||
("Physics", "Laws of Motion & Mechanics", PHYSICS_QUESTIONS, models.ExamEnum.JEE_MAIN, "dps-phy-ch4-20260707.pdf"),
|
||||
("Chemistry", "Chemical Thermodynamics & Bonds", CHEMISTRY_QUESTIONS, models.ExamEnum.JEE_MAIN, "dps-che-ch4-20260707.pdf"),
|
||||
("Mathematics", "Calculus, Algebra & Geometry", MATH_QUESTIONS, models.ExamEnum.JEE_MAIN, "dps-mth-ch4-20260707.pdf"),
|
||||
("Biology", "Cell Biology & Genetics", BIOLOGY_QUESTIONS, models.ExamEnum.NEET, "dps-bio-ch4-20260707.pdf"),
|
||||
]
|
||||
|
||||
for subject, topic, questions, exam, pdf in subjects_data:
|
||||
question_ids = []
|
||||
for q in questions:
|
||||
new_q = models.Question(
|
||||
subject=subject,
|
||||
topic=topic,
|
||||
subtopic="Chapter 4",
|
||||
difficulty="Medium",
|
||||
type=models.QuestionTypeEnum.MCQ_SINGLE,
|
||||
question_text=q["text"],
|
||||
options=q["options"],
|
||||
correct_answer={"value": q["answer"]},
|
||||
solution_text=q["solution"],
|
||||
language=models.MediumEnum.ENGLISH,
|
||||
exam=exam,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(new_q)
|
||||
session.flush()
|
||||
question_ids.append(str(new_q.id))
|
||||
|
||||
# Create the DPS test (PrepPilot AI section)
|
||||
clean_exam_name = exam.value.replace("_", " ")
|
||||
test_obj = models.Test(
|
||||
title=f"{subject} Chapter 4 DPP",
|
||||
type="DPS",
|
||||
exam_pattern=exam,
|
||||
sections={
|
||||
"Section 1": {
|
||||
"questions": question_ids,
|
||||
"time_limit": 30
|
||||
}
|
||||
},
|
||||
total_time=30,
|
||||
difficulty="Medium",
|
||||
scheduled_date=datetime.utcnow(),
|
||||
is_active=True,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(test_obj)
|
||||
print(f"✓ Created DPS test: {subject} Chapter 4 DPP ({clean_exam_name}) with {len(questions)} real questions")
|
||||
|
||||
session.commit()
|
||||
print("\n✅ All real DPP tests seeded successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_real_dpps()
|
||||
@@ -1,138 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
import re
|
||||
import glob
|
||||
from sqlmodel import Session, select, delete
|
||||
from datetime import datetime
|
||||
|
||||
import fitz # PyMuPDF
|
||||
from database import engine, init_db
|
||||
import models
|
||||
|
||||
def parse_pdf(filepath):
|
||||
doc = fitz.open(filepath)
|
||||
text = ""
|
||||
for page in doc:
|
||||
text += page.get_text() + "\n"
|
||||
|
||||
q_block = text.split("Questions\n")[-1].split("Answer Key\n")[0]
|
||||
q_pattern = re.compile(r'^(\d+)\.\s+(.*?)(?=^\d+\.\s+|\Z)', re.DOTALL | re.MULTILINE)
|
||||
|
||||
questions_dict = {}
|
||||
for match in q_pattern.finditer(q_block):
|
||||
q_num = int(match.group(1))
|
||||
if 1 <= q_num <= 15:
|
||||
questions_dict[q_num] = match.group(2).strip()
|
||||
|
||||
answers_dict = {}
|
||||
if "Answer Key\n" in text:
|
||||
a_block = text.split("Answer Key\n")[-1]
|
||||
a_pattern = re.compile(r'^(\d+)\s+(.*?)(?=^\d+\s+|\Z)', re.DOTALL | re.MULTILINE)
|
||||
for match in a_pattern.finditer(a_block):
|
||||
q_num = int(match.group(1))
|
||||
if 1 <= q_num <= 15:
|
||||
# Some answers might get overwritten if footer matches the same number, but
|
||||
# answers are strictly linear 1 to 15, so we only take the first match.
|
||||
if q_num not in answers_dict:
|
||||
answers_dict[q_num] = match.group(2).strip()
|
||||
|
||||
return questions_dict, answers_dict
|
||||
|
||||
async def seed_pdfs():
|
||||
admin_id_str = "b0e17378-864d-439e-8806-708dc48a891b"
|
||||
pdf_files = glob.glob('/app/pdfs_to_parse/*.pdf')
|
||||
|
||||
if not pdf_files:
|
||||
print("No PDFs found in /app/pdfs_to_parse/")
|
||||
return
|
||||
|
||||
with Session(engine) as session:
|
||||
admin = session.exec(select(models.User).limit(1)).first()
|
||||
admin_id = admin.id if admin else uuid.UUID(admin_id_str)
|
||||
|
||||
statement = select(models.Test).where(models.Test.type == "DPS")
|
||||
old_tests = session.exec(statement).all()
|
||||
for t in old_tests:
|
||||
session.delete(t)
|
||||
session.commit()
|
||||
|
||||
for pdf_path in pdf_files:
|
||||
filename = os.path.basename(pdf_path)
|
||||
subject = "Physics"
|
||||
topic = "General"
|
||||
if "phy" in filename.lower():
|
||||
subject = "Physics"
|
||||
topic = "Laws of Motion"
|
||||
elif "che" in filename.lower():
|
||||
subject = "Chemistry"
|
||||
topic = "Chemical Bonding"
|
||||
elif "mth" in filename.lower():
|
||||
subject = "Mathematics"
|
||||
topic = "Calculus"
|
||||
elif "bio" in filename.lower():
|
||||
subject = "Biology"
|
||||
topic = "Genetics"
|
||||
|
||||
print(f"Parsing {filename}...")
|
||||
q_dict, a_dict = parse_pdf(pdf_path)
|
||||
print(f"Found {len(q_dict)} questions and {len(a_dict)} answers.")
|
||||
|
||||
if not q_dict:
|
||||
continue
|
||||
|
||||
question_ids = []
|
||||
for i in range(1, 16):
|
||||
if i not in q_dict:
|
||||
continue
|
||||
q_text = q_dict[i].replace('\x00', '')
|
||||
ans_text = a_dict.get(i, "Solution not provided in PDF.").replace('\x00', '')
|
||||
|
||||
options = {
|
||||
"A": "Correct Answer - Refer to Solution Key below",
|
||||
"B": "Incorrect Placeholder",
|
||||
"C": "Incorrect Placeholder",
|
||||
"D": "Incorrect Placeholder"
|
||||
}
|
||||
|
||||
new_q = models.Question(
|
||||
subject=subject,
|
||||
topic=topic,
|
||||
subtopic="General",
|
||||
difficulty="Medium",
|
||||
type=models.QuestionTypeEnum.NUMERICAL,
|
||||
question_text=q_text,
|
||||
options=None,
|
||||
correct_answer={"value": "Subjective input"},
|
||||
solution_text=ans_text,
|
||||
language=models.MediumEnum.ENGLISH,
|
||||
exam=models.ExamEnum.JEE_MAIN,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(new_q)
|
||||
session.flush()
|
||||
question_ids.append(str(new_q.id))
|
||||
|
||||
if question_ids:
|
||||
test_obj = models.Test(
|
||||
title=f"DPP: {subject} - {topic}",
|
||||
type="DPS",
|
||||
exam_pattern=models.ExamEnum.JEE_MAIN,
|
||||
sections={
|
||||
"Section 1": {
|
||||
"questions": question_ids,
|
||||
"time_limit": 45
|
||||
}
|
||||
},
|
||||
total_time=45,
|
||||
difficulty="Medium",
|
||||
scheduled_date=datetime.utcnow(),
|
||||
is_active=True,
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(test_obj)
|
||||
session.commit()
|
||||
print(f"Successfully created DPP Test: {test_obj.title} with {len(question_ids)} questions.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_pdfs())
|
||||
BIN
backend/test.pdf
BIN
backend/test.pdf
Binary file not shown.
Binary file not shown.
@@ -1,19 +1,23 @@
|
||||
"""
|
||||
worker.py — PragatiVeer Celery Background Tasks
|
||||
Active Tasks:
|
||||
- generate_daily_study_plans: Runs daily at 5:00 AM IST via Celery Beat
|
||||
|
||||
Deferred (Phase 2 — AI LLM):
|
||||
- process_pdf_test: PDF → LLM → Questions pipeline (stubbed in pdf_parser.py)
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
from sqlmodel import Session, select
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
from database import engine
|
||||
import models
|
||||
import storage
|
||||
from pdf_parser import parse_pdf_to_questions
|
||||
|
||||
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
|
||||
|
||||
# Initialize Celery connected to our local Redis broker
|
||||
# Initialize Celery connected to our Redis broker
|
||||
celery_app = Celery(
|
||||
"pragativeer_tasks",
|
||||
broker=REDIS_URL,
|
||||
@@ -29,8 +33,8 @@ celery_app.conf.update(
|
||||
@celery_app.task(name="generate_daily_study_plans", bind=True, default_retry_delay=60, max_retries=3)
|
||||
def generate_daily_study_plans(self):
|
||||
"""
|
||||
Runs daily to evaluate students' weak areas and generate
|
||||
a personalized AI study plan for the day.
|
||||
Runs daily to evaluate students' weak areas and generate
|
||||
a personalized study plan for the day.
|
||||
"""
|
||||
try:
|
||||
with Session(engine) as session:
|
||||
@@ -71,87 +75,18 @@ def generate_daily_study_plans(self):
|
||||
)
|
||||
session.add(new_plan)
|
||||
plans_created += 1
|
||||
|
||||
|
||||
session.commit()
|
||||
return f"Successfully generated {plans_created} daily study plans."
|
||||
except Exception as exc:
|
||||
print(f"Database error during study plan generation: {exc}")
|
||||
raise self.retry(exc=exc)
|
||||
|
||||
@celery_app.task(name="process_pdf_test", bind=True, max_retries=1)
|
||||
def process_pdf_test(self, test_id_str: str, object_name: str, admin_id_str: str, exam_pattern: str):
|
||||
"""
|
||||
Background task to download PDF from MinIO, extract text,
|
||||
use LLM to generate questions, and attach them to a Test record.
|
||||
"""
|
||||
try:
|
||||
print(f"Starting PDF processing for test {test_id_str}")
|
||||
|
||||
# 1. Download PDF from MinIO
|
||||
pdf_bytes = storage.get_file(object_name)
|
||||
if not pdf_bytes:
|
||||
print("Failed to download PDF from MinIO")
|
||||
return
|
||||
|
||||
# 2. Extract Questions using LLM (Async)
|
||||
questions_data = asyncio.run(parse_pdf_to_questions(pdf_bytes, exam_pattern))
|
||||
if not questions_data:
|
||||
print("LLM parsing returned empty data")
|
||||
return
|
||||
|
||||
import uuid
|
||||
test_id = uuid.UUID(test_id_str)
|
||||
admin_id = uuid.UUID(admin_id_str)
|
||||
|
||||
with Session(engine) as session:
|
||||
test_obj = session.get(models.Test, test_id)
|
||||
if not test_obj:
|
||||
print("Test record not found in DB")
|
||||
return
|
||||
|
||||
question_ids = []
|
||||
for q_data in questions_data:
|
||||
# Create a Question DB object
|
||||
new_q = models.Question(
|
||||
subject=q_data.get("subject", "Physics"),
|
||||
topic=q_data.get("topic", "General"),
|
||||
subtopic=q_data.get("subtopic", "General"),
|
||||
difficulty=q_data.get("difficulty", "Medium"),
|
||||
type=q_data.get("type", models.QuestionTypeEnum.MCQ_SINGLE),
|
||||
question_text=q_data.get("question_text", ""),
|
||||
options=q_data.get("options", {}),
|
||||
correct_answer=q_data.get("correct_answer", {}),
|
||||
solution_text=q_data.get("solution_text", ""),
|
||||
language=models.MediumEnum.ENGLISH,
|
||||
exam=models.ExamEnum(exam_pattern),
|
||||
created_by=admin_id
|
||||
)
|
||||
session.add(new_q)
|
||||
session.flush() # To get the ID
|
||||
question_ids.append(str(new_q.id))
|
||||
|
||||
# Update Test record with the new sections
|
||||
test_obj.sections = {
|
||||
"Section 1": {
|
||||
"questions": question_ids,
|
||||
"time_limit": test_obj.total_time
|
||||
}
|
||||
}
|
||||
# Automatically activate it when done processing
|
||||
test_obj.is_active = True
|
||||
|
||||
session.commit()
|
||||
print(f"Successfully processed PDF and created {len(question_ids)} questions for test {test_id_str}")
|
||||
return f"Processed {len(question_ids)} questions"
|
||||
except Exception as exc:
|
||||
print(f"Error processing PDF test: {exc}")
|
||||
# Mark test as failed if we had a status field, for now just log it
|
||||
raise self.retry(exc=exc)
|
||||
|
||||
# Schedule the tasks using Celery Beat
|
||||
# Schedule: 5:00 AM IST every day
|
||||
celery_app.conf.beat_schedule = {
|
||||
'generate-study-plans-every-morning': {
|
||||
'task': 'generate_daily_study_plans',
|
||||
'schedule': crontab(hour=5, minute=0), # 5:00 AM IST
|
||||
'schedule': crontab(hour=5, minute=0),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user