github-actions[bot] commited on
Commit
eba7c64
·
1 Parent(s): b222bcc

🚀 Auto-deploy backend from GitHub (d39af4f)

Browse files
main.py CHANGED
@@ -75,6 +75,7 @@ from services.user_provisioning_service import (
75
  UserProvisioningService,
76
  )
77
  from routes.rag_routes import router as rag_router
 
78
  from rag.curriculum_rag import (
79
  build_analysis_curriculum_context,
80
  build_lesson_prompt,
@@ -974,6 +975,7 @@ class RequestMiddleware(BaseHTTPMiddleware):
974
  app.add_middleware(RequestMiddleware)
975
  app.add_middleware(AuthMiddleware)
976
  app.include_router(rag_router)
 
977
 
978
 
979
  # ─── Global Exception Handler ─────────────────────────────────
 
75
  UserProvisioningService,
76
  )
77
  from routes.rag_routes import router as rag_router
78
+ from routes.curriculum_routes import router as curriculum_router
79
  from rag.curriculum_rag import (
80
  build_analysis_curriculum_context,
81
  build_lesson_prompt,
 
975
  app.add_middleware(RequestMiddleware)
976
  app.add_middleware(AuthMiddleware)
977
  app.include_router(rag_router)
978
+ app.include_router(curriculum_router)
979
 
980
 
981
  # ─── Global Exception Handler ─────────────────────────────────
routes/curriculum_routes.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from fastapi import APIRouter, HTTPException, Query
7
+ from pydantic import BaseModel
8
+
9
+ from services.curriculum_service import (
10
+ get_subject,
11
+ get_subjects,
12
+ get_topic,
13
+ get_topics,
14
+ )
15
+
16
+ logger = logging.getLogger("mathpulse.curriculum")
17
+ router = APIRouter(prefix="/api/curriculum", tags=["curriculum"])
18
+
19
+
20
+ class SubjectResponse(BaseModel):
21
+ id: str
22
+ code: str
23
+ name: str
24
+ gradeLevel: str
25
+ semester: str
26
+ color: str
27
+ pdfAvailable: bool
28
+ topics: list
29
+
30
+
31
+ class TopicResponse(BaseModel):
32
+ id: str
33
+ name: str
34
+ unit: str
35
+
36
+
37
+ @router.get("/subjects", response_model=list[SubjectResponse])
38
+ async def list_subjects(grade_level: Optional[str] = Query(None, description="Filter by grade level (e.g., 'Grade 11', 'Grade 12')")):
39
+ """List all curriculum subjects, optionally filtered by grade level."""
40
+ subjects = get_subjects(grade_level)
41
+ return subjects
42
+
43
+
44
+ @router.get("/subjects/{subject_id}", response_model=SubjectResponse)
45
+ async def get_subject_by_id(subject_id: str):
46
+ """Get a specific subject by ID."""
47
+ subject = get_subject(subject_id)
48
+ if not subject:
49
+ raise HTTPException(status_code=404, detail=f"Subject not found: {subject_id}")
50
+ return subject
51
+
52
+
53
+ @router.get("/subjects/{subject_id}/topics", response_model=list[TopicResponse])
54
+ async def list_subject_topics(subject_id: str):
55
+ """List all topics for a subject."""
56
+ topics = get_topics(subject_id)
57
+ return topics
58
+
59
+
60
+ @router.get("/subjects/{subject_id}/topics/{topic_id}", response_model=TopicResponse)
61
+ async def get_topic_by_id(subject_id: str, topic_id: str):
62
+ """Get a specific topic."""
63
+ topic = get_topic(subject_id, topic_id)
64
+ if not topic:
65
+ raise HTTPException(status_code=404, detail=f"Topic not found: {subject_id}/{topic_id}")
66
+ return topic
services/curriculum_service.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Curriculum Service - Firestore-backed curriculum data.
3
+
4
+ Fetches subjects, topics, and modules from Firestore.
5
+ Falls back to static data if Firestore is unavailable.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Static curriculum data as fallback
15
+ _STATIC_SUBJECTS = [
16
+ {
17
+ "id": "gen-math",
18
+ "code": "GEN MATH",
19
+ "name": "General Mathematics",
20
+ "gradeLevel": "Grade 11",
21
+ "semester": "1st Semester",
22
+ "color": "from-blue-500 to-cyan-500",
23
+ "pdfAvailable": True,
24
+ "topics": [
25
+ {"id": "gen-math-001", "name": "Patterns and Real-Life Relationships", "unit": "Patterns, Relations, and Functions"},
26
+ {"id": "gen-math-002", "name": "Functions as Mathematical Models", "unit": "Patterns, Relations, and Functions"},
27
+ {"id": "gen-math-003", "name": "Function Notation and Evaluation", "unit": "Patterns, Relations, and Functions"},
28
+ {"id": "gen-math-004", "name": "Domain and Range of Functions", "unit": "Patterns, Relations, and Functions"},
29
+ {"id": "gen-math-005", "name": "Operations on Functions", "unit": "Patterns, Relations, and Functions"},
30
+ {"id": "gen-math-006", "name": "Composite Functions", "unit": "Patterns, Relations, and Functions"},
31
+ {"id": "gen-math-007", "name": "Inverse Functions", "unit": "Patterns, Relations, and Functions"},
32
+ {"id": "gen-math-008", "name": "Graphs of Rational Functions", "unit": "Patterns, Relations, and Functions"},
33
+ {"id": "gen-math-009", "name": "Graphs of Exponential Functions", "unit": "Patterns, Relations, and Functions"},
34
+ {"id": "gen-math-010", "name": "Graphs of Logarithmic Functions", "unit": "Patterns, Relations, and Functions"},
35
+ {"id": "gen-math-011", "name": "Simple and Compound Interest", "unit": "Financial Mathematics"},
36
+ {"id": "gen-math-012", "name": "Simple and General Annuities", "unit": "Financial Mathematics"},
37
+ {"id": "gen-math-013", "name": "Present and Future Value", "unit": "Financial Mathematics"},
38
+ {"id": "gen-math-014", "name": "Loans, Amortization, and Sinking Funds", "unit": "Financial Mathematics"},
39
+ {"id": "gen-math-015", "name": "Stocks, Bonds, and Market Indices", "unit": "Financial Mathematics"},
40
+ {"id": "gen-math-016", "name": "Business Decision-Making with Mathematical Models", "unit": "Financial Mathematics"},
41
+ {"id": "gen-math-017", "name": "Propositions and Logical Connectives", "unit": "Logic and Mathematical Reasoning"},
42
+ {"id": "gen-math-018", "name": "Truth Values and Truth Tables", "unit": "Logic and Mathematical Reasoning"},
43
+ {"id": "gen-math-019", "name": "Logical Equivalence and Implication", "unit": "Logic and Mathematical Reasoning"},
44
+ {"id": "gen-math-020", "name": "Quantifiers and Negation", "unit": "Logic and Mathematical Reasoning"},
45
+ {"id": "gen-math-021", "name": "Validity of Arguments", "unit": "Logic and Mathematical Reasoning"},
46
+ ]
47
+ },
48
+ {
49
+ "id": "stats-prob",
50
+ "code": "STAT&PROB",
51
+ "name": "Statistics and Probability",
52
+ "gradeLevel": "Grade 11",
53
+ "semester": "2nd Semester",
54
+ "color": "from-sky-500 to-cyan-500",
55
+ "pdfAvailable": True,
56
+ "topics": [
57
+ {"id": "stat-001", "name": "Random Variables", "unit": "Random Variables"},
58
+ {"id": "stat-002", "name": "Discrete Probability Distributions", "unit": "Random Variables"},
59
+ {"id": "stat-003", "name": "Mean and Variance of Discrete RV", "unit": "Random Variables"},
60
+ {"id": "stat-004", "name": "Normal Distribution", "unit": "Normal Distribution"},
61
+ {"id": "stat-005", "name": "Standard Normal Distribution and Z-scores", "unit": "Normal Distribution"},
62
+ {"id": "stat-006", "name": "Areas Under the Normal Curve", "unit": "Normal Distribution"},
63
+ {"id": "stat-007", "name": "Sampling Distributions", "unit": "Sampling and Estimation"},
64
+ {"id": "stat-008", "name": "Central Limit Theorem", "unit": "Sampling and Estimation"},
65
+ {"id": "stat-009", "name": "Point Estimation", "unit": "Sampling and Estimation"},
66
+ {"id": "stat-010", "name": "Confidence Intervals", "unit": "Sampling and Estimation"},
67
+ {"id": "stat-011", "name": "Hypothesis Testing Concepts", "unit": "Hypothesis Testing"},
68
+ {"id": "stat-012", "name": "T-test", "unit": "Hypothesis Testing"},
69
+ {"id": "stat-013", "name": "Z-test", "unit": "Hypothesis Testing"},
70
+ {"id": "stat-014", "name": "Correlation and Regression", "unit": "Correlation and Regression"},
71
+ ]
72
+ },
73
+ {
74
+ "id": "pre-calc",
75
+ "code": "PRE-CALC",
76
+ "name": "Pre-Calculus",
77
+ "gradeLevel": "Grade 12",
78
+ "semester": "1st Semester",
79
+ "color": "from-orange-500 to-red-500",
80
+ "pdfAvailable": False,
81
+ "topics": [
82
+ {"id": "pre-calc-001", "name": "Conic Sections - Parabola", "unit": "Analytic Geometry"},
83
+ {"id": "pre-calc-002", "name": "Conic Sections - Ellipse", "unit": "Analytic Geometry"},
84
+ {"id": "pre-calc-003", "name": "Conic Sections - Hyperbola", "unit": "Analytic Geometry"},
85
+ {"id": "pre-calc-004", "name": "Conic Sections - Circle", "unit": "Analytic Geometry"},
86
+ {"id": "pre-calc-005", "name": "Systems of Nonlinear Equations", "unit": "Analytic Geometry"},
87
+ {"id": "pre-calc-006", "name": "Sequences and Series", "unit": "Series and Induction"},
88
+ {"id": "pre-calc-007", "name": "Arithmetic Sequences", "unit": "Series and Induction"},
89
+ {"id": "pre-calc-008", "name": "Geometric Sequences", "unit": "Series and Induction"},
90
+ {"id": "pre-calc-009", "name": "Mathematical Induction", "unit": "Series and Induction"},
91
+ {"id": "pre-calc-010", "name": "Binomial Theorem", "unit": "Series and Induction"},
92
+ {"id": "pre-calc-011", "name": "Angles and Unit Circle", "unit": "Trigonometry"},
93
+ {"id": "pre-calc-012", "name": "Trigonometric Functions", "unit": "Trigonometry"},
94
+ {"id": "pre-calc-013", "name": "Trigonometric Identities", "unit": "Trigonometry"},
95
+ {"id": "pre-calc-014", "name": "Sum and Difference Formulas", "unit": "Trigonometry"},
96
+ {"id": "pre-calc-015", "name": "Inverse Trigonometric Functions", "unit": "Trigonometry"},
97
+ {"id": "pre-calc-016", "name": "Polar Coordinates", "unit": "Trigonometry"},
98
+ ]
99
+ },
100
+ {
101
+ "id": "basic-calc",
102
+ "code": "BASIC CALC",
103
+ "name": "Basic Calculus",
104
+ "gradeLevel": "Grade 12",
105
+ "semester": "2nd Semester",
106
+ "color": "from-green-500 to-teal-500",
107
+ "pdfAvailable": True,
108
+ "topics": [
109
+ {"id": "calc-001", "name": "Limits of Functions", "unit": "Limits"},
110
+ {"id": "calc-002", "name": "Limit Theorems", "unit": "Limits"},
111
+ {"id": "calc-003", "name": "One-Sided Limits", "unit": "Limits"},
112
+ {"id": "calc-004", "name": "Infinite Limits and Limits at Infinity", "unit": "Limits"},
113
+ {"id": "calc-005", "name": "Continuity of Functions", "unit": "Limits"},
114
+ {"id": "calc-006", "name": "Definition of the Derivative", "unit": "Derivatives"},
115
+ {"id": "calc-007", "name": "Differentiation Rules", "unit": "Derivatives"},
116
+ {"id": "calc-008", "name": "Chain Rule", "unit": "Derivatives"},
117
+ {"id": "calc-009", "name": "Implicit Differentiation", "unit": "Derivatives"},
118
+ {"id": "calc-010", "name": "Higher-Order Derivatives", "unit": "Derivatives"},
119
+ {"id": "calc-011", "name": "Related Rates", "unit": "Derivatives"},
120
+ {"id": "calc-012", "name": "Extrema and the First Derivative Test", "unit": "Derivatives"},
121
+ {"id": "calc-013", "name": "Concavity and the Second Derivative Test", "unit": "Derivatives"},
122
+ {"id": "calc-014", "name": "Optimization Problems", "unit": "Derivatives"},
123
+ {"id": "calc-015", "name": "Antiderivatives and Indefinite Integrals", "unit": "Integration"},
124
+ {"id": "calc-016", "name": "Definite Integrals and the FTC", "unit": "Integration"},
125
+ {"id": "calc-017", "name": "Integration by Substitution", "unit": "Integration"},
126
+ {"id": "calc-018", "name": "Area Under a Curve", "unit": "Integration"},
127
+ ]
128
+ },
129
+ ]
130
+
131
+ _firestore_db = None
132
+
133
+
134
+ def _get_firestore_db():
135
+ """Initialize Firestore client."""
136
+ global _firestore_db
137
+ if _firestore_db is not None:
138
+ return _firestore_db
139
+
140
+ try:
141
+ import firebase_admin
142
+ from firebase_admin import firestore
143
+ if not firebase_admin._apps:
144
+ # Try service account from env or default credentials
145
+ import json
146
+ svc_account = os.getenv("FIREBASE_SERVICE_ACCOUNT_JSON")
147
+ if svc_account:
148
+ sa_creds = json.loads(svc_account)
149
+ firebase_admin.initialize_app(firebase_admin.Certificate(sa_creds))
150
+ else:
151
+ firebase_admin.initialize_app()
152
+ _firestore_db = firestore.client()
153
+ return _firestore_db
154
+ except Exception as e:
155
+ logger.warning(f"Could not initialize Firestore: {e}")
156
+ return None
157
+
158
+
159
+ def get_subjects(grade_level: Optional[str] = None) -> List[Dict[str, Any]]:
160
+ """
161
+ Fetch all subjects from Firestore.
162
+ Falls back to static data if Firestore unavailable.
163
+ Defaults to Grade 11 (SHS) if no grade specified.
164
+ """
165
+ # Default to Grade 11 (SHS) - only serve Grade 11 students for now
166
+ if grade_level is None:
167
+ grade_level = "Grade 11"
168
+
169
+ db = _get_firestore_db()
170
+
171
+ if db is not None:
172
+ try:
173
+ subjects_ref = db.collection("subjects")
174
+ if grade_level:
175
+ subjects_ref = subjects_ref.where("gradeLevel", "==", grade_level)
176
+
177
+ docs = subjects_ref.stream()
178
+ subjects = []
179
+ for doc in docs:
180
+ data = doc.to_dict()
181
+ if data:
182
+ data["id"] = doc.id
183
+ subjects.append(data)
184
+
185
+ if subjects:
186
+ logger.info(f"Loaded {len(subjects)} subjects from Firestore")
187
+ return subjects
188
+ except Exception as e:
189
+ logger.warning(f"Firestore fetch failed, using static data: {e}")
190
+
191
+ # Static fallback
192
+ if grade_level:
193
+ return [s for s in _STATIC_SUBJECTS if s.get("gradeLevel") == grade_level]
194
+ return list(_STATIC_SUBJECTS)
195
+
196
+
197
+ def get_subject(subject_id: str) -> Optional[Dict[str, Any]]:
198
+ """Fetch a single subject by ID."""
199
+ db = _get_firestore_db()
200
+
201
+ if db is not None:
202
+ try:
203
+ doc = db.collection("subjects").document(subject_id).get()
204
+ if doc.exists:
205
+ data = doc.to_dict()
206
+ data["id"] = doc.id
207
+ return data
208
+ except Exception as e:
209
+ logger.warning(f"Firestore fetch failed for {subject_id}: {e}")
210
+
211
+ # Static fallback
212
+ for subject in _STATIC_SUBJECTS:
213
+ if subject["id"] == subject_id:
214
+ return dict(subject)
215
+ return None
216
+
217
+
218
+ def get_topics(subject_id: str) -> List[Dict[str, Any]]:
219
+ """Fetch all topics for a subject."""
220
+ subject = get_subject(subject_id)
221
+ if subject:
222
+ return subject.get("topics", [])
223
+ return []
224
+
225
+
226
+ def get_topic(subject_id: str, topic_id: str) -> Optional[Dict[str, Any]]:
227
+ """Fetch a single topic."""
228
+ topics = get_topics(subject_id)
229
+ for topic in topics:
230
+ if topic["id"] == topic_id:
231
+ return topic
232
+ return None