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:
2026-09-09 11:28:34 +05:30
parent a28366f0c3
commit ec558d52fe
24 changed files with 812 additions and 1283 deletions

View File

@@ -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.')

View File

@@ -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()

View File

@@ -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.')

View File

@@ -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."

View File

@@ -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(

View File

@@ -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.

View File

View File

@@ -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

View File

@@ -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()

View File

@@ -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())

View File

@@ -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()

View File

@@ -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()

View File

@@ -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())

Binary file not shown.

Binary file not shown.

View File

@@ -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),
},
}