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

@@ -6,6 +6,7 @@
> **Backend Engine:** FastAPI (Python 3.11) + SQLModel (SQLAlchemy) + Celery + Redis
> **Frontend Engine:** Next.js 16 (React 19, TypeScript, TailwindCSS)
> **Database & Storage:** PostgreSQL 15 (PgBouncer connection pooler) + MinIO (S3-compatible)
> **Companion Document:** For page-by-page UI file locations, buttons, modals, and client API mapping, see [PAGEWISE_API_DOCUMENTATION.md](file:///c:/Users/naman/OneDrive/Desktop/pragativeer-v2/PAGEWISE_API_DOCUMENTATION.md).
---

BIN
API_DOCUMENTATION.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,724 @@
# PragatiVeer v2 — Pagewise UI & API Specification Map
> **Document Type:** Comprehensive Pagewise Architecture, Button-to-API Interaction, and UI Specification Map
> **Target Audience:** Frontend Engineers, Full-Stack Developers, Product Managers, QA & DevOps
> **Last Verified:** September 2026
> **Frontend Engine:** Next.js 16 (App Router, React 19, TypeScript, TailwindCSS, Zustand)
> **Backend Engine:** FastAPI (Python 3.11) + PostgreSQL 15 (PgBouncer) + Redis 7 + MinIO S3
---
## 1. Architectural & Network Request Flow
### 1.1 Client-to-Backend Network Pipeline
PragatiVeer v2 operates two primary communication paths between the Next.js frontend and the FastAPI backend:
```mermaid
graph LR
Browser["Client Browser"] -->|1. Page Navigation| NextServer["Next.js 16 App Router (:3000)"]
NextServer -->|2. Route Guard proxy.ts| Edge["Edge Cookie Evaluation"]
Browser -->|3. /api/* Internal Calls| Rewrites["next.config.ts Rewrites"]
Rewrites -->|http://backend:8000/api/*| FastAPI["FastAPI Backend (:8000)"]
Browser -->|4. Direct NEXT_PUBLIC_API_URL| FastAPI
FastAPI -->|Queries| PgBouncer["PgBouncer :5432"]
PgBouncer --> DB[("PostgreSQL 15")]
FastAPI -->|Cache & Tasks| Redis[("Redis 7")]
FastAPI -->|S3 Uploads| MinIO[("MinIO S3 :9000")]
```
1. **Proxy Middleware (`src/proxy.ts`):** Evaluates `auth_token` cookie on all `/dashboard/*` and `/admin/*` routes.
- Non-authenticated visitors heading to `/dashboard` are redirected to `/login`.
- Non-admin visitors (`token !== 'admin'`) heading to `/admin` are redirected to `/login`.
2. **Reverse Proxy Rewrites (`next.config.ts`):** Inside Docker, `/api/:path*` is automatically rewritten to `http://backend:8000/api/:path*`.
3. **Direct Client Fetching:** Certain client stores (e.g. `cbtStore.ts` and `src/app/cbt/[examId]/page.tsx`) use `NEXT_PUBLIC_API_URL` (default `http://localhost:8000`) for high-throughput examination requests.
4. **Session Token Enforcement:** `localStorage.getItem("active_session_token")` provides single-device session validation against `/api/users/{id}/session`.
---
## 2. Master Page Directory
| Route URL | Component File Location | Access Level | Primary Backend APIs Called |
| :--- | :--- | :---: | :--- |
| `/` | `src/app/page.tsx` | Public | None (Static / Marketing) |
| `/about` | `src/app/about/page.tsx` | Public | None (Informational) |
| `/contact` | `src/app/contact/page.tsx` | Public | None (Static Form / Mailto) |
| `/login` | `src/app/login/page.tsx` | Public | `POST /api/auth/login`, `POST /api/users/{id}/session` |
| `/signup` | `src/app/signup/page.tsx` | Public | `POST /api/auth/register`, `POST /api/auth/login`, `POST /api/users/{id}/session` |
| `/forgot-password` | `src/app/forgot-password/page.tsx` | Public | `POST /api/auth/forgot-password` |
| `/upgrade` | `src/app/upgrade/page.tsx` | Public / Auth | None (Routing to `/checkout?tier=...`) |
| `/checkout` | `src/app/checkout/page.tsx` | Authenticated | `POST /api/billing/create-order`, `POST /api/billing/verify-payment` |
| `/cbt/[examId]` | `src/app/cbt/[examId]/page.tsx` | Authenticated | `GET /api/tests/{examId}`, `GET /api/tests/{examId}/questions`, `POST /api/attempts/submit` |
| `/dashboard` | `src/app/dashboard/page.tsx` | Student (`Free`+) | `GET /api/users/{id}`, `GET /api/ai-coach/study-plan/today`, `GET /api/growth/referral/{id}` |
| `/dashboard/cbt` | `src/app/dashboard/cbt/page.tsx` | Student (`Free`+) | `GET /api/users/{id}`, `GET /api/tests`, `GET /api/users/{id}/attempts` |
| `/dashboard/cbt/history/[attemptId]` | `src/app/dashboard/cbt/history/[attemptId]/page.tsx` | Student (`Free`+) | `GET /api/users/{id}/attempts`, `GET /api/tests/{id}`, `GET /api/questions/{id}` |
| `/dashboard/dpp` | `src/app/dashboard/dpp/page.tsx` | Student (`Free`+) | `GET /api/tests?type=DPS`, `GET /api/users/{id}/attempts` |
| `/dpp/review/[dppId]` | `src/app/dpp/review/[dppId]/page.tsx` | Student (`Free`+) | Local DPP attempt state & question review |
| `/dashboard/analytics` | `src/app/dashboard/analytics/page.tsx` | Student (`Free`+) | `GET /api/users/{id}` (Performance graphs & accuracies) |
| `/dashboard/progress` | `src/app/dashboard/progress/page.tsx` | Student (`Free`+) | `GET /api/og/progress` (Progress card export) |
| `/dashboard/streaks` | `src/app/dashboard/streaks/page.tsx` | Student (`Free`+) | `GET /api/users/{id}` (Streak count & freeze tokens) |
| `/dashboard/leaderboard` | `src/app/dashboard/leaderboard/page.tsx` | Student (`Free`+) | Client ranking engine & state filtering |
| `/dashboard/materials` | `src/app/dashboard/materials/page.tsx` | Student (`Free`+) | Formula sheets repository & PDF downloads |
| `/dashboard/rank-predictor` | `src/app/dashboard/rank-predictor/page.tsx` | Student (`Free`+) | Percentile-to-AIR calculation engine |
| `/dashboard/community` | `src/app/dashboard/community/page.tsx` | Student (`Free`+) | Student doubt forum & study channels |
| `/dashboard/settings` | `src/app/dashboard/settings/page.tsx` | Student (`Free`+) | `GET /api/users/{id}`, `PUT /api/users/{id}`, `POST /api/users/{id}/avatar` |
| `/dashboard/ai-coach` | `src/app/dashboard/ai-coach/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/readiness/{id}`, `GET /api/growth/referral/{id}` |
| `/dashboard/ai-coach/tutor` | `src/app/dashboard/ai-coach/tutor/page.tsx` | Student (`AI_COACH`) | `POST /api/ai-coach/chat/stream` (SSE stream) |
| `/dashboard/ai-coach/study-plan` | `src/app/dashboard/ai-coach/study-plan/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/study-plan/today?user_id={id}` |
| `/dashboard/ai-coach/readiness` | `src/app/dashboard/ai-coach/readiness/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/readiness/{id}` |
| `/dashboard/ai-coach/mistakes` | `src/app/dashboard/ai-coach/mistakes/page.tsx` | Student (`AI_COACH`) | `GET /api/mistakes/today?user_id={id}`, `POST /api/mistakes/review` |
| `/dashboard/ai-coach/cbt-dpp` | `src/app/dashboard/ai-coach/cbt-dpp/page.tsx` | Student (`AI_COACH`) | `GET /api/tests?type=DPS`, `GET /api/users/{id}/attempts` |
| `/dashboard/ai-coach/cbt-dpp/[id]` | `src/app/dashboard/ai-coach/cbt-dpp/[id]/page.tsx` | Student (`AI_COACH`) | Submits to `/cbt/[examId]` |
| `/dashboard/ai-coach/cbt-dpp/history/[attemptId]` | `src/app/dashboard/ai-coach/cbt-dpp/history/[attemptId]/page.tsx` | Student (`AI_COACH`) | `GET /api/users/{id}/attempts`, `GET /api/tests/{id}` |
| `/dashboard/ai-coach/heatmap` | `src/app/dashboard/ai-coach/heatmap/page.tsx` | Student (`AI_COACH`) | Mistake frequency matrix by topic & difficulty |
| `/dashboard/ai-coach/college-predictor` | `src/app/dashboard/ai-coach/college-predictor/page.tsx` | Student (`AI_COACH`) | `GET /api/ai-coach/readiness/{id}` + College cutoff mapping |
| `/dashboard/ai-coach/time-optimization` | `src/app/dashboard/ai-coach/time-optimization/page.tsx` | Student (`AI_COACH`) | Time-per-question analysis & speed pace guide |
| `/dashboard/ai-coach/strategy` | `src/app/dashboard/ai-coach/strategy/page.tsx` | Student (`AI_COACH`) | Exam temperament & question selection strategy |
| `/dashboard/ai-coach/weekly-progress` | `src/app/dashboard/ai-coach/weekly-progress/page.tsx` | Student (`AI_COACH`) | Weekly AI mastery trajectory |
| `/admin` | `src/app/admin/page.tsx` | Admin | Real-time system monitoring & KPI metrics |
| `/admin/students` | `src/app/admin/students/page.tsx` | Admin | `GET /api/admin/students` |
| `/admin/students/[studentId]` | `src/app/admin/students/[studentId]/page.tsx` | Admin | `GET /api/admin/students/{id}`, `PATCH /api/admin/students/{id}` |
| `/admin/tests` | `src/app/admin/tests/page.tsx` | Admin | `GET /api/admin/tests`, `PUT /api/admin/tests/{id}/toggle`, `DELETE /api/admin/tests/{id}`, `POST /api/admin/tests/create_manual` |
| `/admin/questions` | `src/app/admin/questions/page.tsx` | Admin | Global question search & question bank table |
| `/admin/coupons` | `src/app/admin/coupons/page.tsx` | Admin | `GET /api/admin/coupons`, `POST /api/admin/coupons`, `DELETE /api/admin/coupons/{id}` |
| `/admin/settings` | `src/app/admin/settings/page.tsx` | Admin | `GET /api/admin/settings`, `POST /api/admin/settings` |
| `/admin/agents` | `src/app/admin/agents/page.tsx` | Admin | AI Agent orchestration & heartbeat monitor |
| Policy Pages | `/privacy`, `/terms`, `/refund`, `/cancellation`, `/disclaimer` | Public | Static legal disclosures |
---
## 3. Exhaustive Pagewise UI & API Breakdowns
---
### 3.1 Authentication & Onboarding Pages
#### A. Login Page (`/login`)
- **File Location:** `src/app/login/page.tsx`
- **Access Guard:** Public (Redirects authenticated users to `/dashboard` or `/admin`)
- **UI Components:**
- Brand Logo linking to `/`
- Email `<input type="email" />`
- Password `<input type="password" />` with show/hide password toggle button
- "Forgot password?" link to `/forgot-password`
- "Sign In" submit button
- "Sign up" link to `/signup`
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Show/Hide Password` | Toggles password visibility | `setShowPassword(!showPassword)` |
| `Sign In Button` | Submits form, authenticates user | `handleLogin(e)` |
| `Forgot password?` | Navigates to password reset | `/forgot-password` |
| `Sign up` | Navigates to registration | `/signup` |
- **APIs Invoked:**
1. `POST /api/auth/login`
- **Trigger:** On form submit (`handleLogin`)
- **Request Body:** `{ "email": "...", "password": "..." }`
- **Response:** `{ "success": true, "role": "user" | "admin", "token": "UUID" | "admin" }`
- **Side Effects:**
- Sets `document.cookie = "auth_token=...; path=/; max-age=2592000; SameSite=Lax"`
- Writes `localStorage.setItem("activeUserId", data.token)`
- Generates `crypto.randomUUID()` session token and writes `localStorage.setItem("active_session_token", sessionToken)`
2. `POST /api/users/{userId}/session`
- **Trigger:** Immediately following successful student login
- **Request Body:** `{ "session_token": "<random_uuid>" }`
- **Response:** `{ "status": "session_registered", ... }`
- **Redirect:** `window.location.href = "/admin"` (if admin) or `"/dashboard"` (if student)
---
#### B. Signup Page (`/signup`)
- **File Location:** `src/app/signup/page.tsx`
- **Access Guard:** Public
- **UI Components:**
- Name `<input type="text" />`
- Email `<input type="email" />`
- Password `<input type="password" />`
- "Create Account" submit button
- "Sign In" link to `/login`
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Create Account Button` | Submits registration | `handleSignup(e)` |
| `Sign in Link` | Navigates to login | `/login` |
- **APIs Invoked:**
1. `POST /api/auth/register`
- **Trigger:** On form submit
- **Request Body:** `{ "name": "...", "email": "...", "password": "..." }`
- **Response:** `{ "success": true, "message": "User registered successfully" }`
2. `POST /api/auth/login` (Auto-login chaining)
- **Trigger:** Immediately upon successful registration
- **Request Body:** `{ "email": "...", "password": "..." }`
- **Response:** `{ "success": true, "token": "<userId>", "role": "user" }`
3. `POST /api/users/{userId}/session`
- Registers active session token and redirects to `/dashboard`.
---
#### C. Forgot Password Page (`/forgot-password`)
- **File Location:** `src/app/forgot-password/page.tsx`
- **Access Guard:** Public
- **UI Components:**
- Email `<input type="email" />`
- "Send Reset Link" button
- Success message banner
- "Back to login" link
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Send Reset Link` | Dispatches password reset email | `handleReset(e)` |
| `Back to login` | Navigates to `/login` | `/login` |
- **APIs Invoked:**
1. `POST /api/auth/forgot-password`
- **Trigger:** On form submit
- **Request Body:** `{ "email": "student@example.com" }`
- **Response:** `{ "message": "If this email is registered, a password reset link has been sent." }`
---
### 3.2 Subscription & Checkout Subsystem
#### A. Pricing & Upgrade Page (`/upgrade`)
- **File Location:** `src/app/upgrade/page.tsx`
- **Access Guard:** Public / Authenticated
- **UI Components:**
- Plan selection cards:
- **Free Plan:** Basic mocks, limited practice sheets (Default)
- **Pro Plan (₹3,499/yr):** Unlimited Full-Syllabus CBT Mocks, Detailed Analytics, Daily DPS
- **AI Coach Plan (₹11,999/yr):** Real-time AI Tutor, College Predictor, Spaced Repetition Notebook, Adaptive Daily Study Plans
- Feature comparison matrix
- FAQ accordion
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Choose Pro` | Navigates to checkout for Pro | `router.push('/checkout?tier=Pro')` |
| `Upgrade to AI Coach` | Navigates to checkout for AI Coach | `router.push('/checkout?tier=Al_Coach')` |
---
#### B. Checkout & Payment Page (`/checkout`)
- **File Location:** `src/app/checkout/page.tsx`
- **Access Guard:** Authenticated (Redirects to `/login?redirect=/checkout?tier=...` if guest)
- **UI Components:**
- Order summary sidebar (Plan name, subtotal, GST 18%, grand total)
- Delivery/Student Address inputs (`Address`, `City`, `Pincode`, `Phone`)
- Payment method selector: Razorpay / Cashfree / Stripe
- "Proceed to Payment" button with active loader
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Proceed to Payment` | Initiates backend order & triggers Razorpay Modal | `handlePayment()` |
- **APIs Invoked:**
1. `POST /api/billing/create-order?user_id={userId}&plan_tier={tier}`
- **Trigger:** On `Proceed to Payment` click
- **Request Body:** `{ "gateway": "razorpay" }`
- **Response:**
```json
{
"id": "order_XXXXXXXXXXXXXX",
"amount": 1199900,
"currency": "INR",
"status": "created"
}
```
2. **Client-Side SDK:** Opens `window.Razorpay(options).open()`
3. `POST /api/billing/verify-payment`
- **Trigger:** Razorpay `handler(response)` callback upon payment approval
- **Request Body:**
```json
{
"gateway": "razorpay",
"razorpay_order_id": "order_XXXXXX",
"razorpay_payment_id": "pay_XXXXXX",
"razorpay_signature": "signature_hash",
"user_id": "<userId>",
"tier": "Al_Coach"
}
```
- **Response:** `{ "status": "success", "tier": "Al_Coach" }`
- **Action on Success:** Updates local user tier and redirects to `/dashboard?payment=success`.
---
### 3.3 Computer-Based Testing (CBT) Examination Engine
#### A. Full-Screen CBT Examination Room (`/cbt/[examId]`)
- **File Location:** `src/app/cbt/[examId]/page.tsx`
- **Access Guard:** Authenticated (`Free`, `Pro`, or `AI_COACH` depending on test gating)
- **UI Components:**
- Header: Candidate Name, Photo, Roll Number, Exam Title, Section Switcher (`Physics`, `Chemistry`, `Mathematics` or `Biology`)
- Floating Exam Timer (Hours : Minutes : Seconds) with visual alert when under 5 minutes
- Question Palette Matrix (Grid 1 to N with NTA standard colors: Green=Answered, Purple=Marked for Review, Red=Unanswered, Grey=Not Visited)
- Question Panel: Question Text (KaTeX LaTeX rendering), Image Diagram from MinIO, Multiple Choice Options (`A`, `B`, `C`, `D`) or Numerical Input box
- Action Bar:
- "Save & Next" button
- "Clear Response" button
- "Mark for Review & Next" button
- "Submit Test" button
- Submit Confirmation Modal: Summary table of Answered, Unanswered, Marked for Review questions
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | State / Store Handler |
| :--- | :--- | :--- |
| `Option Selection (A/B/C/D)` | Selects radio answer | `saveAnswer(questionId, optionKey)` |
| `Numerical Input Change` | Enters integer/decimal answer | `saveAnswer(questionId, value)` |
| `Save & Next` | Commits answer and advances index | `handleNextQuestion()` |
| `Clear Response` | Resets current question answer | `clearAnswer(questionId)` |
| `Mark for Review & Next` | Sets flagged review state and advances | `markForReview(questionId)` |
| `Question Palette Button [i]` | Direct jump to question index `i` | `setCurrentIndex(i)` |
| `Section Tab Switcher` | Filters question palette by Subject | `setActiveSection(sectionName)` |
| `Submit Test Button` | Opens final confirmation dialog | `setIsSubmitModalOpen(true)` |
| `Confirm Final Submission` | Dispatches submission payload | `submitAttemptToServer(userId)` |
- **APIs Invoked:**
1. `GET /api/tests/{examId}?user_id={userId}`
- **Trigger:** On mount (`useEffect`)
- **Response:** Test metadata, sections mapping, total duration, question ID sequence.
2. `GET /api/tests/{examId}/questions`
- **Trigger:** On mount immediately following test metadata
- **Response:** Array of full question objects (LaTeX text, options, subject, topic).
3. `POST /api/attempts/submit`
- **Trigger:** On manual submission confirm OR auto-submit on countdown timeout
- **Request Body (`src/store/cbtStore.ts`):**
```json
{
"test_id": "<examId>",
"user_id": "<userId>",
"time_spent": 10420,
"answers": [
{
"question_id": "<qId>",
"selected_option": "B",
"time_spent": 115,
"is_marked_for_review": false
}
],
"status": "Submitted"
}
```
- **Response:**
```json
{
"id": "<attemptId>",
"score": 240,
"total_questions": 75,
"accuracy": 82.5,
"rank": 412
}
```
- **Redirect:** Navigates directly to `/dashboard/cbt/history/{attemptId}`.
---
#### B. CBT Mock Directory Hub (`/dashboard/cbt`)
- **File Location:** `src/app/dashboard/cbt/page.tsx`
- **Access Guard:** Authenticated (`Free`+)
- **UI Components:**
- Exam Filter Tabs: `All`, `JEE Main`, `JEE Advanced`, `NEET`
- Active Full Mocks Grid (Test Card: Title, Pattern badge, Total Questions, Time limit, Attempts count, "Start Test" or "Review" button)
- Recent Attempts History Table (Exam Title, Score, Accuracy, Date, "View Solution" button)
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Exam Filter Tabs` | Filters active tests by pattern | `setSelectedPattern(pattern)` |
| `Start Test Button` | Navigates into full-screen exam room | `router.push('/cbt/' + test.id)` |
| `View Solution Button` | Navigates to detailed attempt analysis | `router.push('/dashboard/cbt/history/' + attempt.id)` |
- **APIs Invoked:**
1. `GET /api/users/{userId}` (Fetches user's targeted exam e.g. `JEE_MAIN`)
2. `GET /api/tests?user_id={userId}&exam_pattern={pattern}` (Lists available mock tests)
3. `GET /api/users/{userId}/attempts` (Fetches historical attempt records)
---
#### C. Test Attempt Analysis & Solution Review (`/dashboard/cbt/history/[attemptId]`)
- **File Location:** `src/app/dashboard/cbt/history/[attemptId]/page.tsx`
- **Access Guard:** Authenticated
- **UI Components:**
- Score summary card: Final Score, Total Marks, Accuracy %, Positive Marks, Negative Marks, Percentile
- Question-by-question breakdown list:
- Status badge: Correct (+4), Incorrect (-1), Unattempted (0)
- Full LaTeX question rendering
- Student's chosen answer vs Official Correct Answer
- Step-by-step text solution
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Filter by Subject (All/Phys/Chem/Math)` | Filters questions | `setSelectedSubject(sub)` |
| `Filter by Status (Incorrect/Correct)` | Isolates student errors | `setStatusFilter(filter)` |
| `Back to Test Directory` | Navigates back | `/dashboard/cbt` |
- **APIs Invoked:**
1. `GET /api/users/{userId}/attempts` (Locates `attemptId`)
2. `GET /api/tests/{source_id}?user_id={userId}` (Fetches test structure)
3. `GET /api/questions/{questionId}` (Fetches detailed solutions per question)
---
#### D. Daily Practice Sheets (DPS / DPP) Test Center (`/dashboard/dpp`)
- **File Location:** `src/app/dashboard/dpp/page.tsx`
- **Access Guard:** Authenticated (`Free`+ with tiered limits)
- **UI Components:**
- Daily Sheets Calendar / Carousel:
- Biology Ch4 DPS
- Chemistry Ch4 DPS
- Mathematics Ch4 DPS
- Physics Ch4 DPS
- Sheet Status: `Pending` | `Completed` (with Score & Accuracy)
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Solve DPS Button` | Launches CBT engine for that practice sheet | `router.push('/cbt/' + sheet.id)` |
| `Review Solution Button` | Opens attempt breakdown | `router.push('/dashboard/cbt/history/' + attempt.id)` |
- **APIs Invoked:**
1. `GET /api/tests?type=DPS&exam_pattern={pattern}` (Fetches seeded DPS tests)
2. `GET /api/users/{userId}/attempts` (Checks which DPS sheets have been completed)
---
### 3.4 Student Dashboard & Core Learning Hub
#### A. Main Student Dashboard (`/dashboard`)
- **File Location:** `src/app/dashboard/page.tsx`
- **Access Guard:** Authenticated (`Free`+)
- **UI Components:**
- Header: Welcome message, Active Target Exam (`JEE Main` / `NEET`), Streak Badge
- Performance Overview Grid: Tests Taken, Average Accuracy %, Study Streak, Target Score
- Embedded Adaptive Study Plan Card (`AdaptiveStudyPlan.tsx`)
- Embedded Mistake Heatmap Card (`MistakeHeatmap.tsx`)
- Embedded Viral Referral Card (`ReferralCard.tsx`)
- "Share Card" Button (Generates and downloads high-res progress PNG via `html-to-image`)
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Share Card Button` | Exports dashboard card as PNG | `handleExport()` (`html-to-image`) |
| `Start Today's DPS` | Launches daily practice | `/dashboard/dpp` |
| `Explore AI Coach` | Navigates to AI features | `/dashboard/ai-coach` |
- **APIs Invoked:**
1. `GET /api/users/{userId}` via `useActiveUser()`
2. `GET /api/ai-coach/study-plan/today?user_id={userId}` (via `AdaptiveStudyPlan`)
3. `GET /api/growth/referral/{userId}` (via `ReferralCard`)
---
#### B. Student Profile & Settings Page (`/dashboard/settings`)
- **File Location:** `src/app/dashboard/settings/page.tsx`
- **Access Guard:** Authenticated
- **UI Components:**
- Avatar Profile Picture with MinIO upload button
- Personal Information Form: First Name, Last Name, Email (read-only), Target Exam selector (`JEE Main`, `JEE Advanced`, `NEET`), School, State
- "Save Changes" button
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Avatar File Upload Input` | Uploads image directly to MinIO | `handleAvatarUpload(e)` |
| `Save Changes Button` | Updates student profile | `handleSave()` |
- **APIs Invoked:**
1. `POST /api/users/{userId}/avatar`
- **Trigger:** On file input change
- **Request Form-Data:** `file: <binary_image>`
- **Response:** `{ "url": "http://localhost:9000/avatars/user_avatar_....png" }`
2. `PUT /api/users/{userId}`
- **Trigger:** On `Save Changes` click
- **Request Body:**
```json
{
"name": "Rahul Sharma",
"target_exam": "JEE_MAIN",
"school": "Delhi Public School",
"state": "Delhi"
}
```
- **Response:** Updated user record.
---
### 3.5 AI Coach Suite (`/dashboard/ai-coach/*`)
> **Access Restriction:** All pages in this section are gated to `tier == "Al_Coach"`. Users on `Free` or `Pro` tiers see an upgrade paywall with a direct link to `/checkout?tier=Al_Coach`.
#### A. AI Doubt Solver & Tutor Chat (`/dashboard/ai-coach/tutor`)
- **File Location:** `src/app/dashboard/ai-coach/tutor/page.tsx` & `src/components/AI/TutorChat.tsx`
- **Access Guard:** `AI_COACH` Tier
- **UI Components:**
- Real-time chat message history (User messages in brand cyan, AI Tutor responses formatted in Markdown + KaTeX LaTeX equations)
- Chat `<input />` with send button and typing indicator
- Quick Suggestion Chips: *"Explain Photoelectric Effect"*, *"Solve JEE 2024 Math Integration"*, *"Give me a 3-step revision plan"*
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Send Message Button` | Submits doubt to streaming API | `sendMessage(text)` |
| `Suggestion Chips` | Populates input with prompt | `setInput(chipText)` |
- **APIs Invoked:**
1. `POST /api/ai-coach/chat/stream`
- **Trigger:** On sending a message
- **Request Headers:** `Content-Type: application/json`, `Accept: text/event-stream`
- **Request Body:** `{ "user_id": "<userId>", "message": "Explain Lenz's Law" }`
- **Response Stream:** `text/event-stream; charset=utf-8` (Server-Sent Events)
- **Phase 2 Status:** Intentionally stubbed with friendly coming-soon notice to decouple from external LLM providers during initial launch.
---
#### B. Spaced Repetition Mistake Notebook (`/dashboard/ai-coach/mistakes`)
- **File Location:** `src/app/dashboard/ai-coach/mistakes/page.tsx`
- **Access Guard:** `AI_COACH` Tier
- **UI Components:**
- Daily Mistake Card Queue: Displays questions that the student answered incorrectly in previous CBT exams, scheduled for revision today using the SuperMemo-2 (SM-2) algorithm.
- Interactive Answer Selector (`A`, `B`, `C`, `D`)
- "Submit Answer" button
- Post-submission Feedback Panel: Immediate correctness status, explanation, and new scheduled review interval (e.g. *"Reviewed! Next review in 6 days"*).
- "Next Question" button
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Select Answer (A/B/C/D)` | Selects answer option | `setSelectedOption(opt)` |
| `Submit Answer Button` | Submits review and updates SM-2 interval | `submitReview(mistakeId, option)` |
| `Next Question Button` | Advances to next queued mistake | `handleNext()` |
- **APIs Invoked:**
1. `GET /api/mistakes/today?user_id={userId}`
- **Trigger:** On page mount
- **Response:** Array of pending mistake objects where `next_review <= NOW()`.
2. `POST /api/mistakes/review`
- **Trigger:** On `Submit Answer` click
- **Request Body:** `{ "mistake_id": "<mistakeId>", "selected_answer": "B" }`
- **Response:**
```json
{
"status": "reviewed",
"is_correct": true,
"interval": 6,
"next_review": "2026-09-15T00:00:00Z"
}
```
---
#### C. Exam Readiness & College Predictor (`/dashboard/ai-coach/readiness` & `/college-predictor`)
- **File Location:** `src/app/dashboard/ai-coach/readiness/page.tsx`, `college-predictor/page.tsx`, `ReadinessGauge.tsx`, `CollegePredictorCard.tsx`
- **Access Guard:** `AI_COACH` Tier
- **UI Components:**
- Circular SVG Exam Readiness Gauge (0–100%)
- Readiness Breakdown: Accuracy Score, Time Management Score, Consistency Score, Topic Coverage
- AI College Predictor Card: Predicted Rank, Eligible IIT/NIT/AIIMS colleges based on historical cutoffs (Safe, Probable, Reach)
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Recalculate Score Button` | Refetches latest readiness calculation | `fetchReadiness()` |
| `Explore College Cutoffs` | Expands category details | `setCategoryFilter(...)` |
- **APIs Invoked:**
1. `GET /api/ai-coach/readiness/{userId}`
- **Trigger:** On mount
- **Response:**
```json
{
"readiness_score": 78,
"predicted_rank": 3420,
"confidence_interval": "± 400",
"subject_breakdown": {
"Physics": 82,
"Chemistry": 85,
"Mathematics": 67
}
}
```
---
#### D. Adaptive Daily Study Plan (`/dashboard/ai-coach/study-plan`)
- **File Location:** `src/app/dashboard/ai-coach/study-plan/page.tsx` & `src/components/AI/StudyPlanCard.tsx`
- **Access Guard:** `AI_COACH` Tier
- **UI Components:**
- Daily Study Plan Task List: Revision Target, Practice Target, Test Recommendation
- Task checkboxes to mark tasks as completed
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Task Checkbox` | Marks daily task complete | Local task state |
| `Start Recommended Task` | Deep-links to relevant test or DPS | `/dashboard/dpp` |
- **APIs Invoked:**
1. `GET /api/ai-coach/study-plan/today?user_id={userId}`
- **Trigger:** On mount
- **Response:** Daily plan JSON generated by Celery Beat at 5:00 AM IST.
---
### 3.6 Admin Management Console (`/admin/*`)
> **Access Restriction:** Protected by `proxy.ts` (Requires `auth_token=admin` cookie). Non-admin users are immediately redirected to `/login`.
#### A. Command Center Overview (`/admin`)
- **File Location:** `src/app/admin/page.tsx`
- **Access Guard:** Admin Only
- **UI Components:**
- KPI Stat Cards: Total Revenue, Active Students, AI Agent Accuracy %, Total Questions
- System Health & Accuracy Area Chart (`recharts`)
- Quick Action Buttons: "Refresh Data", "Create Test", "Manage Students"
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Refresh Data Button` | Triggers refetch of dashboard metrics | `fetchDashboardData()` |
| `Create Test Quick Link` | Navigates to Test Management | `/admin/tests` |
---
#### B. Student Directory & Profile Control (`/admin/students` & `[studentId]`)
- **File Location:** `src/app/admin/students/page.tsx` and `src/app/admin/students/[studentId]/page.tsx`
- **Access Guard:** Admin Only
- **UI Components:**
- Students Directory Table: Name, Email, Target Exam, Class, Tier Badge (`Free`/`Pro`/`AI Coach`), Streak, Actions
- Search & Filter Bar: Filter by Exam (`JEE Main` / `NEET`) or Tier
- Detail Page (`[studentId]`):
- Full Profile Card
- Tier Override Selector (`Free` -> `Pro` -> `Al_Coach`)
- "Update Tier" button
- "Reset Streak" button
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `View Student Row` | Navigates to individual student control | `router.push('/admin/students/' + student.id)` |
| `Update Tier Button` | Changes student subscription tier | `handleTierUpdate(newTier)` |
| `Reset Streak Button` | Resets student streak counter to 0 | `handleResetStreak()` |
- **APIs Invoked:**
1. `GET /api/admin/students` (Lists all student accounts)
2. `GET /api/admin/students/{studentId}` (Fetches single student record)
3. `PATCH /api/admin/students/{studentId}`
- **Trigger:** On tier change or profile update
- **Request Body:** `{ "tier": "Al_Coach" }`
- **Response:** Updated student record.
---
#### C. Test & DPS Management Center (`/admin/tests`)
- **File Location:** `src/app/admin/tests/page.tsx`
- **Access Guard:** Admin Only
- **UI Components:**
- Active Tests Table: Title, Exam Pattern, Total Time, Scheduled Date, Active Switch toggle, Delete button
- Manual Test Creation Form / JSON Drag-and-Drop Uploader:
- Test Title `<input />`
- Exam Pattern selector (`JEE_MAIN`, `JEE_ADVANCED`, `NEET`)
- Test Type selector (`Mock`, `DPS`)
- Time Limit `<input />` (minutes)
- JSON Question File Uploader (Validates question schema: `question_text`, `options`, `correct_answer`, `subject`, `topic`)
- "Create Test" button
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Active Toggle Switch` | Toggles test visibility to students | `handleToggle(testId)` |
| `Delete Test Button` | Permanently deletes test and its questions | `handleDelete(testId)` |
| `JSON File Drop Zone` | Parses and validates questions JSON | `processFile(file)` |
| `Create Test Button` | Dispatches manual test creation | `handleCreateTest()` |
- **APIs Invoked:**
1. `GET /api/admin/tests`
- **Trigger:** On mount
- **Response:** Array of all existing tests.
2. `PUT /api/admin/tests/{testId}/toggle`
- **Trigger:** On switch toggle
- **Response:** `{ "status": "toggled", "is_active": true|false }`
3. `DELETE /api/admin/tests/{testId}`
- **Trigger:** On delete confirm
- **Response:** `{ "status": "deleted" }`
4. `POST /api/admin/tests/create_manual`
- **Trigger:** On `Create Test` submit
- **Request Body:**
```json
{
"title": "JEE Main Physics Final Mock",
"type": "Mock",
"exam_pattern": "JEE_MAIN",
"time_limit": 180,
"admin_id": "<adminId>",
"questions_json": [ ... ]
}
```
- **Response:** `{ "status": "created", "test_id": "<uuid>", "questions_count": 75 }`
---
#### D. Coupon Management (`/admin/coupons`)
- **File Location:** `src/app/admin/coupons/page.tsx`
- **Access Guard:** Admin Only
- **UI Components:**
- Active Coupons Table: Code, Discount %, Max Discount Amount, Status, Delete button
- New Coupon Creation Card: Code input, Discount percentage input, Max discount input, "Create Coupon" button
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Create Coupon Button` | Submits new coupon | `handleCreate()` |
| `Delete Coupon Button` | Deletes discount coupon | `handleDelete(couponId)` |
- **APIs Invoked:**
1. `GET /api/admin/coupons` (Fetches active coupons list)
2. `POST /api/admin/coupons`
- **Request Body:** `{ "code": "PRAGATI50", "discount_percent": 50, "max_discount_amount": 2000 }`
3. `DELETE /api/admin/coupons/{id}` (Deletes coupon)
---
#### E. Global Platform Settings (`/admin/settings`)
- **File Location:** `src/app/admin/settings/page.tsx`
- **Access Guard:** Admin Only
- **UI Components:**
- Maintenance Mode Switch
- Free Test Quota `<input />` (Number of free mock tests allowed before paywall)
- Allow Registrations Switch
- "Save Settings" button
- **Buttons & Interactive Elements:**
| Button / Trigger | Action | Target / Handler |
| :--- | :--- | :--- |
| `Save Settings Button` | Persists global configuration | `handleSaveSettings()` |
- **APIs Invoked:**
1. `GET /api/admin/settings` (Fetches current settings)
2. `POST /api/admin/settings`
- **Request Body:** `{ "maintenance_mode": false, "free_test_limit": 2, "allow_registrations": true }`
---
## 4. Master Button-to-API Cross Reference Matrix
| Button / UI Element | Page Location | Trigger Type | HTTP Method & Endpoint | Payload / Parameters |
| :--- | :--- | :--- | :--- | :--- |
| **Sign In** | `/login` | Form Submit | `POST /api/auth/login` | `{ email, password }` |
| **Register & Login** | `/signup` | Form Submit | `POST /api/auth/register` | `{ name, email, password }` |
| **Reset Password** | `/forgot-password` | Form Submit | `POST /api/auth/forgot-password` | `{ email }` |
| **Proceed to Payment** | `/checkout` | User Click | `POST /api/billing/create-order` | Query: `?user_id=...&plan_tier=...` |
| **Razorpay Verification** | `/checkout` | SDK Callback | `POST /api/billing/verify-payment` | `{ razorpay_order_id, razorpay_payment_id, ... }` |
| **Start CBT Exam** | `/dashboard/cbt` | Navigation | `GET /api/tests/{examId}` | Query: `?user_id=...` |
| **Fetch Questions** | `/cbt/[examId]` | Page Mount | `GET /api/tests/{examId}/questions` | Path: `{examId}` |
| **Final Submit Exam** | `/cbt/[examId]` | Modal Confirm | `POST /api/attempts/submit` | `{ test_id, user_id, answers, ... }` |
| **Submit Mistake Review** | `/dashboard/ai-coach/mistakes` | User Click | `POST /api/mistakes/review` | `{ mistake_id, selected_answer }` |
| **Send AI Doubt** | `/dashboard/ai-coach/tutor` | Form Submit | `POST /api/ai-coach/chat/stream` | `{ user_id, message }` |
| **Upload Avatar** | `/dashboard/settings` | File Input | `POST /api/users/{id}/avatar` | Form-Data: `file: <image>` |
| **Save Profile** | `/dashboard/settings` | User Click | `PUT /api/users/{id}` | `{ name, target_exam, school, state }` |
| **Toggle Test Status** | `/admin/tests` | Switch Toggle | `PUT /api/admin/tests/{id}/toggle` | Path: `{id}` |
| **Delete Test** | `/admin/tests` | Button Click | `DELETE /api/admin/tests/{id}` | Path: `{id}` |
| **Create Manual Test** | `/admin/tests` | Form Submit | `POST /api/admin/tests/create_manual` | `{ title, exam_pattern, questions_json, ... }` |
| **Update Student Tier** | `/admin/students/[id]` | Dropdown Change | `PATCH /api/admin/students/{id}` | `{ tier: "Al_Coach" }` |
| **Create Coupon** | `/admin/coupons` | Form Submit | `POST /api/admin/coupons` | `{ code, discount_percent, ... }` |
| **Delete Coupon** | `/admin/coupons` | Button Click | `DELETE /api/admin/coupons/{id}` | Path: `{id}` |
| **Save Admin Settings** | `/admin/settings` | User Click | `POST /api/admin/settings` | `{ maintenance_mode, free_test_limit, ... }` |
---
## 5. Codebase Cleanup Audit & Phase 2 Roadmap
### 5.1 Cleaned & Removed Legacy Files
In accordance with production standards, the following 10 dead/legacy files and temporary development artifacts have been hard-deleted:
- `backend/fix.py`: One-time script for manual LaTeX question corrections.
- `backend/fix_from_source.py`: One-time PDF extraction script.
- `backend/fix_ions.py`: One-time chemical formula fixer.
- `backend/seed_dpps.py`: Legacy DPS script using PyMuPDF and OpenAI.
- `backend/seed_dpps_mock.py`: Obsolete mock DPS generator.
- `backend/seed_dpps_real.py`: Obsolete duplicate DPS script.
- `backend/seed_from_dpps.py`: Redundant seeder variant.
- `backend/pragativeer-prd.pdf`: Product Requirements Document binary removed from backend.
- `backend/pragativeer.db`: Abandoned SQLite file (PragatiVeer exclusively uses PostgreSQL).
- `backend/test.pdf` & `backend/test2.pdf`: Temporary test PDFs.
### 5.2 Decoupled Dependencies (`requirements.txt`)
The following heavy external packages were removed from `backend/requirements.txt`:
- `openai==1.14.0`: Decoupled to eliminate reliance on external LLM keys during initial rollout.
- `PyMuPDF==1.23.26`: Decoupled to prevent native C-library compile overhead in production.
### 5.3 Stubbed Modules for Phase 2 AI Integration
- `backend/llm.py`: Cleanly stubbed with an async generator returning a friendly coming-soon notice.
- `backend/pdf_parser.py`: Cleanly stubbed with an empty list return.
- `backend/worker.py`: Cleaned to retain exclusively the active 5:00 AM IST Celery Beat daily study plan scheduler.
- `POST /api/admin/tests/upload`: Returns a structured message directing administrators to use `/api/admin/tests/create_manual` until Phase 2 is provisioned.
---
*Document compiled and verified for PragatiVeer v2 — Production Release*

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 []

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,
@@ -30,7 +34,7 @@ celery_app.conf.update(
def generate_daily_study_plans(self):
"""
Runs daily to evaluate students' weak areas and generate
a personalized AI study plan for the day.
a personalized study plan for the day.
"""
try:
with Session(engine) as session:
@@ -78,80 +82,11 @@ def generate_daily_study_plans(self):
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),
},
}